概要
ここまで個別にWebClientのGETやPOSTを行う方法について紹介してきたが、
今回はHTTP通信を汎用的に実行できるメソッドを作成したのでまとめた。
尚、動作確認を行うための事前準備については、以下に記載している。
概要 RESTなAPIにアクセスするのはRestTemplateではなくWebClientが今後推奨となるため、WebClientの概要についてまとめた。 WebClientを使用するための準備と、どんなメソッドがあるのかを紹介している[…]
汎用通信のやり方
HTTPメソッドごとにWebClient.get()やWebClient.post()を個別のメソッドに用意するのではなく、
共通の処理にリクエスト情報を渡すだけで、任意のHTTP通信(GET/POST/PUT/DELETEなど)を実行できる仕組みを作成する。
全体像
汎用メソッドを使用する際の、全体的な流れは以下となる。
② 汎用メソッド呼び出し: 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 URI uri; // URI
private HttpHeaders headers; // ヘッダー
private final Object body; // リクエストボディ
WebClientの通信に必要なリクエスト情報をフィールドの保持する。
Builderを通して「HTTPメソッド」「URI」「ヘッダー」「リクエストボディ」を構築する。
呼び出し元にてmethod()やheader()などのメソッドを必要な部分だけチェーン形式で呼び出し、最終的にbuild()を呼び出すことでWebClientRequestインスタンスを生成する。
GET/POST/PUT/DELETEなどのHTTPメソッドを設定する。
ヘッダーのキー値とバリュー値を指定することで、ヘッダー情報を設定する。
リクエストボディを設定する。
URIテンプレートを設定する。
キー値とバリュー値を指定することで、クエリパラメータ情報を保持する。
キー値とバリュー値を指定することで、パスパラメータ情報を保持する。
(略)
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);
}
}
ResponseEntityを返却する汎用メソッド。
リクエスト構築クラスと、ResponseEntityのボディに指定したい型を受け取る。
.headers(httpHeaders -> httpHeaders.addAll(req.getHeaders()));
リクエスト情報構築クラスから「HTTPメソッド」「URI」「ヘッダー」を取得する。
? 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メソッドでも共通的に処理できる