接口日志拦截进阶版
# 日志拦截进阶版
# 前言
在 Spring Boot 应用中,记录请求日志是一个常见且必要的需求。通过记录接口请求日志,可以追踪请求方法、接口路径、请求参数及响应结果等信息,帮助开发人员快速定位问题。本文将通过 AOP 切面技术,详细介绍如何实现这种日志记录功能。
# 1. 引入依赖
首先,确保在 pom.xml
中引入必要的依赖:
<dependencies>
<!-- Spring Boot Starter AOP: 提供 AOP 功能支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--表示两个项目之间依赖不传递;不设置optional或者optional是false,表示传递依赖-->
<!--例如:project1依赖a.jar(optional=true),project2依赖project1,则project2不依赖a.jar-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 在 Spring Boot 项目中,Jackson 是默认集成的 -->
<!-- Jackson Databind: 用于将对象序列化为 JSON 字符串 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Lombok: 简化代码编写(如 @Getter, @Setter, @AllArgsConstructor 等) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
</dependencies>
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
spring-boot-starter-aop
:提供了 Spring AOP 功能的支持,可以通过切面(Aspect)来拦截方法,执行额外的逻辑(如日志记录)。jackson-databind
:提供了 JSON 序列化和反序列化的能力,用于将响应对象转换为 JSON 字符串,以便记录到日志中。lombok
:简化了代码编写,通过注解生成常见的代码如构造器、Getter
和Setter
等。需要在开发环境中启用 Lombok 插件来支持自动生成这些代码。
# 2. 日志级别枚举类
该枚举类定义了不同的日志输出级别,如 NONE
、BASIC
、HEADERS
和 BODY
。你可以根据需要选择日志输出的详细程度。
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum RequestLogLevelEnum {
NONE(0), // 不记录日志
BASIC(1), // 仅记录请求方法和响应状态
HEADERS(2), // 记录请求方法、响应状态和请求头信息
BODY(3); // 记录完整的请求和响应,包括请求体
private final int level;
public static final String REQ_LOG_PROPS_PREFIX = "controller.log";
/**
* 判断当前日志级别是否小于等于指定级别
*/
public boolean lte(RequestLogLevelEnum level) {
return this.level <= level.level;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 3. 日志级别配置类
该配置类用于加载日志级别等配置项,支持灵活控制日志输出的粒度。
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Getter
@Setter
@ConfigurationProperties(prefix = RequestLogLevelEnum.REQ_LOG_PROPS_PREFIX)
public class RequestLogProperties {
/**
* 日志记录的级别
* <p>可选值:</p>
* <ul>
* <li>NONE: 不记录日志</li>
* <li>BASIC: 仅记录请求方法和响应状态</li>
* <li>HEADERS: 记录请求方法、响应状态和请求头信息</li>
* <li>BODY: 记录完整的请求和响应,包括请求体</li>
* </ul>
*/
private RequestLogLevelEnum level = RequestLogLevelEnum.BODY; // 默认日志级别为 BODY
private boolean enabled = true; // 控制日志功能是否启用
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 4. AOP 切面实现
该切面类拦截所有 Controller 方法的执行,在方法前后记录请求信息、响应结果及异常信息。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.scholar.springbootscaffolding.config.RequestLogProperties;
import com.scholar.springbootscaffolding.enums.RequestLogLevelEnum;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Slf4j
@Aspect
@Configuration
@AllArgsConstructor
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(prefix = "controller.log", name = "enabled", havingValue = "true", matchIfMissing = true)
public class RequestLogAspect {
private final RequestLogProperties properties;
private final ObjectMapper objectMapper;
@Around("execution(* com.scholar.springbootscaffolding..*(..)) && (@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController))")
public Object aroundApi(ProceedingJoinPoint point) throws Throwable {
RequestLogLevelEnum level = properties.getLevel();
if (RequestLogLevelEnum.NONE == level) {
return point.proceed(); // 如果日志级别为 NONE,直接执行方法
}
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String requestUrl = Objects.requireNonNull(request).getRequestURI();
String requestMethod = request.getMethod();
// 记录请求前的信息
StringBuilder beforeReqLog = new StringBuilder(300);
List<Object> beforeReqArgs = new ArrayList<>();
beforeReqLog.append("\n\n================ 请求开始 ================\n");
beforeReqLog.append("请求方法: {} | 请求路径: {}\n");
beforeReqArgs.add(requestMethod);
beforeReqArgs.add(requestUrl);
logRequestParameters(point, beforeReqLog, beforeReqArgs);
logRequestHeaders(request, level, beforeReqLog, beforeReqArgs);
beforeReqLog.append("=============== 请求结束 ================\n");
// 打印日志
long startNs = System.nanoTime();
log.info(beforeReqLog.toString(), beforeReqArgs.toArray());
// 记录方法执行后的日志
StringBuilder afterReqLog = new StringBuilder(200);
List<Object> afterReqArgs = new ArrayList<>();
afterReqLog.append("\n\n================ 响应开始 ================\n");
try {
Object result = point.proceed();
if (RequestLogLevelEnum.BODY.lte(level)) {
afterReqLog.append("响应结果: {}\n");
afterReqArgs.add(objectMapper.writeValueAsString(result));
}
return result;
} finally {
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
afterReqLog.append("耗时: {} ms | 请求方法: {} | 请求路径: {}\n");
afterReqArgs.add(tookMs);
afterReqArgs.add(requestMethod);
afterReqArgs.add(requestUrl);
afterReqLog.append("=============== 响应结束 ================\n");
log.info(afterReqLog.toString(), afterReqArgs.toArray());
}
}
// 记录请求参数
private void logRequestParameters(ProceedingJoinPoint point, StringBuilder beforeReqLog, List<Object> beforeReqArgs) throws Exception {
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
Object[] args = point.getArgs();
final Map<String, Object> paraMap = new HashMap<>(16);
Object requestBodyValue = null;
for (int i = 0; i < args.length; i++) {
MethodParameter methodParam = new MethodParameter(method, i);
PathVariable pathVariable = methodParam.getParameterAnnotation(PathVariable.class);
if (pathVariable != null) {
continue; // 跳过 PathVariable 注解的参数
}
RequestBody requestBody = methodParam.getParameterAnnotation(RequestBody.class);
RequestParam requestParam = methodParam.getParameterAnnotation(RequestParam.class);
String parameterName = methodParam.getParameterName();
Object value = args[i];
if (requestBody != null) {
requestBodyValue = value; // 处理 @RequestBody 参数
continue;
}
if (value instanceof HttpServletRequest || value instanceof HttpServletResponse || value instanceof HttpSession) {
continue; // 跳过不可序列化的对象
}
// 防止 paraName 为 null 的情况,并尝试从 RequestParam 中获取参数名
String paraName = (requestParam != null && requestParam.value().length() > 0) ? requestParam.value() : parameterName;
paraMap.put(paraName != null ? paraName : "unknown", filterUnserializableObjects(value));
}
if (!paraMap.isEmpty()) {
beforeReqLog.append("请求参数: {}\n");
beforeReqArgs.add(objectMapper.writeValueAsString(paraMap));
}
if (requestBodyValue != null) {
beforeReqLog.append("请求体: {}\n");
beforeReqArgs.add(objectMapper.writeValueAsString(requestBodyValue));
}
}
// 过滤不可序列化的对象
private Object filterUnserializableObjects(Object value) {
if (value == null) {
return null;
}
if ( value instanceof MultipartFile ||value.getClass().getName().startsWith("org.apache.catalina.") || value.getClass().getName().startsWith("javax.servlet.")) {
return "[不可序列化的对象]";
}
return value;
}
// 记录请求头信息
private void logRequestHeaders(HttpServletRequest request, RequestLogLevelEnum level, StringBuilder beforeReqLog, List<Object> beforeReqArgs) {
if (RequestLogLevelEnum.HEADERS.lte(level)) {
Enumeration<String> headers = request.getHeaderNames();
while (headers.hasMoreElements()) {
String headerName = headers.nextElement();
String headerValue = request.getHeader(headerName);
beforeReqLog.append("请求头: {}: {}\n");
beforeReqArgs.add(headerName);
beforeReqArgs.add(headerValue);
}
}
}
}
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# 5. Spring Boot 2.x 和 3.x 中日志切面的自动配置实现
在前面的日志切面总结中,我们讨论了如何通过 AOP 实现对请求的日志记录。为了进一步简化开发流程,可以将这些配置自动化,确保在应用启动时日志切面能够自动生效。
# 1. Spring Boot 2.x 自动配置实现
在 Spring Boot 2.x 中,实现自动配置需要以下几步:
a. 创建自动配置类
首先,我们需要创建一个自动配置类,这个类使用 @Configuration
注解,并注册属性配置类:
@Configuration
@EnableConfigurationProperties(RequestLogProperties.class)
public class LogAutoConfiguration {
}
2
3
4
@Configuration
:标识该类为 Spring 配置类,Spring 会在启动时自动加载。@EnableConfigurationProperties
:启用自定义属性配置类,如RequestLogProperties
,以便从配置文件中读取属性值。
b. 创建 spring.factories
文件
在 Spring Boot 2.x 中,自动配置类需要在 spring.factories
文件中注册。你需要手动创建以下文件:
文件路径:src/main/resources/META-INF/spring.factories
文件内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.youngkbt.log.config.LogAutoConfiguration
2
spring.factories
文件:这是 Spring Boot 2.x 中用于注册自动配置类的标准方式,确保配置类在应用启动时被加载。
c. 配置属性提示
在 META-INF
下添加 additional-spring-configuration-metadata.json
文件
在 src/main/resources/META-INF/
目录下创建 additional-spring-configuration-metadata.json
文件,用于定义配置属性的详细描述和中文提示。
文件内容如下:
{
"properties": [
{
"name": "controller.log.enabled",
"type": "java.lang.Boolean",
"description": "是否启用日志功能",
"defaultValue": true
},
{
"name": "controller.log.level",
"type": "com.scholar.springbootreqlogprostar.enums.RequestLogLevelEnum",
"description": "日志记录的级别",
"defaultValue": "BODY",
"values": [
{
"value": "NONE",
"description": "不记录日志"
},
{
"value": "BASIC",
"description": "仅记录请求方法和响应状态"
},
{
"value": "HEADERS",
"description": "记录请求方法、响应状态和请求头信息"
},
{
"value": "BODY",
"description": "记录完整的请求和响应,包括请求体"
}
]
}
]
}
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
# 2. Spring Boot 3.x 自动配置实现
在 Spring Boot 3.x 中,自动配置方式更为简洁,使用了新的注解和配置文件路径。
a. 创建自动配置类
在 Spring Boot 3.x 中,使用 @AutoConfiguration
注解代替了 @Configuration
,这是 Spring Boot 3.x 提供的新注解:
@AutoConfiguration
@EnableConfigurationProperties(RequestLogProperties.class)
public class LogAutoConfiguration {
}
2
3
4
@AutoConfiguration
:Spring Boot 3.x 中的新注解,专门用于标记自动配置类。@EnableConfigurationProperties
:同样用于启用并绑定自定义属性配置类。
b. 创建 AutoConfiguration.imports
文件
在 Spring Boot 3.x 中,不再使用 spring.factories
文件,而是改用 AutoConfiguration.imports
文件进行自动配置类的注册。你需要手动创建以下文件:
文件路径:src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件内容如下:
cn.youngkbt.log.config.LogAutoConfiguration
AutoConfiguration.imports
文件:这是 Spring Boot 3.x 中新的自动配置类注册方式,更加灵活且易于管理。
c. 配置属性提示
在 META-INF
下添加 additional-spring-configuration-metadata.json
文件
在 src/main/resources/META-INF/
目录下创建 additional-spring-configuration-metadata.json
文件,用于定义配置属性的详细描述和中文提示。
文件内容如下:
{
"properties": [
{
"name": "controller.log.enabled",
"type": "java.lang.Boolean",
"description": "是否启用日志功能",
"defaultValue": true
},
{
"name": "controller.log.level",
"type": "com.scholar.springbootreqlogprostar.enums.RequestLogLevelEnum",
"description": "日志记录的级别",
"defaultValue": "BODY",
"values": [
{
"value": "NONE",
"description": "不记录日志"
},
{
"value": "BASIC",
"description": "仅记录请求方法和响应状态"
},
{
"value": "HEADERS",
"description": "记录请求方法、响应状态和请求头信息"
},
{
"value": "BODY",
"description": "记录完整的请求和响应,包括请求体"
}
]
}
]
}
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
# 3. API 和参数的作用及位置说明
在自动配置过程中,我们使用了一些重要的注解和配置项。下面是它们的作用及具体使用位置:
@ConditionalOnProperty
:- 作用:用于控制自动配置是否启用。可以通过配置项(如
controller.log.enabled
)动态决定是否加载配置。 - 使用位置:通常在自动配置类中用于条件加载 Bean。例如:
@ConditionalOnProperty(prefix = "controller.log", name = "enabled", havingValue = "true", matchIfMissing = true) public class LogAutoConfiguration { // 当配置项 `controller.log.enabled=true` 时,自动加载此配置 }
1
2
3
4- 作用:用于控制自动配置是否启用。可以通过配置项(如
@Around
:- 作用:在 AOP 中定义环绕通知,拦截目标方法执行前后执行自定义逻辑。
- 使用位置:在切面类中,用于拦截方法并实现日志记录。例如:
@Around("execution(* com.example..*Controller.*(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { // 日志记录逻辑 return joinPoint.proceed(); }
1
2
3
4
5ProceedingJoinPoint
:- 作用:在 AOP 环绕通知中,用于调用被拦截的方法并获取其执行结果。
- 使用位置:在切面方法中,用于执行原方法并获取返回值。例如:
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { Object result = joinPoint.proceed(); // 调用原方法 return result; }
1
2
3
4RequestLogLevelEnum
:- 作用:定义日志输出的不同级别(如
BASIC
、HEADERS
、BODY
),用于控制日志的详细程度。 - 使用位置:在切面类中,根据配置选择日志输出的详细级别。例如:
if (RequestLogLevelEnum.BODY.lte(properties.getLevel())) { // 输出详细日志 }
1
2
3- 作用:定义日志输出的不同级别(如
总结
- 在 Spring Boot 2.x 中,自动配置依赖
@Configuration
注解和spring.factories
文件。 - 在 Spring Boot 3.x 中,自动配置简化为使用
@AutoConfiguration
注解和AutoConfiguration.imports
文件。 - 需要手动创建的文件:
- Spring Boot 2.x:
META-INF/spring.factories
- Spring Boot 3.x:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
- Spring Boot 2.x:
- 这些配置确保了日志切面在应用启动时自动生效,简化了开发和配置流程。
# 6. 日志拦截使用流程
在完成上述配置后,你可以在 Spring Boot 应用中轻松启用请求日志拦截。以下是如何使用该日志拦截器的详细步骤:
# 1. 添加配置文件
在 application.yml
或 application.properties
中添加相关配置,控制日志功能的启用和日志级别:
application.yml
示例:
controller:
log:
enabled: true # 是否启用日志拦截
level: BODY # 日志级别,可选值为 NONE, BASIC, HEADERS, BODY
2
3
4
application.properties
示例:
controller.log.enabled=true
controller.log.level=BODY
2
controller.log.enabled
: 控制日志功能的总开关。controller.log.level
: 控制日志输出的详细程度。
# 2. 日志级别说明
NONE
: 不记录任何日志。BASIC
: 记录基本信息,如请求方法、路径和响应状态。HEADERS
: 在BASIC
的基础上,记录请求和响应头信息。BODY
: 在HEADERS
的基础上,记录请求体和响应体,适合调试和分析详细的请求内容。
# 3. 启动应用
在启动应用时,确保配置文件中的设置已经生效。当应用运行后,每次收到请求时,日志切面都会按照配置记录相关日志。
# 4. 查看日志输出
当应用接收到 HTTP 请求时,日志将会按照配置的日志级别输出到控制台或指定的日志文件中。以下是 BODY
级别日志输出的一个示例:
================ 请求开始 ================
请求方法: GET | 请求路径: /user/data
请求参数: {"size":10,"page":1}
请求头: x-forwarded-host: localhost:8080
请求头: token: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzcG9uZ2Vib2JfMl8wIiwiaWF0IjoxNzI0NzA2ODI2LCJleHAiOjE3MjQ3MTA0MjZ9.KCvnWWtlNXVrhdZf58yKvTApBR9in_RfbVlFmqeSdD4CO9vzw2jmknKG3dSPWnPDiYjOZqUuHEdV5ZburzF52A
// 此处省略....
=============== 请求结束 ================
================ 响应开始 ================
响应结果: {"code":200,"message":"操作成功","data":{"total":3,"data":[{"id":1,"username":"scholar","email":"55434818@qq.com","status":1,"avatar":"https://web-183.oss-cn-beijing.aliyuncs.com/typora/202408251033134.jpg","createTime":"2024-08-25 14:39:59"},{"id":6,"username":"Admin","email":"wuyimin183@163.com","status":1,"avatar":"https://foruda.gitee.com/avatar/1683819079997905854/11622596_spongebob_2_0_1683819079.png","createTime":"2024-08-26 04:23:40"}
// 此处省略...
耗时: 106 ms | 请求方法: GET | 请求路径: /user/data
=============== 响应结束 ================
2
3
4
5
6
7
8
9
10
11
12
13
通过这些日志信息,开发人员可以方便地追踪请求的全过程,包括参数、头信息以及响应结果。
# 7.日志拦截进阶版实现过程总结
在 Spring Boot 项目中实现日志拦截功能,需要遵循以下步骤,这些步骤环环相扣,通过多个模块的组合,最终实现了完整的日志拦截与记录功能。
# 1. 引入依赖
在开始日志拦截的实现之前,需要先在项目中引入一些必要的依赖:
spring-boot-starter-aop
:用于实现 AOP 功能。AOP 是日志拦截的核心技术,通过它可以在方法执行前后插入自定义逻辑。jackson-databind
:提供对象到 JSON 的序列化和反序列化功能。这个依赖在日志中用于将请求和响应内容转换成 JSON 格式,方便记录日志。lombok
:通过注解生成常见的代码(如 Getter、Setter、构造器等),简化开发过程。
这些依赖为日志拦截提供了基础支持,确保日志功能的正常运行。
# 2. 日志级别控制与配置管理
日志功能的一个重要特点是可以根据需求调整日志输出的详细程度。为了实现这一点,定义了一个日志级别的枚举类 RequestLogLevelEnum
,并创建了配置类 RequestLogProperties
,用以管理和读取日志相关配置。
RequestLogLevelEnum
类:定义了不同的日志级别,包括NONE
、BASIC
、HEADERS
和BODY
。这些级别分别控制日志的详细程度,从不记录到记录完整的请求和响应内容。public enum RequestLogLevelEnum { NONE(0), BASIC(1), HEADERS(2), BODY(3); private final int level; public static final String REQ_LOG_PROPS_PREFIX = "controller.log"; public boolean lte(RequestLogLevelEnum level) { return this.level <= level.level; } }
1
2
3
4
5
6
7
8
9
10RequestLogProperties
类:这是一个配置类,绑定了外部配置文件中的日志配置项。通过@ConfigurationProperties(prefix = RequestLogLevelEnum.REQ_LOG_PROPS_PREFIX)
注解,Spring Boot 会自动读取配置文件中controller.log
开头的配置项。然后 将配置文件中以
controller.log
为前缀的配置项映射到RequestLogProperties
类的对应属性上,这些配置项会自动映射到RequestLogProperties
类中的level
和enabled
属性中。@ConfigurationProperties(prefix = RequestLogLevelEnum.REQ_LOG_PROPS_PREFIX) public class RequestLogProperties { private RequestLogLevelEnum level = RequestLogLevelEnum.BODY; // 默认日志级别为 BODY private boolean enabled = true; // 控制日志功能是否启用 }
1
2
3
4
5通过该配置类,日志功能的开启、日志级别的调整都可以通过配置文件灵活控制,例如在
application.yml
中:controller: log: enabled: true level: BODY
1
2
3
4
RequestLogLevelEnum
的映射原理:
level
是一个枚举类型(RequestLogLevelEnum
),Spring Boot 在读取配置文件时,会尝试将配置项的值(如BODY
)转换为RequestLogLevelEnum
枚举中的相应值。- 当配置文件中指定
controller.log.level: BODY
时,Spring Boot 会将这个字符串BODY
自动解析为RequestLogLevelEnum.BODY
,并赋值给RequestLogProperties
类的level
属性。
Spring Boot 如何进行转换:
- Spring Boot 内置了枚举类型的转换器,当配置项的值是枚举名称(如
BODY
、BASIC
等)时,Spring 会自动将这些值转换为对应的枚举实例。 - 转换过程是通过调用枚举类的
valueOf
方法实现的。即当配置文件中出现controller.log.level: BODY
时,Spring Boot 会执行RequestLogLevelEnum.valueOf("BODY")
,将字符串BODY
转换为枚举实例RequestLogLevelEnum.BODY
。
配置值的注入:
- 当 Spring Boot 应用启动时,Spring 会自动扫描带有
@ConfigurationProperties
注解的类,并根据配置文件中的配置项将值注入到这些类的属性中。 - 因此,
RequestLogProperties
类中的level
属性会被自动赋值为RequestLogLevelEnum.BODY
,而enabled
属性则会根据配置文件中的值赋值为true
或false
。
# 3. 日志拦截的核心逻辑实现
日志拦截的核心逻辑通过 AOP 切面实现,切面类 RequestLogAspect
扮演了日志记录的主角。
切面拦截:通过
@Around
注解和ProceedingJoinPoint
,拦截所有 Controller 层的请求。根据日志配置的不同级别,在方法执行前后记录请求和响应信息。@Around("execution(* com.scholar.springbootscaffolding..*(..)) && (@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController))") public Object aroundApi(ProceedingJoinPoint point) throws Throwable { // 根据配置的日志级别进行控制 if (RequestLogLevelEnum.NONE == properties.getLevel()) { return point.proceed(); // 如果日志级别为 NONE,直接执行方法 } // 记录请求信息 logRequest(point); // 执行方法并记录响应信息 return logResponse(point); }
1
2
3
4
5
6
7
8
9
10
11
12
13请求信息记录:在方法执行前,记录请求路径、方法、请求参数、请求头等信息。这些信息通过
HttpServletRequest
和切面拦截的参数获取。响应信息记录:在方法执行后,根据日志级别,记录返回的响应内容。响应内容通过 Jackson 序列化为 JSON 格式,以便记录到日志中。
# 4. 自动配置的实现
自动配置是 Spring Boot 的一大特点,能够让功能在不需要显式配置的情况下自动生效。为了实现日志功能的自动配置,分为以下几个步骤:
LogAutoConfiguration
类:这个类是自动配置的核心,使用@Configuration
(Spring Boot 2.x)或@AutoConfiguration
(Spring Boot 3.x)注解标记。该类注册了RequestLogAspect
和RequestLogProperties
,确保在应用启动时,日志拦截功能可以自动生效。@AutoConfiguration @EnableConfigurationProperties(RequestLogProperties.class) public class LogAutoConfiguration { // 在这里定义自动装配的 Bean }
1
2
3
4
5配置文件注册:为了让 Spring Boot 知道这个自动配置类的存在,在 Spring Boot 2.x 中,需要在
spring.factories
中注册该类;在 Spring Boot 3.x 中,则需要在AutoConfiguration.imports
中注册。Spring Boot 2.x:
文件路径:
src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.demo.config.LogAutoConfiguration
1
2Spring Boot 3.x:
文件路径:
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.demo.config.LogAutoConfiguration
1
这一步确保了在应用启动时,Spring Boot 会自动加载并配置日志拦截功能,无需手动配置,便于在项目中灵活应用。
# 5. 总结流程的逻辑关联
整个实现流程紧密关联,逐步递进:
- 依赖引入 是日志拦截功能实现的基础。
- 日志级别的控制 是功能的核心,决定了日志输出的详细程度。
- 日志配置的读取 是实现灵活控制的关键,配合 AOP 切面实现了自动记录请求和响应。
- 自动配置 则确保了日志功能的自动生效,提升了开发和部署的便捷性。
这种环环相扣的实现方式,确保了项目中所有 HTTP 请求和响应都能被有效监控和记录,同时具备良好的扩展性和可配置性。
# 8. 添加请求过滤器
增强这个功能的目的就是因为我发现有的时候经常被拦截器拦住,可能是token出现问题或者其它原因,抓包又比较麻烦,所以干脆添加一个过滤器,默认是关闭的,它的功能是跟拦截Controller层的功能是重叠的,所以一般在排查问题的时候可以开启。
# 1. 在日志配置类中添加过滤器开关
首先,修改 RequestLogProperties
类,增加一个用于控制过滤器启用的配置项。
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Getter
@Setter
@ConfigurationProperties(prefix = RequestLogLevelEnum.REQ_LOG_PROPS_PREFIX)
public class RequestLogProperties {
private RequestLogLevelEnum level = RequestLogLevelEnum.BODY; // 默认日志级别为 BODY
private boolean enabled = true; // 控制日志功能是否启用
private boolean filterEnabled = false; // 控制过滤器是否启用,默认为关闭
}
2
3
4
5
6
7
8
9
10
11
12
# 2. 创建过滤器类
接下来,创建一个过滤器类 RequestLogFilter
,该过滤器将根据配置的开关来决定是否拦截请求并记录日志。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.scholar.springbootscaffolding.config.RequestLogProperties;
import com.scholar.springbootscaffolding.enums.RequestLogLevelEnum;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Objects;
@Slf4j
@WebFilter(filterName = "requestLogFilter", urlPatterns = "/*")
@AllArgsConstructor
public class RequestLogFilter implements Filter {
private final RequestLogProperties properties;
private final ObjectMapper objectMapper;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化方法,可以在这里做一些初始化工作
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!properties.isFilterEnabled()) {
// 如果过滤器开关未启用,直接放行请求
chain.doFilter(request, response);
return;
}
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String requestUrl = httpServletRequest.getRequestURI();
String requestMethod = httpServletRequest.getMethod();
// 记录请求信息
StringBuilder reqLog = new StringBuilder(300);
reqLog.append("\n\n================ 过滤器请求开始 ================\n");
reqLog.append("请求方法: {} | 请求路径: {}\n");
log.info(reqLog.toString(), requestMethod, requestUrl);
// 记录请求参数
logRequestParameters(httpServletRequest, reqLog);
// 记录请求头信息
logRequestHeaders(httpServletRequest, reqLog);
reqLog.append("=============== 过滤器请求结束 ================\n");
log.info(reqLog.toString());
// 放行请求
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 销毁方法,可以在这里做一些清理工作
}
// 记录请求参数
private void logRequestParameters(HttpServletRequest request, StringBuilder reqLog) throws IOException {
Enumeration<String> parameterNames = request.getParameterNames();
if (parameterNames.hasMoreElements()) {
reqLog.append("请求参数: \n");
while (parameterNames.hasMoreElements()) {
String paramName = parameterNames.nextElement();
String paramValue = request.getParameter(paramName);
reqLog.append(paramName).append(": ").append(paramValue).append("\n");
}
}
}
// 记录请求头信息
private void logRequestHeaders(HttpServletRequest request, StringBuilder reqLog) {
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames.hasMoreElements()) {
reqLog.append("请求头: \n");
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
reqLog.append(headerName).append(": ").append(headerValue).append("\n");
}
}
}
}
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
# 3. 修改自动配置类
确保 RequestLogFilter
过滤器能够根据配置自动加载。修改 LogAutoConfiguration
类,注册过滤器。
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class LogAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = RequestLogLevelEnum.REQ_LOG_PROPS_PREFIX, name = "filterEnabled", havingValue = "true")
public FilterRegistrationBean<RequestLogFilter> requestLogFilter(RequestLogProperties properties, ObjectMapper objectMapper) {
FilterRegistrationBean<RequestLogFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new RequestLogFilter(properties, objectMapper));
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 4. 配置文件示例
最后,在 application.yml
或 application.properties
中添加配置项来控制过滤器的启用:
controller:
log:
enabled: true
level: BODY
filterEnabled: true # 启用过滤器
2
3
4
5