知识 - 请求日志输出
# 前言
在 Spring Boot 应用中,实现接口请求日志记录功能,能够记录包括请求方法、接口路径及请求参数等核心信息,并提供灵活的开关配置。
实现逻辑是利用 AOP 切面在 Controller 进行切入,获取请求的数据并打印处理。
# 实现
# 依赖
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 枚举
@Getter
@RequiredArgsConstructor
public enum RequestLogLevelEnum {
/**
* No logs.
*/
NONE(0),
/**
* Logs request and response lines.
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1 (3-byte body)
*
* <-- 200 OK (22ms, 6-byte body)
* }</pre>
*/
BASIC(1),
/**
* Logs request and response lines and their respective headers.
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
* <-- END HTTP
* }</pre>
*/
HEADERS(2),
/**
* Logs request and response lines and their respective headers and bodies (if present).
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
*
* Hi?
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
*
* Hello!
* <-- END HTTP
* }</pre>
*/
BODY(3);
/**
* 级别
*/
private final int level;
/**
* 请求日志配置前缀
*/
public static final String REQ_LOG_PROPS_PREFIX = "controller.log";
/**
* 控制台日志是否启用
*/
public static final String CONSOLE_LOG_ENABLED_PROP = "controller.log.console.enabled";
/**
* 当前版本 小于和等于 比较的版本
*
* @param level LogLevel
* @return 是否小于和等于
*/
public boolean lte(RequestLogLevelEnum level) {
return this.level <= level.level;
}
}
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
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
# 配置项
@Getter
@Setter
@ConfigurationProperties(value = RequestLogLevelEnum.REQ_LOG_PROPS_PREFIX)
public class RequestLogProperties {
/**
* 日志级别配置,默认:BODY
*/
private RequestLogLevelEnum level = RequestLogLevelEnum.BODY;
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 切面
@Slf4j
@Aspect
@Configuration
@AllArgsConstructor
@ConditionalOnClass(LogAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(value = RequestLogLevelEnum.REQ_LOG_PROPS_PREFIX + ".enabled", havingValue = "true", matchIfMissing = true)
public class RequestLogAspect {
private final RequestLogProperties properties;
/**
* AOP 环切 控制器 R 返回值
* Response:响应类
*
* @param point JoinPoint
* @return Object
* @throws Throwable 异常
*/
@Around(
"execution(!static cn.youngkbt.core.http.Response *(..)) && " +
"(@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();
}
HttpServletRequest request = WebUtil.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================ Request Start ================\n");
// 打印路由
beforeReqLog.append("===> {}: {}");
beforeReqArgs.add(requestMethod);
beforeReqArgs.add(requestUrl);
// 打印请求参数
logIngArgs(point, beforeReqLog, beforeReqArgs);
// 打印请求 headers
logIngHeaders(request, level, beforeReqLog, beforeReqArgs);
beforeReqLog.append("================ Request End ================\n");
// 打印执行时间
long startNs = System.nanoTime();
log.info(beforeReqLog.toString(), beforeReqArgs.toArray());
// aop 执行后的日志
StringBuilder afterReqLog = new StringBuilder(200);
// 日志参数
List<Object> afterReqArgs = new ArrayList<>();
afterReqLog.append("\n\n================ Response Start ================\n");
try {
Object result = point.proceed();
// 打印返回结构体
if (RequestLogLevelEnum.BODY.lte(level)) {
afterReqLog.append("===Result=== {}\n");
afterReqArgs.add(JacksonUtil.toJsonStr(result));
}
return result;
} finally {
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
afterReqLog.append("<=== {}: {} ({} ms)\n");
afterReqArgs.add(requestMethod);
afterReqArgs.add(requestUrl);
afterReqArgs.add(tookMs);
afterReqLog.append("================ Response End ================\n");
log.info(afterReqLog.toString(), afterReqArgs.toArray());
}
}
/**
* 激励请求参数
*
* @param point ProceedingJoinPoint
* @param beforeReqLog StringBuilder
* @param beforeReqArgs beforeReqArgs
*/
public void logIngArgs(ProceedingJoinPoint point, StringBuilder beforeReqLog, List<Object> beforeReqArgs) {
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
Object[] args = point.getArgs();
// 请求参数处理
final Map<String, Object> paraMap = new HashMap<>(16);
// 一次请求只能有一个 request body
Object requestBodyValue = null;
for (int i = 0; i < args.length; i++) {
// 读取方法参数
MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
// PathVariable 参数跳过
PathVariable pathVariable = methodParam.getParameterAnnotation(PathVariable.class);
if (pathVariable != null) {
continue;
}
RequestBody requestBody = methodParam.getParameterAnnotation(RequestBody.class);
String parameterName = methodParam.getParameterName();
Object value = args[i];
// 如果是body的json则是对象
if (requestBody != null) {
requestBodyValue = value;
continue;
}
// 处理 参数
if (value instanceof HttpServletRequest) {
paraMap.putAll(((HttpServletRequest) value).getParameterMap());
continue;
} else if (value instanceof WebRequest) {
paraMap.putAll(((WebRequest) value).getParameterMap());
continue;
} else if (value instanceof HttpServletResponse) {
continue;
} else if (value instanceof MultipartFile) {
MultipartFile multipartFile = (MultipartFile) value;
String name = multipartFile.getName();
String fileName = multipartFile.getOriginalFilename();
paraMap.put(name, fileName);
continue;
} else if (value instanceof List) {
List<?> list = (List<?>) value;
AtomicBoolean isSkip = new AtomicBoolean(false);
for (Object o : list) {
if ("StandardMultipartFile".equalsIgnoreCase(o.getClass().getSimpleName())) {
isSkip.set(true);
break;
}
}
if (isSkip.get()) {
paraMap.put(parameterName, "此参数不能序列化为json");
continue;
}
}
// 参数名
RequestParam requestParam = methodParam.getParameterAnnotation(RequestParam.class);
String paraName = parameterName;
if (requestParam != null && StringUtil.hasText(requestParam.value())) {
paraName = requestParam.value();
}
if (value == null) {
paraMap.put(paraName, null);
} else if (ClassUtil.isPrimitiveOrWrapper(value.getClass())) {
paraMap.put(paraName, value);
} else if (value instanceof InputStream) {
paraMap.put(paraName, "InputStream");
} else if (value instanceof InputStreamSource) {
paraMap.put(paraName, "InputStreamSource");
} else if (JacksonUtil.canSerialize(value)) {
// 判断模型能被 json 序列化,则添加
paraMap.put(paraName, value);
} else {
paraMap.put(paraName, "此参数不能序列化为json");
}
}
// 请求参数
if (paraMap.isEmpty()) {
beforeReqLog.append("\n");
} else {
beforeReqLog.append(" Parameters: {}\n");
beforeReqArgs.add(JacksonUtil.toJsonStr(paraMap));
}
if (requestBodyValue != null) {
beforeReqLog.append("====Body===== {}\n");
beforeReqArgs.add(JacksonUtil.toJsonStr(requestBodyValue));
}
}
/**
* 记录请求头
*
* @param request HttpServletRequest
* @param level 日志级别
* @param beforeReqLog StringBuilder
* @param beforeReqArgs beforeReqArgs
*/
public void logIngHeaders(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("===Headers=== {}: {}\n");
beforeReqArgs.add(headerName);
beforeReqArgs.add(headerValue);
}
}
}
}
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
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# 容器装配
主要加载 RequestLogProperties 配置类。
@AutoConfiguration
@EnableConfigurationProperties({RequestLogProperties.class})
public class LogAutoConfiguration {
}
1
2
3
4
5
2
3
4
5
Spring Boot 3.x 需要在 resource 下建立 META-INF/spring
路径,然后创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件,内容为
cn.youngkbt.log.config.LogAutoConfiguration
1
这样 Spring 会自动扫描该文件的容器装配类,将里面涉及的类注入到 Spring 容器。
编辑此页 (opens new window)
更新时间: 2024/06/15, 16:39:27