背景
現(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í)待错,我也就是這么做的籽孙,如下圖:
我把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信息等蜓萄,都是可以的。