一、背景
去年高峰壓測(cè)的時(shí)候,有個(gè)服務(wù)是專(zhuān)門(mén)調(diào)用其它系統(tǒng)的,在測(cè)試接口http請(qǐng)求的時(shí)候,那TPS唰唰的往下掉,還專(zhuān)門(mén)用Arthas看了一下方法執(zhí)行時(shí)間,那家伙,我sleep 2s,看時(shí)間都3-4s了,所以就想著后面優(yōu)化一下。
所以,這不就到我們的主角SpringWebflux了,就想著用服務(wù)用Webflux,Http請(qǐng)求直接用WebClient了。但是有一個(gè)問(wèn)題就不得不思考了,怎么記錄請(qǐng)求的日志呢?這個(gè)日志很重要,必須的記錄的清楚,不然不用系統(tǒng)間溝通(扯皮背鍋)著實(shí)難搞。
二、怎么記錄日志?
2.1 思考記錄
打印日志,那第一步得去看看官網(wǎng)咯?看看官網(wǎng)提供了什么解決方案
2.1.1 官網(wǎng)尋答案
打開(kāi)官網(wǎng),翻到Filter這一頁(yè),發(fā)現(xiàn)著實(shí)有可以記錄日志的,我把官網(wǎng)代碼粘貼在下圖。
WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();
可以看到,我們是可以拿到Request的,這讓我一頓高興,那這操作起來(lái)不簡(jiǎn)單? 但是呢,凡事不能高興太早。
拿到這ClientRequest去代碼一頓敲(clientRequest.xx),這個(gè)點(diǎn)都被我按爛了,發(fā)現(xiàn)并沒(méi)有可以獲取RequestBody的方法,只有Headers,URI... 可惜這都不是我要的啊。沒(méi)辦法,那就只有換別的方法了。
順便點(diǎn)了一下filter的ExchangeFilterFunction,發(fā)現(xiàn)里面有方法還能拿到Rresponse。代碼如下:
static ExchangeFilterFunction ofResponseProcessor(Function<ClientResponse, Mono<ClientResponse>> processor) {
Assert.notNull(processor, "ClientResponse Function must not be null");
return (request, next) -> next.exchange(request).flatMap(processor);
}
然后拿著Response又來(lái)一頓操作,可以拿到Response Body等,但是...但是...如果我們?cè)赗esponse里面把Response Body 使用掉,就會(huì)報(bào)錯(cuò):nested exception is java.lang.IllegalStateException: The client response body can only be consumed once。如圖所示。

沒(méi)有辦法,查看了ClientResponse實(shí)現(xiàn)類(lèi)org.springframework.web.reactive.function.client.DefaultClientResponse,這個(gè)是個(gè)包權(quán)限的類(lèi),雖然很想直接用,比如:requestDescription(URL路徑),getBody, 但是是在拿不到啊~~難受
既然如此,那就只有從別的地方尋找。
2.1.2 其它方案
在官網(wǎng)上WebFlux有三種方案,分別是集成Jetty, Netty, HttpComponent5。由于我使用的是HttpComponent5,所以就直接從http這個(gè)尋找方案了。
尋尋覓覓,找到了了相似的方案,也可以添加Interceptor。我就直接貼出代碼,如下圖。

但是在實(shí)現(xiàn)時(shí),發(fā)現(xiàn)了有兩個(gè)問(wèn)題。
問(wèn)題一
打印Request日志的時(shí)候會(huì)出現(xiàn)兩次。
問(wèn)題二
打印Response的時(shí)候也會(huì)出現(xiàn)nested exception is java.lang.IllegalStateException: The client response body can only be consumed once這個(gè)異常。
出現(xiàn)這個(gè)問(wèn)題,沒(méi)有發(fā)現(xiàn)啥好用的解決方案。
2.1.3 Google
后來(lái)就想著想換成Jetty和Netty,就google了一波,我直接把連接貼出來(lái),就不過(guò)多描述了。 連接如下:
Logging Spring WebClient Calls | Baeldung 這個(gè)方案也嘗試了一波,但是效果如上。會(huì)出現(xiàn)問(wèn)題一,或者在獲取Body的時(shí)候出現(xiàn)問(wèn)題。
2.2 解決方案
在經(jīng)歷了好幾天Debug和測(cè)試后,就選擇了妥協(xié)方案,直接在webClient請(qǐng)求的時(shí)候打印日志。這還算是一個(gè)完美的解決方案。
我貼出一部分代碼,如下。
/**
* 打印全流程日志,需要外部傳入?yún)?shù)
*/
public static Function<ClientResponse, Mono<String>> logging(String url, String method, Object reqBody) {
return (clientResponse -> clientResponse.bodyToMono(String.class).doOnSuccess(body -> {
if (log.isDebugEnabled()) {
log.info("\n" +
"TraceId : {}, {}\n" +
"URI : {}, \n" +
"Param : {}, \n" +
"RespHeader : {}, \n" +
"RespStatus : {}, \n" +
"Response : {}", clientResponse.logPrefix(), method, url, JsonUtils.toJson(reqBody),
clientResponse.headers().asHttpHeaders(), clientResponse.rawStatusCode(),
StringUtils.abbreviate(body, 4000));
}
}));
}
在獲取到Body的時(shí)候,直接轉(zhuǎn)化成String.class,如果直接轉(zhuǎn)成ParameterizedTypeReference就會(huì)失去部分原請(qǐng)求的數(shù)據(jù),這樣在扯皮的時(shí)候不好找證據(jù)呀?。ㄎ覀兇蛴∏逦娜罩臼菫榱烁玫牟樵?xún)問(wèn)題,不是扯皮︿( ̄︶ ̄)︿ )
這樣打印出來(lái)的日志就是我們想要的了,舒服啊~ 這樣看起來(lái)才舒服。
還寫(xiě)了一些其它的小工具,獲取連接,使用方式如下:

三、總結(jié)
以上是探索WebClient的打印日志的方式,還有不足,歡迎大家討論,提出更好的方式。謝謝!