【Spring MVC】WebClientにて汎用的なAPI通信(GET/POST/PUT/DELETE)を行う方法

概要

ここまで個別にWebClientのGETやPOSTを行う方法について紹介してきたが、
今回はHTTP通信を汎用的に実行できるメソッドを作成したのでまとめた。

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

あわせて読みたい

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

WebClientの導入

 

汎用通信のやり方

HTTPメソッドごとにWebClient.get()やWebClient.post()を個別のメソッドに用意するのではなく、
共通の処理にリクエスト情報を渡すだけで、任意のHTTP通信(GET/POST/PUT/DELETEなど)を実行できる仕組みを作成する。

 

全体像

汎用メソッドを使用する際の、全体的な流れは以下となる。

① リクエスト情報構築: WebClientRequest
② 汎用メソッド呼び出し: WebApiClient
③ 結果の取得: ResponseEntity<T>

WebClientRequestクラスにて任意のHTTP通信に必要なリクエスト情報を構築し、汎用メソッドを呼び出してResponseEntityを受け取る。

 

事前準備

リクエスト情報構築クラスと汎用メソッドを持つAPIアクセスクラスを用意する。

 

リクエスト情報構築クラス

WebClientのAPI通信に必要な情報を保持するクラスを用意する。
Builderパターンで必要なリクエスト情報をシンプルに構築していく。

 

WebClientRequest.java


package com.example.webclient_prototype.biz;

import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.web.util.UriComponentsBuilder;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * WebClientにて必要なリクエスト情報を保持するクラス
 */
@Data
@AllArgsConstructor
public class WebClientRequest {

	private final HttpMethod method;
	private final URI uri;
	private HttpHeaders headers;
	private final Object body;

	/** WebClientRequest構築後に別途ヘッダーを設定したいとき */
	public void addHeader(String name, String val) {
		this.headers.add(name, val);
	}

	public static Builder builder() {
		return new Builder();
	}

	public static class Builder {
		private HttpMethod method;
		private HttpHeaders headers = new HttpHeaders();
		private Object body;
		private String urlTemplate;
		private final Map<String, Object> queryParams = new HashMap<>();
		private final Map<String, Object> pathParams = new HashMap<>();

		public Builder method(HttpMethod method) {
			this.method = method;
			return this;
		}

		public Builder header(String name, String val) {
			this.headers.add(name, val);
			return this;

		}

		public Builder body(Object body) {
			this.body = body;
			return this;
		}

		public Builder url(String urlTemplate) {
			this.urlTemplate = urlTemplate;
			return this;
		}

		public Builder queryParam(String key, String val) {
			this.queryParams.put(key, val);
			return this;
		}

		public Builder pathParam(String key, String val) {
			this.pathParams.put(key, val);
			return this;
		}

		public WebClientRequest build() {
			if (method == null) {
				throw new IllegalStateException("HTTP method must not be null.");
			}

			if (urlTemplate == null) {
				throw new IllegalStateException("URI must not be null.");
			}

			UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(urlTemplate);
			// クエリパラメータ設定
			if (queryParams != null) {
				for (Map.Entry<String, Object> entry : queryParams.entrySet()) {
					uriBuilder.queryParam(entry.getKey(), entry.getValue());
				}
			}

			// 最終的にURIを生成
			URI uri = uriBuilder.buildAndExpand(pathParams != null ? pathParams : Collections.emptyMap()) // パスパラメータ設定
					.encode() // URIエンコード
					.toUri();
			return new WebClientRequest(this.method, uri, this.headers, this.body);
		}
	}
}

 

private final HttpMethod method; // HTTPメソッド
private final URI uri; // URI
private HttpHeaders headers; // ヘッダー
private final Object body; // リクエストボディ

WebClientの通信に必要なリクエスト情報をフィールドの保持する。
Builderを通して「HTTPメソッド」「URI」「ヘッダー」「リクエストボディ」を構築する。

 

