上一篇:Spring Boot 3.x Web单元测试最佳实践
下一篇:Spring Boot 3.x Web MVC实战:实现流缓存的request
前面我们在《Spring Boot 3.x Rest API最佳实践之统一响应结构》中学习响应的统一拦截处理,顺带完成了响应结果的记录;而对于请求内容咱们也必须进行日志记录,以确保排查问题时有据可循。为此,本小节咱们利用filter
组件来实现这一需求。如果觉得对你有帮助,记得点赞收藏,关注小卷,后续更精彩!
文章目录
- filter开发
- filter配置
- 实现logParams方法
- 实现logRequestBody方法
- 完善CustomErrorAttributes异常输出
- 存在的问题:流不可重复读
filter开发
因为统一请求日志记录属于基础功能,我们在 com.juan.demo.common
基础包下来创建,以便后续可以将其抽出来做成一个common
子模块进行维护。
这里我们从OncePerRequestFilter
继承,这样可以确保一个请求内部无论经过多少次转发,始终会被拦截一次。
package com.juan.demo.common.web.filter;import ...@Slf4j
public class RequestLogFilter extends OncePerRequestFilter {@Overrideprotected 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
。
package com.juan.demo.common.web;import ...@Configuration
public class WebConfig {@Beanpublic RequestLogFilter requestLogFilter() {// new一个filter bean实例return new RequestLogFilter();}@Beanpublic 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(...)
方法实现数组内容的拼接。
package com.juan.demo.common.web.filter;import ...@Slf4j
public class RequestLogFilter extends OncePerRequestFilter {@Overrideprotected 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
信息。
package com.juan.demo.common.web.filter;import ...@Slf4j
public class RequestLogFilter extends OncePerRequestFilter {@Resourceprivate ObjectMapper objectMapper;@Overrideprotected void doFilterInternal(...) ... {...logRequestBody(request);filterChain.doFilter(request, response);}...@SneakyThrowsprivate 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()
,存在着流不可重复读的问题。下一小节我们将巧妙的通过流信息缓存来解决这个问题,大家加油!