RestTemplate打印日志的正確姿勢(shì)

背景

現(xiàn)在隨著微服務(wù)的逐漸發(fā)展访圃,越來(lái)越多的公司開(kāi)始采用微服務(wù)架構(gòu)肴甸,但隨之而來(lái)的問(wèn)題是蹄溉,服務(wù)間如何調(diào)用咨油,如何還能負(fù)載均衡,還必須開(kāi)箱即用柒爵?答案就是Spring的RestTemplate啦(并不一定要使用RestTemplate役电,也不一定要是微服務(wù)架構(gòu),涉及到Http調(diào)用的服務(wù)棉胀,方式都可以法瑟,我這個(gè)是為了引出主題),但伴隨而來(lái)的是一個(gè)問(wèn)題唁奢,如何打印好日志霎挟,留下現(xiàn)場(chǎng)(防止扯皮),這才是我們關(guān)心的驮瞧。這也是我們這篇文章的主題-RestTemplate打印日志的正確姿勢(shì)氓扛。好了,不湊字?jǐn)?shù)了论笔,接下來(lái)進(jìn)入正文。

1. 老實(shí)人干事

作為一個(gè)老實(shí)人千所,對(duì)于打日志這類(lèi)請(qǐng)求狂魔,就是通過(guò)代理,找出核心方法淫痰,在方法后最楷,打印日志參數(shù)即可。實(shí)時(shí)待错,我也就是這么做的籽孙,如下圖:


手動(dòng)打印日志

我把RestTemplate的最底層方法進(jìn)行了包裝,然后在執(zhí)行后火俄,返回結(jié)果前但因日志犯建。這樣做,說(shuō)實(shí)話瓜客,沒(méi)毛病适瓦,并且我拿去運(yùn)行,放在線上運(yùn)行了好長(zhǎng)一段時(shí)間谱仪,沒(méi)出任何問(wèn)題玻熙。但是..但是...,現(xiàn)實(shí)就是偏偏就欺(bu)負(fù)(xi)老(xin)實(shí)(ren)人(zhen)

2. 老實(shí)人翻盤(pán)

為什么說(shuō)是欺(bu)負(fù)(xi)老(xin)實(shí)(ren)人(zhen)呢疯攒?最近我在看代碼的時(shí)候嗦随,發(fā)現(xiàn)了ResTemplate提供了一個(gè)方法RestTemplate#setInterceptors,我們通過(guò)名稱就知道敬尺,是設(shè)置攔截器的枚尼。只看名稱贴浙,那還不一定,我們需要去瞅瞅源碼才行姑原。

2.1 源碼追蹤

setInterceptors方法如下:

// 設(shè)置攔截器
public void setInterceptors(List<ClientHttpRequestInterceptor> interceptors) {
        // Take getInterceptors() List as-is when passed in here
        if (this.interceptors != interceptors) {
            this.interceptors.clear();
            this.interceptors.addAll(interceptors);
            // 對(duì)攔截器進(jìn)行排序
            AnnotationAwareOrderComparator.sort(this.interceptors);
        }
}

setInterceptors邏輯很簡(jiǎn)單悬而,就是添加攔截器。我們接下來(lái)要看看攔截器是在哪里使用的锭汛,這也是我們需要確認(rèn)的點(diǎn)笨奠。接著往下。

// 構(gòu)建RequestFactory
public ClientHttpRequestFactory getRequestFactory() {
  // 獲取攔截器
  List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
  if (!CollectionUtils.isEmpty(interceptors)) {
    // 攔截器不為空
    ClientHttpRequestFactory factory = this.interceptingRequestFactory;
    if (factory == null) {
      // 使用攔截器構(gòu)建的RequestFactory
      factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
      this.interceptingRequestFactory = factory;
    }
    return factory;
  }
  else {
    // 返回默認(rèn)的 SimpleClientHttpRequestFactory唤殴,可以自己查看一下
    return super.getRequestFactory();
  }
}

