Webflux 中 exchange 與 retrieve 的差異

在什麼情況下?

我當時正在使用 Spring Boot 2.x.x 版本的 Webflux。

當我在程式碼中使用 Webclient 時,我通常會在請求後使用 exchange()。原因是我所使用的 API 在失敗時,會回傳 200 HTTP 狀態碼 並帶有錯誤訊息主體。

當時我犯了一個大錯,我以為 exchange()retrieve() 是一樣的。

如果你也這麼想,那就錯了。它們是不同的。那麼 exchangeretrieve 之間有什麼區別呢?

回應方式不同

根據 官方文件

  • 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.bodyToMonobodyToFlux 時:


.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)

參考資料

AD