美文网首页
SpringBoot打印请求体与响应体

SpringBoot打印请求体与响应体

作者: 谢随安 | 来源:发表于2020-07-21 09:19 被阅读0次

    一、前言

    在工作中,出现了需要打印每次请求中调用方传过来的requestBody的需求

    出现这个需求的原因是我在和某平台做联调工作,出现了一个比较恶心的情况。

    有一些事件通知需要由他们调用我们的http接口来实现事件通知,但是这个http接口的数据格式是由他们定义的(照搬其他地方的),而他们给的相关文档很烂,示例中缺乏某些字段,而字段表里的字段又没有分级,因此很难弄清楚他们请求的字段有哪些。

    自己写的类不一定能正确反序列化它的所有字段,如果反序列化有误,不清楚它传来的xml长什么样子,也无法解决问题

    总结一下问题原因:

    1. 我们写的接口,要由他们定义字段类型,但文档写的烂,字段定义的不清楚,不能提供维护以及答疑支持
    2. 配合程度有限,不能提供请求的xml

    这两点带来的问题是当反序列化出现问题,不自己打印它们请求过来的xml,就没法快速找到问题原因,因此,需要我们通过某种手段打印出requestBody的内容

    二、传统请求参数的打印

    通常,最简单的HTTP GET请求可以通过写一个继承HandlerInterceptorAdapter的拦截器来实现,形如:

    package com.chasel.interceptor;
    
    import com.alibaba.fastjson.JSON;
    import com.cmic.origin.internal.gateway.core.util.IpUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Component;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Enumeration;
    import java.util.Map;
    
    /**
     * @author XieLongzhen
     * @date 2018/12/26 18:46
     */
    @Slf4j
    @Component
    public class HttpInterceptor extends HandlerInterceptorAdapter {
    
        private ThreadLocal<Long> startTime = new ThreadLocal<>();
    
        /**
         * 预处理回调方法,实现处理器的预处理(如检查登陆),第三个参数为响应的处理器,自定义Controller
         * <p>
         * 返回值:
         * true表示继续流程(如调用下一个拦截器或处理器)
         * false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器
         * 此时我们需要通过response来产生响应;
         *
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            startTime.set(System.currentTimeMillis());
            String uri = request.getRequestURI();
            Map paramMap = request.getParameterMap();
            log.info("用户访问地址:{}, 来路地址: {}, 请求参数: {}", uri, IpUtil.getRemoteIp(request), JSON.toJSON(paramMap));
            log.info("----------------请求头.start.....");
            Enumeration<String> enums = request.getHeaderNames();
            while (enums.hasMoreElements()) {
                String name = enums.nextElement();
                log.info(name + ": {}", request.getHeader(name));
            }
            log.info("----------------请求头.end!");
            return super.preHandle(request, response, handler);
        }
    
    
        /**
         * 在任何情况下都会对返回的请求做处理
         * <p>
         * 即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间
         * 还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
         *
         * @param request
         * @param response
         * @param handler
         * @param ex
         * @throws Exception
         */
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            log.info("请求处理结束. 处理耗时: {}", System.currentTimeMillis() - startTime.get());
            startTime.remove();
            super.afterCompletion(request, response, handler, ex);
        }
    }
    

    三、为什么打印requestBody是一个问题?

    请求参数可以通过 request.getParameterMap() 来获得,但要获取requestBody,只能通过request.getInputStream() 来获取输入流,但是由于request 的inputStream和response 的outputStream默认情况下是只能读一次,若在拦截器中读取打印了,后面业务就读取不到了(别想着读完还能写回去,死了这条心叭)

    3.1 解决办法

    在头痛烦闷的尝试了各种办法后偶然看了这篇文章受到了启发

    https://stackoverflow.com/questions/10210645/http-servlet-request-lose-params-from-post-body-after-read-it-once?tdsourcetag=s_pctim_aiomsg

    Spring为了解决这个问题,为Request与Response分别封装了 ContentCachingRequestWrapper 与 ContentCachingResponseWrapper 包裹类得这两个流信息可重复读(缓存机制,在读取输入流以后缓存下来)

    3.1.1 初步解决方案

    通过 ContentCachingRequestWrapper 这个类可以简单的实现requestBody的打印

    package com.chasel.filter;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.util.ContentCachingRequestWrapper;
    import org.springframework.web.util.ContentCachingResponseWrapper;
    
    import javax.servlet.*;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * @author XieLongzhen
     * @date 2019/10/9 14:38
     */
    @Slf4j
    public class LogFilter implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void destroy() {
    
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
    
            ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
            ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
    
            try {
                chain.doFilter(requestWrapper, responseWrapper);
            } finally {
    
                String requestBody = new String(requestWrapper.getContentAsByteArray());
                log.info("请求body: {}", requestBody);
            }
    
        }
    }
    

    然后就可以打印出请求body的内容了

    3.1.2 解决方案优化

    后来我又发现Spring提供了一个过滤器抽象类AbstractRequestLoggingFilter,它为请求日志的打印提供了更丰富的功能,但使用的时候也要注意一些小细节(小坑)

    要使用这个过滤器,只要按照你的需要实现它的两个抽象类就可以

    protected abstract void beforeRequest(HttpServletRequest request, String message);
    protected abstract void afterRequest(HttpServletRequest request, String message);
    

    核心代码如下

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
          throws ServletException, IOException {
    
       boolean isFirstRequest = !isAsyncDispatch(request);
       HttpServletRequest requestToUse = request;
    
       if (isIncludePayload() && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
          requestToUse = new ContentCachingRequestWrapper(request, getMaxPayloadLength());
       }
    
       boolean shouldLog = shouldLog(requestToUse);
       if (shouldLog && isFirstRequest) {
          beforeRequest(requestToUse, getBeforeMessage(requestToUse));
       }
       try {
          filterChain.doFilter(requestToUse, response);
       }
       finally {
          if (shouldLog && !isAsyncStarted(requestToUse)) {
             afterRequest(requestToUse, getAfterMessage(requestToUse));
          }
       }
    }
    

    同样,你可以直接使用Spring提供的 AbstractRequestLoggingFilter 的实现类 ServletContextRequestLoggingFilter

    public class ServletContextRequestLoggingFilter extends AbstractRequestLoggingFilter {
    
        /**
         * Writes a log message before the request is processed.
         */
        @Override
        protected void beforeRequest(HttpServletRequest request, String message) {
            getServletContext().log(message);
        }
    
        /**
         * Writes a log message after the request is processed.
         */
        @Override
        protected void afterRequest(HttpServletRequest request, String message) {
            getServletContext().log(message);
        }
    
    }
    

    使用Spring提供的过滤器的好处是,除了requestBody以外,还可以很方便的根据需要打印更详细请求信息,以下是 createMessage() 的完整代码

    protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
       StringBuilder msg = new StringBuilder();
       msg.append(prefix);
       msg.append("uri=").append(request.getRequestURI());
    
       if (isIncludeQueryString()) {
          String queryString = request.getQueryString();
          if (queryString != null) {
             msg.append('?').append(queryString);
          }
       }
    
       if (isIncludeClientInfo()) {
          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);
          }
       }
    
       if (isIncludeHeaders()) {
          msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders());
       }
    
       if (isIncludePayload()) {
          String payload = getMessagePayload(request);
          if (payload != null) {
             msg.append(";payload=").append(payload);
          }
       }
    
       msg.append(suffix);
       return msg.toString();
    }
    

    可以看到它能帮你生产的信息包含了uri、请求参数、客户端信息、会话信息、远程用户信息、headers以及payload,并且这些都是根据你的需要配置的

    生成效果如下:

    3.1.3 注册Filter

    只需要在继承WebMvcConfigurationSupport的配置类中注册这个Filter即可

    @Bean
    public FilterRegistrationBean loggingFilterRegistration() {
        FilterRegistrationBean<ServletContextRequestLoggingFilter> registration = new FilterRegistrationBean<>();
        ServletContextRequestLoggingFilter filter = new ServletContextRequestLoggingFilter();
        filter.setIncludePayload(true);
        filter.setMaxPayloadLength(9999);
        registration.setFilter(filter);
        registration.setUrlPatterns(Collections.singleton("/notifications/*"));
        return registration;
    }
    

    3.1.4 遇到的坑

    其中 setIncludePayload() 以及 setMaxPayloadLength() 就是我在使用中遇到的坑。因为AbstractRequestLoggingFilter 的includePayload属性的默认值是false,不会打印payload信息,同时maxPayloadLength默认值是50,会导致打印的requestBody不完整

    贴一下它们的相关代码

    protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
       StringBuilder msg = new StringBuilder();
       msg.append(prefix);
       msg.append("uri=").append(request.getRequestURI());
    
       ...
        // 只有 includePayload 为true时才打印payload信息
       if (isIncludePayload()) {
          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) {
              // 取的是buf.length与maxPayloadLength的最小值
             int length = Math.min(buf.length, getMaxPayloadLength());
             try {
                return new String(buf, 0, length, wrapper.getCharacterEncoding());
             }
             catch (UnsupportedEncodingException ex) {
                return "[unknown]";
             }
          }
       }
       return null;
    }
    

    四、弊端

    但是使用这两个包裹类会有一些潜在的问题,ContentCachingRequestWrapper类缓存请求是通过消耗输入流来进行缓存的,因此这是一个不小的代价,它使得过滤器链中的其他过滤器无法再读取输入流。

    可见:https://github.com/spring-projects/spring-framework/issues/20577

    相关文章

      网友评论

          本文标题:SpringBoot打印请求体与响应体

          本文链接:https://www.haomeiwen.com/subject/yvbbkktx.html