public static class Builder {
WebClientRequestオブジェクトを構築するための補助クラス(ビルダークラス)。
呼び出し元にてmethod()やheader()などのメソッドを必要な部分だけチェーン形式で呼び出し、最終的にbuild()を呼び出すことでWebClientRequestインスタンスを生成する。

 

public Builder method(HttpMethod method) {
Builderパターンのメソッド。
GET/POST/PUT/DELETEなどのHTTPメソッドを設定する。

 

public Builder header(String name, String val) {
Builderパターンのメソッド。
ヘッダーのキー値とバリュー値を指定することで、ヘッダー情報を設定する。

 

public Builder body(Object body) {
Builderパターンのメソッド。
リクエストボディを設定する。

 

public Builder url(String urlTemplate) {
Builderパターンのメソッド。
URIテンプレートを設定する。

 

public Builder queryParam(String key, String val) {
Builderパターンのメソッド。
キー値とバリュー値を指定することで、クエリパラメータ情報を保持する。

 

public Builder pathParam(String key, String val) {
Builderパターンのメソッド。
キー値とバリュー値を指定することで、パスパラメータ情報を保持する。

 

public WebClientRequest build() {
(略)
return new WebClientRequest(this.method, uri, this.headers, this.body);

Builderパターンのメソッド。
Builderオブジェクトが保持している情報をもとに、WebClientRequestインスタンスを生成する。
※HTTPメソッドとURIは必須としている

 

APIアクセスクラス

WebClientを使用した汎用メソッドを用意する。

 

WebApiClient.java


package com.example.webclient_prototype.biz;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;

import com.example.webclient_prototype.config.WebClientConfig;
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.http.inserter.PreProcessingBodyInserter;
import com.example.webclient_prototype.resource.error.ApiErrorInfo;

import reactor.core.publisher.Mono;

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

	/** ロガー */
	private static final Logger logger = LoggerFactory.getLogger(WebClientConfig.class);

	@Autowired
	private WebClient webClient;

	/**
	 * 汎用的なAPI通信を行う
	 * 
	 * @param <T>
	 * @param req
	 * @param typeRef
	 * @return
	 */
	public <T> ResponseEntity<T> callForEntity(WebClientRequest req, ParameterizedTypeReference<T> typeRef) {
		return call(req, typeRef);
	}

	/**
	 * リクエスト送信
	 * 
	 * @param <T>
	 * @param request
	 * @param typeRef
	 * @return
	 */
	private <T> ResponseEntity<T> call(WebClientRequest req, ParameterizedTypeReference<T> typeRef) {
		WebClient.RequestBodySpec spec = webClient.method(req.getMethod()).uri(req.getUri())
				.headers(httpHeaders -> httpHeaders.addAll(req.getHeaders()));

		Mono<ResponseEntity<T>> mono = (req.getBody() != null)
				? spec.body(PreProcessingBodyInserter.fromObject(req.getBody(),
						b -> logger.debug("\n\n----------★★リクエストボディログ★★----------\n★Request Body: {}\n", b)))
						.exchangeToMono(res -> handleResponse(res, typeRef))
				: spec.exchangeToMono(res -> handleResponse(res, typeRef))
				.doOnError(e -> {
					logger.warn("WebClientエラー発生: {}", e.toString());
				}).onErrorResume(e -> {
					// 特定の想定された例外はそのまま通す
					if (e instanceof ClientErrorException || e instanceof ServerErrorException
							|| e instanceof UnknownErrorException) {
						return Mono.error(e); // rethrow
					} else {
						// 想定外の例外を IllegalStateException にラップして通知
						return Mono.error(new IllegalStateException("想定外エラー", e));
					}
				});

		return mono.block();
	}

	/**
	 * API疎通結果(レスポンス)を制御する
	 * 
	 * @param <T>
	 * @param res
	 * @param typeRef
	 * @return 200ステータスの場合、ResponseEntityを返却
	 */
	private <T> Mono<ResponseEntity<T>> handleResponse(ClientResponse res, ParameterizedTypeReference<T> typeRef) {
		if (res.statusCode().is4xxClientError()) {
			return res.bodyToMono(ApiErrorInfo.class).flatMap(body -> Mono
					.error(new ClientErrorException(body.getErrorTitle(), body.getErrorMsg(), body.getStatus())));
		} else if (res.statusCode().is5xxServerError()) {
			return res.bodyToMono(ApiErrorInfo.class)
					.flatMap(body -> Mono.error(new ServerErrorException(body.getErrorMsg())));
		} else if (!res.statusCode().is2xxSuccessful()) {
			return res.createException()
					.flatMap(ex -> Mono.error(new UnknownErrorException(ex.getMessage())));
		}

		return res.toEntity(typeRef);
	}

}

 

public <T> ResponseEntity<T> callForEntity(WebClientRequest req, ParameterizedTypeReference<T> typeRef) {

ResponseEntityを返却する汎用メソッド。
リクエスト構築クラスと、ResponseEntityのボディに指定したい型を受け取る。

 

WebClient.RequestBodySpec spec = webClient.method(req.getMethod()).uri(req.getUri())
.headers(httpHeaders -> httpHeaders.addAll(req.getHeaders()));

リクエスト情報構築クラスから「HTTPメソッド」「URI」「ヘッダー」を取得する。

 

Mono<ResponseEntity<T>> mono = (req.getBody() != null)
? spec.body(PreProcessingBodyInserter.fromObject(req.getBody(),..).exchangeToMono(res -> handleResponse(res, typeRef))
: spec.exchangeToMono(res -> handleResponse(res, typeRef))

リクエスト情報構築クラスにボディが存在する場合、リクエストボディを取得してexchangeToMonoでリクエスト送信する。(POST、PUTなど)
上記以外の場合、ボディを設定せずexchangeToMonoでリクエスト送信する。(GET、DELETEなど)

 

使用例

呼び出し元でリクエスト情報構築クラスを設定し、汎用メソッドを呼び出す。

 

GET通信

汎用メソッドを使用してGETリクエストを行う。

パスパラメータ

動作確認用クラス


// リクエスト情報構築
var req = WebClientRequest.builder().method(HttpMethod.GET)
		.url("http://localhost:8080/rest_prototype/rest01/{id}")
		.pathParam("id", "2") // パスパラメータ
		.build();

// API通信
ResponseEntity<Resource> resEntity = client.callForEntity(req, 
		new ParameterizedTypeReference<Resource>() {});

 

クエリパラメータ

動作確認用クラス


// リクエスト情報構築
var req = WebClientRequest.builder()
		.method(HttpMethod.GET)
		.url("http://localhost:8080/rest_prototype/rest05/")
		.queryParam("name", "ご") // クエリパラメータ
		.build();

// API通信
ResponseEntity<List<Resource>> resEntity = client.callForEntity(req,
		new ParameterizedTypeReference<List<Resource>>() {});

 

POST通信

汎用メソッドを使用してPOSTリクエストを行う。

 

動作確認用クラス


// リクエスト情報構築
var body = new Resource("4", "パリ", LocalDate.of(2025, 5, 1));
var req = WebClientRequest.builder()
		.method(HttpMethod.POST)
		.url("http://localhost:8080/rest_prototype/rest02/create")
		.body(body)
		.build();

// API通信
ResponseEntity<Void> resEntity = client.callForEntity(req,
		new ParameterizedTypeReference<Void>() {});

 

PUT通信

汎用メソッドを使用してPUTリクエストを行う。

 

動作確認用クラス


// リクエスト情報構築
var body = new Resource("1", "パソコン", LocalDate.of(2025, 3, 1));
var req = WebClientRequest.builder()
		.method(HttpMethod.PUT)
		.url("http://localhost:8080/rest_prototype/rest03/{id}/")
		.pathParam("id", "1")
		.body(body)
		.build();

// API通信
ResponseEntity<Void> resEntity = client.callForEntity(req, 
		new ParameterizedTypeReference<Void>() {});

 

DELETE通信

汎用メソッドを使用してDELETEリクエストを行う。

 

動作確認用クラス


// リクエスト情報構築
var req = WebClientRequest.builder()
		.method(HttpMethod.DELETE)
		.url("http://localhost:8080/rest_prototype/rest04/{id}/")
		.pathParam("id", "2")
		.build();

// API通信
ResponseEntity<Void> resEntity = client.callForEntity(req,
		new ParameterizedTypeReference<Void>() {});

 

 

まとめ

 

☑ リクエスト情報は専用のWebClientRequestクラスのBuilderパターンで柔軟に構築することができる

☑ 1つの共通メソッドに集約することで、実装・保守がシンプルになる

☑ どんなHTTPメソッドでも共通的に処理できる

スポンサーリンク