【Spring MVC】WebClientでGET通信(exchangeToMonoでハンドリング)

概要

exchangeToMonoメソッドを使用した、基本的なGET通信の使用方法についてまとめた。
今回はリクエスト送信後に意図しないレスポンスを受け取った際のハンドリング方法について紹介する。

尚、動作確認を行うための事前準備については、以下に記載している。

あわせて読みたい

概要 RESTなAPIにアクセスするのはRestTemplateではなくWebClientが今後推奨となるため、WebClientの概要についてまとめた。 WebClientを使用するための準備と、どんなメソッドがあるのかを紹介している[…]

WebClientの導入

 

基本的な使い方

WebClient#retrieveにてリクエスト送信した際には、都度ResponseSpec#onStatusを呼び出してステータスハンドリングを行っていた。
WebClient#exchangeToMonoでは、引数でClientResponseを取得できるため、ステータス以外にも用途に応じて柔軟なエラー判定を実装できる。

 

ClientResponse

exchangeToMonoでリクエスト送信後、ClientResponseを使用して以下のような情報を取得できる。

 

種類 取得方法
ステータス ClientResponse#statusCode() 200, 400, 500など
ヘッダー ClientResponse#headers().asHttpHeaders() Content-Type, Transfer-Encodingなど
ボディ ClientResponse#bodyToMono(指定の型.class) JSON文字列, DTOなど

 

実装方法

ClientResponseを使用した、レスポンス結果の各種ハンドリング方法について紹介する。

 

ステータスハンドリング

4xx系と5xx系の場合はエラーとしたいとき。

 

webClient.get()
.uri(URI情報)
.exchangeToMono(res -> { // ⇒引数でClientResponseを取得
  if (res.statusCode().is4xxClientError()) { // ⇒4xxエラーハンドリング
    return Mono.error(new RuntimeException("Client error"));
  } else if (res.statusCode().is5xxServerError()) { // ⇒5xxエラーハンドリング
    return Mono.error(new RuntimeException("Server error"));
  }
  return res.toEntity(responseType);
})

 

ヘッダーハンドリング

レスポンスのコンテンツがJSONではない場合にエラーとしたいとき。

 

webClient.get()
.uri(uri)
.exchangeToMono(res -> {
  // ⇒ヘッダーを取得してJSON以外の場合はエラーハンドリング
  HttpHeaders headers = res.headers().asHttpHeaders();
  MediaType mediaType = headers.getContentType();
  if (mediaType == null || !mediaType.includes(MediaType.APPLICATION_JSON)) {
    // 意図しないヘッダーが返却された場合の処理
    return Mono.error(new IllegalStateException("Unexpected Content-Type"));
  }
  return res.toEntity(responseType);
})
.block();

 

ボディハンドリング

レスポンスボディに”error”を含む場合にエラーとしたいとき。
※その他の場所の解説は割愛

 

webClient.get()
.uri(uri)
.exchangeToMono(res -> {
  // ⇒レスポンスボディを取得して"error"文字列を含む場合はエラーハンドリング
  return res.bodyToMono(String.class).flatMap(body -> {
  if (body.contains("error")) {
    return Mono.error(new RuntimeException("error has occurred"));
  }
  T mapped = null;
  try {
    // String を responseType に変換
    mapped = new ObjectMapper().readValue(body, responseType);
  } catch (JsonProcessingException e) {
    Mono.error(new IllegalStateException("Unexpected error has occurred"));
  }
  ResponseEntity<T> entity = ResponseEntity
    .status(res.statusCode())
    .headers(res.headers().asHttpHeaders())
    .body(mapped);
    return Mono.just(entity);
  });
})
.block();

 

使用例

APIアクセスクラスにメソッドを追加して動作確認を行う。

 

APIアクセスクラス

get + exchangeToMono + ClientResponseを使用して、ステータスハンドリング付きのGET通信を行うメソッドを追加する。
処理の流れとしては2xx系以外のステータスが返却された場合、例外をスローする。

 

WebApiClient.java


package com.example.webclient_prototype.biz;

import java.net.URI;

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.exception.ClientErrorException;
import com.example.webclient_prototype.exception.ServerErrorException;
import com.example.webclient_prototype.exception.UnknownErrorException;
import com.example.webclient_prototype.resource.error.ApiErrorInfo;

import reactor.core.publisher.Mono;

/**
 * WebClientを使用したAPI疎通クラス
 */
@Component
public class WebApiClient {

	@Autowired
	private WebClient webClient;

	/**
	 * 指定した型のボディを含むResponseEntityを取得する
	 * 
	 * @param uri
	 * @param responseType
	 * @return
	 */
	public <T> ResponseEntity<T> getEntity(URI uri, Class<T> responseType) {
		return webClient.get().uri(uri).exchangeToMono(res -> {
			if (res.statusCode().is4xxClientError()) {
				// 4xx系ステータス
				return res.bodyToMono(ApiErrorInfo.class).flatMap(body -> Mono
						.error(new ClientErrorException(body.getErrorTitle(), body.getErrorMsg(), body.getStatus())));
			} else if (res.statusCode().is5xxServerError()) {
				// 5xx系ステータス
				return res.bodyToMono(ApiErrorInfo.class)
						.flatMap(body -> Mono.error(new ServerErrorException(body.getErrorMsg())));
			} else if (!res.statusCode().is2xxSuccessful()) {
				// 2xx, 4xx, 5xx以外の想定外ステータス
				return Mono.error(new UnknownErrorException("想定外エラー"));
			}
			// 2xx系の処理
			return res.toEntity(responseType);
		}).block();
	}
}

 

if (res.statusCode().is4xxClientError()) {
(略)
} else if (res.statusCode().is5xxServerError()) {
(略)
} else if (!res.statusCode().is2xxSuccessful()) {

exchangeToMonoの引数に取得したClientResponseを用いて、任意のステータスハンドリングを行っている。

 

res.bodyToMono(ApiErrorInfo.class)

ClientResponseが保持するレスポンスボディ情報を、APIエラー情報クラスにバインドしている。

 

body -> Mono.error(new ClientErrorException(body.getErrorTitle(), …)

「Mono.error( … )」とすることで、例外オブジェクトを保持するMonoクラスを生成し、block()が呼ばれたタイミングでMonoが保持する例外がスローされる。
上記は、ClientResponseから必要な情報を取得して自作例外オブジェクトを生成している。

 

動作確認

動作確認を行う。

 

動作確認用クラス


try {
	// GETリクエスト(400エラー発生)
	client.getEntity(URI.create("http://localhost:8080/rest_prototype/rest06/?hogeDate=2025-02-01"), Resource.class);

} catch (ClientErrorException e) {
	// 動作確認
	System.out.println(e);
}

 

APIアクセスクラスに定義したメソッドにより、4xx系ステータスが返却された場合はClientErrorExceptionがスローされる。

 

コンソール


ClientErrorException(status=400, title=入力エラー, detailMessage=入力に誤りあり)

 

 

まとめ

 

☑ exchangeToMono()は、引数で受け取ったClientResponseをもとに必要なハンドリング処理を行う

☑ ClientResponseはレスポンス全体にアクセスできるため、用途に応じてステータス/ヘッダー/ボディを自由に処理できる

☑ retrieve() + onStatus()に比べて、より柔軟で詳細な制御が可能なのが大きな特徴

 

 

スポンサーリンク