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)); } }

# 总结 {#总结}

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

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