この記事はNTTコムウェア Advent Calendar 2023 13日目の記事です。
はじめに
こんにちは、NTTコムウェアの田村です。
普段はMacchinetta Framework、Springプロジェクトに関する社内からの問合せ対応や技術検証を行っています。
今回は Spring Framework 6.1 から新しく登場したRESTクライアントである RestClient について公式ドキュメントを参照しつつ試してみました。
Spring Boot では 3.2 からRestClientをサポートしています。
本記事では Spring Framework 6.1.1 をもとに説明しています。
6.1.1 では RestClient による API 応答結果が no response body の場合、null ではなくエラーが返却されることが報告されています( 6.1.2 で修正される予定)。
RestClient とは
REST API を呼び出すためのクライアントです。
Spring Framework では同期クライアントとして RestTemplate がありましたが、その後継として RestClient の利用が推奨されています。
※RestTemplate は Spring Framework 5.x からメンテナンスモードでした。
非同期およびストリーミングの場合はリアクティブクライアントである WebClient が推奨されています。
RestClient の作成
RestClient を用意する単純な方法は create メソッドを実行することです。
RestClient defaultClient = RestClient.create();
これだけだと何をしているか分からないためソースを見ていきます。
public interface RestClient {
// 略
/**
* Create a new {@code RestClient}.
* @see #create(String)
* @see #builder()
*/
static RestClient create() {
return new DefaultRestClientBuilder().build();
}
// 略
}
RestClientはインターフェースであり、create メソッドでDefaultRestClientBuilderのbuildメソッドを実行しています。
final class DefaultRestClientBuilder implements RestClient.Builder {
// 略
@Override
public RestClient build() {
ClientHttpRequestFactory requestFactory = initRequestFactory();
UriBuilderFactory uriBuilderFactory = initUriBuilderFactory();
HttpHeaders defaultHeaders = copyDefaultHeaders();
List<HttpMessageConverter<?>> messageConverters = (this.messageConverters != null ?
this.messageConverters : initMessageConverters());
return new DefaultRestClient(requestFactory,
this.interceptors, this.initializers, uriBuilderFactory,
defaultHeaders,
this.statusHandlers,
messageConverters,
this.observationRegistry,
this.observationConvention,
new DefaultRestClientBuilder(this)
);
}
// 略
}
DefaultRestClientBuilder はRestClient.Builder インターフェースの実装クラスです。
build メソッド内ではクライアントリクエストファクトリやヘッダー情報など RestClient に必要な情報を設定して、DefaultRestClient のインスタンスを生成しています。
デフォルトで設定されるクライアントリクエストファクトリとメッセージコンバーターについては後述します。
これらの構成をカスタマイズしたい場合は下記公式ページのサンプルのように一度 builder メソッドで DefaultRestClientBuilder を呼び出し、変更を行ってから build メソッドを実行します。
RestClient customClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
.baseUrl("https://example.com")
.defaultUriVariables(Map.of("variable", "foo"))
.defaultHeader("My-Header", "Foo")
.requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer)
.build();
RestClient を使用する
リクエストを実行する手順は主に以下になります。
- リクエストのメソッドを設定する
- 接続先の URL を指定する
- ヘッダー情報を設定する
- レスポンスを取得する
- レスポンスを型変換する
RestClient の作成で接続先の URL やヘッダー情報を設定している場合、それらがデフォルト値として付与されます。
使用例を以下に示しますが、メソッドチェーンで簡単に使用することが可能です(簡単なのでほぼ公式サンプル通りです)。
GET
String 型でbodyを受け取りたい場合
String result = restClient.get()
.uri("https://example.com")
.retrieve()
.body(String.class);
レスポンスエンティティを受け取りたい場合
ResponseEntity<String> result = restClient.get()
.uri("https://example.com")
.retrieve()
.toEntity(String.class);
List 型で body を受け取りたい場合
List<DemoModel> demoModels = restClient.get()
.uri("https://example.com")
.retrieve()
.body(new ParameterizedTypeReference<>() {});
POST
toBodilessEntity メソッドを利用することで body の無いレスポンスを取得することができます。
ResponseEntity<Void> response = restClient.post()
.uri("https://petclinic.example.com/pets/new")
.contentType(APPLICATION_JSON)
.body(pet)
.retrieve()
.toBodilessEntity();
PUT
ResponseEntity<Void> response = restClient.put()
.uri("https://petclinic.example.com/pets/1")
.contentType(APPLICATION_JSON)
.body(pet)
.retrieve()
.toBodilessEntity();
DELETE
ResponseEntity<Void> response = restClient.delete()
.uri("https://petclinic.example.com/pets/1")
.retrieve()
.toBodilessEntity();
エラー処理をカスタマイズする
ステータスコード4xxのときに MyCustomRuntimeException を発生させる。
String result = restClient.get()
.uri("https://example.com/this-url-does-not-exist")
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders());
})
.body(String.class);
レスポンスへの処理を独自実装する
retrieve メソッドの代わりに exchange メソッドを利用することで HTTP リクエストおよびレスポンスの処理を実装できます。
Pet result = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id)
.accept(APPLICATION_JSON)
.exchange((request, response) -> {
if (response.getStatusCode().is4xxClientError()) {
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders());
}
else {
Pet pet = convertResponse(response);
return pet;
}
});
HTTP メッセージ変換
HTTP リクエストおよびレスポンスの body は HttpMessageConverter インターフェースを介して変換されます。
DefaultRestClientBuilder に対して messageConverters の指定をすることで追加ができます。
RestClient customClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
.build();
メッセージコンバーターはデフォルトで以下のものが設定されます。
- ByteArrayHttpMessageConverter
- StringHttpMessageConverter
- ResourceHttpMessageConverter
- AllEncompassingFormHttpMessageConverter
- KotlinSerializationJsonHttpMessageConverter
- クラスパスに kotlinx.serialization.json.Json が含まれる場合
- MappingJackson2HttpMessageConverter
- クラスパスに com.fasterxml.jackson.databind.ObjectMapper が含まれる場合
- GsonHttpMessageConverter
- クラスパスに com.google.gson.Gson が含まれる場合
- JsonbHttpMessageConverter
- クラスパスに jakarta.json.bind.Jsonb が含まれる場合
- MappingJackson2SmileHttpMessageConverter
- クラスパスに com.fasterxml.jackson.dataformat.smile.SmileFactory が含まれる場合
- MappingJackson2CborHttpMessageConverter
- クラスパスに com.fasterxml.jackson.dataformat.cbor.CBORFactory が含まれる場合
private List<HttpMessageConverter<?>> initMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<>();
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
this.messageConverters.add(new ResourceHttpMessageConverter(false));
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (kotlinSerializationJsonPresent) {
this.messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
this.messageConverters.add(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
this.messageConverters.add(new JsonbHttpMessageConverter());
}
if (jackson2SmilePresent) {
this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
}
if (jackson2CborPresent) {
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
}
}
return this.messageConverters;
}
クライアントリクエストファクトリ
HTTP リクエストを実行するためのクライアント HTTP ライブラリを設定可能です。
DefaultRestClientBuilder に対して requestFactory の指定をすることで設定できます。
RestClient customClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.build();
クライアントリクエストファクトリの優先度は以下の通りです。
- ユーザー指定
- HttpComponentsClientHttpRequestFactory
- クラスパスに Apache HTTP Client が含まれる場合
- JettyClientHttpRequestFactory
- クラスパスに Jetty HTTP Client が含まれる場合
- JdkClientHttpRequestFactory
- クラスパスに Java HTTP Client が含まれる場合
- SimpleClientHttpRequestFactory
private ClientHttpRequestFactory initRequestFactory() {
if (this.requestFactory != null) {
return this.requestFactory;
}
else if (httpComponentsClientPresent) {
return new HttpComponentsClientHttpRequestFactory();
}
else if (jettyClientPresent) {
return new JettyClientHttpRequestFactory();
}
else if (jdkClientPresent) {
// java.net.http module might not be loaded, so we can't default to the JDK HttpClient
return new JdkClientHttpRequestFactory();
}
else {
return new SimpleClientHttpRequestFactory();
}
}
SimpleClientHttpRequestFactory は RestTemplate でもクライアントリクエストファクトリを指定しない場合に使われていたデフォルト設定です。
RestTemplate からの移行
カスタマイズした RestTemplate を RestClient に移行したい場合は create メソッドもしくは builder メソッドに RestTemplate を引数として渡すことで設定を引き継いだ RestClient を作成することができます。
RestClient defaultClient = RestClient.create(restTemplate);
RestClient customClient = RestClient.builder(restTemplate)
// 略
.build();
引き継がれる項目は以下のrestTemplate.getXXX()
で取得可能なものに限られるため、その他に独自実装を加えている場合はご注意ください。
public DefaultRestClientBuilder(RestTemplate restTemplate) {
Assert.notNull(restTemplate, "RestTemplate must not be null");
if (restTemplate.getUriTemplateHandler() instanceof UriBuilderFactory builderFactory) {
this.uriBuilderFactory = builderFactory;
}
this.statusHandlers = new ArrayList<>();
this.statusHandlers.add(StatusHandler.fromErrorHandler(restTemplate.getErrorHandler()));
this.requestFactory = restTemplate.getRequestFactory();
this.messageConverters = new ArrayList<>(restTemplate.getMessageConverters());
if (!CollectionUtils.isEmpty(restTemplate.getInterceptors())) {
this.interceptors = new ArrayList<>(restTemplate.getInterceptors());
}
if (!CollectionUtils.isEmpty(restTemplate.getClientHttpRequestInitializers())) {
this.initializers = new ArrayList<>(restTemplate.getClientHttpRequestInitializers());
}
this.observationRegistry = restTemplate.getObservationRegistry();
this.observationConvention = restTemplate.getObservationConvention();
}
また、RestTemplate のメソッドと RestClient のメソッドの対応表が公式ページで公開されています。
感想
RestClient を試してみましたが、RestTemplate よりもメソッドチェーンで直感的に操作ができるようになったと感じました。
WebClient を利用している人にとってもクラスの構成が似ているので扱いやすいかと思います。
RestTemplate 自体はまだ非推奨にはなっていませんが、Spring Framework 6.1 以降を利用する場合は同期クライアントとして RestClient を採用していきたいと思います。
記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。