在什麼情況下?
我當時正在使用 Spring Boot 2.x.x 版本的 Webflux。
當我在程式碼中使用 Webclient 時,我通常會在請求後使用 exchange()。原因是我所使用的 API 在失敗時,會回傳 200 HTTP 狀態碼 並帶有錯誤訊息主體。
當時我犯了一個大錯,我以為 exchange() 和 retrieve() 是一樣的。
如果你也這麼想,那就錯了。它們是不同的。那麼 exchange 和 retrieve 之間有什麼區別呢?
回應方式不同
根據 官方文件:
-
WebClient.ResponseSpec
retrieve()- 當回應狀態為 200 OK 時,取得回應主體 (Response body)
- 預設情況下,4xx 和 5xx 的回應會導致 WebClientResponseException。若要自訂錯誤處理,請使用 onStatus 處理器。
-
reactor.core.publisher.Mono<ClientResponse>
exchange()- 自 Spring 5.3 起,由於存在記憶體和/或連線洩漏的可能性,此方法已被標記為不建議使用
-
當使用
exchange()時,應用程式必須負責消耗任何回應內容,無論情境為何(成功、錯誤、意外資料等)。如果不這樣做,可能會導致記憶體洩漏。 -
請改用
exchangeToMono(Function)或exchangeToFlux(Function)
-
exchangeToMono,exchangeToFlux- 自 Spring >= 5.3 起提供
- 自 Spring Boot 版本 >= 2.4.x 起提供
何時會發生記憶體洩漏
當你在以下程式碼中沒有使用 ClientResponse.bodyToMono 或 bodyToFlux 時:
.exchange()
.flatMap { clientResponse ->
if (clientResponse.statusCode() === HttpStatus.OK) {
// 請求成功
clientResponse.bodyToMono(CLASS::class.java)
}
else {
// HTTP 請求失敗。在這裡發生了記憶體洩漏
throw Exception()
}
}
在失敗的情況下,你會面臨記憶體洩漏,因為你沒有消耗回應主體。
如果你沒有消耗回應主體,那麼你的連線就不會被中斷。
現在,發生了什麼事……?你的 HTTP 連線池將會被已使用的執行緒填滿。當沒有更多可用執行緒時,你的請求將會失敗。
如何解決?
如果你使用的 Spring Boot 版本高於 2.4.x,請像下面這樣使用 exchangeToMono:
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else if (response.statusCode().is4xxClientError()) {
return response.bodyToMono(ErrorContainer.class);
}
else {
return Mono.error(response.createException());
}
});
如果你使用的 Spring Boot 版本低於 2.4.x,請使用 retrieve(),或者在拋出異常之前消耗掉你的回應主體。
範例如下:
clientResponse.bodyToMono<String>().defaultIfEmpty("").map {
throw Exception()
}
結論
我認為在使用函式庫之前,我應該養成正確閱讀和使用文件的習慣。
函式庫並不會自動處理所有事情。
求知若飢,虛心若愚 (Stay Hungry, Stay Foolish)