SpringMVC打印请求参数和响应数据最优方案
作者:brucelwl
项目中经常需要打印http请求的参数和响应数据, 但会出现如下问题:
- 打印Request Body参数: 请求
content-type
是application/josn
格式,如果直接从HttpServletRequest
中获取输入流, 解析请求数据, 会导致SpringMVC后续的请求异常, 读取输入流异常. - 打印响应数据: 如果直接从
HttpServletResponse
中获取输出流, 解析响应数据, 会导致Web服务器(Tomcat)后续响应请求异常.
本文讲解如何在SpringBoot中使用最优方案实现该功能.
常见方案(非最优方案)
常见方案就是通过实现javax.servlet.Filter
对HttpServletRequest
和HttpServletResponse
进行包装, 将输入/输出流复制一份, 转成字符串打印, 并且不影响后续处理.而且SpringMVC已经为我们提供了相应的包装类实现
org.springframework.web.util.ContentCachingRequestWrapper
org.springframework.web.util.ContentCachingResponseWrapper
但在使用ContentCachingResponseWrapper
时, 一定要记住必须调用ContentCachingResponseWrapper#copyBodyToResponse()
将响应数据写回HttpServletResponse
的ServletOutputStream
,这样才能成功返回数据到客户端。
注意: 这个并不是最优方案,并且存在诸多缺点
示例代码如下:
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { if (isStream(request)) { filterChain.doFilter(request, response); return; } boolean isFirstRequest = !isAsyncDispatch(request); HttpServletRequest requestToUse = request; ContentCachingResponseWrapper responseToUse = new ContentCachingResponseWrapper(response); if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) { requestToUse = new ContentCachingRequestWrapper(request); } try { filterChain.doFilter(requestToUse, responseToUse); } finally { accessLog(requestToUse, responseToUse); responseToUse.copyBodyToResponse(); } } private boolean isStream(HttpServletRequest request) { return MediaType.TEXT_EVENT_STREAM_VALUE.equalsIgnoreCase(request.getHeader(HttpHeaders.ACCEPT)) || MediaType.TEXT_EVENT_STREAM_VALUE.equalsIgnoreCase(request.getHeader(HttpHeaders.CONTENT_TYPE)); }
这个方案存在如下几个缺点:
- 使用包装类包装
HttpServletRequest
和HttpServletResponse
会导致输入流和输出流被多拷贝一次 - 并不是所有的请求类型都适合对
HttpServletResponse
包装, 即使用Spring提供的ContentCachingResponseWrapper
也无法实现, 例如SpringMVC所支持的SSE
(org.springframework.web.servlet.mvc.method.annotation.SseEmitter
)请求,会导致无法向客户端相应数据. 需要对text/event-stream
请求做特殊处理.
SpringWeb 5.1.14版本中ShallowEtagHeaderFilter#disableContentCaching
方法的注释中已经说明
/** * This method can be used to disable the content caching response wrapper * of the ShallowEtagHeaderFilter. This can be done before the start of HTTP * streaming for example where the response will be written to asynchronously * and not in the context of a Servlet container thread. * @since 4.2 */ public static void disableContentCaching(ServletRequest request) { Assert.notNull(request, "ServletRequest must not be null"); request.setAttribute(STREAMING_ATTRIBUTE, true); }
不方便获取Request Body对应的实体类类型, 不方便知道响应请求的实体类类型
最优方案
实际上SpringMVC提供了如下两个接口,用于在解析Request Body后和响应请求前回调
org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice
RequestBodyAdvice用于解析Request Body后回调, 可以实现该接口得到解析后的Body实体类,RequestBodyAdviceAdapter适配了RequestBodyAdviceorg.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
ResponseBodyAdvice用于在向http请求响应数据之前回调, 可以实现该接口得到响应的实体类
为了能够一块打印请求和响应数据, 必须在请求时记录Request Body对象, 这里就考虑使用HttpServletRequest
的setAttribute
记录,但是RequestBodyAdvice
中无法拿到HttpServletRequest
,因此需要在请求时通过ThreadLocal
绑定. 见示例ThreadBindRequestContainer
@Slf4j @ControllerAdvice public class RequestResponseBodyAdvice extends RequestBodyAdviceAdapter implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { HttpServletRequest request = ThreadBindRequestContainer.getServletRequest(); Method method = parameter.getMethod(); String declaringClass = method.getDeclaringClass().getSimpleName(); String handlerMethod = method.toString().substring(method.toString().indexOf(declaringClass)); request.setAttribute("handlerMethod", handlerMethod); request.setAttribute("req_body", body); return body; } @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); servletRequest.setAttribute("resp_body", body); return body; } }
public class ThreadBindRequestContainer { private static final ThreadLocal<HttpServletRequest> threadLocal = new ThreadLocal<>(); public static void bind(HttpServletRequest request) { threadLocal.set(request); } public static void remove() { threadLocal.remove(); } public static HttpServletRequest getServletRequest() { return threadLocal.get(); } }
@Slf4j public class RequestResponseBodyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ThreadBindRequestContainer.bind(request); log.info("请求线程" + Thread.currentThread().getName()); return HandlerInterceptor.super.preHandle(request, response, handler); } @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse response, Object handler, Exception ex) throws Exception { ThreadBindRequestContainer.remove(); String requestURI = req.getRequestURI(); Object req_body = req.getAttribute("req_body"); Object resp_body = req.getAttribute("resp_body"); Object handlerMethod = req.getAttribute("handlerMethod"); log.info("请求url:{},处理方法:{}, 参数:{}, 响应参数:{}", requestURI, handlerMethod, req_body, resp_body); log.info("退出线程" + Thread.currentThread().getName() + "\n"); } }
到此这篇关于SpringMVC打印请求参数和响应数据最优方案的文章就介绍到这了,更多相关SpringMVC打印请求参数和响应数据内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!