代码来咯,看代码多是一件美事呦,你他奶奶的为什么不看代码,给我看。

工作的时候,尤其是接口联调的时候,打印http请求记录是甩锅和解决问题的最好的方式。尤其是请求体里的内容。就是这个获取请求体的内容其实还有点门道,我就被坑过,你直接在拦截器里直接获取request里面的body,拦截器里能获取到,Controller里面就获取不到了。具体原因不深究,就是Body是用流传的,你读取了之后这个流就被消耗了,然后后面的代码再从request里面就读取不到了。

引入spring-boot-starter-actuator

1
2
3
4
5
<!-- 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

引入之后,配置存储方式httpTraceRepository,这个实例就是添加在内存中存储最近100次请求记录

1
2
3
4
5
6
7
@Configuration
public class ConfigTrace {
@Bean
public HttpTraceRepository httpTraceRepository(){
return new InMemoryHttpTraceRepository();
}
}

最后在application.properties中加入

1
management.endpoints.web.exposure.include=*

运行项目访问http://localhost:8080/actuator/httptrace

得到这样的JSON数据(已经格式化),

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
{
"traces": [
{
"timestamp": "2022-02-19T05:08:05.370267100Z",
"principal": null,
"session": null,
"request": {
"method": "GET",
"uri": "http://localhost:8080/index?a=%E4%BB%8E%E5%89%8D%E4%BB%8E%E5%89%8D&b=%E6%9C%89%E4%B8%AA%E4%BA%BA%E7%88%B1%E4%BD%A0%E5%BE%88%E4%B9%85",
"headers": {
"host": [
"localhost:8080"
],
"content-type": [
"application/x-www-form-urlencoded"
],
"connection": [
"Keep-Alive"
],
"accept-encoding": [
"gzip,deflate"
],
"user-agent": [
"Apache-HttpClient/4.5.13 (Java/11.0.12)"
]
},
"remoteAddress": null
},
"response": {
"status": 200,
"headers": {
"Keep-Alive": [
"timeout=60"
],
"Connection": [
"keep-alive"
],
"Content-Length": [
"35"
],
"Date": [
"Sat, 19 Feb 2022 05:08:05 GMT"
],
"Content-Type": [
"text/plain;charset=UTF-8"
]
}
},
"timeTaken": 120
}
]
}

引入这个依赖的好处,就是几乎不需要写任何代码,引入,然后配置一下即可查看,缺点也有就是不能记录post请求的请求体,也无法记录返回值,持久化,目前我没有研究,但是我感觉可以。可以自行查阅资料研究。

使用springmvc自带的一个类

增加一个配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean
CommonsRequestLoggingFilter loggingFilter(){
CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
// 记录 客户端 IP信息
loggingFilter.setIncludeClientInfo(true);
// 记录请求头
loggingFilter.setIncludeHeaders(true);
// 如果记录请求头的话,可以指定哪些记录,哪些不记录
// loggingFilter.setHeaderPredicate();
// 记录 请求体 特别是POST请求的body参数
loggingFilter.setIncludePayload(true);
// 请求体的大小限制 默认50
loggingFilter.setMaxPayloadLength(10000);
//记录请求路径中的query参数
loggingFilter.setIncludeQueryString(true);
return loggingFilter;
}

CommonsRequestLoggingFilter可以点进去看一下,就是一个springboot提供得一个类。可以自己进行改造。接下来再配置一下logback就行。配置如下。看不懂或者不理解看这个logback配置详情

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
<?xml version="1.0" encoding="UTF-8" ?>

<configuration>
<!--
输出日志到控制台
-->
<appender name="soutAppender" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%date %level [%thread] %logger{10}.%class{0}#%method[%file:%line] %n%msg%n</pattern>
</layout>
</appender>

<!--
输出日志到文件
-->
<appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<append>true</append>
<prudent>false</prudent>
<File>logs/log.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/%d{yyyy-MM-dd}/log%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 单个日志文件最大体积 -->
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!-- 对日志进行格式化 -->
<pattern>%date %level [%thread] %logger{10}.%class{0}#%method[%file:%line] %n%msg%n</pattern>
<charset>utf-8</charset>
</encoder>
</appender>

<!-- 这个是重点 -->
<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="debug">
</logger>

<root level="info">
<appender-ref ref="soutAppender"/>
<appender-ref ref="fileAppender"/>
</root>
</configuration>

配置好打印的日志应该是这样

