SpringBoot打印请求体与响应体

2020-07-21  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读