概要
WebClientにてHTTP通信時、リクエストボディをログ出力させる方法についてまとめた。
尚、HTTPメソッドやヘッダー等の情報をログ出力させる方法については以下を参照。
概要 WebClientにてHTTP通信時、リクエスト内容をログ出力させる方法についてまとめた。 ログ出力対象は、「HTTPメソッド」「URL」「ヘッダー」となる。 ※「ボディ」は今回対象外 前提 WebCli[…]
前提
WebClientのBean定義方法などについては以下を参照。
概要 RESTなAPIにアクセスするのはRestTemplateではなくWebClientが今後推奨となるため、WebClientの概要についてまとめた。 WebClientを使用するための準備と、どんなメソッドがあるのかを紹介している[…]
リクエストボディの参照タイミング
リクエストボディをPOST等で送信する場合、以下のような流れで処理される。
POST送信の流れ
WebClient.post()
.bodyValue(リクエストボディ) // ⇒リクエストボディをBodyInserterに変換
.exchangeToMono() // ⇒リクエスト送信を開始
└─ exchange()
└─ request.writeTo()
└─ BodyInserter.insert() // ⇒ここでリクエストボディが実際に送信される
WebClient.post()によるリクエスト処理では、exchangeToMono()などで送信が開始された後、
内部でBodyInserter.insert()が呼び出され、その中でリクエストボディがJSON化されて送信される。
このときリクエストボディを参照できるのはBodyInserter.insert()のタイミングのみとなる。
そのため、リクエストボディをログ出力したい場合は、BodyInserterをラップしてinsert()メソッド内にログ出力処理を追加する必要がある。
リクエストボディのログ処理
BodyInserterをラップしてinsert()メソッドをカスタマイズしたうえで、WebClientのリクエストボディとして設定する。
BodyInserterのカスタマイズ
BodyInserterをラップしたPreProcessingBodyInserterを用意する。
このクラスはリクエスト送信前に、リクエストボディに対して何らかの任意の処理を挟むラッパークラスとなる。
※ログ出力専用ではなく、汎用的な送信前処理を行うクラス
PreProcessingBodyInserter.java
package com.example.webclient_prototype.http.inserter;
import java.util.function.Consumer;
import org.springframework.http.client.reactive.ClientHttpRequest;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import reactor.core.publisher.Mono;
/**
* リクエストボディの送信前にカスタマイズ処理を実施するBodyInserterラッパークラス
*
* @param <T> リクエストボディの型
*/
public class PreProcessingBodyInserter<T> implements BodyInserter<T, ClientHttpRequest> {
/** リクエストボディ */
private final T body;
/** リクエスト送信前に実行するカスタム処理(ログ出力など) **/
private final Consumer<T> preProcessor;
/**
* コンストラクタ
* リクエストボディと送信前のカスタム処理を設定する
*
* @param body
* @param bodyLogger
*/
public PreProcessingBodyInserter(T body, Consumer<T> preProcessor) {
this.body = body;
this.preProcessor = preProcessor;
}
/**
* リクエストボディ送信前の処理つきBodyInserterを生成
*
* @param body
* @return
*/
public static <T> PreProcessingBodyInserter<T> fromObject(T body, Consumer<T> preProcessor) {
return new PreProcessingBodyInserter<>(body, preProcessor);
}
/**
* カスタマイズしたリクエストボディ送信処理
* Counsumerで定義したメソッドを送信前に実施する
*
* @param outputMessage
* @param context
* @return
*/
@Override
public Mono<Void> insert(ClientHttpRequest outputMessage, BodyInserter.Context context) {
// カスタム処理の実行
preProcessor.accept(body);
// リクエスト送信処理
return BodyInserters.fromValue(body).insert(outputMessage, context);
}
}
private final T body;
private final Consumer<T> preProcessor;
public PreProcessingBodyInserter(T body, Consumer<T> preProcessor) {
リクエストボディと、関数型インターフェースのConsumerをフィールドで保持する。
コンストラクタでフィールドを初期化する。
静的ファクトリーメソッドにてリクエストボディと送信前カスタマイズ処理(Consumer)を受け取り、
ラップしたカスタムクラスのインスタンスを生成して返却する。
public Mono<Void> insert(ClientHttpRequest outputMessage, BodyInserter.Context context) {
BodyInserter.insert()をオーバーライドしている。
これにより、ラップしたこのクラスをWebClientのボディに使用することで、内部でこのinsert()が呼ばれる。
ラップしたクラスの生成時に渡したカスタマイズ処理(Consumer)を実行する。
リクエストボディをもとに、任意の処理を行っている。
内部で呼ばれていた、もともとのリクエストボディ送信処理を呼び出す。
これにより、カスタマイズ処理が実行された後でもともとのボディ送信処理の軌道に戻すことができる。
WebClientの設定
作成したラッパークラスをリクエストボディに設定する。
WebApiClient.java
package com.example.webclient_prototype.biz;
import java.net.URI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import com.example.webclient_prototype.config.WebClientConfig;
import com.example.webclient_prototype.http.inserter.PreProcessingBodyInserter;
/**
* WebClientを使用したAPI疎通クラス
*/
@Component
public class WebApiClient {
/** ロガー */
private static final Logger logger = LoggerFactory.getLogger(WebClientConfig.class);
@Autowired
private WebClient webClient;
public ResponseEntity<Void> post(URI uri, Object requestBody) {
return webClient.post().uri(uri)
.body(PreProcessingBodyInserter.fromObject(requestBody,
b -> logger.debug("\n\n----------★★リクエストボディログ★★----------\n★Request Body: {}\n", b)))
.exchangeToMono(res -> res.toBodilessEntity())
.block();
}
}
WebClientのリクエストボディに、ラップしたBodyInserterを設定している。
これにより、リクエストボディが送信されるタイミングでカスタムクラスのPreProcessingBodyInserter.insert()が呼ばれる。
「b -> logger.debug(…)」は、Consumerという「引数を1つ受け取って処理する」関数型インターフェースに渡すラムダ式となる。
このコードでは、リクエストボディをログ出力させる処理をConsumerとして渡しておき、accept(body)が呼ばれたタイミングでボディをもとにログ出力が行われる。
動作確認
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());
コンソール
----------★★リクエストログ★★----------
★Request Method: POST
★Request URI: http://localhost:8080/rest_prototype/rest02/create
★Request Headers:
Content-Type: application/json
Accept: application/json, application/problem+json
----------★★リクエストボディログ★★----------
★Request Body: Resource(id=4, name=パスタ, hogeDate=2022-05-01)
★★動作確認★★
ステータス:201 CREATED
ヘッダー:[Location:"http://localhost:8080/rest_prototype/rest01/4", Content-Length:"0", Date:"Sun, 29 Jun 2025 04:18:16 GMT"]
ボディ:null
「★★リクエストログ★★」という箇所については、以前の記事で作成したログ処理となるため今回は関係ない。
今回の実装にて「★★リクエストボディログ★★」にリクエストボディが出力されたことがわかる。
まとめ
☑ WebClientのリクエストボディはBodyInserter.insert()で初めて参照できるため、ログ出力もこのタイミングで行う必要がある
☑ リクエストボディのログ出力には、BodyInserterをラップしinsert()メソッドをオーバーライドしてログ処理を差し込む方法が有効
☑ この仕組みにより、WebClientの標準的なボディ送信処理を保ちつつ、送信直前に安全にログを取得できる