Code Ease Code Ease
  • 个人博客网站 (opens new window)
  • 好用的工具网站 (opens new window)
  • Java核心基础
  • 框架的艺术
  • 分布式与微服务
  • 开发经验大全
  • 设计模式
  • 版本新特性
数据库系列
大数据+AI
  • xxl-job
运维与Linux
  • 基于SpringBoot和BootStrap的论坛网址
  • 基于VuePress的个人博客网站
  • 基于SpringBoot开发的小功能
  • 做一个自己的IDEA插件
程序人生
关于我
  • 分类
  • 标签
  • 归档

神秘的鱼仔

你会累是因为你在走上坡路
  • 个人博客网站 (opens new window)
  • 好用的工具网站 (opens new window)
  • Java核心基础
  • 框架的艺术
  • 分布式与微服务
  • 开发经验大全
  • 设计模式
  • 版本新特性
数据库系列
大数据+AI
  • xxl-job
运维与Linux
  • 基于SpringBoot和BootStrap的论坛网址
  • 基于VuePress的个人博客网站
  • 基于SpringBoot开发的小功能
  • 做一个自己的IDEA插件
程序人生
关于我
  • 分类
  • 标签
  • 归档
服务器
  • Java核心基础

  • 框架的艺术

    • Spring

    • Mybatis

    • SpringBoot

      • 如何用SpringBoot(2.3.3版本)快速搭建一个项目
      • 一步步带你看SpringBoot(2.3.3版本)自动装配原理
      • SpringBoot配置文件及自动配置原理详解,这应该是SpringBoot最大的优势了吧
      • SpringBoot整合jdbc、durid、mybatis详解,数据库的连接就是这么简单
      • SpringBoot整合SpringSecurity详解,认证授权从未如此简单
      • SpringBoot整合Shiro详解,还在自己写登陆注册早落伍了
      • SpringBoot如何实现异步、定时任务?
      • 如何在SpringBoot启动时执行初始化操作,两个简单接口就可以实现
      • 如何使用SpringBoot写一个属于自己的Starter
      • SpringBoot请求日志,如何优雅地打印
        • 前言
        • 准备工作
        • 基于注解的实现
        • 基于配置文件的实现
        • 总结
      • 主线程的用户信息,到子线程怎么丢了
    • MQ

    • Zookeeper

    • netty

  • 分布式与微服务

  • 开发经验大全

  • 版本新特性

  • Java
  • 框架的艺术
  • SpringBoot
CodeEase
2024-06-19
目录

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

作者:鱼仔
博客首页: codeease.top (opens new window)
公众号:Java鱼仔

# 前言

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

10-1.png

# 准备工作

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1
2
3
4

# 基于注解的实现

这个功能的原理很简单,就是一个基于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 {
}
1
2
3
4
5
6
7
8
9
10
11
12

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

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 {
}
1
2
3
4
5
6
7
8
9
10
11
12

接着定义一个类叫做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());
    }


}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

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

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

效果可以见文章开头。

# 基于配置文件的实现

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

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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

yml文件是这样写的

logging:
  includePaths:
    - /api/**
    - /api/v1/**
    - /test/**
1
2
3
4
5

配置打日志要实现的效果是,只有白名单配置包含的路径才需要打印日志。 接着编写一个叫做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));
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

# 总结

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

上次更新: 2025/04/29, 17:22:06
如何使用SpringBoot写一个属于自己的Starter
主线程的用户信息,到子线程怎么丢了

← 如何使用SpringBoot写一个属于自己的Starter 主线程的用户信息,到子线程怎么丢了→

最近更新
01
AI大模型部署指南
02-18
02
半个月了,DeepSeek为什么还是服务不可用
02-13
03
Python3.9及3.10安装文档
01-23
更多文章>
Theme by Vdoing | Copyright © 2023-2025 备案图标 浙公网安备33021202002405 | 浙ICP备2023040452号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式