どのような状況で?
私は Spring Boot 2.x.x バージョンで Webflux を使用していました。
コード内で Webclient を使用する際、通常はリクエストの後に exchange() を使用していました。なぜなら、使用していた API が失敗した際に 200 HTTP ステータス とエラーボディを返していたからです。
当時、私は大きな勘違いをしていました。exchange() と retrieve() が同じものだと思い込んでいたのです。
もしあなたもそう思っているなら、それは間違いです。両者は異なります。では、exchange と retrieve にはどのような違いがあるのでしょうか?
レスポンスが異なる
公式ドキュメント によると:
-
WebClient.ResponseSpec
retrieve()- レスポンスステータスが 200 OK の場合のレスポンスボディ
- デフォルトでは、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 Hunger, Stay Foolish(ハングリーであれ、愚かであれ)