一、Springboot使用Aop保存接口请求日志到mysql

1、添加aop依赖

        <!-- aop日志 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2、新建接口保存数据库的实体类RequestLog.java

package com.example.springboot.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Getter;
import lombok.Setter;

/**
 * <p>
 * 请求日志
 * </p>
 *
 * @author Sca_jie
 * @since 2023-09-28
 */
@Getter
@Setter
@TableName("request_log")
public class RequestLog implements Serializable {

    private static final long serialVersionUID = 1L;

    // 主键-自增
    @TableId(value = "number", type = IdType.AUTO)
    private Integer number;

    // 用户账号
    private String id;

    // 携带token
    private String token;

    // 接口路径
    private String url;
    
    // 请求类型
    private String method;

    // 携带参数
    private String params;

    // ip地址
    private String ip;

    // 结果
    private String result;

    // 接口发起时间
    private LocalDateTime startDate;

    // 接口结束时间
    private LocalDateTime endDate;

    // 响应耗时
    private String responseTime;
}

3、新建一个注解RequestLogAnnotation.java,用于特定类使用aop(这里是给全局异常留的)

package com.example.springboot.annotation;

import java.lang.annotation.*;

/**
 * 请求记录日志注解
 */
@Target({ElementType.TYPE, ElementType.METHOD}) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented
public @interface RequestLogAnnotation {
    String value() default "";
}

4、(核心)新建aop面切类RequestLogAspect.java拦截请求并保存日志

package com.example.springboot.common;

import cn.hutool.core.net.NetUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import com.example.springboot.annotation.RequestLogAnnotation;
import com.example.springboot.entity.RequestLog;
import com.example.springboot.mapper.RequestLogMapper;
import com.example.springboot.utils.CookieUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * 日志记录
 *
 */
@Aspect
@Component
public class RequestLogAspect {

    @Autowired(required = false)
    RequestLogMapper requestLogMapper;

    /**
     * execution是给指定区域,切入点
     * annotation是让特定类使用注解,切入点
     */
    @Pointcut("execution(* com.example.springboot.controller.*.*(..)) || " +
            "@annotation(com.example.springboot.annotation.RequestLogAnnotation)")
    public void logPointCut() {

    }

    // 请求的开始处理时间(不同类型)
    Long startTime = null;
    LocalDateTime startDate;

    @Before("logPointCut()")
    public void beforeRequest() {
        startTime = System.currentTimeMillis();
        startDate = LocalDateTime.now();
    }

    @AfterReturning(value = "logPointCut()", returning = "result")
    public void saveLog(JoinPoint joinPoint, Object result) {

        // 获取请求头
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        HttpServletResponse response = requestAttributes.getResponse();

        //从切面织入点处通过反射机制获取织入点处的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        //获取切入点所在的方法
        Method method = signature.getMethod();

        // 初始化日志表的实体类
        RequestLog requestLog = new RequestLog();

        //获取操作
        RequestLogAnnotation requestLogAnnotation = method.getAnnotation(RequestLogAnnotation.class);

//        // 获取@SystemLogAnnotation(value = "用户登录")中的注解value
//        if (systemLogAnnotation != null) {
//            String value = systemLogAnnotation.value();
//            requestLog.setSName(value);
//        }

        // 获取cookies
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            // 获取token
            for(Cookie cookie : cookies){
                if(cookie.getName().equals("token")){
                    requestLog.setToken(cookie.getValue());
                }
            }

            // 获取id
            String id = CookieUtil.getid(cookies);
            if (id != "" | id != null) {
                requestLog.setId(id);
            }
        }

        // 区分get和post获取参数
        String params = "{}";
        if (request.getMethod().equals("GET")) {
            params = JSONObject.toJSONString(request.getParameterMap());
        } else if (request.getMethod().equals("POST")) {
            params = JSONUtil.toJsonStr(joinPoint.getArgs());
        }

        // 获取用户真实ip地址
        String ip;
        if (request.getHeader("x-forwarded-for") == null) {
            ip = request.getRemoteAddr();
        } else {
            ip = request.getHeader("x-forwarded-for");
        }

        if (ip.equals("0:0:0:0:0:0:0:1")) {
            ip = "127.0.0.1";
        }

        // 用户Ip
        requestLog.setIp(ip);
        // 接口请求类型
        requestLog.setMethod(request.getMethod());
        // 请求参数(区分get和post)
        requestLog.setParams(params);
        // 请求接口路径
        requestLog.setUrl(request.getRequestURI().toString());
        // 返回结果
        requestLog.setResult(JSONObject.toJSONString(result));
        // 请求开始时间
        requestLog.setStartDate(startDate);
        // 请求结束时间
        requestLog.setEndDate(LocalDateTime.now());
        // 请求共计时间(ms)
        requestLog.setResponseTime(String.valueOf(System.currentTimeMillis() - startTime));

        // 保存日志到mysql
        requestLogMapper.insert(requestLog);
    }
}

5、前面在@Pointcut切入点那里定义了controller目录下所有的都自动切入aop,这里可以不使用@RequestLogAnnotation,其他文件夹下的特殊类或方法可以在对应位置添加注解@RequestLogAnnotation

    @RequestLogAnnotation(value = "获取上传记录")
    @GetMapping("/getlist")
    public Result getlist (@RequestParam(required = false) String id) {
        if (id == null) {
            return Result.success(404, "参数缺失");
        } else {
            List<UploadLog> page = uploadLogService.getlist(id);
            return Result.success(200, page.toString());
        }

    }

效果如下

 二、解决Interceptor拦截器中引用mapper和service为null

背景

当我们项目中同时使用Interceptor拦截器和aop日志拦截时,被Interceptor拦截器所拦截的请求不会通过aop日志保存到数据库(防止恶意爬虫)。

但是项目如果需要记录这些被拦截的非法请求的话,目前暂时的解决方法是在Interceptor拦截器所拦截非法的请求之前再使用前面的RequestLogMapper再重新进行保存一次(只针对非法请求,因为合法请求会通过Aop日志拦截)。

但这时候又出现了新的问题,在Interceptor创建时mapper和service还没来得及注入,会导致mapper和service引用为null,就需要在创建前先行赋值,如下:

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    /**
     * 白名单
     */
    private static String[] WhiteList = {"/user/login", "/user/register"};

    /**
     * 解决在Token拦截器中无法使用mapper和service的情况(无Bean)
     * @return
     */
    @Bean
    public TokenInterceptor myTokenInterceptor () {
        return new TokenInterceptor();
    }

    /**
     * http请求拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 除excludePathPatterns内包含的接口,其他接口都要经过拦截,执行LogInterceptor()
        registry.addInterceptor(myTokenInterceptor())
                .excludePathPatterns(WhiteList);
    }
}

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