51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

SpringBoot请求日志,如何优雅地打印

# 前言 {#前言}

上一篇文章介绍了如何使用MyBatis的Plugin,来实现SQL的日志打印,这篇文章介绍一下如何将SpringBoot的请求日志,优雅地打印到日志中。 实现效果是这样的,只需要在需要打印的接口上加上一个注解,或者增加一项配置项,一个很详细的请求出入参等信息就被打印出来了。

# 准备工作 {#准备工作}

因为这个功能采用了AOP切面的功能,因此需要先引入AOP的依赖,版本按实际填写即可

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

# 基于注解的实现 {#基于注解的实现}

这个功能的原理很简单,就是一个基于AOP实现的小功能,但是在查问题时还挺有用的,本次实现两个注解,只打印请求入参的注解和入参出参都打印的注解。 第一个是只打印请求的注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 记录请求日志
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface BeforeLog {
}

第二个是同时打印出入参的注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 记录请求和响应日志
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AroundLog {
}

接着定义一个类叫做LogAspect,这是打日志功能的切面类,在实现上只需要先定义切入点,然后通过@Before和@Around来实现日志的打印,注释全都写在代码上了:

import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.support.MultipartFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author by: 神秘的鱼仔
 * @ClassName: LogAspect
 * @Description: 日志切面类
 * @Date: 2024/6/18 22:41
 */
@Slf4j
@Aspect
@Component
public class LogAspect {

    /**
     * Before切入点
     */
    @Pointcut("@annotation(com.mybatisflex.test.annotation.BeforeLog)")
    public void beforePointcut() {
    }

    /**
     * Around切入点
     */
    @Pointcut("@annotation(com.mybatisflex.test.annotation.AroundLog)")
    public void aroundPointcut() {
    }


    /**
     * 记录请求日志的切面
     * @param joinPoint
     */
    @Before("beforePointcut()")
    public void doBefore(JoinPoint joinPoint) {
        try {
            addLog(joinPoint,"",0);
        }catch (Exception e){
            log.error("doBefore日志记录异常,异常信息为:",e);
        }
    }

    /**
     * 记录请求和响应日志的切面
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("aroundPointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        Object result = null;
        try {
            long startTime = System.currentTimeMillis();
            result = joinPoint.proceed(args);
            long endTime = System.currentTimeMillis();
            long time = endTime - startTime;
            addLog(joinPoint,JSONUtil.toJsonStr(result),time);
        }catch (Exception e){
            log.error("doAround日志记录异常,异常信息为:",e);
            throw e;
        }
        return result;
    }

    /**
     * 日志记录入库操作
     */
    public void addLog(JoinPoint joinPoint, String outParams, long time) {
        HttpServletRequest request = ((ServletRequestAttributes)
                Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        log.info("\n\r=======================================\n\r" +
                        "请求地址:{} \n\r" +
                        "请求方式:{} \n\r" +
                        "请求类方法:{} \n\r" +
                        "请求方法参数:{} \n\r" +
                        "返回报文:{} \n\r" +
                        "处理耗时:{} ms \n\r" +
                        "=======================================\n\r",
                request.getRequestURI(),
                request.getMethod(),
                joinPoint.getSignature(),
                JSONUtil.toJsonStr(filterArgs(joinPoint.getArgs())),
                outParams,
                String.valueOf(time)
        );
    }

    /**
     * 过滤参数
     * @param args
     * @return
     */
    private List<Object> filterArgs(Object[] args) {
        return Arrays.stream(args).filter(object -> !(object instanceof MultipartFilter)
                && !(object instanceof HttpServletRequest)
                && !(object instanceof HttpServletResponse)
        ).collect(Collectors.toList());
    }


}

接下来只需要在想要打印日志的接口上增加对应的注解,这个功能就实现了

    @PostMapping("/testLog")
    @AroundLog
    public Result<Person> testLog(@RequestBody TestRequest request){
        Person person = new Person(1,"鱼仔",27,"浙江","测试");
        return Result.success(person);
    }

效果可以见文章开头。

# 基于配置文件的实现 {#基于配置文件的实现}

如果接口特别多,一个个写注解的方式总是觉得太麻烦,这个时候就可以换个思路,采用配置文件的方式来实现。 先定义用来读取配置文件的类:

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author by: 神秘的鱼仔
 * @ClassName: LoggingProperties
 * @Description:
 * @Date: 2024/6/19 20:09
 */
@Component
@ConfigurationProperties(prefix = "logging")
public class LoggingProperties {
    private List<String> includePaths;

    public List<String> getIncludePaths() {
        return includePaths;
    }

    public void setIncludePaths(List<String> includePaths) {
        this.includePaths = includePaths;
    }
}

yml文件是这样写的

logging:
  includePaths:
    - /api/**
    - /api/v1/**
    - /test/**

配置打日志要实现的效果是,只有白名单配置包含的路径才需要打印日志。 接着编写一个叫做WhiteListLogAspect的类,原理和上面的类似

import cn.hutool.json.JSONUtil;
import com.mybatisflex.test.properties.LoggingProperties;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.support.MultipartFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author by: 神秘的鱼仔
 * @ClassName: WhiteListLogAspect
 * @Description:
 * @Date: 2024/6/19 19:50
 */
@Slf4j
@Aspect
@Component
public class WhiteListLogAspect {

    @Autowired
    private LoggingProperties loggingProperties;

    private AntPathMatcher pathMatcher = new AntPathMatcher();

    @Before("execution(* com.mybatisflex.test.controller..*(..))")
    public void doBefore(JoinPoint joinPoint) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String requestURI = request.getRequestURI();
        if (shouldLog(requestURI)) {
            addLog(joinPoint,"",0);
        }
    }

    /**
     * 日志记录入库操作
     */
    public void addLog(JoinPoint joinPoint, String outParams, long time) {
        HttpServletRequest request = ((ServletRequestAttributes)
                Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        log.info("\n\r=======================================\n\r" +
                        "请求地址:{} \n\r" +
                        "请求方式:{} \n\r" +
                        "请求类方法:{} \n\r" +
                        "请求方法参数:{} \n\r" +
                        "返回报文:{} \n\r" +
                        "处理耗时:{} ms \n\r" +
                        "=======================================\n\r",
                request.getRequestURI(),
                request.getMethod(),
                joinPoint.getSignature(),
                JSONUtil.toJsonStr(filterArgs(joinPoint.getArgs())),
                outParams,
                String.valueOf(time)
        );
    }

    /**
     * 过滤参数
     * @param args
     * @return
     */
    private List<Object> filterArgs(Object[] args) {
        return Arrays.stream(args).filter(object -> !(object instanceof MultipartFilter)
                && !(object instanceof HttpServletRequest)
                && !(object instanceof HttpServletResponse)
        ).collect(Collectors.toList());
    }

    private boolean shouldLog(String requestURI) {
        return loggingProperties.getIncludePaths().stream().anyMatch(pattern -> pathMatcher.match(pattern, requestURI));
    }
}

# 总结 {#总结}

打印请求日志的最大作用就是当出现问题时,基于出入参能比较容易地排查问题,不过响应日志一般不会打在生产日志上,因为返回的数据会过于庞大。具体还是看项目的实际情况了。

赞(2)
未经允许不得转载:工具盒子 » SpringBoot请求日志,如何优雅地打印