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

一酝锅、前言

在工作中壹将,出現(xiàn)了需要打印每次請求中調(diào)用方傳過來的requestBody的需求

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

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

自己寫的類不一定能正確反序列化它的所有字段,如果反序列化有誤静袖,不清楚它傳來的xml長什么樣子觉鼻,也無法解決問題

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

  1. 我們寫的接口,要由他們定義字段類型队橙,但文檔寫的爛滑凉,字段定義的不清楚,不能提供維護(hù)以及答疑支持
  2. 配合程度有限喘帚,不能提供請求的xml

這兩點帶來的問題是當(dāng)反序列化出現(xiàn)問題,不自己打印它們請求過來的xml咒钟,就沒法快速找到問題原因吹由,因此,需要我們通過某種手段打印出requestBody的內(nèi)容

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

通常倾鲫,最簡單的HTTP GET請求可以通過寫一個繼承HandlerInterceptorAdapter的攔截器來實現(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)方法萍嬉,實現(xiàn)處理器的預(yù)處理(如檢查登陸)乌昔,第三個參數(shù)為響應(yīng)的處理器,自定義Controller
     * <p>
     * 返回值:
     * true表示繼續(xù)流程(如調(diào)用下一個攔截器或處理器)
     * false表示流程中斷(如登錄檢查失斎雷贰)磕道,不會繼續(xù)調(diào)用其他的攔截器或處理器
     * 此時我們需要通過response來產(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("用戶訪問地址:{}, 來路地址: {}, 請求參數(shù): {}", 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>
     * 即在視圖渲染完畢時回調(diào)行冰,如性能監(jiān)控中我們可以在此記錄結(jié)束時間并輸出消耗時間
     * 還可以進(jìn)行一些資源清理溺蕉,類似于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("請求處理結(jié)束. 處理耗時: {}", System.currentTimeMillis() - startTime.get());
        startTime.remove();
        super.afterCompletion(request, response, handler, ex);
    }
}

三悼做、為什么打印requestBody是一個問題疯特?

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

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為了解決這個問題组题,為Request與Response分別封裝了 ContentCachingRequestWrapper 與 ContentCachingResponseWrapper 包裹類得這兩個流信息可重復(fù)讀(緩存機(jī)制,在讀取輸入流以后緩存下來)

3.1.1 初步解決方案

通過 ContentCachingRequestWrapper 這個類可以簡單的實現(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("請求body: {}", requestBody);
        }

    }
}

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

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

后來我又發(fā)現(xiàn)Spring提供了一個過濾器抽象類AbstractRequestLoggingFilter吃衅,它為請求日志的打印提供了更豐富的功能往踢,但使用的時候也要注意一些小細(xì)節(jié)(小坑)

要使用這個過濾器,只要按照你的需要實現(xiàn)它的兩個抽象類就可以

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 的實現(xiàn)類 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以外,還可以很方便的根據(jù)需要打印更詳細(xì)請求信息趣效,以下是 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瘦癌、請求參數(shù)、客戶端信息跷敬、會話信息讯私、遠(yuǎn)程用戶信息、headers以及payload西傀,并且這些都是根據(jù)你的需要配置的

生成效果如下:

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屬性的默認(rèn)值是false,不會打印payload信息拥褂,同時maxPayloadLength默認(rèn)值是50娘锁,會導(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時才打印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類緩存請求是通過消耗輸入流來進(jìn)行緩存的莫秆,因此這是一個不小的代價,它使得過濾器鏈中的其他過濾器無法再讀取輸入流悔详。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末镊屎,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子茄螃,更是在濱河造成了極大的恐慌缝驳,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件责蝠,死亡現(xiàn)場離奇詭異喷户,居然都是意外死亡克握,警方通過查閱死者的電腦和手機(jī)告材,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門或南,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肴敛,你說我怎么就攤上這事署海÷鸸海” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵砸狞,是天一觀的道長捻勉。 經(jīng)常有香客問我,道長刀森,這世上最難降的妖魔是什么踱启? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮研底,結(jié)果婚禮上埠偿,老公的妹妹穿的比我還像新娘。我一直安慰自己榜晦,他們只是感情好冠蒋,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著乾胶,像睡著了一般抖剿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上识窿,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天斩郎,我揣著相機(jī)與錄音,去河邊找鬼喻频。 笑死孽拷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的半抱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼膜宋,長吁一口氣:“原來是場噩夢啊……” “哼窿侈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起秋茫,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤史简,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后肛著,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體圆兵,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年枢贿,在試婚紗的時候發(fā)現(xiàn)自己被綠了殉农。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡局荚,死狀恐怖超凳,靈堂內(nèi)的尸體忽然破棺而出愈污,到底是詐尸還是另有隱情,我是刑警寧澤轮傍,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布暂雹,位于F島的核電站,受9級特大地震影響创夜,放射性物質(zhì)發(fā)生泄漏杭跪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一驰吓、第九天 我趴在偏房一處隱蔽的房頂上張望涧尿。 院中可真熱鬧,春花似錦棚瘟、人聲如沸现斋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庄蹋。三九已至,卻和暖如春迷雪,著一層夾襖步出監(jiān)牢的瞬間限书,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工章咧, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留倦西,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓赁严,卻偏偏與公主長得像扰柠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子疼约,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354