概要
WebClientにてHTTP通信時、レスポンス内容をログ出力させる方法についてまとめた。
ログ出力対象は、「HTTPステータス」「ヘッダー」「ボディ」となる。
※レスポンスの場合、リクエスト時と異なりボディの参照はフィルターで可能
前提
リクエスト時のログ出力を行う方法の続きとなる。
概要 WebClientにてHTTP通信時、リクエスト内容をログ出力させる方法についてまとめた。 ログ出力対象は、「HTTPメソッド」「URL」「ヘッダー」となる。 ※「ボディ」は今回対象外 前提 WebCli[…]
概要 WebClientにてHTTP通信時、リクエストボディをログ出力させる方法についてまとめた。 尚、HTTPメソッドやヘッダー等の情報をログ出力させる方法については以下を参照。
ExchangeFilterFunction
リクエスト時と同様、ExchangeFilterFunctionインターフェースを利用してレスポンスログを出力する「フィルター」機能を作成していく。
このフィルターをWebClientに適用させることで、API通信時に共通処理としてレスポンスログを出力させることができる。
基本的な使い方
ExchangeFilterFunctionにレスポンスログ出力フィルター処理を定義して、WebClientのフィルターに登録する
実装方法
レスポンスログ出力処理とWebClientのフィルター登録を行う。
レスポンスログ出力処理
ExchangeFilterFunctionインターフェースに、HTTP通信後のレスポンスログ出力処理を定義する。
WebClientConfig.java
/**
* WebClientのレスポンスログ出力
* @return
*/
private ExchangeFilterFunction logResponse() {
return ExchangeFilterFunction.ofResponseProcessor(res -> {
// HTTPステータスとヘッダー情報取得
StringBuilder sb = createLogWithoutBody(res);
// レスポンスボディ判定
long length = res.headers().contentLength().orElse(-1L);
boolean hasChunked = res.headers().asHttpHeaders()
.getFirst("Transfer-Encoding") != null &&
res.headers().asHttpHeaders().getFirst("Transfer-Encoding").equalsIgnoreCase("chunked");
if (length == 0 && !hasChunked) {
// ボディなしの場合
sb.append("★Response Body: ").append("No Body\n");
logger.debug(sb.toString());
return Mono.just(res);
}
// ボディありの場合
return res.bodyToMono(String.class)
.flatMap(body -> {
sb.append("★Response Body: ").append(body).append("\n");
logger.debug(sb.toString()); // ログにボディ部追加
// bodyを再度、呼び出し元で使えるように再作成
ClientResponse newResponse = ClientResponse.create(res.statusCode())
.headers(headers -> headers.addAll(res.headers().asHttpHeaders()))
.body(body)
.build();
return Mono.just(newResponse);
});
});
}
/**
* レスポンスボディ以外のログ情報を作成する
* @param res
* @return
*/
private StringBuilder createLogWithoutBody(ClientResponse res) {
var sb = new StringBuilder();
sb.append("\n\n----------★★レスポンス★★----------\n")
.append("★Response Status Code: ").append(res.statusCode()).append("\n")
.append("★Response Headers:\n");
res.headers().asHttpHeaders().forEach((name, values) -> values.forEach(
value -> sb.append(" ")
.append(name)
.append(": ")
.append(value)
.append("\n")));
return sb;
}
ログ出力処理を定義したExchangeFilterFunctionを返却するメソッドを用意する。
ExchangeFilterFunction.ofResponseProcessor()を使用することで、WebClientのHTTP通信後のフィルター処理を定義できる。
引数にClientResponseを受け取って、ログ出力に必要な情報を参照する。
createLogWithoutBody()にて
ClientResponse#statusCodeを使用して、ログ出力用のStringBuilderにHTTPステータスコードを設定している。
createLogWithoutBody()にて
ClientResponse#headersを使用して、ログ出力用のStringBuilderにヘッダー情報を設定している。
レスポンスボディが存在するかどうか判定している。
sb.append(“★Response Body: “).append(“No Body\n”);
logger.debug(sb.toString());
return Mono.just(res);
レスポンスにボディがない場合は”No Body”をログに設定し、WebClientの呼び出し元にClientResponseを返却している。
sb.append(“★Response Body: “).append(body).append(“\n”);
logger.debug(sb.toString()); // ログにボディ部追加
// bodyを再度、呼び出し元で使えるように再作成
ClientResponse newResponse = ClientResponse.create(res.statusCode())
.headers(headers -> headers.addAll(res.headers().asHttpHeaders()))
.body(body)
.build();
レスポンスボディの内容をログ設定した後、ボディを再利用できるように新たなClientResponseを構築して返却している。
これにより、res.bodyToMono()でボディを一度消費しても、後続の処理で引き続き利用可能になる。
※ClientResponseのボディはストリームとして一度しか読み取れないため、読み込んだ後にそのままresを返しても呼び出し元ではボディが取得できなくなる。
フィルター登録
DIコンテナで管理されているWebClientに対して、
レスポンスログ出力を定義したExchangeFilterFunctionをフィルターとして登録する。
WebClientConfig.java
@Bean
public WebClient webClient(ReactorClientHttpConnector reactorClientHttpConnector) {
return WebClient.builder()
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE + ", " + MediaType.APPLICATION_PROBLEM_JSON_VALUE)
.clientConnector(reactorClientHttpConnector)
.filter(logRequest())
.filter(logResponse())
.build();
}
動作確認
DIコンテナからWebClientを取得して、何らかのHTTP通信を行う。
レスポンスボディなし
POST通信を行う。
動作確認用クラス
// リクエストボディ
var req = new Resource("4", "パスタ", LocalDate.of(2022, 5, 1));
// POSTリクエスト
ResponseEntity<Void> resEntity = client.post(URI.create("http://localhost:8080/rest_prototype/rest02/create"), req);
// 動作確認
System.out.println("★★動作確認★★");
System.out.println("ステータス:" + resEntity.getStatusCode());
System.out.println("ヘッダー:" + resEntity.getHeaders());
System.out.println("ボディ:" + resEntity.getBody());
コンソール
----------★★レスポンス★★----------
★Response Status Code: 201 CREATED
★Response Headers:
Location: http://localhost:8080/rest_prototype/rest01/4
Content-Length: 0
Date: Sun, 29 Jun 2025 11:58:29 GMT
★Response Body: No Body
★★動作確認★★
ステータス:201 CREATED
ヘッダー:[Location:"http://localhost:8080/rest_prototype/rest01/4", Content-Length:"0", Date:"Sun, 29 Jun 2025 11:58:29 GMT"]
ボディ:null
レスポンスボディあり
GET通信を行う。
動作確認用クラス
// GETリクエスト
ResponseEntity<List<Resource>> resEntity = client.getEntityList(URI.create("http://localhost:8080/rest_prototype/rest05/"), Resource.class);
// 動作確認
System.out.println("★★動作確認★★");
System.out.println("ステータス:" + resEntity.getStatusCode());
System.out.println("ヘッダー:" + resEntity.getHeaders());
System.out.println("ボディ:" + resEntity.getBody());
コンソール
----------★★レスポンス★★----------
★Response Status Code: 200 OK
★Response Headers:
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 29 Jun 2025 12:01:53 GMT
★Response Body: [{"id":"1","name":"りんご","hogeDate":"2025-02-01"},{"id":"2","name":"ごりら","hogeDate":"2024-06-05"},{"id":"3","name":"らっぱ","hogeDate":"2023-05-10"},{"id":"4","name":"パスタ","hogeDate":"2022-05-01"}]
★★動作確認★★
ステータス:200 OK
ヘッダー:[Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Sun, 29 Jun 2025 12:01:53 GMT"]
ボディ:[Resource(id=1, name=りんご, hogeDate=2025-02-01), Resource(id=2, name=ごりら, hogeDate=2024-06-05), Resource(id=3, name=らっぱ, hogeDate=2023-05-10), Resource(id=4, name=パスタ, hogeDate=2022-05-01)]
まとめ
☑ WebClientのレスポンス内容をログ出力するには、ExchangeFilterFunction.ofResponseProcessor()を利用する
☑ ClientResponseを利用することで、「HTTPステータス」「ヘッダー」「ボディ」などの情報を参照できる
☑ レスポンスボディは一度しか読み取れないため、ログ出力後も呼び出し元で利用したい場合はClientResponseを再構築する必要がある