1
2
3
4
5
6
7
8
9
2022-02-19 19:02:28,608 DEBUG [http-nio-8080-exec-1] o.s.w.f.CommonsRequestLoggingFilter.CommonsRequestLoggingFilter#beforeRequest[CommonsRequestLoggingFilter.java:47] 
Before request [GET /index?a=%E4%BB%8E%E5%89%8D%E4%BB%8E%E5%89%8D&b=%E6%9C%89%E4%B8%AA%E4%BA%BA%E7%88%B1%E4%BD%A0%E5%BE%88%E4%B9%85, client=127.0.0.1, headers=[host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/11.0.12)", accept-encoding:"gzip,deflate", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"]]
2022-02-19 19:02:28,668 DEBUG [http-nio-8080-exec-1] o.s.w.f.CommonsRequestLoggingFilter.CommonsRequestLoggingFilter#afterRequest[CommonsRequestLoggingFilter.java:55]
After request [GET /index?a=%E4%BB%8E%E5%89%8D%E4%BB%8E%E5%89%8D&b=%E6%9C%89%E4%B8%AA%E4%BA%BA%E7%88%B1%E4%BD%A0%E5%BE%88%E4%B9%85, client=127.0.0.1, headers=[host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/11.0.12)", accept-encoding:"gzip,deflate", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"]]
2022-02-19 19:02:39,737 DEBUG [http-nio-8080-exec-3] o.s.w.f.CommonsRequestLoggingFilter.CommonsRequestLoggingFilter#beforeRequest[CommonsRequestLoggingFilter.java:47]
Before request [POST /save, client=127.0.0.1, headers=[content-length:"14", host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/11.0.12)", accept-encoding:"gzip,deflate", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"]]
2022-02-19 19:02:39,743 DEBUG [http-nio-8080-exec-3] o.s.w.f.CommonsRequestLoggingFilter.CommonsRequestLoggingFilter#afterRequest[CommonsRequestLoggingFilter.java:55]
After request [POST /save, client=127.0.0.1, headers=[content-length:"14", host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/11.0.12)", accept-encoding:"gzip,deflate", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"], payload=a=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C]

payload中得东西为请求体。一次请求会打印两条日志,Before request为过滤器前,After request为过滤器后的记录,这种方式其实已经挺好用了。但是还有个问题就是不能记录返回值,也有方法。

记录返回值

继承ResponseBodyAdvice和使用RestControllerAdvice注解来实现。

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

@Slf4j
@RestControllerAdvice
public class RestBodyAdvice implements ResponseBodyAdvice<Object> {
private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 10000;
public static final String REQUEST_MESSAGE_PREFIX = "Request [";
public static final String REQUEST_MESSAGE_SUFFIX = "]";
private final ObjectMapper objectMapper = new ObjectMapper();


/**
* 顶级异常
* 异常的信息也能捕获到
*/
@ExceptionHandler(Exception.class)
ResponseEntity<Object> handlerException(Exception e,
HttpServletRequest rq, HttpServletResponse rp) {
log.error("顶级异常: {}",e.getMessage());
e.printStackTrace();
return ResponseEntity.ok("系统错误");
}

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}

@SneakyThrows
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {

ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request;

log.debug(createRequestMessage(servletServerHttpRequest.getServletRequest(), REQUEST_MESSAGE_PREFIX, REQUEST_MESSAGE_SUFFIX));
// 这里注意一下,如果body已经是JOSN数据了在进行转JSON处理会报错。
log.debug("Response Body ["+ objectMapper.writeValueAsString(body) +"]");
return body;
}


private boolean checkPrimitive(Object body) {
Class<?> clazz = body.getClass();
return clazz.isPrimitive()
|| clazz.isArray()
|| Collection.class.isAssignableFrom(clazz)
|| body instanceof Number
|| body instanceof Boolean
|| body instanceof Character
|| body instanceof String;
}


protected String createRequestMessage(HttpServletRequest request, String prefix, String suffix) {
StringBuilder msg = new StringBuilder();
msg.append(prefix);
msg.append(request.getMethod()).append(" ");
msg.append(request.getRequestURI());


String queryString = request.getQueryString();
if (queryString != null) {
msg.append('?').append(queryString);
}


String client = request.getRemoteAddr();
if (StringUtils.hasLength(client)) {
msg.append(", client=").append(client);
}
HttpSession session = request.getSession(false);
if (session != null) {
msg.append(", session=").append(session.getId());
}
String user = request.getRemoteUser();
if (user != null) {
msg.append(", user=").append(user);
}

HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
msg.append(", headers=").append(headers);

String payload = getMessagePayload(request);
if (payload != null) {
msg.append(", payload=").append(payload);
}

msg.append(suffix);
return msg.toString();
}

protected String getMessagePayload(HttpServletRequest request) {
ContentCachingRequestWrapper wrapper =
WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
int length = Math.min(buf.length, DEFAULT_MAX_PAYLOAD_LENGTH);
try {
return new String(buf, 0, length, wrapper.getCharacterEncoding());
} catch (UnsupportedEncodingException ex) {
return "[unknown]";
}
}
}
return null;
}
}

然后再配置logback

1
2
3
<logger name="com.example.springboothttptrace.RestBodyAdvice" level="debug">

</logger>

配置一下,输出的日志为

1
2
3
4
2022-02-21 10:19:29,880 DEBUG [http-nio-8080-exec-3] c.e.s.RestBodyAdvice.RestBodyAdvice#beforeBodyWrite[RestBodyAdvice.java:64] 
Request [POST /save, client=127.0.0.1, headers=[content-length:"14", host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/11.0.12)", accept-encoding:"gzip,deflate", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"], payload=a=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C]
2022-02-21 10:19:29,880 DEBUG [http-nio-8080-exec-3] c.e.s.RestBodyAdvice.RestBodyAdvice#beforeBodyWrite[RestBodyAdvice.java:66]
Response Body ["你好世界"]

至此,http的请求记录就全部记录到了,payload中为请求体,请求体和返回内容。自己可以按需使用,开发过程中建议打印全部日志,方便调试,上线后,可以不打印。去掉也很方便,直接在logback配置中配置一下就可以了。

封面

宋仁何
宋仁何