getRequestFactory中搜尋到了攔截器的使用般婆,是通過(guò)攔截器,構(gòu)建了一個(gè)InterceptingClientHttpRequestFactory對(duì)象朵逝。目前蔚袍,我們通過(guò)名字大意可以看到,是構(gòu)建了一個(gè)攔截器的請(qǐng)求工廠配名,但還是沒(méi)有具體信息啤咽,讓我們接著往下,看看這個(gè)InterceptingClientHttpRequestFactory里面是怎么使用的渠脉。

省略中間簡(jiǎn)單的邏輯宇整,我們找到了最終邏輯。在InterceptingRequestExecution源代碼如下:

private class InterceptingRequestExecution implements ClientHttpRequestExecution {

  private final Iterator<ClientHttpRequestInterceptor> iterator;

  public InterceptingRequestExecution() {
    // 胡哦哦去攔截器的迭代器
    this.iterator = interceptors.iterator();
  }

  @Override
  public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
    if (this.iterator.hasNext()) {
      ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
      //每次將當(dāng)前對(duì)象傳入芋膘,循環(huán)迭代調(diào)用攔截器
      return nextInterceptor.intercept(request, body, this);
    }
    else {
      // 當(dāng)攔截器執(zhí)行完鳞青,開(kāi)始執(zhí)行請(qǐng)求邏輯
      HttpMethod method = request.getMethod();
      Assert.state(method != null, "No standard HTTP method");
      ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
      request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
      ...
      // 執(zhí)行請(qǐng)求,并返回執(zhí)行結(jié)果對(duì)象为朋。ClientHttpResponse
      return delegate.execute();
    }
  }
}

在執(zhí)行execute方法時(shí)臂拓,循環(huán)調(diào)用攔截器方法。當(dāng)攔截器執(zhí)行完后习寸,構(gòu)建請(qǐng)求對(duì)象胶惰,執(zhí)行請(qǐng)求。請(qǐng)求執(zhí)行完融涣,返回請(qǐng)求結(jié)果對(duì)象童番。

3. 撥開(kāi)迷霧

源碼看我我TM悟了,原來(lái)別人提供了攔截器威鹿,我還自己搞那么復(fù)雜剃斧。這不是吃力不討好嗎?那不行忽你,我的改造我的代碼幼东。

3.1 實(shí)現(xiàn)攔截器接口-ClientHttpRequestInterceptor

貼上我寫(xiě)的代碼,如下所示:

/**
 * <br>日志攔截器</br>
 *
 * @author fattyca1
 * @since 1.0
 */
@Slf4j
public class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {
        // 用來(lái)記錄接口執(zhí)行時(shí)間的最小接收值
    private final long timeoutMs;

    public LoggingRequestInterceptor(long timeoutMs) {
        this.timeoutMs = timeoutMs;
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {

        long start = System.currentTimeMillis();
        ClientHttpResponse response = execution.execute(request, body);
        long cost = System.currentTimeMillis() - start;
        if (cost > timeoutMs) {
            log.warn("Request uri: [{}], Cost times: {}ms", request.getURI(), cost);
        }
        // 打印日志
        trace(request, body, response);
        return response;
    }

    private void trace(HttpRequest request, byte[] body, ClientHttpResponse response) throws IOException {
        // 記錄日志
        String responseStr = IOUtils.toString(response.getBody(), StandardCharsets.UTF_8);
        log.info(
                "\n" +
                        "URI          : {}, \n" +
                        "Method       : {}, \n" +
                        "Headers      : {}, \n" +
                        "Param        : {}, \n" +
                        "RespStatus   : {}, \n" +
                        "Response     : {}", request.getURI(),
                request.getMethod(), request.getHeaders(), new String(body, StandardCharsets.UTF_8), response.getStatusCode(), responseStr);
    }
}

代碼邏輯很簡(jiǎn)單,實(shí)現(xiàn)攔截器根蟹,在攔截器中執(zhí)行請(qǐng)求的時(shí)候脓杉,根據(jù)入?yún)ⅲ鰠⒓虼⒋蛴〗Y(jié)果球散。

