SpringMVC打印请求参数和响应数据最优方案

项目中经常需要打印http请求的参数和响应数据, 但会出现如下问题:

  • 打印Request Body参数: 请求content-typeapplication/josn格式,如果直接从HttpServletRequest中获取输入流, 解析请求数据, 会导致SpringMVC后续对请求的处理异常, 读取输入流异常.
  • 打印响应数据: 如果直接从HttpServletResponse中获取输出流, 解析响应数据, 会导致Web服务器(Tomcat)后续响应请求异常.

本文讲解如何在SpringBoot中使用最优方案实现对RequestBody和响应数据的日志打印.

常见方案(非最优方案)

常见方案就是通过实现javax.servlet.FilterHttpServletRequestHttpServletResponse进行包装, 将输入/输出流复制一份, 转成字符串打印, 并且不影响后续处理.
而且SpringMVC已经为我们提供了相应的包装类实现

  • org.springframework.web.util.ContentCachingRequestWrapper
  • org.springframework.web.util.ContentCachingResponseWrapper

但在使用ContentCachingResponseWrapper时, 一定要记住必须调用ContentCachingResponseWrapper#copyBodyToResponse()将响应数据写回HttpServletResponseServletOutputStream,这样才能成功返回数据到客户端。

注意: 这个并不是最优方案,并且存在诸多缺点

示例代码如下:

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));
    }

这个方案存在如下几个缺点:

  1. 使用包装类包装HttpServletRequestHttpServletResponse会导致输入流和输出流被多拷贝一次

  2. 并不是所有的请求类型都适合对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);
    	}
    
  3. 不方便获取Request Body对应的实体类类型, 不方便知道响应请求的实体类类型

最优方案

实际上SpringMVC提供了如下两个接口,用于在解析Request Body后和响应请求前回调

  1. org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice
    RequestBodyAdvice用于解析Request Body后回调, 可以实现该接口得到解析后的Body实体类,RequestBodyAdviceAdapter适配了RequestBodyAdvice
  2. org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
    ResponseBodyAdvice用于在向http请求响应数据之前回调, 可以实现该接口得到响应的实体类

为了能够一块打印请求和响应数据, 必须在请求时记录Request Body对象, 这里就考虑使用HttpServletRequestsetAttribute记录,但是RequestBodyAdvice中无法拿到HttpServletRequest,因此需要在请求时通过ThreadLocal绑定. Spring已经给我们提供了相关的功能, 见org.springframework.web.context.request.RequestContextHolder

@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 = RequestResponseBodyInterceptor.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;
    }
}
@Slf4j
public class RequestResponseBodyInterceptor implements HandlerInterceptor {
    public static HttpServletRequest getServletRequest() {
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        return servletRequestAttributes.getRequest();
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        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 {
        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");
    }

}
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
SpringMVC是一个用于构建Web应用程序的Java框架。在SpringMVC中,处理请求响应是通过使用控制器方法来实现的。控制器方法使用@RequestMapping注解来映射请求的URL,并通过方法参数来获取请求参数响应对象。 在处理请求时,可以使用@RequestParam注解来获取请求参数的值。例如,可以在方法参数上使用@RequestParam注解来获取请求中的特定参数值。另外,也可以使用@PathVariable注解来获取URL中的路径参数。 在处理响应时,可以使用@ResponseBody注解将方法的返回值直接写入HTTP响应体中,而不是进行视图跳转。这样可以直接返回字符串、JSON格式的数据或其他类型的数据。 另外,在SpringMVC中,还有一些核心组件,如处理器映射器、处理器适配器和视图解析器。处理器映射器负责将请求映射到相应的控制器方法,处理器适配器负责将请求参数绑定到方法参数,并调用相应的控制器方法,视图解析器负责将方法的返回值解析为视图。 总结起来,SpringMVC通过控制器方法、注解和核心组件来处理请求响应,使得开发者可以方便地构建Web应用程序。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *3* [springMVC请求响应](https://blog.csdn.net/weixin_38088097/article/details/105840310)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [SpringMVC请求响应](https://blog.csdn.net/weixin_51146329/article/details/123166475)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值