SpringBoot打印請(qǐng)求體與響應(yīng)體

一、前言

在工作中,出現(xiàn)了需要打印每次請(qǐng)求中調(diào)用方傳過(guò)來(lái)的requestBody的需求

出現(xiàn)這個(gè)需求的原因是我在和某平臺(tái)做聯(lián)調(diào)工作,出現(xiàn)了一個(gè)比較惡心的情況。

有一些事件通知需要由他們調(diào)用我們的http接口來(lái)實(shí)現(xiàn)事件通知,但是這個(gè)http接口的數(shù)據(jù)格式是由他們定義的(照搬其他地方的),而他們給的相關(guān)文檔很爛,示例中缺乏某些字段,而字段表里的字段又沒(méi)有分級(jí),因此很難弄清楚他們請(qǐng)求的字段有哪些。

自己寫(xiě)的類(lèi)不一定能正確反序列化它的所有字段,如果反序列化有誤,不清楚它傳來(lái)的xml長(zhǎng)什么樣子,也無(wú)法解決問(wèn)題

總結(jié)一下問(wèn)題原因:

  1. 我們寫(xiě)的接口,要由他們定義字段類(lèi)型,但文檔寫(xiě)的爛,字段定義的不清楚,不能提供維護(hù)以及答疑支持
  2. 配合程度有限,不能提供請(qǐng)求的xml

這兩點(diǎn)帶來(lái)的問(wèn)題是當(dāng)反序列化出現(xiàn)問(wèn)題,不自己打印它們請(qǐng)求過(guò)來(lái)的xml,就沒(méi)法快速找到問(wèn)題原因,因此,需要我們通過(guò)某種手段打印出requestBody的內(nèi)容

二、傳統(tǒng)請(qǐng)求參數(shù)的打印

通常,最簡(jiǎn)單的HTTP GET請(qǐng)求可以通過(guò)寫(xiě)一個(gè)繼承HandlerInterceptorAdapter的攔截器來(lái)實(shí)現(xiàn),形如:

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

    /**
     * 預(yù)處理回調(diào)方法,實(shí)現(xiàn)處理器的預(yù)處理(如檢查登陸),第三個(gè)參數(shù)為響應(yīng)的處理器,自定義Controller
     * <p>
     * 返回值:
     * true表示繼續(xù)流程(如調(diào)用下一個(gè)攔截器或處理器)
     * false表示流程中斷(如登錄檢查失?。?,不會(huì)繼續(xù)調(diào)用其他的攔截器或處理器
     * 此時(shí)我們需要通過(guò)response來(lái)產(chǎn)生響應(yīng);
     *
     * @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("用戶(hù)訪(fǎng)問(wèn)地址:{}, 來(lái)路地址: {}, 請(qǐng)求參數(shù): {}", uri, IpUtil.getRemoteIp(request), JSON.toJSON(paramMap));
        log.info("----------------請(qǐng)求頭.start.....");
        Enumeration<String> enums = request.getHeaderNames();
        while (enums.hasMoreElements()) {
            String name = enums.nextElement();
            log.info(name + ": {}", request.getHeader(name));
        }
        log.info("----------------請(qǐng)求頭.end!");
        return super.preHandle(request, response, handler);
    }


    /**
     * 在任何情況下都會(huì)對(duì)返回的請(qǐng)求做處理
     * <p>
     * 即在視圖渲染完畢時(shí)回調(diào),如性能監(jiān)控中我們可以在此記錄結(jié)束時(shí)間并輸出消耗時(shí)間
     * 還可以進(jìn)行一些資源清理,類(lèi)似于try-catch-finally中的finally,但僅調(diào)用處理器執(zhí)行鏈中
     *
     * @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("請(qǐng)求處理結(jié)束. 處理耗時(shí): {}", System.currentTimeMillis() - startTime.get());
        startTime.remove();
        super.afterCompletion(request, response, handler, ex);
    }
}

三、為什么打印requestBody是一個(gè)問(wèn)題?

請(qǐng)求參數(shù)可以通過(guò) request.getParameterMap() 來(lái)獲得,但要獲取requestBody,只能通過(guò)request.getInputStream() 來(lái)獲取輸入流,但是由于request 的inputStream和response 的outputStream默認(rèn)情況下是只能讀一次,若在攔截器中讀取打印了,后面業(yè)務(wù)就讀取不到了(別想著讀完還能寫(xiě)回去,死了這條心叭)

3.1 解決辦法

在頭痛煩悶的嘗試了各種辦法后偶然看了這篇文章受到了啟發(fā)

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

Spring為了解決這個(gè)問(wèn)題,為Request與Response分別封裝了 ContentCachingRequestWrapper 與 ContentCachingResponseWrapper 包裹類(lèi)得這兩個(gè)流信息可重復(fù)讀(緩存機(jī)制,在讀取輸入流以后緩存下來(lái))

3.1.1 初步解決方案

通過(guò) ContentCachingRequestWrapper 這個(gè)類(lèi)可以簡(jiǎn)單的實(shí)現(xiàn)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("請(qǐng)求body: {}", requestBody);
        }

    }
}

然后就可以打印出請(qǐng)求body的內(nèi)容了

3.1.2 解決方案優(yōu)化

后來(lái)我又發(fā)現(xiàn)Spring提供了一個(gè)過(guò)濾器抽象類(lèi)AbstractRequestLoggingFilter,它為請(qǐng)求日志的打印提供了更豐富的功能,但使用的時(shí)候也要注意一些小細(xì)節(jié)(小坑)

要使用這個(gè)過(guò)濾器,只要按照你的需要實(shí)現(xiàn)它的兩個(gè)抽象類(lèi)就可以

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 的實(shí)現(xiàn)類(lèi) 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提供的過(guò)濾器的好處是,除了requestBody以外,還可以很方便的根據(jù)需要打印更詳細(xì)請(qǐng)求信息,以下是 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();
}

可以看到它能幫你生產(chǎn)的信息包含了uri、請(qǐng)求參數(shù)、客戶(hù)端信息、會(huì)話(huà)信息、遠(yuǎn)程用戶(hù)信息、headers以及payload,并且這些都是根據(jù)你的需要配置的

生成效果如下:

3.1.3 注冊(cè)Filter

只需要在繼承WebMvcConfigurationSupport的配置類(lèi)中注冊(cè)這個(gè)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() 就是我在使用中遇到的坑。因?yàn)锳bstractRequestLoggingFilter 的includePayload屬性的默認(rèn)值是false,不會(huì)打印payload信息,同時(shí)maxPayloadLength默認(rèn)值是50,會(huì)導(dǎo)致打印的requestBody不完整

貼一下它們的相關(guān)代碼

protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
   StringBuilder msg = new StringBuilder();
   msg.append(prefix);
   msg.append("uri=").append(request.getRequestURI());

   ...
    // 只有 includePayload 為true時(shí)才打印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;
}

四、弊端

但是使用這兩個(gè)包裹類(lèi)會(huì)有一些潛在的問(wèn)題,ContentCachingRequestWrapper類(lèi)緩存請(qǐng)求是通過(guò)消耗輸入流來(lái)進(jìn)行緩存的,因此這是一個(gè)不小的代價(jià),它使得過(guò)濾器鏈中的其他過(guò)濾器無(wú)法再讀取輸入流。

可見(jiàn):https://github.com/spring-projects/spring-framework/issues/20577

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容