3.2 構(gòu)建RestTemplate對(duì)象

構(gòu)建RestTemplate對(duì)象代碼如下所示:

private RestTemplateWrapper buildHttpRestTemplate(CloseableHttpClient client) {
    RestTemplateWrapper restTemplate = new RestTemplateWrapper();
    HttpComponentsClientHttpRequestFactory clientHttpRequestFactory
            = new HttpComponentsClientHttpRequestFactory(client);
    // 連接超時(shí)
    clientHttpRequestFactory.setConnectTimeout(httpClientProperties.getConnectionTimeOut());
    // 數(shù)據(jù)讀取超時(shí)時(shí)間
    clientHttpRequestFactory.setReadTimeout(httpClientProperties.getSocketTimeOut());
    // 連接不夠用的等待時(shí)間
    clientHttpRequestFactory.setConnectionRequestTimeout(httpClientProperties.getConnectionRequestTimeout());
    // 需要支持從response重復(fù)獲取body數(shù)據(jù)
    // restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(clientHttpRequestFactory));
    restTemplate.setRequestFactory(clientHttpRequestFactory);
    // 設(shè)置攔截器
    restTemplate.setInterceptors(Collections.singletonList(new LoggingRequestInterceptor(httpClientProperties.getLogTimeoutMs())));
    return restTemplate;
}

我們通過(guò)一些列操作,到了構(gòu)建RestTemplate對(duì)象這散庶,并且設(shè)置了攔截器蕉堰,可以開(kāi)開(kāi)心心的去注入RestTemplate對(duì)象啦。

3.3 坑悲龟,你躲開(kāi)了嗎

大家拿著RestTemplate開(kāi)開(kāi)心心去使用的時(shí)候屋讶,發(fā)現(xiàn)了沒(méi),會(huì)出錯(cuò)P虢獭C笊! 是的轻腺,會(huì)報(bào)錯(cuò)@纸!贬养!好氣诀拭,我辛辛苦苦搞半天,還會(huì)出錯(cuò)煤蚌,錯(cuò)誤如下:

Caused by: java.io.IOException: Attempted read from closed stream.

出現(xiàn)這個(gè)錯(cuò)誤原因,主要是因?yàn)閞esponse.getBody()只能獲取一次细卧,在后續(xù)RestTemplate獲取輸出轉(zhuǎn)化時(shí)會(huì)再次調(diào)用response.getBody()尉桩。

 // 攔截器的打印日志代碼中 response.getBody() 只能獲取一次,然后會(huì)關(guān)閉流贪庙,有興趣的可以看看源碼
 String responseStr = IOUtils.toString(response.getBody(), StandardCharsets.UTF_8);

既然知道問(wèn)題了,那我們就得修改,那怎么修改呢格嘁?經(jīng)過(guò)一番查找欢摄,找出了這個(gè)類(lèi)BufferingClientHttpRequestFactory,瞅瞅這個(gè)注釋(我懷疑就是坑5寂屈扎!不然為啥正好有一個(gè)這樣的包裝類(lèi)):

/**
 * Wrapper for a {@link ClientHttpRequestFactory} that buffers
 * all outgoing and incoming streams in memory.
 *
 * <p>Using this wrapper allows for multiple reads of the
 * {@linkplain ClientHttpResponse#getBody() response body}.
 */
public class BufferingClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper {
  
}

注釋中清楚地寫(xiě)著,可以多次調(diào)用ClientHttpResponse#getBody()×秘埃現(xiàn)在我們知道怎么解決這個(gè)問(wèn)題了鹰晨,那么就根據(jù)它的使用方法,重新構(gòu)建RestTemplate對(duì)象。代碼如下(上方構(gòu)建RestTemplate對(duì)象中的注釋打開(kāi)):

