1. 概述
本教程將學習如何實現高效的 <em >RestTemplate</em> 請求/響應日誌記錄。 這對於調試兩個服務器之間的交互特別有用。
不幸的是,Spring Boot 沒有提供一種方便的方法來檢查或記錄簡單的 JSON 響應體。
我們將探索多種方法來記錄 HTTP 頭部或,更重要的是,HTTP 實體。
注意:Spring 的 <em >RestTemplate</em> 將被棄用,將被 <em >WebClient</em> 取代。 您可以在這裏找到使用 <em >WebClient</em> 的類似文章:記錄 Spring WebClient 調用。
2. 使用 RestTemplate 進行基本日誌記錄
讓我們從在 application.properties 文件中配置 RestTemplate 日誌記錄開始:
logging.level.org.springframework.web.client.RestTemplate=DEBUG因此,我們可以僅看到基本信息,例如請求 URL、方法、請求體和響應狀態:
o.s.w.c.RestTemplate - HTTP POST http://localhost:8082/spring-rest/persons
o.s.w.c.RestTemplate - Accept=[text/plain, application/json, application/*+json, */*]
o.s.w.c.RestTemplate - Writing [my request body] with org.springframework.http.converter.StringHttpMessageConverter
o.s.w.c.RestTemplate - Response 200 OK然而,響應體在這裏未被記錄,這不太理想,因為它包含了最重要的信息。
為了解決這個問題,我們將選擇使用 Apache HttpClient 或 Spring 攔截器。
3. 使用 Apache HttpClient 記錄日誌
首先,我們需要讓 RestTemplate 使用 Apache HttpClient 實現。
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>5.2.1</version>
</dependency>當創建 RestTemplate 實例時,我們應該告訴它我們正在使用 Apache HttpClient:
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());然後,讓我們在 application.properties文件中配置客户端日誌記錄器:
logging.level.org.apache.http=DEBUG
logging.level.httpclient.wire=DEBUG現在我們可以看到請求/響應頭和內容主體。
o.a.http.headers - http-outgoing-0 >> POST /spring-rest/persons HTTP/1.1
o.a.http.headers - http-outgoing-0 >> Accept: text/plain, application/json, application/*+json, */*
// ... more request headers
o.a.http.headers - http-outgoing-0 >> User-Agent: Apache-HttpClient/4.5.9 (Java/1.8.0_171)
o.a.http.headers - http-outgoing-0 >> Accept-Encoding: gzip,deflate
org.apache.http.wire - http-outgoing-0 >> "POST /spring-rest/persons HTTP/1.1[\r][\n]"
org.apache.http.wire - http-outgoing-0 >> "Accept: text/plain, application/json, application/*+json, */*[\r][\n]"
org.apache.http.wire - http-outgoing-0 >> "Content-Type: text/plain;charset=ISO-8859-1[\r][\n]"
// ... more request headers
org.apache.http.wire - http-outgoing-0 >> "[\r][\n]"
org.apache.http.wire - http-outgoing-0 >> "my request body"
org.apache.http.wire - http-outgoing-0 << "HTTP/1.1 200 [\r][\n]"
org.apache.http.wire - http-outgoing-0 << "Content-Type: application/json[\r][\n]"
// ... more response headers
org.apache.http.wire - http-outgoing-0 << "Connection: keep-alive[\r][\n]"
org.apache.http.wire - http-outgoing-0 << "[\r][\n]"
org.apache.http.wire - http-outgoing-0 << "21[\r][\n]"
org.apache.http.wire - http-outgoing-0 << "["Lucie","Jackie","Danesh","Tao"][\r][\n]"
然而,這些日誌冗餘且不便於調試。
在下一章中,我們將探討如何解決這個問題。
4. 使用 RestTemplate 攔截器記錄請求體
我們可以通過配置 RestTemplate 的攔截器,作為另一種解決方案。
4.1. 日誌攔截器實現
首先,我們創建一個新的 LoggingInterceptor 以自定義日誌記錄 。此攔截器將請求體記錄為簡單的字節數組。但是,對於響應,我們需要讀取整個體內容流:
public class LoggingInterceptor implements ClientHttpRequestInterceptor {
static Logger LOGGER = LoggerFactory.getLogger(LoggingInterceptor.class);
@Override
public ClientHttpResponse intercept(
HttpRequest req, byte[] reqBody, ClientHttpRequestExecution ex) throws IOException {
LOGGER.debug("Request body: {}", new String(reqBody, StandardCharsets.UTF_8));
ClientHttpResponse response = ex.execute(req, reqBody);
InputStreamReader isr = new InputStreamReader(
response.getBody(), StandardCharsets.UTF_8);
String body = new BufferedReader(isr).lines()
.collect(Collectors.joining("\n"));
LOGGER.debug("Response body: {}", body);
return response;
}
}請注意,此攔截器會影響響應內容本身,正如下一章將要發現的。
4.2. 使用 Interceptor 與 RestTemplate
現在,我們需要處理一個流式問題:當攔截器消費響應流時,我們的客户端應用程序將看到一個空響應體。
為了避免這種情況,我們應該使用 BufferingClientHttpRequestFactory:它會將流內容緩衝到內存中。 這樣,它就可以被攔截器和我們的客户端應用程序兩次讀取:
ClientHttpRequestFactory factory =
new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
RestTemplate restTemplate = new RestTemplate(factory);然而,使用該工廠會導致性能下降,我們將會在下一小節中詳細説明。
然後我們可以將我們的日誌攔截器添加到 RestTemplate 實例中——我們將它附加在現有的攔截器之後,如果存在的話:
List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new LoggingInterceptor());
restTemplate.setInterceptors(interceptors);
因此,日誌中僅包含必要的信息:
c.b.r.l.LoggingInterceptor - Request body: my request body
c.b.r.l.LoggingInterceptor - Response body: ["Lucie","Jackie","Danesh","Tao"]4.3. RestTemplate攔截器缺點
正如之前所述,使用 BufferingClientHttpRequestFactory 存在嚴重缺點:它會抵消流式傳輸的好處。因此,將整個請求體數據加載到內存中可能會使我們的應用程序暴露於性能問題之中。更糟糕的是,它可能導致 OutOfMemoryError。
為了防止這種情況發生,一種可能的選項是假設這些冗長的日誌在數據量增加時會被關閉,這通常發生在生產環境中。例如,我們可以僅在 DEBUG 級別被啓用時使用緩衝的 RestTemplate 實例:
RestTemplate restTemplate = null;
if (LOGGER.isDebugEnabled()) {
ClientHttpRequestFactory factory
= new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
restTemplate = new RestTemplate(factory);
} else {
restTemplate = new RestTemplate();
}同樣,我們還將確保攔截器僅在 DEBUG 日誌啓用時讀取響應:
if (LOGGER.isDebugEnabled()) {
InputStreamReader isr = new InputStreamReader(response.getBody(), StandardCharsets.UTF_8);
String body = new BufferedReader(isr)
.lines()
.collect(Collectors.joining("\n"));
LOGGER.debug("Response body: {}", body);
}5. 結論
`RestTemplate 的請求/響應日誌記錄並非易事,因為 Spring Boot 默認不包含該功能。
幸運的是,我們發現可以使用 Apache HttpClient 記錄器來獲取詳細的交換數據跟蹤信息。
或者,我們可以實現一個自定義攔截器以獲取更易讀的日誌。但是,對於大量數據量,這可能會導致性能下降。