上一篇:Spring Boot 3.x Web单元测试最佳实践
下一篇:Spring Boot 3.x Web MVC实战:实现流缓存的request
前面我们在《Spring Boot 3.x Rest API最佳实践之统一响应结构》中学习响应的统一拦截处理,顺带完成了响应结果的记录;而对于请求内容咱们也必须进行日志记录,以确保排查问题时有据可循。为此,本小节咱们利用filter
组件来实现这一需求。如果觉得对你有帮助,记得点赞收藏,关注小卷,后续更精彩!
文章目录
filter开发
因为统一请求日志记录属于基础功能,我们在 com.juan.demo.common
基础包下来创建,以便后续可以将其抽出来做成一个common
子模块进行维护。
这里我们从OncePerRequestFilter
继承,这样可以确保一个请求内部无论经过多少次转发,始终会被拦截一次。
java
package com.juan.demo.common.web.filter;
import ...
@Slf4j
public class RequestLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("============ {}:{}", request.getMethod(), request.getRequestURL());
filterChain.doFilter(request, response);
}
}
这里我们先做最简单的实现:仅打印下请求方法和请求路径,然后继续将请求放行。
filter配置
在com.juan.demo.common.web
包下创建一个@Configuration
修饰的配置类,在内部通过@Bean
修饰的方法来创建相关bean
,包括了filter
本身的bean
和用于filter bean
注册和配置的FilterRegistrationBean
类型的bean
。
java
package com.juan.demo.common.web;
import ...
@Configuration
public class WebConfig {
@Bean
public RequestLogFilter requestLogFilter() {
// new一个filter bean实例
return new RequestLogFilter();
}
@Bean
public FilterRegistrationBean<RequestLogFilter> requestLogFilterRegistrationBean() {
FilterRegistrationBean<RequestLogFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(requestLogFilter());
registrationBean.setName("requestLogFilter");
registrationBean.addUrlPatterns("/*"); // todo 暂时对所有的请求拦截
registrationBean.setOrder(1);
return registrationBean;
}
}
运行先前开发的web
单元测试用例,看到请求被RequestLogFilter
拦截到了:
实现logParams方法
这里主要考察request.getParameterMap()
方法的使用。注意,它返回的value是一个字符串数组,一个参数key可以对应多个value,在输出日志时,用了org.apache.commons:commons-lang3
依赖中的StringUtils
工具类的join(...)
方法实现数组内容的拼接。
java
package com.juan.demo.common.web.filter;
import ...
@Slf4j
public class RequestLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("============ {}:{}", request.getMethod(), request.getRequestURL());
logParams(request);
filterChain.doFilter(request, response);
}
private void logParams(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
int size = parameterMap.size();
if (size == 0) return;
StringBuilder paramBuilder = new StringBuilder();
Set<String> paramNames = parameterMap.keySet();
int index = 0;
for (String paramName : paramNames) {
paramBuilder.append(paramName).append("=").append(StringUtils.join(parameterMap.get(paramName), ","));
if (index < size - 1) {
paramBuilder.append(", ");
}
index++;
}
log.info("============ params:{}", paramBuilder);
}
}
测试结果:
同样,客户端提交的formData
形式的数据,也能被拦截输出:
实现logRequestBody方法
实现logRequestBody
方法,逻辑是:判断请求内容类型为json
格式时,就借助objectMapper
的readTree
方法,传入request.getInputStream()
方法获取的流信息,从而读取到树形的JsonNode
信息,再序列化为json
信息。
java
package com.juan.demo.common.web.filter;
import ...
@Slf4j
public class RequestLogFilter extends OncePerRequestFilter {
@Resource
private ObjectMapper objectMapper;
@Override
protected void doFilterInternal(...) ... {
...
logRequestBody(request);
filterChain.doFilter(request, response);
}
...
@SneakyThrows
private void logRequestBody(HttpServletRequest request) {
if (checkContentType(request, MediaType.APPLICATION_JSON_VALUE)) {
log.info("============ body:{}", objectMapper.readTree(request.getInputStream()));
}
}
private boolean checkContentType(HttpServletRequest request, String contentType) {
return StringUtils.startsWith(request.getContentType(), contentType);
}
}
测试下添加购物车,发现报了400
的http
状态码,并提示错误信息:
再看看日志,并没有抛出异常堆栈信息:
完善CustomErrorAttributes异常输出
在CustomErrorAttributes
中加一段逻辑:
再测试就发现控制台抛出的错误了:
body信息确实打印到日志了,但是spring web
模块在controller
层进行请求信息转换成对象时,框架内部会再次调用request.getInputStream()
就报出这样的错误了。
存在的问题:流不可重复读
很显然,这里无法两次调用request.getInputStream()
,存在着流不可重复读的问题。下一小节我们将巧妙的通过流信息缓存来解决这个问题,大家加油!