private RestTemplateWrapper buildHttpRestTemplate(CloseableHttpClient client) {
  RestTemplateWrapper restTemplate = new RestTemplateWrapper();
  HttpComponentsClientHttpRequestFactory clientHttpRequestFactory
    = new HttpComponentsClientHttpRequestFactory(client);
  // 連接超時(shí)
  clientHttpRequestFactory.setConnectTimeout(httpClientProperties.getConnectionTimeOut());
  // 數(shù)據(jù)讀取超時(shí)時(shí)間
  clientHttpRequestFactory.setReadTimeout(httpClientProperties.getSocketTimeOut());
  // 連接不夠用的等待時(shí)間
  clientHttpRequestFactory.setConnectionRequestTimeout(httpClientProperties.getConnectionRequestTimeout());
  //通過(guò)BufferingClientHttpRequestFactory對(duì)象包裝現(xiàn)有的ResquestFactory模蜡,用來(lái)支持多次調(diào)用getBody()方法
  restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(clientHttpRequestFactory));
  // 打印記錄
  restTemplate.setInterceptors(Collections.singletonList(new LoggingRequestInterceptor(httpClientProperties.getLogTimeoutMs())));
  return restTemplate;
}

至此漠趁,我們?cè)陂_(kāi)始運(yùn)行,才發(fā)行可以走的通忍疾。

4 總結(jié)

阿~ 疏通了闯传,舒服了,代碼看著也舒服了卤妒,這樣才是代碼的正確打開(kāi)方式甥绿。這樣也讓我們對(duì)代碼設(shè)計(jì)有的更深的了解,在設(shè)計(jì)代碼時(shí)荚孵,需要預(yù)留一些接口設(shè)置妹窖,用來(lái)拓展,或方便別人收叶。

這只是Interceptor一次小小的使用骄呼,并不代表只能用來(lái)打印日志,我們可以根據(jù)攔截器設(shè)置cookie判没,header信息等蜓萄,都是可以的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末澄峰,一起剝皮案震驚了整個(gè)濱河市嫉沽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌俏竞,老刑警劉巖绸硕,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異魂毁,居然都是意外死亡玻佩,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)席楚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)咬崔,“玉大人,你說(shuō)我怎么就攤上這事烦秩】逅梗” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵只祠,是天一觀的道長(zhǎng)兜蠕。 經(jīng)常有香客問(wèn)我,道長(zhǎng)铆农,這世上最難降的妖魔是什么牺氨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任狡耻,我火速辦了婚禮,結(jié)果婚禮上猴凹,老公的妹妹穿的比我還像新娘夷狰。我一直安慰自己,他們只是感情好郊霎,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布沼头。 她就那樣靜靜地躺著,像睡著了一般书劝。 火紅的嫁衣襯著肌膚如雪进倍。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天购对,我揣著相機(jī)與錄音猾昆,去河邊找鬼。 笑死骡苞,一個(gè)胖子當(dāng)著我的面吹牛垂蜗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播解幽,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼贴见,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了躲株?” 一聲冷哼從身側(cè)響起片部,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎霜定,沒(méi)想到半個(gè)月后档悠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡望浩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年站粟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片曾雕。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖助被,靈堂內(nèi)的尸體忽然破棺而出剖张,到底是詐尸還是另有隱情,我是刑警寧澤揩环,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布搔弄,位于F島的核電站,受9級(jí)特大地震影響丰滑,放射性物質(zhì)發(fā)生泄漏顾犹。R本人自食惡果不足惜倒庵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望炫刷。 院中可真熱鬧擎宝,春花似錦、人聲如沸浑玛。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)顾彰。三九已至极阅,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間涨享,已是汗流浹背筋搏。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厕隧,地道東北人奔脐。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像栏账,于是被迫代替她去往敵國(guó)和親帖族。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容