5.2. RESTクライアント(HTTPクライアント)


5.2.1. Overview

本節では、RESTful Web Service(REST API)を呼び出す際に利用するRESTクライアントの実装について説明を行う。

本ガイドラインでは、主に外部システム連携の手段としてのRESTを扱う。
外部システム連携では多くの場合、同期的な情報のやり取りを行うことが想定されるため、本ガイドラインでも同期的な通信を中心に取り扱う。
非同期通信については簡単な実装例を参考情報としてAppendix(非同期通信の利用について)に記載しているので必要に応じて参照されたい。

5.2.1.1. RESTクライアント実装の種類

Spring Frameworkでは、RESTful Web Service(REST API)を呼び出すためのクライアント実装として、RestTemplate, RestClient, WebClient, HTTP Service Clientsの4種類の提供を行っている。
本項では、同期通信で利用するクライアント実装の機能や特徴の違いを説明する。

5.2.1.1.1. RestTemplateを利用したクライアント実装

RestTemplateは、Spring Framework 3.0から導入されているRESTクライアント実装であり、REST APIへの同期通信をサポートしたクライアント実装である。
非同期通信に関しては、Spring Framework 6.0にてAsyncRestTemplateが削除されたことにより、WebClientを利用した実装が推奨されており、現在は同期通信のみをサポートしている。
なお、同期通信においてもSpring Framework 7.0にて今後廃止とする宣言がされており、Spring Framework 7.1にて正式に非推奨とし、Spring Framework 8.0にて削除を行う予定とされている。
そして、RestTemplateの移行先としてはRestClientが紹介されている。
RestTemplateが非推奨となった経緯は、The state of HTTP clients in Springを参照されたい。
TERASOLUNA Server Framework for Java (5.x) においても、現在利用中のユーザを考慮し、RestTemplate関する記載を残しているが、今後、RESTクライアントの実装を行う際はRestClientの利用を推奨する。
また、技術的負債になることを防ぐため、既存のRestTemplateを用いた実装についても、RestClientへの移行を推奨する。
RestTemplateからRestClientへの移行については、Migrating from RestTemplate to RestClientにて詳しく記載されているので参照されたい。
以下に、RestTemplateを利用したクライアントアプリケーションが、どのようにREST APIを公開しているサーバサイドアプリケーションにアクセスし、RESTによる通信を実現するかを示す。
RestTemplate内で利用されるクラスの詳細な説明は、「RESTクライアント実装の構成要素と利用上のポイント」にてまとめて説明を行っているのでこちらを参照されたい。
Overview of RestTemplate processing

項番

処理

説明

(1)
アプリケーションからの呼び出し
RestTemplateのメソッドを実行し、REST API(Web API)の呼び出し依頼を行う。
(2)
URL生成
UriBuilderFactoryの実装クラスが、UriBuilderの生成を行う。
生成したUriBuilderを使用して、URLのパラメータを置換し送信先のURLの組み立てを行う。
なお、UriBuilderFactoryの実装クラスを指定していない場合は、デフォルト実装クラスであるDefaultUriBuilderFactoryが使用される。
(3)
リクエスト生成・初期化
ClientHttpRequestFactoryの実装クラスがClientHttpRequestの生成を行う。
なお、ClientHttpRequestFactoryの実装クラスを指定していない場合は、デフォルト実装クラスであるSimpleClientHttpRequestFactoryが使用される。
ClientHttpRequest生成後、ClientHttpRequestInitializerを設定している場合、ClientHttpRequestInitializerの実装クラスを使用してClientHttpRequest(リクエスト)の初期化処理を行う。

リクエストの生成(ClientHttpRequestFactory)」にて詳しく説明を行うが、 TERASOLUNA Server Framework for Java (5.x) では、ClientHttpRequestFactoryの実装クラスはHttpComponentsClientHttpRequestFactoryの使用を推奨とする。
(4)
リクエスト送信前処理
HttpMessageConverterの実装クラスを使用して、Javaオブジェクトをリクエストボディに設定する電文(JSON等)に変換する。
なお、実際に利用されるHttpMessageConverterの実装クラスは、Javaオブジェクトの型とメディアタイプをもとにRestTemplateが決定する。
(5)
リクエスト送信
ClientHttpRequestInterceptorを設定している場合、ClientHttpRequestInterceptorの実装クラスを使用してリクエスト送信前後の共通処理を行う。
ClientHttpRequestに電文(JSON等)をリクエストボディに設定して、REST API(Web API)にHTTP経由でリクエストを送信する。
REST API(Web API)からレスポンスを受信後、ClientHttpRequestClientHttpResponseを生成し、受信したレスポンスデータを設定する。
(6)
エラーハンドリング
ResponseErrorHandlerの実装クラスにて、ClientHttpResponseに設定されているHTTPステータスコードをもとに、エラー判定及びエラー処理を行う。
なお、ResponseErrorHandlerの実装クラスを指定していない場合は、デフォルト実装クラスであるDefaultResponseErrorHandlerが使用される。
(7)
レスポンス取得後処理
レスポンス取得後の後処理として、ResponseExtractorの実装クラスが実行される。
なお、ResponseExtractorの実装クラスを指定していない場合は、デフォルト実装クラスであるHttpMessageConverterExtractorが使用される。
後処理時に、HttpMessageConverterの実装クラスを使用して、レスポンスボディに設定されている電文(JSON等)をJavaオブジェクトに変換する。
リクエスト送信前処理と同様でHttpMessageConverterは、Javaオブジェクトの型とメディアタイプをもとに決定される。
(8)
アプリケーションへ実行結果返却
REST API(Web API)の呼び出し結果(Javaオブジェクト)をアプリケーションへ返却する。

5.2.1.1.2. RestClientを利用したクライアント実装

RestClientは、Spring Framework 6.1から導入されているRESTクライアント実装であり、RestTemplateの後継として登場した同期通信をサポートしたRESTクライアント実装である。
特徴としては流れるようなインターフェース(fluent interface)を用いたAPIを提供しており、設定やリクエストの手続きをメソッドチェーンで記載できる。

RestTemplateで説明した通りに、同期通信に関してはRestClientを使うことが推奨されている。
また、移行性についての考慮もされており、RestTemplateで提供されている機能はRestClientでも同等に利用できる。
RestTemplateからRestClientへの移行については、Migrating from RestTemplate to RestClientにて詳しく記載されているので参照されたい。
以下に、RestClient を利用したクライアントアプリケーションが、どのようにREST APIを公開しているサーバサイドアプリケーションにアクセスし、RESTによる通信を実現するかを示す。
RestClient内で利用されるクラスの詳細な説明は、「RESTクライアント実装の構成要素と利用上のポイント」にてまとめて説明を行っているのでこちらを参照されたい。
Overview of RestClient processing

項番

処理

説明

(1)
アプリケーションからの呼び出し
RestClientのメソッドを実行し、REST API(Web API)の呼び出し依頼を行う。
RestClientは、RequestBodyUriSpecResponseSpecを内部的に保持し、これらを使って流れるようなインターフェース(fluent interface)を実現している。
メソッドチェーンで呼び出しを行う際にリクエストの設定(エンドポイント、HTTPメソッド、ヘッダ、ボディ等)を行うエントリーポイントとしてRequestBodyUriSpecを提供し、レスポンスの設定(受信時のデータ型、例外ハンドラ等)を行うエントリーポイントとしてResponseSpecを提供している。
なお、RestClientはインターフェースとなっており、実装クラスとしてはDefaultRestClientが使用される。
(2)
URL生成
UriBuilderFactoryの実装クラスが、UriBuilderの生成を行う。
生成したUriBuilderを使用して、URLのパラメータを置換し送信先のURLの組み立てを行う。
なお、UriBuilderFactoryの実装クラスを指定していない場合は、デフォルト実装クラスであるDefaultUriBuilderFactoryが使用される。
(3)
リクエスト生成・初期化
ClientHttpRequestFactoryの実装クラスがClientHttpRequestの生成を行う。
なお、ClientHttpRequestFactoryの実装クラスを指定していない場合は、使用するHttpClientライブラリによって実装クラスが決定される。
ClientHttpRequest生成後、ClientHttpRequestInitializerを設定している場合、ClientHttpRequestInitializerの実装クラスを使用してClientHttpRequest(リクエスト)の初期化処理を行う。

リクエストの生成(ClientHttpRequestFactory)」にて詳しく説明を行うが、 TERASOLUNA Server Framework for Java (5.x) では、ClientHttpRequestFactoryの実装クラスはHttpComponentsClientHttpRequestFactoryの使用を推奨とする。
(4)
リクエスト送信前処理
HttpMessageConverterの実装クラスを使用して、Javaオブジェクトをリクエストボディに設定する電文(JSON等)に変換する。
なお、選択されるHttpMessageConverterは、Javaオブジェクトの型とメディアタイプをもとに決定される。
(5)
リクエスト送信
ClientHttpRequestInterceptorを設定している場合、ClientHttpRequestInterceptorの実装クラスを使用してリクエスト送信前後の共通処理を行う。
ClientHttpRequestに電文(JSON等)をリクエストボディに設定して、REST API(Web API)にHTTP経由でリクエストを送信する。
REST API(Web API)からレスポンスを受信後、ClientHttpRequestClientHttpResponseを生成し、受信したレスポンスデータを設定する。
(6)
レスポンス取得後処理 / エラーハンドリング
レスポンス取得後の後処理として、ExchangeFunctionが実行される。
ExchangeFunctionによりStatusHandlerが実行され、エラー判定及びエラー処理を行う。
エラー判定処理後に、HttpMessageConverterの実装クラスを使用して、レスポンスボディに設定されている電文(JSON等)をJavaオブジェクトに変換する。
なお、リクエスト送信前処理と同様でHttpMessageConverterは、Javaオブジェクトの型とメディアタイプをもとに決定される。
(7)
アプリケーションへ実行結果返却
REST API(Web API)の呼び出し結果(Javaオブジェクト)をアプリケーションへ返却する。

5.2.1.1.3. HTTP Service Clientsを利用したクライアント実装

HTTP Service Clientsは、Spring Framework 6.0から導入された新しいRESTクライアントの実装であり、インターフェースとアノテーションを活用したシンプルな構成が特徴である。
従来のRestTemplateRestClientWebClientとは異なり、インターフェースとアノテーションを定義するだけで、REST API(Web API)へのアクセスを簡潔に記述できるように設計されている。
この記載方法はSpringの@RestControllerによるコントローラ実装と類似しており、直感的で理解しやすい構成である。
また、インターフェースにREST API(Web API)へのアクセス方法や制約(エンドポイント、HTTPメソッド、ヘッダ等)を定義できるため、利用者はロジックと設定を分離することができ、従来のRESTクライアント実装と比べてコードの見通しが良い。
HTTP Service Clients自体は、RestTemplateRestClientWebClientの設定を流用できるため、既存の設定を活かしながら追加で導入するといった移行の容易さも特徴である。
業務ロジックのコードの見通しを良くしたい場合や、REST API(Web API)のアクセス方法をインターフェースに集約して管理したい場合は、HTTP Service Clientsを利用すると便利である。
以下に、HTTP Service Clientsを利用したクライアントアプリケーションが、どのようにREST APIを公開しているサーバサイドアプリケーションにアクセスし、RESTによる通信を実現するかを示す。
HTTP Service Clients内で利用されるクラスの詳細な説明は、「RESTクライアント実装の構成要素と利用上のポイント」にてまとめて説明を行っているのでこちらを参照されたい。
Overview of RestClient processing

項番

処理

説明

(1)
アプリケーションからの呼び出し
開発者が作成した@HttpExchangeアノテーションを付与したインターフェースのメソッドを実行し、REST API(Web API)の呼び出し依頼を行う。
なお、インターフェースの実装はSpring Frameworkが生成した動的プロキシが使用される。

@HttpExchangeアノテーションは、HTTP Service Clientsであることを表現するためのアノテーションであり、エンドポイント、HTTPメソッド等の情報を定義できる。
(2)
Adapterの実行
動的プロキシからHttpServiceProxyFactoryが呼び出され、実行したインターフェースのメソッドの情報をもとにHttpServiceMethodを取得する。
その後、HttpServiceProxyFactoryからHttpServiceMethodが実行され、リクエスト情報の取得を行う。
リクエスト情報取得後、HttpServiceMethodHttpExchangeAdapterを実行する。
なお、HttpExchangeAdapterの実装クラスは、RestTemplateRestClientWebClientのどの設定を利用するかによって決定される。
(3)
RESTクライアントの実行
HttpExchangeAdapterから、各RESTクライアント実装を実行する。

各RESTクライアント実装

説明

RestTemplateの場合
RestTemplateAdapterからRequestEntityを呼び出し、RestTemplateに引き渡すリクエスト情報を設定する。
その後、RestTemplateAdapterからRestTemplateを実行し、以降の処理はRestTemplateと同様の処理が実行される。
RestClientの場合
RestClientAdapterからRestClientRequestBodyUriSpecを呼び出し、RestClientに引き渡すリクエスト情報を設定する。
その後、RestClientAdapterからRestClientを実行し、以降の処理はRestClientと同様の処理が実行される。
WebClientの場合
WebClientAdapterからWebClientRequestBodyUriSpecを呼び出し、WebClientに引き渡すリクエスト情報を設定する。
その後、WebClientAdapterからWebClientを実行し、以降の処理はWebClientと同様の処理が実行される。
(4)
アプリケーションへ実行結果返却
REST API(Web API)の呼び出し結果(Javaオブジェクト)をアプリケーションへ返却する。

5.2.1.2. RESTクライアント実装の構成要素と利用上のポイント

上述のように、RESTクライアントの各実装方式は、おおむね同様の処理パターンを持っており、その構成要素についても共通のものが多い。
本項では、各RESTクライアント実装で共通な処理内容と、そこで利用される構成要素の概要を示し、利用上、有益と思われるいくつかの内容について記載する。

5.2.1.2.1. URLの生成(UriBuilderFactory

org.springframework.web.util.UriBuilderFactoryは、URIテンプレート(プレースホルダ付きのURLの元となる文字列)と、URIテンプレート変数の値(プレースホルダを埋めるためのパラメータ)を受け取り、リクエスト送信先のURLを生成するインターフェースである。

具体的には以下のようなURL文字列を生成する。

  • URIテンプレート: http://example.com/api/users/{userId}

  • URIテンプレート変数の値: 12345

  • 生成されるURL: http://example.com/api/users/12345

UriBuilderFactoryは、UriBuilderを生成するためのファクトリであり、実際のURL文字列の生成はUriBuilderで行われる。
デフォルトでは、org.springframework.web.util.DefaultUriBuilderFactoryから、DefaultUriBuilderが作られ、URL生成に利用される。

URIテンプレート変数の値をURIテンプレートに埋め込む際に、URIテンプレート変数の値に対してフォーマット変換を行いたい場合などは、UriBuilderFactoryを拡張することが考えられる。
この場合、変換処理が実装されたUriBuilderの実装クラスと、そのUriBuilderの実装クラスが使われるようにしたUriBuilderFactoryの実装クラスを作成し、このUriBuilderFactoryの実装クラスが使われるように、Bean定義を行うことで実現できる。
URLの生成では、RestClientRestTemplateの両者で UriBuilderFactoryが利用されているため、同様の方法で拡張が可能である。
UriBuilderFactoryの具体的なカスタマイズの例は、「URL生成処理をカスタマイズする方法(UriBuilderFactory)」を参照されたい。

5.2.1.2.2. リクエストの生成(ClientHttpRequestFactory

RestClientRestTemplateは、サーバとの通信処理を以下の3つのインタフェースの実装クラスに委譲することで実現している。
  • org.springframework.http.client.ClientHttpRequestFactory

  • org.springframework.http.client.ClientHttpRequest

  • org.springframework.http.client.ClientHttpResponse

ClientHttpRequestFactoryは、サーバとの通信処理を行うClientHttpRequestインタフェースの実装クラスの生成を行う。
その後、ClientHttpRequestがサーバと通信を行い、通信結果を保持するClientHttpResponseインタフェースの実装クラスを生成し、通信結果を設定する。

この3つのインタフェースのうち、ClientHttpRequestFactoryについては、Spring Framework側でいくつかの実装が用意されており、細かな点で動作が異なることがあるため、利用者側で何を利用するか意識しておく必要がある。
Spring Frameworkが提供している主なClientHttpRequestFactoryの実装クラスは以下の通りである。
Spring Frameworkが提供している主なClientHttpRequestFactoryの実装クラス

項番

クラス名

説明

(1)
org.springframework.http.client.
HttpComponentsClientHttpRequestFactory
Apache HttpComponents HttpClientのAPIを使用して同期型の通信処理を行うための実装クラス。(HttpClient 5.2以上が必要)
(2)
org.springframework.http.client.
JettyClientHttpRequestFactory
JettyのAPIを使用して同期型の通信処理を行うための実装クラス。
(3)
org.springframework.http.client.
ReactorClientHttpRequestFactory
Reactor NettyのAPIを使用して同期型の通信処理を行うための実装クラス。
(4)
org.springframework.http.client.
JdkClientHttpRequestFactory
Java SE標準のHttpClientのAPIを使用して同期型の通信処理を行うための実装クラス。
HttpClientの実装としては、JDK 11にて追加されたjava.net.http.HttpClientを利用しており、SimpleClientHttpRequestFactoryよりも高機能な実装クラスである。
(5)
org.springframework.http.client.
SimpleClientHttpRequestFactory
Java SE標準のHttpURLConnectionのAPIを使用して同期型の通信処理を行うための実装クラス。
HttpClientの実装としては、JDK初期から存在するjava.net.HttpURLConnectionを使用しており、単純な通信設定のみが行える。
なお、RestTemplate利用時にデフォルトで設定される実装クラスである。

使用するClientHttpRequestFactoryの実装クラスについて

ClientHttpRequestFactoryのデフォルト実装クラスは、RestClientRestTemplateで異なる。
RestClientを利用する場合は、クラスパス上に存在するHttpClientライブラリ検出し、優先順位に基づいてClientHttpRequestFactoryの実装が自動的に決定される。
RestTemplateを利用する場合は、SimpleClientHttpRequestFactoryが固定で選択される。

前述の通り、RestClientではクラスローダーに読み込まれたライブラリをもとに自動的に決定されるため、競合するHttpClientライブラリが依存関係に存在する場合は、意図しない実装クラスが選択される可能性がある。
また、RestTemplateにおいても、デフォルトで設定されるSimpleClientHttpRequestFactoryは、コネクションプールやプロキシ認証等の機能を持たず、他のClientHttpRequestFactoryの実装と比較すると機能が限定されている。
したがって、 TERASOLUNA Server Framework for Java (5.x) では、ClientHttpRequestFactoryの実装クラスとしては、高機能な通信設定が行えるHttpComponentsClientHttpRequestFactoryの利用を推奨する。
また、ClientHttpRequestFactoryの実装を固定するために、ClientHttpRequestFactoryの実装クラスを明示的に指定する設定を推奨する。
以下が、推奨設定のコード例である。
  • ApplicationContextConfig.java

@Bean("restClient")
public RestClient restClient() {
    return RestClient.builder()
                .requestFactory(new HttpComponentsClientHttpRequestFactory())
                .build();
}

ClientHttpRequestFactoryの設定について

ClientHttpRequestFactoryでは、通信設定のカスタマイズが可能となっており、以下のような設定が可能である。
それぞれの設定の詳細については、以下のリンクを参照されたい。

Note

Content-Lengthヘッダについて

Spring Framework 6.1よりメモリ使用量を減らすために、ほとんどのClientHttpRequestFactory実装クラスはサーバへ送信する前にリクエストボディをバッファリングしなくなった。これにより、Content-Lengthヘッダが設定されなくなったため、Content-Lengthヘッダを設定する必要がある場合はBufferingClientHttpRequestFactoryを利用しバッファリングする必要がある。

  • Bean定義ファイルの定義例

    • ApplicationContextConfig.java

    @Bean("restClient")
    public RestClient restClient() {
        return RestClient.builder()
                    .requestFactory(new HttpComponentsClientHttpRequestFactory())
                    .bufferContent((uri , method) -> true)  // (1)
                    .build();
    }
    

    項番

    説明

    (1)
    RestClient.Builder.bufferContent(BiPredicate)を使用することで、内部的にBufferingClientHttpRequestFactoryが生成され、バッファリングが有効になる。
    引数のBiPredicateには、URIとHTTPメソッドが引き渡されるため、それらを利用してバッファリングする条件を記載することができる。
    上記の例では、すべてのリクエストに対してバッファリングを有効にしている。

5.2.1.2.3. リクエスト初期化(ClientHttpRequestInitializer

org.springframework.http.client.ClientHttpRequestInitializerは、Httpリクエスト送信前にClientHttpRequestをカスタマイズするためのインタフェースである。
Httpリクエスト送信前のカスタマイズポイントには、 ClientHttpRequestInitializerの他に、 ClientHttpRequestInterceptorも存在するが、それぞれ用途が異なる。両者の違いについては「ClientHttpRequestInterceptorとClientHttpRequestInitializerの使い分けについて」を参照されたい。
ClientHttpRequestInitializerはユーザがRestClientRestTemplateに設定した場合のみ動作し、ClientHttpRequestFactoryによるClientHttpRequestの生成後、ClientHttpRequestInitializerに実装したユーザ定義の初期化処理が実行される。
したがって、ClientHttpRequestInitializerはデフォルト実装はなく、拡張ポイントとして提供されているインターフェースである。
また、ClientHttpRequestInitializer@FunctionalInterfaceで提供されているため、RestClientではラムダ式を使用して簡潔に記述することが可能である。

ClientHttpRequestInitializerの利用が適しているケースには、リクエストヘッダにカスタムヘッダを一括で設定したい場合などがある。
RestClientを利用する場合、このようなケースにはdefaultHeaderメソッドも利用可能である。
一方で、RestTemplate利用する場合はRestClientのような代替手段が無いため ClientHttpRequestInitializerの利用が適切である。
リクエストヘッダの一括設定の方法については、「リクエストヘッダの設定」を参照されたい。

5.2.1.2.4. メッセージ変換(HttpMessageConverter

org.springframework.http.converter.HttpMessageConverterは、アプリケーションで扱うJavaオブジェクトとサーバと通信するための電文(JSON等)を相互に変換するためのインタフェースである。

Spring Frameworkは主要なメディアタイプ向けのHttpMessageConverter実装を提供しており、RestClientおよびRestTemplateが使用するHttpMessageConverterは、これらのクライアント生成時に登録され、リクエスト/レスポンスのボディ変換に利用される。
自動で登録されるHttpMessageConverterには、標準で登録されるHttpMessageConverterと、依存ライブラリの有無により登録されるHttpMessageConverterが存在する。
加えて、独自で変換処理を実装したHttpMessageConverterを必要に応じて追加登録することも可能である。
なお、HttpMessageConverterはクライアント側とサーバ側のそれぞれで登録されるため、本節で説明しているHttpMessageConverterはクライアント側で利用されるものであることに留意されたい。
これらのHttpMessageConverterは、標準の構成では以下の順で登録される。
  1. 独自で変換処理を実装したHttpMessageConverter

  2. 標準で登録されるHttpMessageConverter

  3. 依存ライブラリの有無により登録されるHttpMessageConverter

メッセージ変換時には、登録されているHttpMessageConverterの順序に従って、Javaオブジェクトの型とメディアタイプに適用可能なHttpMessageConverterが選択される。

HttpMessageConverterが対応するJavaオブジェクトの型とメディアタイプについては、以下のJavaDocを参照されたい。

標準で登録されるHttpMessageConverter

依存ライブラリの有無により登録されるHttpMessageConverter

上記以外のHttpMessageConverterに関しては、HttpMessageConverterの実装クラスの一覧を参照されたい。


ガイドラインで使用するHttpMessageConverterについて

本節で説明する実装は、クライアント、サーバの双方で、Spring Frameworkが提供するHttpMessageConverterを同等の構成で登録していることを前提とする。
また、JSON形式の通信を前提としており、org.springframework.http.converter.json.JacksonJsonHttpMessageConverterによる変換処理を例に説明を行う。
各Restクライアント実装の送受信で使用するJavaBeanのフィールド名は、JSONのプロパティ名と一致していることを前提とし、特別なマッピング等は行っていない。

5.2.1.2.5. リクエスト前後処理(ClientHttpRequestInterceptor

org.springframework.http.client.ClientHttpRequestInterceptorは、サーバとの通信の前後に共通的な処理を実装するためのインタフェースである。
類似した拡張ポイントとしてClientHttpRequestInitializerもあるため、違いについては「ClientHttpRequestInterceptorとClientHttpRequestInitializerの使い分けについて」を参照されたい。
ClientHttpRequestInterceptorを使用すると、サーバとの通信時のリクエストやレスポンスのログを出力するといった共通的な処理をRestTemplateや、RestClientに適用することができる。
ClientHttpRequestInterceptorを使用した共通処理の実装方法については、「リクエスト送信前後の共通処理の適用(ClientHttpRequestInterceptor)」を参照されたい。

ClientHttpRequestInterceptorの動作仕様

ClientHttpRequestInterceptorは複数適用することができ、RestTemplateや、RestClientに登録した順番でチェーン実行される。
これはサーブレットフィルタの動作によく似ており、最後に実行されるチェーン先としてClientHttpRequestによるHTTP通信処理が登録されている。
ClientHttpRequestInterceptorを複数適用した場合の実装例は、「複数のClientHttpRequestInterceptorを適用する方法」を参照されたい。

Warning

ClientHttpRequestInterceptor使用時の注意点

ClientHttpRequestInterceptorを利用すると、リクエストボディが引数として渡される関係でボディ部がメモリ上に全て展開されてしまう。したがって、リクエストボディに大量のデータを含む場合はメモリ使用量が増大する可能性があるので留意して使用する必要がある。


5.2.1.2.6. ClientHttpRequestInterceptorClientHttpRequestInitializerの使い分けについて

ClientHttpRequestInterceptorClientHttpRequestInitializerは、どちらもリクエスト送信時の共通処理を記述するためのインタフェースであるが、以下のように目的と使用方法が異なるため、使い分けが重要である。

項番

種類

説明

(1)
ClientHttpRequestInitializer
ClientHttpRequestFactoryClientHttpRequestを生成した後に実行される。
リクエストボディを読み込まず、ClientHttpRequestのみに対してカスタマイズを行える拡張ポイントであるため、リクエストヘッダに対して特定のヘッダを追加する等のClientHttpRequestに限定された共通処理を記述するのに適している。
(2)
ClientHttpRequestInterceptor
実行タイミングとしては、ClientHttpRequestがサーバとの通信を行う前後に実行される。
したがって、リクエスト前後の共通処理であるログ出力等を追加したい場合に利用するのに適している。
リクエスト前後処理(ClientHttpRequestInterceptor)」でも述べたとおり、ClientHttpRequestInterceptorを利用するとリクエストボディがメモリ上に展開されるため、メモリ使用量が増大し、パフォーマンスに影響を与える可能性がある。
したがって、リクエストヘッダの設定のみを行いたい場合等は、責務の問題もあるがリソース的にもClientHttpRequestInitializerを利用する方が適切である。

5.2.1.2.7. エラーハンドリング(StatusHandlerResponseErrorHandler

Spring Frameworkには、RESTクライアントによるサーバ通信時のエラーに対応するためのエラーハンドラとしてStatusHandlerと、ResponseErrorHandlerが存在しており、RESTクライアントの実装ごとに利用するエラーハンドラが異なる。
各RESTクライアント実装では、デフォルトでHTTPステータスコードに応じたエラーハンドリングが実装されたエラーハンドラが設定されている。詳細は以下の通り。
  • RestClient: 利用可能なエラーハンドラの実装としてStatusHandlerを使用した実装と、ResponseErrorHandlerを使用した実装の2つが存在する。デフォルトでは、StatusHandlerが利用される。

  • RestTemplate:利用可能なエラーハンドラ実装は ResponseErrorHandlerを使用したもののみ。デフォルトでは、DefaultResponseErrorHandlerが利用される。

デフォルトで使用されるエラーハンドラー実装はHTTPステータスコードに応じて、以下のようなハンドリングを行う。
デフォルト実装のハンドリング仕様は、StatusHandlerDefaultResponseErrorHandlerで同様である。
エラーハンドラーの動作仕様

項番

HTTPステータスコード

動作仕様

(1)
2xx(正常系)
エラー処理は行わない。
(2)
4xx(クライアントエラー系)
org.springframework.web.client.HttpClientErrorExceptionを発生させる。
(3)
5xx(サーバエラー系)
org.springframework.web.client.HttpServerErrorExceptionを発生させる。
上記の動作仕様を変更したい場合は、 StatusHandlerResponseErrorHandlerの実装を作成して、各RESTクライアントに設定する必要がある。
StatusHandlerResponseErrorHandlerのカスタマイズ方法と設定については、「エラーハンドリング」を参照されたい。

5.2.1.2.8. レスポンス取得後処理(ExchangeFunctionResponseExtractor

Spring Frameworkでは、RESTクライアントでサーバからのレスポンスを取得したあとの、ボディの抽出や変換などの処理(レスポンス取得後処理)を実現する仕組みとして、 ExchangeFunctionResponseExtractorを提供している。
また、RESTクライアントの実装方法によって、利用されるレスポンス取得後処理の仕組みが異なる。詳細は以下の通り。
Comparison of ExchangeFunction and ResponseExtractor

ExchangeFunction ( RestClient ) の場合

org.springframework.web.client.ExchangeFunctionを使用して後処理を実装する。
デフォルトで利用される実装が提供されており、org.springframework.web.client.DefaultRestClient内で定義されているExchangeFunctionの関数実装が利用される。
動作としては、DefaultRestClientがレスポンスを受信した後に実行され、後処理としてHTTPステータスのエラー判定処理、メッセージの変換処理を行う。
RestTemplateとの違いは、HTTPステータスのエラー判定処理をレスポンス受信後処理(ExchangeFunction) で実装する必要がある点にある。

ResponseExtractor ( RestTemplate ) の場合

org.springframework.web.client.ResponseExtractorを使用して後処理を実装する。
デフォルトで利用される実装が提供されており、 org.springframework.web.client.HttpMessageConverterExtractorが利用される。
動作としては、RestTemplateがレスポンスを受信し、HTTPステータスのエラー判定処理を行った後に実行され、後処理としてメッセージの変換処理を行う。

受信したレスポンスを操作する必要がある場合などは ExchangeFunctionResponseExtractorをカスタマイズする。実際の方法は、「ファイルダウンロード」で説明しているので、必要に応じてこちらも参照されたい。


5.2.2. How to use(同期通信)

本節では、RestClientRestTemplateを使用した、同期通信を行うRESTクライアントの実装方法について説明する。
RestClientを用いた実装におけるBean定義例では、Spring FrameworkにおいてBuilderパターンを利用したメソッドチェーンによる実装を前提としているため、Java Configを利用した設定例のみを示す。

本ガイドラインでは、GETメソッドとPOSTメソッドを使用したクライアント処理の実装例のみを紹介するが、Spring Frameworkが提供するRESTクライアント実装は他のHTTPメソッド(PUT, PATCH, DELETE, HEAD, OPTIONSなど)もサポートしており、同様に実装することができる。
他のHTTPメソッドを利用する場合も含め、本ガイドラインで紹介する以外の利用方法については、RestClientのJavadocRestTemplateのJavadocを参照されたい。

RESTクライアントを利用した同期通信のための基本的な設定と、データ取得、登録を行うには以下のような手順で実装を行う。
この他、以下のような内容についても実装例を示しているが、アプリケーションの要件に依存すると思われるので、必要に応じて参照してほしい。

5.2.2.1. 同期通信のセットアップ

同期通信を行う場合は、RestClientまたはRestTemplateをDIコンテナに登録し、RESTクライアントを利用するコンポーネントにインジェクションする。


5.2.2.1.1. 依存ライブラリ設定

RestClient / RestTemplateを使用するためにpom.xmlに、Spring Frameworkのspring-webライブラリと、HttpClientの実装としてApache HttpComponents HttpClientライブラリを追加する。
マルチプロジェクト構成の場合は、domainプロジェクトのpom.xmlに追加する。
<dependencies>

    <!-- (1) -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
    </dependency>
    <!-- (2) -->
    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
    </dependency>

</dependencies>

項番

説明

(1)
Spring Frameworkのspring-webライブラリをdependenciesに追加する。
(2)
Apache HttpComponents HttpClient を依存ライブラリに追加する。
本設定は、ClientHttpRequestFactoryのインターフェース実装として、HttpComponentsClientHttpRequestFactoryを利用するための設定である。

5.2.2.1.2. RESTクライアントのBean定義

RestClient / RestTemplateのBean定義を行い、DIコンテナに登録する。これらのBeanは、クライアントアプリケーションにおける、リクエスト送信の起点となる。
また、RestClient / RestTemplateのBean定義時にはClientHttpRequestFactoryを通して実際のリクエスト作成で利用する実装を選択することができる。
本ガイドラインでは原則として、Apache HttpComponents HttpClientを使用してリクエストを作成するClientHttpRequestFactory実装であるHttpComponentsClientHttpRequestFactoryの利用を前提とする。

HttpComponentsClientHttpRequestFactoryは、Spring Framework 6.1よりサーバへのリクエスト送信を行う前にリクエストボディをバッファリングしないようになったため、リクエストのContent-Lengthヘッダが設定されなくなった。
Content-Lengthヘッダを設定したい場合は、「Content-Lengthヘッダについて」に記載している設定を追加する必要がある。

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

@Bean("restClient")
public RestClient restClient() {
    return RestClient.builder()
              .requestFactory(new HttpComponentsClientHttpRequestFactory())  // (1)
              .build();  // (2)
}

項番

説明

(1)
RestClient.BuilderrequestFactoryメソッドに、org.springframework.http.client.HttpComponentsClientHttpRequestFactoryを設定する。
HttpComponentsClientHttpRequestFactoryを利用した通信設定については「リクエスト生成処理のカスタマイズ方法(ClientHttpRequestFactory)」を参照されたい。
(2)
RestClient.Builderbuildメソッドを使用してRestClientのBeanを生成し、DIコンテナに登録する。

代表的なRestClient / RestTemplateのBeanのカスタマイズ方法は、下記で実装例を示しているため、必要に応じて参照されたい。


5.2.2.1.3. RESTクライアントの利用

RESTクライアントを利用する場合は、DIコンテナに登録されているRestClient / RestTemplateをインジェクションする。

RESTクライアントのインジェクション例

@Service
public class AccountServiceImpl implements AccountService {

    @Inject
    RestClient restClient;

    // omitted

}

5.2.2.2. 基準となるURLを設定する

baseUrlを利用すると、REST APIにアクセスする際の基準となるURLをあらかじめ設定しておくことができる。

例えば以下のURLにおいて、先頭の「http://localhost:8080/api/v1」をbaseUrlとして設定しておくことで、クライアント側の実装では後続のパス「/users」のみを指定することで、完全なURLが自動的に生成される。
なお、baseUrlを指定していても、RESTクライアントを使用する際に指定するURIに絶対URLを明示的に指定した場合は、baseUrlの設定は無効となる。
これにより、共通的なベースURLを設定しつつ、特定のリクエストでのみ異なるURL(別ドメイン等)へ送信することも可能となっている。

URLの例

  • URL: http://localhost:8080/api/v1/users

  • baseUrl: http://localhost:8080/api/v1

  • クライアントで指定するパス: /users


baseUrlの設定方法は、利用するRESTクライアント実装によって異なる。

  • RestClientRestClient.builder().baseUrl(...)メソッドを使用して設定する。

  • RestTemplateDefaultUriBuilderFactorybaseUrlを設定して、RestTemplateに適用する必要がある。

以下に設定例を示す。
  • ApplicationContextConfig.java

@Value("${api.baseUrl:http://localhost:8080/api/v1}")  // (1)
private String url;

@Bean("restClient")
public RestClient restClient() {
    return RestClient.builder().baseUrl(url)  // (2)
        .requestFactory(new HttpComponentsClientHttpRequestFactory())
        .build();

}

項番

説明

(1)
プロパティファイルから基準となるURLを取得し設定を行う。
(2)
RestClient.BuilderbaseUrlメソッドを使用して、(1)のURLを設定する。

利用時のURLは以下の通りとなる。

private String uri = "/users/{id}";  // (1)

@Inject
private RestClient restClient;

public User getUser(String id) {
    return restClient.get().uri(uri, id)
            .retrieve()
            .body(User.class);
}

項番

説明

(1)
baseUrlで指定したURL以降のパスのみを指定する。

baseUrlを設定しない、または絶対URLを指定してbaseUrlの設定を上書きする場合は、以下の実装を行う。

@Value("${api.baseUrl:http://localhost:8080/api/v2}/users/{id}")  // (1)
private String uri;

@Inject
private RestClient restClient;

public User getUser(String id) {
    return restClient.get().uri(uri, id)
            .retrieve()
            .body(User.class);
}

項番

説明

(1)
プロパティファイルから絶対パスで記載されたURLを取得し、利用するURLとして指定する。

5.2.2.3. データの取得(GETリクエスト送信)

RestClient / RestTemplateは、GETリクエスト送信を行うためのメソッドを複数提供している。代表的なメソッドを以下に示す。
これ以外のメソッドについては、「RestClientのJavadoc」、「RestTemplateのJavadoc」を参照されたい。
代表的なメソッド一覧

項番

用途

RestClientのメソッド

RestTemplateのメソッド

(1)
レスポンスボディを任意のデータ型で取得する
RestClient.get().uri(...).retrieve().body(...)
RestTemplate.getForObject(...)
(2)
レスポンス情報を含む結果を取得する
RestClient.get().uri(...).retrieve().toEntity(...)
RestTemplate.getForEntity(...)

5.2.2.3.1. GETメソッドを指定したリクエスト送信の実装

GETメソッドでリクエスト送信を行い、レスポンスボディを任意のデータ型で取得する実装の方法を以下に示す。
以下の実装例は、Userを表現するJavaBeanクラスにてレスポンスボディを受け取る実装である。

bodyメソッドの使用例

フィールド宣言部

@Value("${api.url:http://localhost:8080/api}")
private String uri;

メソッド内部

User user = restClient.get()  // (1)
    .uri(uri) // (2)
    .retrieve() // (3)
    .body(User.class); // (4)

項番

説明

(1)
getメソッドを使用してHTTPメソッドにGETを設定し、RequestHeadersUriSpecを取得する。
(2)
RequestHeadersUriSpec.uriメソッドを使用して、リクエスト送信先のURLを設定する。

他にもリクエストに対する設定を行う必要がある場合は、RequestHeadersUriSpecを利用して、リクエストの設定を行う。
(3)
RequestHeadersUriSpecでリクエストに対する設定を行った後、retrieveメソッドを呼び出し、レスポンスの設定(受信時のデータ型、例外ハンドラ等)を行うためのResponseSpecを取得する。
retrieveメソッドでは、ResponseSpecの生成、取得までを行い、リクエストの送信は行われない。
リクエストの送信は、ResponseSpecのメソッド(bodytoEntity等)が呼び出されたタイミングで行われる。
(4)
ResponseSpec.bodyメソッドにてレスポンスボディを受け取る型を指定し、リクエストの送信を行う。
レスポンスボディのデータはHttpMessageConverterによってbodyの引数に指定したJavaクラスへ変換された後、返却される。

5.2.2.3.2. ステータスコードやレスポンスヘッダを含めたレスポンスの取得

HTTPステータスコード、レスポンスヘッダ、レスポンスボディを取得する必要がある場合は、以下のように実装する。
実装例はGETメソッドを使用した場合であるが、POSTメソッドやPUTメソッドなど他のHTTPメソッドを使用した場合も同様に実装できる。

toEntityメソッドの使用例

インポート宣言

import org.springframework.http.ResponseEntity;

フィールド宣言部

@Value("${api.url:http://localhost:8080/api}")
private String uri;

メソッド内部

ResponseEntity<User> responseEntity =
        restClient.get().uri(uri) // (1)
                .retrieve()
                .toEntity(User.class); // (2)
HttpStatusCode statusCode = responseEntity.getStatusCode(); // (3)
HttpHeaders header = responseEntity.getHeaders(); // (4)
User user = responseEntity.getBody(); // (5)

項番

説明

(1)
RestClient.getメソッドを使用してHTTPメソッドにGETを設定する。
(2)
ResponseSpec.toEntityメソッドを使用した場合は、戻り値はorg.springframework.http.ResponseEntityとなる。
(3)
HTTPステータスコードはResponseEntitygetStatusCodeメソッドを用いて取得する。
(4)
レスポンスヘッダはResponseEntitygetHeadersメソッドを用いて取得する。
(5)
レスポンスボディはResponseEntitygetBodyメソッドを用いて取得する。

Note

ResponseEntityとは

ResponseEntityはHTTPレスポンスを表すクラスで、HTTPステータスコード、レスポンスヘッダ、レスポンスボディの情報を取得することができる。

詳細はResponseEntityのJavadocを参照されたい。


5.2.2.3.3. コレクション形式のデータ取得

サーバから応答されるレスポンスボディの電文(JSON等)がコレクション形式の場合は、以下の実装を行う。

コレクション形式のデータの取得例

List<User> users = //(1)
    restClient.get().uri(uri)
            .retrieve()
            .body(new ParameterizedTypeReference<List<User>>(){}); //(2)

項番

説明

(1)
List<レスポンスデータの型>を指定する。
(2)
ResponseSpec.bodyメソッドの引数にorg.springframework.core.ParameterizedTypeReferenceのインスタンスを指定し、型パラメータにList<レスポンスデータの型>を指定する。

5.2.2.4. データの登録(POSTリクエスト送信)

RestClient / RestTemplateは、POSTリクエスト送信を行うためのメソッドを複数提供している。代表的なメソッドの一覧を以下に示す。
これ以外のメソッドについては、「RestClientのJavadoc」、「RestTemplateのJavadoc」を参照されたい。
代表的なメソッド一覧

項番

用途

RestClientのメソッド

RestTemplateのメソッド

(1)
レスポンスボディを任意のデータ型で取得する
RestClient.post().uri(...).body(...).retrieve().body(...)
RestTemplate.postForObject(...)
(2)
レスポンス情報を含む結果を取得する
RestClient.post().uri(...).body(...).retrieve().toEntity(...)
RestTemplate.postForEntity(...)
「レスポンス情報を含む結果を取得する」方法については、GETメソッドを指定した実装と同様のため、「ステータスコードやレスポンスヘッダを含めたレスポンスの取得」を参照されたい。

5.2.2.4.1. POSTメソッドを指定したリクエスト送信の実装

POSTメソッドでリクエスト送信を行い、レスポンスボディを任意のデータ型で取得する実装の方法を以下に示す。
以下の実装例は、Userを表現するJavaBeanクラスにてレスポンスボディを受け取る実装である。

bodyメソッドの使用例

フィールド宣言部

@Value("${api.url:http://localhost:8080/api}")
private String uri;

メソッド内部

var user = new User();

// omitted

User user = restClient.post()  // (1)
        .uri(uri)  // (2)
        .body(user)  // (3)
        .retrieve() // (4)
        .body(User.class);  // (5)

項番

説明

(1)
postメソッドを使用してHTTPメソッドにPOSTを設定し、RequestBodyUriSpecを取得する。
(2)
RequestBodyUriSpec.uriメソッドを使用して、リクエスト送信先のURLを設定する。

他にもリクエストに対する設定を行う必要がある場合は、RequestBodyUriSpecを利用して、リクエストの設定を行う。
(3)
RequestBodyUriSpec.bodyメソッドを使用してリクエストボディを設定する。
設定されたリクエストボディのオブジェクトは、HttpMessageConverterによって電文形式(JSON等)に変換される。
(4)
RequestBodyUriSpecでリクエストに対する設定を行った後、retrieveメソッドを呼び出し、レスポンスの設定(受信時のデータ型、例外ハンドラ等)を行うためのResponseSpecを取得する。
retrieveメソッドでは、ResponseSpecの生成、取得までを行い、リクエストの送信は行われない。
リクエストの送信は、ResponseSpecのメソッド(bodytoEntity等)が呼び出されたタイミングで行われる。
(5)
ResponseSpec.bodyメソッドにてレスポンスボディを受け取る型を指定し、リクエストの送信を行う。
レスポンスボディのデータはHttpMessageConverterによってbodyの引数に指定したJavaクラスへ変換された後、返却される。

5.2.2.5. エラーハンドリング

5.2.2.5.1. 例外ハンドリング(デフォルトの動作)

デフォルトで利用するエラーハンドラーは「エラーハンドリング(StatusHandler、ResponseErrorHandler)」で記載している通り、HTTPステータスコードに応じて特定の例外オブジェクトを返却する。
本項では、デフォルトのエラーハンドラが返却する例外オブジェクトのハンドリング方法について説明する。
なお、以降で示している例外ハンドリングに関する実装例はあくまでも例外処理の方法を説明するための例であり、例外ハンドリングのベストプラクティスを示すものではない。アプリケーション実装の際には、要件に応じて適切な例外ハンドリングを行うこと。

例外ハンドリングの実装例

フィールド宣言部

@Value("${api.retry.maxCount}")
int retryMaxCount;

@Value("${api.retry.retryWaitTimeCoefficient}")
int retryWaitTimeCoefficient;

メソッド内部

int retryCount = 0;
while (true) {
    try {

        User user = restClient.get().uri(uri)
                .retrieve()
                .body(User.class);

        break;

    } catch (HttpServerErrorException e) { // (1)

        if (retryCount == retryMaxCount) {
            throw e;
        }

        retryCount++;

        if (logger.isWarnEnabled()) {
            logger.warn("An error ({}) occurred on the server. (The number of retries:{} Times)", e.getStatusCode(), retryCount);  // (2)
        }

        try {
            Thread.sleep(retryWaitTimeCoefficient * retryCount);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
        }

        // omitted
    }

}

項番

説明

(1)
例外をキャッチしてエラー処理を行う。
上記の例では、サーバエラー(500系)時にthrowされるHttpServerErrorExceptionをキャッチしてリトライ処理を行っている。
(2)
必要に応じて例外オブジェクトからレスポンスデータを取得し、ログ出力や処理の判定等に利用する。
エラー時のレスポンスデータ(HTTPステータスコード、レスポンスヘッダ、レスポンスボディなど)は、例外クラスのgetterメソッドを呼び出すことで取得することができる。
取得可能なレスポンスデータについては、RestClientResponseExceptionを参照されたい。

5.2.2.5.2. HTTPステータスコードでハンドリング(エラーハンドラの拡張)

エラーハンドラを拡張し、HTTPステータスコードでハンドリングする方法について説明を行う。
エラーハンドラの拡張用途は、HTTPステータスコードでハンドリングする方法以外にもあるため、本項で説明するのは一例である。
エラーハンドリング(StatusHandler、ResponseErrorHandler)」で記載している通り、エラーハンドラの実装方法はRestClientRestTemplateで異なる。
RestClientでは、StatusHandlerと、ResponseErrorHandlerインタフェースの実装クラスを使った2つのエラーハンドリングの実装方法があるので、どちらかを拡張しRestClientに設定することで、独自のエラー処理を行うことができる。
RestTemplateでは、ResponseErrorHandlerインタフェースの実装クラスをRestTemplateに設定することで、独自のエラー処理を行うことができる。

以下の例では、サーバエラー及びクライアントエラーが発生した場合でもResponseEntityを返却し、HTTPステータスコードでエラーハンドリングが行えるように拡張をしている。

Bean定義ファイルの定義例(StatusHandler)

以下は、StatusHandlerを利用した実装例である。
  • ApplicationContextConfig.java

@Bean("restClient")
public RestClient restClient() {
    return RestClient.builder()
                .requestFactory(new HttpComponentsClientHttpRequestFactory())
                .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> {})  // (1)
                .build();
}

項番

説明

(1)
RestClient.BuilderdefaultStatusHandlerメソッドにPredicate<HttpStatusCode>と、ResponseSpec.ErrorHandlerを設定する。
上記の実装例では、HttpStatusCodeがサーバエラー(5xx系)及びクライアントエラー(4xx系)の場合、例外を発生させずに処理を継続するようにしている。
HttpStatusCodeで利用できるメソッドについては、「HttpStatusCodeのJavadoc」を参照されたい。

なお、上記の実装は処理を継続し正常終了扱いとしているため、デフォルトの動作と異なり後続のメッセージ変換処理(HttpMessageConverter) は動作する。
そのため、サーバ側から返却するレスポンスのBody部は、サーバエラー(5xx系)及びクライアントエラー(4xx系)が発生した際もHttpMessageConverterで型変換できるような値を返却する必要がある。

クライアント処理の実装例

int retryCount = 0;
while (true) {

    responseEntity = restClient.get().uri(uri)
                        .retrieve()
                        .toEntity(User.class);

    if (responseEntity.getStatusCode() == HttpStatus.OK) { // (1)

        break;

    } else if (responseEntity.getStatusCode() == HttpStatus.SERVICE_UNAVAILABLE) { // (2)

        if (retryCount == retryMaxCount) {
            break;
        }

        retryCount++;

        if (logger.isWarnEnabled()) {
            logger.warn("An error ({}) occurred on the server. (The number of retries:{} Times)",
                responseEntity.getStatusCode(), retryCount);
        }

        try {
            Thread.sleep(retryWaitTimeCoefficient * retryCount);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
        }

        // omitted
    }
}

項番

説明

(1)
上記の実装例では、エラー時にもResponseEntityを返すようにエラーハンドラを拡張しているので、返却されたResponseEntityからHTTPステータスコードを取得して、処理結果が正常であったか確認する必要がある。
(2)
エラー発生時も返却されたResponseEntityからHTTPステータスコードを取得して、その値に応じて処理を制御することができる。

5.2.2.5.3. RestClient呼び出し毎のエラーハンドリング(エラーハンドラの拡張)

RestClientのみの実装となるが、共通のエラーハンドラとは別にRestClientの呼び出し毎にエラーハンドラの拡張が行える。
本項では、RestClientonStatusメソッドを使用して、RestClientの呼び出し毎でエラーハンドリングを行う方法について説明する。

RestClient呼び出し毎のエラーハンドリングの実装例

User user = restClient.get().uri(uri)
                .retrieve()
                .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> { // (1)
                    // omitted
                })
                .body(User.class);

項番

説明

(1)
defaultStatusHandlerの実装と同様に、onStatusメソッドにPredicate<HttpStatusCode>ResponseSpec.ErrorHandlerを設定する。
上記の実装例では、サーバエラー(5xx系)のHTTPステータスコードが返却された場合のエラーハンドリングを行っている。
HttpStatusCodeで利用できるメソッドについては、「HttpStatusCodeのJavadoc」を参照されたい。

なお、onStatusメソッドの記載がある場合は、defaultStatusHandlerの実装より優先されて処理される。
上記の実装例の動作としては、サーバエラー(5xx系)のHTTPステータスコードが返却された場合にのみ適用され、クライアントエラー(4xx系)のHTTPステータスコードが返却された場合はdefaultStatusHandlerの実装が適用される。

5.2.2.6. リクエストヘッダの設定

リクエストヘッダを設定する方法には、アプリケーション全体で共通のヘッダを設定する方法と、リクエスト送信単位でヘッダを設定する方法がある。
クライアントアプリケーション内で、複数回のリクエスト送信処理を記述するとき、各リクエストのヘッダが同一で良い場合には、リクエスト送信処理ごとにリクエストヘッダを設定する処理を書いていると冗長なコードを記述することになる。
このような場合にはアプリケーション全体で共通のリクエストヘッダを設定するようにし、共通ではないリクエストヘッダを設定する必要がある実装箇所のみ、リクエスト送信単位でリクエストヘッダの設定を行ったほうがよい。

5.2.2.6.1. アプリケーション全体で共通のリクエストヘッダを設定する方法

リクエスト初期化(ClientHttpRequestInitializer)」で記載の通り、RestClientではdefaultHeaderRestTemplateではClientHttpRequestInitializerを使用することで共通のリクエストヘッダの設定が可能である。
リクエスト送信単位でリクエストヘッダの設定を変えたい場合は、「リクエスト送信単位でリクエストヘッダを設定する方法」を参照されたい。

以下は、リクエストヘッダに共通のカスタムヘッダを付与する例である。

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

@Bean("restClient")
public RestClient restClient() {
    return RestClient.builder()
            .requestFactory(new HttpComponentsClientHttpRequestFactory())
            .defaultHeader("Custom-Header", "CustomHeaderValue") // (1)
            .build();
}

項番

説明

(1)
RestClient.BuilderdefaultHeaderメソッドを使用してリクエストヘッダにカスタムヘッダを付与している。
上記の例では、Custom-HeaderというキーでCustomHeaderValueという値を設定している。
複数のリクエストヘッダを設定する場合は、RestClient.BuilderdefaultHeadersメソッドを使用して実装することができる。

Note

ClientHttpRequestInitializerを使った実装

RestClientの場合も、以下のようにrequestInitializerメソッドを使ってClientHttpRequestInitializerを使った実装も可能である。
RestClientを利用する場合、リクエストヘッダの設定には、ClientHttpRequestInitializerを利用する事もできるが、リクエストヘッダのみを設定したい際にはdefaultHeaderメソッドの利用を推奨する。
defaultHeaderメソッドを利用するほうが、 ClientHttpRequestInitializerによる実装よりも簡潔に記述でき、設定の意味も明瞭になる。

5.2.2.6.2. リクエスト送信単位でリクエストヘッダを設定する方法

本項の設定は、リクエスト送信単位でヘッダの設定を変えたい場合に利用する。
リクエストの設定は、基本的にはアプリケーション全体で共通のリクエストヘッダを設定する方法を用いて設定を行えばよいが、特定のリクエスト送信のみヘッダのカスタマイズを行いたい場合は、本項で説明する方法を用いて実装を行う。
実装方法は、RestClientRestTemplateで異なる。

RestClientで実装する場合、Content-TypeAcceptといった代表的なHTTPヘッダについては、RequestBodyUriSpecに専用の設定メソッド(contentTypeaccept)が用意されているため、これを利用すると良い。
一方で、設定用のメソッドが用意されていない、リクエストヘッダを設定する場合は、RequestBodyUriSpecheaderメソッドを使用して設定することができる。
詳細はRequestBodyUriSpecのJavadocを参照されたい。

RestTemplateで実装する場合は、RequestEntityのメソッドを使用してHTTPヘッダを設定する。
こちらにも RestClientと同様に代表的なHTTPヘッダ用の設定メソッドがあるため、Content-TypeAcceptといったヘッダの設定にはこちらを用いるのが良い。
設定用のメソッドが無いヘッダは、RequestEntity.HeadersBuilderheaderメソッドを使用して設定する。
詳細はRequestEntityのJavadocを参照されたい。
本ガイドラインでは、以下のHTTPヘッダについて設定方法を示す。

5.2.2.6.2.1. Content-Typeヘッダの設定
サーバへデータを送信する場合は、通常Content-Typeヘッダの指定が必要となる。
Content-Typeヘッダをリクエスト送信時に設定するには、以下の通り実装を行う。

Content-Typeヘッダの設定例

インポート宣言

import org.springframework.http.ResponseEntity;

フィールド宣言部

@Value("${api.url:http://localhost:8080/api}")
private String uri;

メソッド内部

var user = new User();

// omitted

ResponseEntity<User> responseEntity = restClient.post().uri(uri)
                .contentType(MediaType.APPLICATION_JSON) // (1)
                .body(user)
                .retrieve()
                .toEntity(User.class);

User user = responseEntity.getBody();

項番

説明

(1)
RequestBodyUriSpec.contentTypeメソッドを使用し、Content-Typeヘッダの値を指定する。
上記の実装例では、送信時のデータ形式がJSONであることを示す「application/json」を設定している。

5.2.2.6.2.2. Acceptヘッダの設定
サーバから取得するデータの形式を指定する場合は、Acceptヘッダの指定が必要となる。
なお、サーバが複数のデータ形式のレスポンスをサポートしていない場合は、Acceptヘッダを明示的に指定しなくてもよいケースもある。
Acceptヘッダをリクエスト送信時に設定するには、以下の通り実装を行う。

Acceptヘッダの設定例

メソッド内部

ResponseEntity<User> responseEntity = restClient.get().uri(uri)
                .accept(MediaType.APPLICATION_JSON) // (1)
                .retrieve()
                .toEntity(User.class);

User user = responseEntity.getBody();

項番

説明

(1)
RequestBodyUriSpec.acceptメソッドを使用して、Acceptヘッダの値を設定する。
上記の実装例では、取得可能なデータ形式がJSONであることを示す「application/json」を設定している。

5.2.2.6.2.3. 任意のリクエストヘッダの設定
サーバへアクセスするために、リクエストヘッダの設定が必要になるケースや、カスタムヘッダを付与するケースでは以下の通り設定を行う。
以下は、カスタムヘッダの設定を行う例である。

カスタムヘッダの実装例

メソッド内部

ResponseEntity<User> responseEntity = restClient.get().uri(uri)
                .header("Custom-Header", "CustomValue") // (1)
                .retrieve()
                .toEntity(User.class);

User user = responseEntity.getBody();

項番

説明

(1)
RequestBodyUriSpec.headerメソッドを使用してリクエストヘッダの名前と値を設定する。

5.2.2.7. 認証要求の設定

RESTクライアント側が実装すべき認証要求の方法は、RESTサーバの採用する認証方式に依存するが、本項では認証ヘッダを利用した認証要求の実装方法を説明する。
認証ヘッダを利用した認証方式としては、Bearerトークン認証(APIキー認証)やBasic認証などがある。
APIキー認証に関しては、X-API-Key等の独自ヘッダを利用することもあるため、サーバが利用する認証の仕様に応じてヘッダ名を変更する必要がある。
以下では、Basic認証を例とした認証ヘッダの設定方法を説明する。

認証ヘッダに関する仕様は、RFC 9110に記載されているのでこちらを参照されたい。

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

@Value("${api.auth.username}") // (1)
String username;

@Value("${api.auth.password}") // (2)
String password;

@Bean("restClient")
public RestClient restClient() {
    String base64Credentials = Base64.getEncoder()
            .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));  // (3)
    return RestClient.builder()
            .requestFactory(new HttpComponentsClientHttpRequestFactory())
            .defaultHeader("Authorization", "Basic " + base64Credentials)  // (4)
            .build();
}

項番

説明

(1)
Basic認証で利用するユーザ名を設定する。
(2)
Basic認証で利用するパスワードを設定する。
(3)
ユーザ名とパスワードを「”:“ 」でつなげ、バイト配列に変換する。
バイト配列をJava標準のjava.util.Base64を使用してBase64エンコードを行う。
(4)
RestClient.BuilderdefaultHeaderメソッドを使用して、AuthorizationヘッダにBasic認証の資格情報を設定する。
上記ではBasic認証の例としているため、認証スキームとしてBasicを指定して認証情報を設定している。

Note

Spring Security 5より、org.springframework.security.crypto.codec.Base64は非推奨になったため、Java標準のjava.util.Base64に置き換えることを推奨する。


5.2.2.8. 通信タイムアウトの設定

サーバとの通信に対してタイムアウト時間を指定したい場合は、ClientHttpRequestFactoryの実装クラスを定義し、実装クラスのプロパティ値を設定することで実現することができる。
本項では、ClientHttpRequestFactoryの実装の定義と、タイムアウトの設定方法について説明を行う。

5.2.2.8.1. HttpComponentsClientHttpRequestFactoryを使用した通信タイムアウトの設定

サーバとの通信に対してタイムアウト時間を指定したい場合は、ClientHttpRequestFactoryの実装クラスのプロパティ値を設定することで実現することができる。
以下は、HttpComponentsClientHttpRequestFactoryを使用した定義例である。
  • ApplicationContextConfig.java

// (1)
@Value("${api.connectTimeout:2000}")
private int connectTimeout;

// (2)
@Value("${api.readTimeout:2000}")
private int readTimeout;

// omitted

@Bean("clientHttpRequestFactory")
public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
    // (3)
    var connectionConfig = ConnectionConfig.custom()
            .setConnectTimeout(Timeout.ofMilliseconds(connectTimeout))
            .build();
    // (4)
    var poolManager = PoolingHttpClientConnectionManagerBuilder.create()
            .setDefaultConnectionConfig(connectionConfig)
            .build();
    // (5)
    var httpClient = HttpClientBuilder.create()
            .setConnectionManager(poolManager)
            .build();
    // (6)
    var bean = new HttpComponentsClientHttpRequestFactory(httpClient);
    bean.setReadTimeout(readTimeout); // (7)
    return bean;
}

@Bean("timeoutRestClient")
public RestClient timeoutRestClient() {
    return RestClient.builder()
            .requestFactory(clientHttpRequestFactory()) // (8)
            .build();
}

項番

説明

(1)
サーバとの接続タイムアウトの時間(ミリ秒)を設定する。
(2)
レスポンスデータの読み込みタイムアウトの時間(ミリ秒)を設定する。
(3)
org.apache.hc.client5.http.config.ConnectionConfig.customメソッドにてBuilderを生成する。
生成したBuildersetConnectTimeoutメソッドに、(1)の接続タイムアウト時間を設定する。
buildメソッドにてConnectionConfigのインスタンスを生成する。
(4)
org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder.createメソッドにてPoolingHttpClientConnectionManagerBuilderを生成する。
生成したPoolingHttpClientConnectionManagerBuildersetDefaultConnectionConfigメソッドに、(3)で生成したConnectionConfigのインスタンスを設定する。
buildメソッドにてPoolingHttpClientConnectionManagerのインスタンスを生成する。
(5)
org.apache.hc.client5.http.impl.classic.HttpClientBuilder.createメソッドにてHttpClientBuilderを生成する。
生成したHttpClientBuildersetConnectionManagerメソッドに、(4)で生成したPoolingHttpClientConnectionManagerのインスタンスを設定する。
buildメソッドにてHttpClientのインスタンスを生成する。
(6)
HttpComponentsClientHttpRequestFactoryのコンストラクタに、(5)で生成したHttpClientのインスタンスを設定して、HttpComponentsClientHttpRequestFactoryのインスタンスを生成する。
(7)
HttpComponentsClientHttpRequestFactorysetReadTimeoutメソッドに、(2)の読み込みタイムアウト時間を設定する。
setReadTimeoutメソッドで設定した読み込みタイムアウト時間は、org.apache.hc.client5.http.config.RequestConfig.BuildersetResponseTimeoutに設定される。
(8)
RestClient.BuilderrequestFactoryメソッドを使用して生成したHttpComponentsClientHttpRequestFactoryを設定する。
HttpComponentsClientHttpRequestFactoryで設定したタイムアウト値を超えた場合はorg.springframework.web.client.ResourceAccessExceptionが発生する。

Note

タイムアウト発生時の起因例外

HttpComponentsClientHttpRequestFactoryで設定したタイムアウト値を超えた場合は、タイムアウトの種類に関わらずorg.springframework.web.client.ResourceAccessExceptionが発生する。
したがって、タイムアウトの種類に応じて例外ハンドリングを行いたい場合は、ResourceAccessExceptionの起因例外の型を確認する必要がある。
ResourceAccessExceptionの起因例外として、接続タイムアウト発生時はorg.apache.hc.client5.http.ConnectTimeoutException、読み込みタイムアウト発生時はjava.net.SocketTimeoutExceptionが設定されているため、これらの型を確認することでタイムアウトの種類を判別することができる。
なお、上記挙動に関しては使用するClientHttpRequestFactoryの実装によって異なるため、ClientHttpRequestFactoryが利用するHttpClientの仕様を確認してから実装を行うこと。

5.2.2.9. ファイルアップロード(マルチパートリクエスト)

ファイルアップロード(マルチパートリクエスト)を行う場合は、以下のように実装する。

ファイルアップロードの実装例

MultiValueMap<String, Object> multiPartBody = new LinkedMultiValueMap<>();  //(1)
multiPartBody.add("file", new ClassPathResource("/uploadFiles/User.txt"));  //(2)

restClient.post().uri(uri)
        .contentType(MediaType.MULTIPART_FORM_DATA)  //(3)
        .body(multiPartBody)  //(4)
        .retrieve()
        .body(Void.class);

項番

説明

(1)
マルチパートリクエストとして送信するデータを格納するためにMultiValueMapを生成する。
(2)
パラメータ名をキーに指定して、アップロードするファイルをMultiValueMapに追加する。
上記例では、fileというパラメータ名を指定して、クラスパス上に配置されているファイルをアップロードファイルとして追加している。
(3)
Content-Typeヘッダのメディアタイプをmultipart/form-dataに設定する。
MultiValueMapにString値以外が設定されている場合、Content-TypeヘッダはFormHttpMessageConverterによってmultipart/form-dataが設定される。
したがって、上記の例では省略しても問題はない。
(4)
アップロードするファイルが格納されているMultiValueMapをリクエストボディに設定する。

Note

Spring Frameworkが提供するResourceクラスについて

Spring Frameworkはリソースを表現するインタフェースとしてorg.springframework.core.io.Resourceを提供しており、ファイルをアップロードする際に使用することができる。

Resourceインタフェースの主な実装クラスは以下の通りである。

  • org.springframework.core.io.PathResource

  • org.springframework.core.io.FileSystemResource

  • org.springframework.core.io.ClassPathResource

  • org.springframework.core.io.UrlResource

  • org.springframework.core.io.InputStreamResource (ファイル名をサーバに連携できない)

  • org.springframework.core.io.ByteArrayResource (ファイル名をサーバに連携できない)


5.2.2.10. ファイルダウンロード

ファイルダウンロードの実装方法は、レスポンス取得後処理でInputStreamを使用してレスポンスボディを少しずつファイルに書き出す方法と、byte配列でレスポンスボディを一括取得する方法がある。
byte配列で一括取得する方法は、ダウンロードするファイルのサイズによっては、java.lang.OutOfMemoryErrorが発生する可能性があるため、基本的にはInputStreamを使用した実装を推奨する。
ダウンロードするファイルのサイズが小さく、メモリに展開しても問題ない場合に限り、byte配列で一括取得する実装を検討するとよい。

InputStreamを使用してファイルに書き出す方法

レスポンス取得後処理にてレスポンスボディをInputStreamで読み込み、読み込んだ内容をファイルに書き出す実装例を示す。
以下は、「レスポンス取得後処理(ExchangeFunction、ResponseExtractor)」に記載しているExchangeFunctionResponseExtractorを使用したダウンロードの実装例である。

インポート宣言

import org.springframework.util.FileCopyUtils;

メソッド内部

File rcvFile =
        restClient.get().uri(uri).exchange((request, response) -> {  // (1)
            // (2)
            if (response.getStatusCode().isError()) {
                throw response.createException();
            }
            File rcvFile = File.createTempFile("rcvFile", "zip");  // (3)
            FileCopyUtils.copy(response.getBody(), new FileOutputStream(rcvFile));  // (4)
            return rcvFile;  // (5)
        });  // (6)

// omitted

項番

説明

(1)
RequestBodyUriSpec.exchangeメソッドを利用し、レスポンス取得後処理を実装する。
引数にはExchangeFunctionが定義されているため、関数にレスポンス取得後処理を実装する。
(2)
エラー判定及びエラー処理を実装する。
ExchangeFunctionの引数として定義されているConvertibleClientHttpResponseからステータスコードを取得し、クライアントエラー及びサーバエラーの場合、ConvertibleClientHttpResponsecreateExceptionメソッドを使用して例外をスローする。
ConvertibleClientHttpResponsecreateExceptionメソッドを使用することにより、StatusHandlerで生成される例外をスローすることができる。

レスポンス取得後処理(ExchangeFunction、ResponseExtractor)」にて説明している通り、ExchangeFunctionはエラー判定、エラー処理、レスポンスボディの変換処理を行う必要があるため、RestTemplateとは異なり、エラー判定及びエラー処理も実装する必要がある。
(3)
出力するファイルを生成する。
(4)
レスポンスボディをInputStreamで読み込み、FileCopyUtilsを使用して、(3)で生成したファイルにレスポンスボディを少しずつ書き出す。
(5)
ファイル作成後、作成したファイルを返却する。

Note

HTTPヘッダや、ステータスコードも返却する場合

HTTPヘッダや、ステータスコードも含めて返却したい場合は、 ResponseEntityに格納して返却すればよい。

ResponseEntity<File> responseEntity =
        restClient.get().uri(uri).exchange((request, response) -> {
            if (response.getStatusCode().isError()) {
                throw response.createException();
            }
            File rcvFile = File.createTempFile("rcvFile", "zip");
            FileCopyUtils.copy(response.getBody(), new FileOutputStream(rcvFile));
            return ResponseEntity.status(response.getStatusCode())
                    .headers(response.getHeaders()).body(rcvFile);
        });
(6)
RestClientを実行してファイルのダウンロードを行い、出力したファイルを取得する。

byte配列で一括取得する方法

以下は、レスポンスボディをbyte配列で一括取得する実装例である。
ダウンロードするファイルのサイズが小さい場合のみ、以下の方法で実装を行うこと。
byte[] downloadContent = restClient.get().uri(uri)
        .retrieve()
        .body(byte[].class); //(1)

項番

説明

(1)
ダウンロードファイルを指定したデータ型で扱う。
上記の例では、ダウンロードしたファイルをバイト配列で取得する。

Warning

byte配列でファイルをダウンロードする際の注意点

サイズの大きなファイルをデフォルトで登録されているHttpMessageConverterを使用して byte配列で取得すると、 java.lang.OutOfMemoryErrorが発生する可能性がある。
また、org.springframework.core.io.Resourceで取得した場合も、メッセージ変換で利用されるResourceHttpMessageConverterByteArrayResourceを生成する際にレスポンスボディをbyte配列で取得するため、同様の事象が発生する。

5.2.3. How to extend(同期通信)

本節では、同期通信実装の拡張方法について説明する。
本節の内容は、「How to use(同期通信)」だけでは実現できない動作や設定をカスタマイズしたい場合に必要な内容である。

RESTクライアント実装の構成要素と利用上のポイント」にて説明を行った構成要素のカスタマイズ方法は、以下の通りである。

5.2.3.1. URL生成処理をカスタマイズする方法(UriBuilderFactory

RESTサーバが公開するAPIには、一定のパターンに従うプレースホルダを含んでいる物が存在することが想定される。(例えば、日付を含むURLなど)このような場合、RESTクライアント側では、リクエスト送信先のURLを生成する処理をカスタマイズして、サーバが期待する形式のURLを生成できると便利である。
以下では、RESTサーバ側が期待するURLに特定形式でフォーマットされた日付が含まれる場合を例にとり、RESTクライアントでリクエスト先URLの生成処理をカスタマイズして、RESTサーバが期待するURLをRESTクライアントが生成できるようにする例を示す。
RESTクライアントでのリクエスト先URLの生成処理のカスタマイズは UriBuilderFactoryおよびUriBuilderの実装を拡張するすることで実現する。
URL生成処理のカスタマイズが、ある特定のリクエスト送信処理のみだけに限定される場合は、 UriBuilderFactoryUriBuilderを利用せず、個別のリクエスト送信処理の中でURLをカスタマイズすることも可能だが、そうでない場合は、UriBuilderFactoryUriBuilderを利用することで実装の重複を避け、コードのメンテンス性を落とさずにカスタマイズが可能である。
なお、 UriBuilderFactoryおよびUriBuilderは日付の利用に関係なく、リクエスト先URLの生成処理一般のためのカスタマイズポイントとして機能する。

5.2.3.1.1. URIテンプレート変数値のフォーマット変換

以下は、URIテンプレート変数の値(LocalDate)を、特定のフォーマット文字列に変換するUriBuilderFactoryの実装例である。
  • URIテンプレート:http://localhost:8080/api/order/{date}

  • URIテンプレート変数の値:LocalDate.of(2025, 1, 1);

  • 送信時のURL:http://localhost:8080/api/order/20250101

UriBuilderFactoryの拡張

// (1)
public class CustomUriBuilderFactory extends DefaultUriBuilderFactory {

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

    // Default Constructor
    public CustomUriBuilderFactory() {
        super();
    }

    // Constructor with baseUri
    public CustomUriBuilderFactory(String baseUriTemplate) {
        super(baseUriTemplate);
    }

    // Constructor with UriComponentsBuilder
    public CustomUriBuilderFactory(UriComponentsBuilder baseUri) {
        super(baseUri);
    }

    // (2)
    @Override
    public UriBuilder uriString(String uriTemplate) {
        return new CustomUriBuilder(super.uriString(uriTemplate));
    }

    // (2)
    @Override
    public UriBuilder builder() {
        return new CustomUriBuilder(super.builder());
    }

    // (3)
    private class CustomUriBuilder implements UriBuilder {

        private final UriBuilder delegate;

        public CustomUriBuilder(UriBuilder delegate) {
            this.delegate = delegate;
        }

        // (4)
        @Override
        public URI build(Map<String, ?> uriVariables) {
            Map<String, Object> convertedUriVariables =
                                    uriVariables.entrySet().stream()
                                                .collect(Collectors.toMap(Map.Entry::getKey, this::convert));
            return delegate.build(convertedUriVariables);
        }

        @Override
        public URI build(Object... uriVariables) {
            Object[] convertedUriVariables =
                                    Arrays.stream(uriVariables)
                                                .map(this::convert)
                                                .toArray();
            return delegate.build(convertedUriVariables);
        }

        // (5)
        private Object convert(Object value) {
            if(value instanceof LocalDate date) {
                return FORMATTER.format(date);
            }
            return value;
        }

        // Delegate all other methods to the original UriBuilder

        // (6)
        @Override
        public UriBuilder scheme(String scheme) {
            return delegate.scheme(scheme);
        }

        @Override
        public UriBuilder userInfo(String userInfo) {
            return delegate.userInfo(userInfo);
        }

        // ... other methods from UriBuilder
    }
}

項番

説明

(1)
UriBuilderFactoryの実装クラスとして、CustomUriBuilderFactoryを生成する。
上記の例では、DefaultUriBuilderFactoryを拡張した実装クラスを作成して、必要なメソッドのみをオーバーライドしている。
コンストラクタに関しては、DefaultUriBuilderFactoryと同様のコンストラクタを用意している。
(2)
UriBuilderを生成するメソッドをオーバーライドして、独自のUriBuilderを生成するよう実装を行う。
変更箇所以外はDefaultUriBuilderの実装をそのまま使用するため、CustomUriBuilderのコンストラクタ引数にてDefaultUriBuilderを引き渡すようにしている。
(3)
UriBuilderの実装クラスとしてCustomUriBuilderを生成する。
(4)
buildメソッドにパラメータの変換処理を作成する。
上記の例では、URIテンプレート変数の値が設定されているコレクションを受け取り、全ての値に対して変換処理を行い返却を行っている。
なお、カスタマイズした変換処理以外はDefaultUriBuilderの実装をそのまま使用するため、DefaultUriBuilderbuildメソッドを最後に実行している。
(5)
(4)の各要素で呼び出す変換処理を実装する。
上記の例では、LocalDate型の場合のみ、指定したフォーマット文字列に変換して返却するようにしている。
(6)
変換処理以外のメソッドは、DefaultUriBuilderの実装をそのまま使用するため、DefaultUriBuilderのメソッドをそのまま呼び出すように実装を行う。

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

// (1)
@Bean("customUriBuilderFactory")
public CustomUriBuilderFactory customUriBuilderFactory() {
    return new CustomUriBuilderFactory();
}

@Bean("restClient")
public RestClient restClient() {
    return RestClient.builder()
            .requestFactory(new HttpComponentsClientHttpRequestFactory())
            .uriBuilderFactory(customUriBuilderFactory()) // (2)
            .build();

項番

説明

(1)
CustomUriBuilderFactoryのBeanを定義する。
(2)
RestClient.BuilderuriBuilderFactoryメソッドに、CustomUriBuilderFactoryのBeanを設定する。

5.2.3.2. リクエスト生成処理のカスタマイズ方法(ClientHttpRequestFactory

ClientHttpRequestFactoryを使用したリクエスト生成処理のカスタマイズ方法について説明する。
タイムアウトの設定については、「通信タイムアウトの設定」にて説明を行っているのでこちらを参照されたい。

5.2.3.2.1. カスタマイズしたキーストアファイルの利用(SSL/TLS)

JDKのデフォルトのキーストアファイルでは信頼できると見なせないサーバ証明書を返すサーバと通信する場合等、使用するキーストアファイルを変更したいケースがある。
ここでは、その一例として、SSL自己署名証明書を使用するケースを想定して説明する。
テスト環境などでSSL自己署名証明書を使用する場合は、以下のように実装する。

FactoryBeanの実装例

RestClient / RestTemplateに設定するClientHttpRequestFactoryorg.springframework.beans.factory.FactoryBeanにて実装する。
サンプルコードであるため、HttpClientConnectionManagerHttpClientの設定値は業務要件に応じ適切に設定されたい。
import java.security.KeyStore;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.cookie.StandardCookieSpec;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.http.ssl.TLS;
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
import org.apache.hc.core5.pool.PoolReusePolicy;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

public class RequestFactoryBean implements
                                FactoryBean<ClientHttpRequestFactory>,
                                DisposableBean { // (18)

    private String keyStoreFileName;

    private char[] keyStorePassword;

    private HttpComponentsClientHttpRequestFactory factory; // (18)

    @Override
    public ClientHttpRequestFactory getObject() throws Exception {

        // (1)
        SSLContext sslContext = SSLContext.getInstance("TLS");

        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        ks.load(this.getClass().getClassLoader().getResourceAsStream(
                this.keyStoreFileName), this.keyStorePassword);

        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory
                .getDefaultAlgorithm());
        kmf.init(ks, this.keyStorePassword);

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(
                TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ks);

        sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

        // @formatter:off
        PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() // (2)
                .setTlsSocketStrategy(new DefaultClientTlsStrategy(sslContext)) // (3)
                .setDefaultTlsConfig( // (4)
                        TlsConfig.custom()
                        .setSupportedProtocols(TLS.V_1_3, TLS.V_1_2)
                        .setHandshakeTimeout(Timeout.ofMilliseconds(1L))
                        .build())
                .setDefaultSocketConfig( // (5)
                        SocketConfig.custom()
                        .setSoTimeout(Timeout.ofMinutes(1L))
                        .build())
                .setMaxConnTotal(1) // (6)
                .setMaxConnPerRoute(1) // (7)
                .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT) // (8)
                .setConnPoolPolicy(PoolReusePolicy.LIFO) // (9)
                .setDefaultConnectionConfig( // (10)
                        ConnectionConfig.custom()
                        .setTimeToLive(TimeValue.ofMinutes(1L)) // (11)
                        .setConnectTimeout(Timeout.ofSeconds(5L)) // (12)
                        .build())
                .build();
        // @formatter:on

        // @formatter:off
        CloseableHttpClient httpClient = HttpClients.custom() // (13)
                .setConnectionManager(connectionManager) // (14)
                .setDefaultRequestConfig(
                        RequestConfig.custom()
                        .setResponseTimeout(Timeout.ofSeconds(10L)) // (15)
                        .setCookieSpec(StandardCookieSpec.STRICT) // (16)
                        .build())
                .build();
        // @formatter:on

        // (17)
        this.factory = new HttpComponentsClientHttpRequestFactory(httpClient);

        return this.factory;
    }

    @Override
    public Class<?> getObjectType() {
        return ClientHttpRequestFactory.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

    public void setKeyStoreFileName(String keyStoreFileName) {
        this.keyStoreFileName = keyStoreFileName;
    }

    public void setKeyStorePassword(char[] keyStorePassword) {
        this.keyStorePassword = keyStorePassword;
    }

    // (18)
    @Override
    public void destroy() throws Exception {
        if (this.factory != null) {
            this.factory.destroy();
        }
    }
}

項番

説明

(1)
後述のBean定義で指定されたキーストアファイルのファイル名とパスワードを元に、SSLコンテキストを作成する。
使用するSSL自己署名証明書のキーストアファイルは、クラスパス上に配置する。
(2)
(1)で作成したSSLコンテキストを設定するためのHttpClientConnectionManagerを作成する。
ここでは、実装クラスとしてorg.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerを指定している。
(3)
(1)で作成したSSLコンテキストを設定する。
(4)
Tls Configのデフォルト値を設定する。
Socketから提供されるInputStreamread()のブロック時間がHandshakeTimeoutを越えた場合、通常、java.net.SocketTimeoutExceptionが発生するが、TCPコネクション確立後、SSLハンドシェイクを行っている間にこのタイムアウトが起きた場合には、org.apache.hc.client5.http.ConnectTimeoutExceptionが発生する。

Note

HandshakeTimeoutはSSLハンドシェイクが完了する十分な長さを確保すること。

SSLハンドシェイク完了前にHandshakeTimeoutによってタイムアウトした際に、SSLハンドシェイクが中断される(SSLハンドシェイクに関する続きの通信を行わなくなる)がTCPコネクションのcloseも行われない事象を確認している。この状態になると、サーバ側で処理を破棄しない限りサーバ側はSSLハンドシェイクの続きを待つこととなってしまう。

そのため、SSLハンドシェイク中に通信障害や通信相手のハードウェア障害等に遭遇し通信相手からのパケットが届かない時間が長く続く場合(クライアント側で通信の継続を諦めなければならない場合)以外においては、HandshakeTimeoutを利用したタイムアウトは避けたほうが良い。

SSLハンドシェイク完了後のHTTPSリクエストに対するレスポンスのタイムアウトには、後述のResponseTimeoutが利用できるため、

  • HTTPSリクエストに対する応答に関するタイムアウトにはResponseTimeout

  • ResponseTimeoutの有効範囲外(HTTPSリクエスト送信前)となるSSLハンドシェイク中の応答に関するタイムアウトにはHandshakeTimeout(正常稼働時にはタイムアウトしないよう長めに設定)

という具合に使い分けること。なお、HandshakeTimeoutが未設定の場合はConnectTimeoutが設定されている。

(5)
Socket Configのデフォルト値を設定する。

Note

SoTimeoutの設定について

SocketConfig.BuilderSoTimeoutは、Apache HttpComponents HttpClient 5.5まではSSLハンドシェイクのタイムアウト値にも適用されていた。 しかしながら、バージョン5.5.1からはSSLハンドシェイクのタイムアウト値は、TlsConfig.BuilderHandshakeTimeoutの設定を利用するように変更された。

そのため、SSLハンドシェイクのタイムアウト設定を行う場合は、(4)のHandshakeTimeoutに設定を行う必要がある。

(6)
最大合計接続数を設定する。
最大合計接続数を超える場合、後続処理はコネクションの取得を待機する。
(7)
宛先(ホスト名 + ポート番号 及び スキーマ定義)ごとの最大接続数を設定する。
最大接続数を超える場合、後続処理はコネクションの取得を待機する。
(8)
(9)
(10)
コネクションに関連するデフォルト値を設定する。
(11)
コネクションが接続されてから切断されるまでの最大生存時間を設定する。
使用状況にかかわらず、ConnectionTimeToLiveを越えた場合にコネクションを破棄する。
(12)
新しい接続が確立されるまでのタイムアウトのデフォルト値を設定する。
接続の確立にConnectTimeout以上かかった場合、org.apache.hc.client5.http.HttpHostConnectExceptionが発生する。
(13)
作成したSSLコンテキストを利用する org.apache.hc.client5.http.impl.classic.CloseableHttpClientを作成する。
(14)
(2)で作成したHttpClientConnectionManagerを設定する。
(15)
レスポンスタイムアウトのデフォルト値を設定する。
レスポンス返却にResponseTimeout以上かかった場合、java.net.SocketTimeoutExceptionが発生する。
(16)
(17)
作成したHttpClientを利用するClientHttpRequestFactoryを作成する。
(18)
FactoryBeanで生成したオブジェクトのライフサイクルはDIコンテナで管理されないため、破棄時に特定の処理を実行するにはFactoryBeanDisposableBeanインタフェースのdestroyメソッドを実装する必要がある。
ここでは、getObjectメソッドで生成した HttpComponentsClientHttpRequestFactorydestroyメソッドを呼び出し、HttpClientをクローズしている。このため、生成したオブジェクトを変数に保持している。

Note

通信先のアプリケーションがTLS 1.2以前のバージョンにしか対応していない等の理由により、使用するTLSのバージョンをJVMレベルで変更するにはHTTP通信におけるTLS(Transport Layer Security) v1.3のサポートを参照されたい。

ただし、JVMレベルで設定してしまうと一つのクライアントアプリからTLS 1.2とTLS 1.3を利用した別々のアプリケーションに接続するような要件を実現することができない。このような場合は、HttpClientごとに利用するTLSのバージョンを指定するような実装を検討されたい。


Bean定義ファイルの定義例

SSL自己署名証明書を使用したSSL通信を行うClientHttpRequestFactoryを、RestClient / RestTemplateに設定する。

  • ApplicationContextConfig.java

@Value("${rscl.keystore.filename}")
private String keystoreFilename;

@Value("${rscl.keystore.password}")
private String keystorePassword;

// omitted

@Bean("httpsRestClient")
public RestClient httpsRestClient() throws Exception {
    return RestClient.builder()
            .requestFactory(httpsRequestFactoryBean().getObject()) // (1)
            .build();
}

// (1)
@Bean("httpsRequestFactoryBean")
public RequestFactoryBean httpsRequestFactoryBean() {
    var factory = new RequestFactoryBean();
    factory.setKeyStoreFileName(keystoreFilename);
    factory.setKeyStorePassword(keystorePassword.toCharArray());
    return factory;
}

項番

説明

(1)
作成したRequestFactoryBeanRestClient.BuilderrequestFactoryメソッドに指定する。
RequestFactoryBeanには、キーストアファイルのファイル名とパスワードを渡す。

RESTクライアントの使用方法

RestClient / RestTemplateの使い方については、SSL自己署名証明書を使用しない場合と同様であるため、以下を参照されたい。


5.2.3.2.2. HTTP Proxyサーバの設定方法

サーバへアクセスする際にHTTP Proxyサーバを経由する必要がある場合は、システムプロパティやJVM起動引数、またはRestClient / RestTemplateのBean定義にてHTTP Proxyサーバの設定が必要である。
システムプロパティやJVM起動引数に設定した場合、アプリケーション全体に影響を与えてしまうため、RestClient / RestTemplate毎にHTTP Proxyサーバの設定を行う例を紹介する。
HTTP Proxyサーバの設定は、ClientHttpRequestFactoryインタフェースの実装クラスであるHttpComponentsClientHttpRequestFactoryを利用して設定を行う。
本項では、Proxy認証を行わない場合と、Proxy認証を行う場合の2通りの設定方法について説明する。

5.2.3.2.2.1. Proxy認証を行わない場合の設定

資格情報が不要なHTTP Proxyサーバの接続先の指定については、以下の通り実装を行う。

Bean定義ファイル

  • ApplicationContextConfig.java

// (2)
@Value("${rscl.http.proxyHost}")
private String httpProxyHost;

// (3)
@Value("${rscl.http.proxyPort}")
private int httpProxyPort;

// (1)
@Bean
public HttpHost httpHost() {
    return new HttpHost(httpProxyHost, httpProxyPort); // (2) (3)
}

// (4)
@Bean("proxyHttpClientBuilder")
public HttpClientBuilder proxyHttpClientBuilder() {
    HttpClientBuilder bean = HttpClientBuilder.create();
    bean.setProxy(httpHost()); // (5)
    return bean;
}

// (6)
@Bean("httpComponentsClientHttpRequestFactory")
public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory() {
    return new HttpComponentsClientHttpRequestFactory(proxyHttpClientBuilder().build()); // (7)
}

// (8)
@Bean("proxyRestClient")
public RestClient proxyRestClient() {
    return RestClient.builder()
            .requestFactory(httpComponentsClientHttpRequestFactory()) // (9)
            .build();
}

項番

説明

(1)
org.apache.hc.core5.http.HttpHostにHTTP Proxyサーバの設定を行う。
(2)
HttpHostのコンストラクタ引数のhostnameに、プロパティファイルに設定されたキーrscl.http.proxyHostの値をHTTP Proxyサーバのホスト名として設定する。
(3)
HttpHostのコンストラクタ引数のportに、プロパティファイルに設定されたキーrscl.http.proxyPortの値をHTTP Proxyサーバのポート番号として設定する。
(4)
org.apache.hc.client5.http.impl.classic.HttpClientBuilderを使用し、org.apache.hc.client5.http.classic.HttpClientの設定を行う。
(5)
HttpClientBuildersetProxyメソッドに、HTTP Proxyサーバの設定を行ったorg.apache.hc.core5.http.HttpHostを設定する。
(6)
HttpComponentsClientHttpRequestFactoryにプロキシ設定を行ったHttpClientを設定する。
(7)
HttpComponentsClientHttpRequestFactoryのコンストラクタの引数に、HttpClientBuilderから生成したHttpClientを設定する。
(8)
RestClientのBean定義を行う。
(9)
RestClient.BuilderrequestFactoryメソッドを使用して、(6)で生成したHttpComponentsClientHttpRequestFactoryを設定する。

5.2.3.2.2.2. Proxy認証を行う場合の設定
HTTP Proxyサーバにアクセスする際に資格情報(ユーザ名とパスワード)が必要な場合は、org.apache.http.impl.client.BasicCredentialsProviderを使用し資格情報を設定する。
BasicCredentialsProvidersetCredentialsメソッドが引数を2つ取るため、セッターインジェクションを利用してBeanを生成することができない。
このため、org.springframework.beans.factory.FactoryBeanを利用してBeanを生成する。

FactoryBeanクラス

import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Value;

// (1)
public class BasicCredentialsProviderFactoryBean implements FactoryBean<BasicCredentialsProvider> {

    // (2)
    @Value("${rscl.http.proxyHost}")
    String host;

    // (3)
    @Value("${rscl.http.proxyPort}")
    int port;

    // (4)
    @Value("${rscl.http.proxyUserName}")
    String userName;

    // (5)
    @Value("${rscl.http.proxyPassword}")
    String password;

    @Override
    public BasicCredentialsProvider getObject() throws Exception {

        // (6)
        var authScope = new AuthScope(this.host, this.port);

        // (7)
        char[] passwordCharArray = this.password == null ? null
                : this.password.toCharArray();
        var usernamePasswordCredentials = new UsernamePasswordCredentials(this.userName, passwordCharArray);

        // (8)
        var credentialsProvider = new BasicCredentialsProvider();

        credentialsProvider.setCredentials(authScope,
                usernamePasswordCredentials);

        return credentialsProvider;
    }

    @Override
    public Class<?> getObjectType() {
        return BasicCredentialsProvider.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

項番

説明

(1)
org.springframework.beans.factory.FactoryBeanを実装したBasicCredentialsProviderFactoryBeanクラスを定義する。
Beanの型にBasicCredentialsProviderを設定する。
(2)
プロパティファイルに設定されたキーrscl.http.proxyHostの値をHTTP Proxyサーバのホスト名として、インスタンス変数に設定する。
(3)
プロパティファイルに設定されたキーrscl.http.proxyPortの値をHTTP Proxyサーバのポート番号として、インスタンス変数に設定する。
(4)
プロパティファイルに設定されたキーrscl.http.proxyUserNameの値をHTTP Proxyサーバのユーザ名として、インスタンス変数に設定する。
(5)
プロパティファイルに設定されたキーrscl.http.proxyPasswordの値をHTTP Proxyサーバのパスワードとして、インスタンス変数に設定する。
(6)
org.apache.http.auth.AuthScope を作成し資格情報のスコープを設定する。この例は、HTTP Proxyサーバのホスト名とポート番号を指定したものである。その他の設定方法については、AuthScope (Apache HttpClient API)を参照されたい。
(7)
org.apache.http.auth.UsernamePasswordCredentialsを作成し資格情報を設定する。
(8)
org.apache.http.impl.client.BasicCredentialsProviderを作成し、setCredentialsメソッドを使用し、資格情報のスコープと資格情報を設定する。

Bean定義ファイル

  • ApplicationContextConfig.java

// (1)
@Bean
public BasicCredentialsProviderFactoryBean basicCredentialsProviderFactoryBean() {
    return new BasicCredentialsProviderFactoryBean();
}

@Bean
public HttpHost httpHost() {
    return new HttpHost(httpProxyHost, httpProxyPort);
}

@Bean("proxyHttpClientBuilder")
public HttpClientBuilder proxyHttpClientBuilder() throws Exception {
    HttpClientBuilder bean = HttpClientBuilder.create();
    bean.setDefaultCredentialsProvider(basicCredentialsProviderFactoryBean().getObject()); // (1)
    bean.setProxy(httpHost());
    return bean;
}

@Bean("httpComponentsClientHttpRequestFactory")
public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory() {
    return new HttpComponentsClientHttpRequestFactory(proxyHttpClientBuilder()
            .build());
}

@Bean("proxyRestClient")
public RestClient proxyRestClient() {
    return RestClient.builder()
            .requestFactory(httpComponentsClientHttpRequestFactory())
            .build();
}

項番

説明

(1)
HttpClientBuildersetDefaultCredentialsProviderメソッドに、BasicCredentialsProviderを設定する。
BasicCredentialsProviderは、FactoryBeanを実装したBasicCredentialsProviderFactoryBeanを使用しBeanを作成する。

5.2.3.3. メッセージ変換処理の設定(HttpMessageConverter

5.2.3.3.1. カスタムメッセージコンバータの登録

デフォルトで登録されている HttpMessageConverterで電文変換の要件を満たせない場合は、カスタマイズした HttpMessageConverterを作成、登録することで要件に対応する事ができる。
ただし、カスタマイズしたメッセージコンバータを登録すると、デフォルトで利用されていた HttpMessageConverterは利用されなくなる。このため、カスタムメッセージコンバータ以外にも、要件を満たすためのメッセージコンバータが必要な場合は、必要なHttpMessageConverter実装をすべて登録する必要がある。
カスタムメッセージコンバータを利用するには以下のように実装する。

カスタムメッセージコンバータの作成

import java.io.IOException;
import java.lang.reflect.Type;

import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;

// (1)
public class CustomHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {

    public CustomHttpMessageConverter() {
        // (2)
        super(new MediaType("text", "custom-format"), new MediaType("application", "custom-format"));
    }

    // (3)
    @Override
    protected void writeInternal(Object t, @Nullable Type type, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        // omitted
    }

    // (3)
    @Override
    protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        // omitted
    }

    // (3)
    @Override
    public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        // omitted
    }

    // omitted

}

項番

説明

(1)
org.springframework.http.converter.AbstractGenericHttpMessageConverterを継承した実装クラスを作成する。
AbstractGenericHttpMessageConverterGenericHttpMessageConverterインターフェースを実装した抽象クラスであり、「コレクション形式のデータ取得」で説明したParameterizedTypeReferenceを利用したコレクション型を扱うことができる。
(2)
本コンバータが対応するMediaTypeを設定する。
なお、複数のMediaTypeに対応する場合は、配列で複数指定する必要がある。
(3)
要件にあわせて変換処理を実装する。
メソッドの説明は、AbstractGenericHttpMessageConverterのJavadocを参照されたい。

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

// (1)
@Bean("customHttpMessageConverter")
public CustomHttpMessageConverter customHttpMessageConverter() {
    return new CustomHttpMessageConverter();
}

@Bean("restClient")
public RestClient restClient() {
    return RestClient.builder()
            .requestFactory(new HttpComponentsClientHttpRequestFactory())
            .configureMessageConverters(f ->  (2)
                 f.registerDefaults()  (3)
                  .addCustomConverter(customHttpMessageConverter())  (4)
            )
            .build();
}

項番

説明

(1)
カスタムメッセージコンバータのBeanを定義する。
(2)
RestClient.BuilderconfigureMessageConvertersメソッドにてHttpMessageConverterを登録する。
(3)
関数の引数にHttpMessageConverters.ClientBuilderが渡されるため、ClientBuilderregisterDefaultsメソッドを呼び出し、デフォルトのHttpMessageConverterの登録を行う。
(4)
ClientBuilderaddCustomConverterメソッドにて(1)で定義したBeanを登録する。

なお、ClientBuilderにはカスタムメッセージコンバータの追加以外にも、各種メディアタイプに対応するHttpMessageConverterの置き換え用のメソッド(with~)が用意されている。
詳しくは、HttpMessageConverters.BuilderのJavadocを参照されたい。
これらの置き換え用のメソッドを使用することで、既存のメディアタイプに対応するHttpMessageConverterの変更も可能である。

5.2.3.4. リクエスト送信前後の共通処理の適用(ClientHttpRequestInterceptor

ClientHttpRequestInterceptorを使用することで、サーバとの通信処理の前後に任意の処理を実行させることができる。
ここでは、リクエスト送信前後のログ出力処理を適用する方法を紹介する。

5.2.3.4.1. リクエスト送信前後のロギング処理

サーバとの通信ログを出力したい場合は、以下のような実装を行う。

通信ログ出力の実装例

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

@Bean("restClient")
public RestClient restClient() {
    return RestClient.builder()
            .requestFactory(new HttpComponentsClientHttpRequestFactory())
            .requestInterceptor((request, body, execution) -> {  // (1)
                if (logger.isInfoEnabled()) {
                    var requestBody = new String(body, StandardCharsets.UTF_8);
                    // (2)
                    logger.info("Request Header {}", request.getHeaders());
                    logger.info("Request Body {}", requestBody);
                }

                ClientHttpResponse response = execution.execute(request, body);  // (3)

                if (logger.isInfoEnabled()) {
                    // (4)
                    logger.info("Response Header {}", response.getHeaders());
                    logger.info("Response Status Code {}", response.getStatusCode());
                }
                return response; // (5)
            })
            .build();
}

項番

説明

(1)
RestClient.BuilderrequestInterceptorメソッドにリクエスト送信前後の処理を実装する。
(2)
リクエスト送信前の共通処理を実装する。
上記の実装例では、リクエストヘッダとリクエストボディの内容をログに出力している。
(3)
関数の引数として受け取ったorg.springframework.http.client.ClientHttpRequestExecutionexecuteメソッドを実行し、リクエストの送信を行う。
(4)
レスポンス受信後の共通処理を実装する。
上記の実装例では、レスポンスヘッダとステータスコードの内容をログに出力している。
(5)
(3)で受信したレスポンスを返却する。

5.2.3.4.2. 複数のClientHttpRequestInterceptorを適用する方法

複数のClientHttpRequestInterceptorを適用するには、以下のような実装を行う。

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

@Bean("restClient")
public RestClient restClient() {
    return RestClient.builder()
            .requestFactory(new HttpComponentsClientHttpRequestFactory())
            .requestInterceptors(interceptors -> { // (1)
                interceptors.add((request, body, execution) -> { // (2)
                    if (logger.isInfoEnabled()) {
                        var requestBody = new String(body, StandardCharsets.UTF_8);

                        logger.info("Request Header {}", request.getHeaders());
                        logger.info("Request Body {}", requestBody);
                    }

                    ClientHttpResponse response = execution.execute(request, body);

                    if (logger.isInfoEnabled()) {
                        logger.info("Response Header {}", response.getHeaders());
                        logger.info("Response Status Code {}", response.getStatusCode());
                    }
                    return response;
                });

                interceptors.add((request, body, execution) -> { // (3)
                    // omitted
                    return execution.execute(request, body);
                });
            })
            .build();
}

項番

説明

(1)
RestClient.BuilderrequestInterceptorsメソッドの引数にはConsumer<List<ClientHttpRequestInterceptor>>が設定されているため、関数を呼び出しClientHttpRequestInterceptorを追加する。
Listに追加した順番でClientHttpRequestInterceptorのチェーンが実行される。
上記の例では、リクエスト送信前は(2)、(3)の順に実行され、レスポンス受信後は(3)、(2)の順に実行される。
(2)
(1)で取得したList<ClientHttpRequestInterceptor>ClientHttpRequestInterceptorを追加する。
(3)
(2)と同様に、登録したいClientHttpRequestInterceptorを追加する。

5.2.4. How to use(HTTP Service Clients)

本節では、HTTP Service Clientsを使用したクライアント処理の実装方法について説明する。

HTTP Service Clientsの設定方法は、@HttpExchangeアノテーションを付与したインターフェースを、インターフェース単位で設定・登録する方法と、インターフェースをグループ単位で設定し一括登録する方法の2通りがある。
インターフェース単位で設定・登録する場合は、HttpServiceProxyFactoryにクライアント設定を行ったうえで、@HttpExchangeを付与したインターフェースのクライアント実装(動的プロキシ)を作成し、DIコンテナに登録を行う。
インターフェースをグループ単位で設定し一括登録する場合は、HttpServiceGroupConfigurerにてグループ単位でクライアント設定を行う。
そのうえで、@ImportHttpServicesにグループに含める@HttpExchangeを付与したインターフェースを指定し、指定したインターフェースのクライアント実装(動的プロキシ)を一括作成して、DIコンテナに登録を行う。

@HttpExchangeアノテーションを付与したインターフェースの作成粒度に制約はないため、一つのインターフェースにてサーバへのアクセスを全て集約して定義することも可能ではあるが、可読性やメンテナンス性を考慮すると意味のある単位で分割して定義を行う方が望ましい。
適切な分割単位としては、RESTful Web Serviceの章で紹介している「リソース指向アーキテクチャ」のリソース単位で作成するのが可読性やメンテナンス性の観点からも適している。
TERASOLUNA Server Framework for Java (5.x) では、@HttpExchangeアノテーションを付与したインターフェースは、リソース単位で作成することを推奨し、以降の説明においてもリソース単位で作成することを前提として説明を行う。

それぞれの特徴は、以下の通りである。
  • インターフェース単位で設定・登録する場合(HttpServiceProxyFactoryを利用した設定)

    アクセスするリソース単位で繰り返しBean定義を行う必要があるため、小規模なシステムでHTTP Service Clientsを数個程度利用する場合に適している。
    また、クライアントの設定をカスタマイズ(ClientHttpRequestFactoryClientHttpRequestInterceptorの設定等)する場合、HTTP Service Clientsに直接設定するのではなく、元となる各RESTクライアント実装に設定を行う必要があるため、既にRestClientRestTemplateを利用していてHTTP Service Clientsに移行したい場合に適している。
  • グループ単位で設定を集約して一括登録する場合(@ImportHttpServicesアノテーションとHttpServiceGroupConfigurerを利用した設定)

    一括でまとめてBean定義を行うことができるため、中規模以上のシステムでHTTP Service Clientsを多数利用する場合に適している。
    クライアントの設定をカスタマイズする際には、RestClientRestTemplateで利用する拡張ポイントが RestClientHttpServiceGroupConfigurer経由で同じように利用できるため、HTTP Service Clientsだけで設定を完結できる。
    これにより設定内容を HTTP Service Clientsに集約できるため、新規でRESTクライアントを実装する場合に適している。

5.2.4.1. HTTP Service Clients(インターフェース単位で設定・登録する場合)のセットアップ

HttpServiceProxyFactoryを利用しインターフェース単位で設定・登録する場合は、以下の手順でセットアップを行う。

  1. RestClientRestTemplateのいずれかのBeanをDIコンテナに登録する。
  2. @HttpExchangeアノテーションを付与したインターフェースを作成する。
  3. DIコンテナに登録したRESTクライアントのBeanを、Adapterを介してHttpServiceProxyFactoryに設定する。
    HttpServiceProxyFactoryを使用して@HttpExchangeアノテーションを付与したインターフェースの動的プロキシを生成し、DIコンテナに登録する。
  4. RESTクライアントを利用するコンポーネントにて、@HttpExchangeアノテーションを付与したインターフェースの動的プロキシをインジェクションする。

5.2.4.1.1. RESTクライアントのBean定義(インターフェース単位で設定・登録する場合)

RestClientRestTemplateのいずれかのBeanをDIコンテナに登録する。
本手順はRestClientRestTemplateの設定と同様のため、「同期通信のセットアップ」を参照されたい。

なお、HttpServiceProxyFactoryを利用する場合は、各RESTクライアント実装の設定を引き継ぐためHTTP Service Clientsで設定できる内容は限られている。
具体的には、REST APIへのアクセス方法や「HTTP Service ClientsでのURIテンプレート変数値のフォーマット変換」等の一部のみが設定可能であり、ClientHttpRequestFactoryClientHttpRequestInterceptor等の拡張ポイントは設定できない。
したがって、共通的な設定に関しては各RESTクライアント実装にて設定を行う必要がある。
設定についてはRestClientRestTemplateにて説明を行っているので、以下を参照されたい。

5.2.4.1.2. HTTP Service Clientsの定義(インターフェース単位で設定・登録する場合)

@HttpExchangeアノテーションを付与したインターフェースを作成する。
インターフェースの定義は、RestClientRestTemplateのいずれの場合でも同様である。

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;

@HttpExchange(url = "/api/v1")  // (1)
public interface UserApi {  // (2)

    @GetExchange(value = "users/{id}")
    User getUser(@PathVariable("id") String id);

}

項番

説明

(1)
REST APIのURLとマッピングするアノテーションとして@HttpExchangeアノテーションを定義する。
url属性には、本インターフェースの基準となるURLを指定する。
上記の例では、「基準となるURLを設定する」を設定したうえで、/api/v1をこのインターフェースで利用する共通のURLとして設定している。
なお、@HttpExchangeアノテーションはクラスで指定をせず、メソッドに付与することも可能である。
(2)
インターフェースとして定義を行う。
実装は動的プロキシとして作成されるため、実装クラスは不要である。

5.2.4.1.3. HTTP Service ClientsのBean定義(インターフェース単位で設定・登録する場合)

RestClientRestTemplateのBeanを、Adapterを介してHTTP Service Clientsに設定する。

  • ApplicationContextConfig.java

@Value("${api.baseUrl:http://localhost:8080}")
private String url;

@Bean("restClient")
public RestClient restClient() {
    return RestClient.builder()
                    .baseUrl(url)
                    .requestFactory(new HttpComponentsClientHttpRequestFactory())
                    .build();
}

// (1)
@Bean("userApi")
public UserApi userApi() {
    RestClientAdapter adapter = RestClientAdapter.create(restClient());  // (2)
    HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();  // (3)
    return factory.createClient(UserApi.class);  // (4)
}

項番

説明

(1)
HTTP Service ClientsのBean定義を行う。
返却値として「HTTP Service Clientsの定義(インターフェース単位で設定・登録する場合)」で定義したインターフェースを設定する。
(2)
org.springframework.web.client.support.RestClientAdaptercreateメソッドを使用してRestClientAdapterのインスタンスを生成する。
引数には、RestClientのBeanを指定する。
(3)
org.springframework.web.service.invoker.HttpServiceProxyFactorybuilderForメソッドを使用して、(2)で生成したAdapterを設定する。
その後、buildメソッドを実行してHttpServiceProxyFactoryのインスタンスを生成する。
(4)
HttpServiceProxyFactorycreateClientメソッドを使用して、(1)で定義したインターフェースの型(クラス)を設定する。
返却値としてHTTP Service ClientsのBean(動的プロキシ)を返却し、DIコンテナに登録する。

5.2.4.1.4. HTTP Service Clientsの利用(インターフェース単位で設定・登録する場合)

DIコンテナに登録したHTTP Service ClientsのBeanをインジェクションする。

@Service
public class AccountServiceImpl implements AccountService {

    @Inject
    private UserApi userApi;  // (1)

    @Override
    public User getAccount(String id) {
        return userApi.getUser(id);  // (2)
    }

    // omitted

}

項番

説明

(1)
@Injectアノテーションを付与し、HTTP Service ClientsのBeanをインジェクションする。
(2)
HTTP Service Clientsの定義(インターフェース単位で設定・登録する場合)」で定義したメソッドを呼び出し、処理を実行する。

5.2.4.2. HTTP Service Clients(グループ単位で設定を集約して一括登録する場合)のセットアップ

@ImportHttpServicesアノテーションとHttpServiceGroupConfigurerを利用してグループ単位で一括で設定を行う場合は、以下の手順でセットアップを行う。

  1. @HttpExchangeアノテーションを付与したインターフェースを作成する。
  2. @HttpExchangeアノテーションを付与したインターフェースをHTTPサービスグループに登録し、HTTPサービスグループ単位で設定を行う。

    Note

    HTTPサービスグループとは

    同じクライアント設定とHttpServiceProxyFactoryを共有する@HttpExchangeを付与したインターフェースの集合である。 グループ定義は@ImportHttpServicesで行い、定義したグループ単位のクライアント設定はHttpServiceGroupConfigurerにて適用する。

  3. RESTクライアントを利用するコンポーネントにて、@HttpExchangeアノテーションを付与したインターフェースの動的プロキシをインジェクションする。

5.2.4.2.1. HTTP Service Clientsの定義(グループ単位で設定を集約して一括登録する場合)

@HttpExchangeアノテーションを付与したインターフェースを作成する。
インターフェースの定義については、インターフェース単位で設定・登録する場合と同様のため「HTTP Service Clientsの定義(インターフェース単位で設定・登録する場合)」を参照されたい。

5.2.4.2.2. HTTP Service ClientsのBean定義(グループ単位で設定を集約して一括登録する場合)

作成したHTTP Service ClientsをHTTPサービスグループに登録し、HTTPサービスグループ単位で設定を行う。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.support.RestClientHttpServiceGroupConfigurer;
import org.springframework.web.service.registry.ImportHttpServices;

// (1)
@Configuration
@ImportHttpServices(group = "first", types = { UserApi.class })  // (2)
@ImportHttpServices(group = "second", basePackages = "com.example.restclient.api") // (3)
public class HttpServiceClientsConfig {

    private static final Logger logger = LoggerFactory.getLogger(HttpServiceClientsConfig.class);

    @Value("${api.baseUrl:http://localhost:8080}")
    private String url;

    @Bean
    @Order(0)  // (4)
    RestClientHttpServiceGroupConfigurer defaults() {
        // (5)
        return groups -> groups.forEachGroup((group, clientBuilder, factoryBuilder) -> {
            // (6)
            clientBuilder.baseUrl(url)
                        .requestFactory(new HttpComponentsClientHttpRequestFactory())
                        .defaultHeader("Default-Header", "TestHeader")
                        .requestInterceptor((request, body, execution) -> {
                            if (logger.isInfoEnabled()) {
                                var requestBody = new String(body, StandardCharsets.UTF_8);

                                logger.info("Request Header {}", request.getHeaders());
                                logger.info("Request Body {}", requestBody);
                            }

                            ClientHttpResponse response = execution.execute(request, body);

                            if (logger.isInfoEnabled()) {
                                logger.info("Response Header {}", response.getHeaders());
                                logger.info("Response Status Code {}", response.getStatusCode());
                            }
                            return response;
                        });
        });
    }

    @Bean
    @Order(1)  // (4)
    RestClientHttpServiceGroupConfigurer firstGroup() {
        // (7)
        return groups -> groups.filterByName("first").forEachGroup((group, clientBuilder, factoryBuilder) -> {
            clientBuilder.defaultHeader("First-Header", "TestHeader");
        });
    }

    @Bean
    @Order(2)  // (4)
    RestClientHttpServiceGroupConfigurer secondGroup() {
        // (7)
        return groups -> groups.filterByName("second").forEachGroup((group, clientBuilder, factoryBuilder) -> {
            clientBuilder.defaultHeader("Second-Header", "TestHeader");
        });

    }
}

項番

説明

(1)
HTTP Service ClientsのBean定義を行うクラスを作成する。
ApplicationContextConfigに記載しても問題はないが、他の定義と混在し煩雑になりやすいため、別クラスに分けることを推奨する。
ApplicationContextConfigには、@Import({ HttpServiceClientsConfig.class })を追加し、HttpServiceClientsConfigの定義を取り込む。
(2)
@ImportHttpServicesアノテーションを付与し、HTTP Service ClientsをHTTPサービスグループに登録する。
クラス単位で登録する場合は、group属性にグループ名を指定し、types属性にHTTP Service Clientsのインターフェースの型(クラス)を指定する。
同一グループに複数のHTTP Service Clientsを登録する場合は、types属性に配列で指定する。

(2)、(3)は設定方法の違いを示すために両方記載しているが、どちらか一方の方法で登録すれば問題ない。
(3)
@ImportHttpServicesアノテーションを付与し、HTTP Service ClientsをHTTPサービスグループに登録する。
パッケージを指定して一括で登録する場合は、group属性にグループ名を指定し、basePackages属性に登録するパッケージ名を指定する。
basePackagesを指定することにより、パッケージ配下に存在する@HttpExchangeアノテーションを付与した全てのインターフェースが登録される。

(2)、(3)は設定方法の違いを示すために両方記載しているが、どちらか一方の方法で登録すれば問題ない。
また、HTTPサービスグループは、他にも手動で登録する方法などもあるので、詳しくは「HTTP Service Clients - HTTP Service groups」を参照されたい。
(4)
@Orderアノテーションを付与し、HTTPサービスグループの設定順序を指定する。
上記の例では、defaultsメソッドで全てのグループに対して共通の設定を行った後に、firstGroupメソッドとsecondGroupメソッドでそれぞれのグループに対して個別の設定を行っている。

Warning

Orderアノテーションの設定順序について

@Orderアノテーションの引数の値が小さいHttpServiceGroupConfigurerほど優先度が高く、先に適用される。
そのため、同じ設定項目に対して複数のHttpServiceGroupConfigurerで設定を行う場合、設定の種類に応じて次の挙動となる。
  • ClientHttpRequestFactoryのように単一の設定しか保持できない項目

    後から適用された設定で上書きされる。
    そのため、複数のHttpServiceGroupConfigurerで設定した場合は、@Orderアノテーションの引数の値が大きい設定が有効となる。
  • ClientHttpRequestInterceptorのように複数設定を保持できる項目

    @Orderアノテーションの引数の値が小さい順に設定が追加される。

そのため、共通設定を先に適用し、個別設定で上書きまたは追加を行う場合は、共通設定の@Orderの値を小さくし、個別設定の@Orderの値を大きくする必要がある。
(5)
org.springframework.web.client.support.RestClientHttpServiceGroupConfigurerを利用して、HTTPサービスグループの設定を行う。
RestClientHttpServiceGroupConfigurerは、@FunctionalInterfaceで提供されているため、関数にて設定を行う。
上記の例では、グループをフィルタリングせずにforEachGroupメソッドを使用して全てのグループに対して設定を行っている。
(6)
forEachGroupの引数としてDefaultRestClientBuilderが引き渡されるため、必要に応じて共通のカスタマイズ設定を行う。
設定内容はRestClientと同様のため、以下を参照されたい。
(7)
グループ単位で設定する場合は、filterByNameメソッドを使用してグループをフィルタリングして設定を行う。

Note

HttpServiceGroupConfigurerについて

HttpServiceGroupConfigurerのサブインターフェースとして、RestClientHttpServiceGroupConfigurerWebClientHttpServiceGroupConfigurerが定義されている。
これらのインターフェースが、DefaultRestClientBuilderDefaultWebClientBuilderを仲介し、RestClientWebClientの設定を行っている。
したがって、同期通信を行う場合はRestClientHttpServiceGroupConfigurerを、非同期通信を行う場合はWebClientHttpServiceGroupConfigurerを使用する必要がある。

5.2.4.2.3. HTTP Service Clientsの利用(グループ単位で設定を集約して一括登録する場合)

DIコンテナに登録したHTTP Service ClientsのBeanをインジェクションする。
HTTP Service Clientsの利用に関しては、インターフェース単位で設定・登録する場合と同様のため「HTTP Service Clientsの利用(インターフェース単位で設定・登録する場合)」を参照されたい。

5.2.4.3. データの取得(GETリクエスト送信)

本項では、HTTP Service Clientsを使用したデータ取得について説明を行う。

5.2.4.3.1. GETメソッドを指定したリクエスト送信の実装

GETメソッドでリクエスト送信を行い、結果を取得する場合の実装の説明を行う。
以下は、HTTP Service Clientsのインターフェースの定義である。

HTTP Service Clientsの定義

// (1)
@GetExchange(value = "users/{id}")  // (2)
User getUser(@PathVariable("id") String id);  // (3)

項番

説明

(1)
返却値としてレスポンスボディの型を定義する。
返却値についての説明は、次項の「HTTP Service Clientsの返却値について」にて説明を行う。
(2)
HTTPのメソッドにGETを指定する場合は、@GetExchangeアノテーションを付与する。
value属性にはURLを指定する。
@HttpExchangemethod属性にGETを指定した場合と同等である。
(3)
URIテンプレート変数の値を設定する場合は、メソッドに引数を定義し@PathVariableアノテーションを付与する。
アノテーションの引数にはURIテンプレート変数の名前を指定する。

5.2.4.3.2. HTTP Service Clientsの返却値について

HTTP Service Clientsで指定できる返却値は、他のRESTクライアントと同様でリクエストボディの型とResponseEntityが指定可能である。
下記以外の実装については、「HTTP Service Clients - Return Values」を参照されたい。

HTTP Service Clientsの定義

@GetExchange(value = "users/{id}")
User getUser(@PathVariable("id") String id);  // (1)

@GetExchange(value = "users/{id}")
ResponseEntity<User> getUserResponseEntity(@PathVariable("id") String id);  // (2)

項番

説明

(1)
返却値としてレスポンスボディの型を定義する。
レスポンスボディのみ取得したい場合に使用する。
(2)
返却値としてResponseEntity<T>を定義する。
総称型にはレスポンスボディの型を指定する。
HTTPステータスコード、レスポンスヘッダ、レスポンスボディを取得する必要がある場合に使用する。

5.2.4.4. データの登録(POSTリクエスト送信)

本項では、HTTP Service Clientsを使用したデータ登録について説明を行う。
他のHTTPメソッド(PUT, PATCH, DELETE, HEAD, OPTIONSなど)もサポートしており、同じような要領で使用することができる。
他の実装については、「HttpExchangeのJavadoc」を参照されたい。

5.2.4.4.1. POSTメソッドを指定したリクエスト送信の実装

POSTメソッドでリクエスト送信を行い、結果を取得する場合の実装の説明を行う。
以下は、HTTP Service Clientsのインターフェースの定義である。

HTTP Service Clientsの定義

@PostExchange(value = "users")  // (1)
User createUser(@RequestBody User user);  // (2)

項番

説明

(1)
HTTPのメソッドにPOSTを指定する場合は、@PostExchangeアノテーションを付与する。
value属性にはURLを指定する。
(2)
リクエストボディに値を設定する場合は、メソッドに引数を定義し@RequestBodyアノテーションを付与する。

5.2.4.5. リクエストヘッダの設定

リクエストヘッダを設定する方法は、アプリケーション全体で共通のヘッダを設定する方法と、リクエスト送信単位でヘッダを設定する方法がある。
HTTP Service Clientsのインターフェースで定義できるのは、後者のリクエスト送信単位でヘッダを設定する方法である。
共通のリクエストヘッダについては「アプリケーション全体で共通のリクエストヘッダを設定する方法」と同様の設定を行う必要がある。

以下は、HTTP Service Clientsを利用したリクエスト送信単位でヘッダを設定する実装例である。

HTTP Service Clientsの定義

@GetExchange(value = "users/{id}")
User getUserSetHeader(@RequestHeader("Custom-Header") String customValue,  // (1)
                     @PathVariable("id") String id);

項番

説明

(1)
リクエストヘッダを設定する場合は、@RequestHeaderアノテーションを付与する。
アノテーションの引数にはヘッダ名を指定する。

5.2.4.6. エラーハンドリング

HTTP Service Clientsを利用した際のエラーのハンドリングに関しては、同期通信のエラーハンドリングと同様でtry-catch文を使用して例外ハンドリングを行う。
ハンドリング方法に関しては、同期通信の「エラーハンドリング」を参照されたい。

5.2.4.7. ファイルアップロード(マルチパートリクエスト)

HTTP Service Clientsにおけるファイルアップロードについて説明を行う。
HTTP Service Clientsでファイルアップロード(マルチパートリクエスト)を行うには、@RequestPartアノテーションを使用してリクエストボディにファイルを設定する必要がある。

HTTP Service Clientsの定義

@PostExchange(value = "users/upload")
void upload(@RequestPart("file") Resource resource);  // (1)

項番

説明

(1)
マルチパートデータを設定する場合は、@RequestPartアノテーションを付与する。
アノテーションの引数にはパート名を指定し、ファイルを渡す際の型はResource型を指定する。
なお、ファイルを渡す際の型はMultipartFile型を指定することも可能である。

HTTP Service Clientsの呼び出し

@Override
public void upload(String path) {
    var resource = new ClassPathResource(path);  // (1)
    userApi.upload(resource);  // (2)
}

項番

説明

(1)
ファイルをResourceの実装クラスを使用して取得する。
(2)
HTTP Service Clientsのメソッドに(1)のResourceを引数として設定し、アップロードを実行する。

5.2.4.8. ファイルダウンロード

HTTP Service Clientsでは返却値としてInputStreamResourceなどの型を指定することで、レスポンスボディをファイルとして受け取ることが出来る。
ファイルダウンロード」でも説明しているとおり、HTTP Service ClientsにおいてもResourceを返却値に指定した場合はダウンロードするファイルのサイズによっては、java.lang.OutOfMemoryErrorが発生する可能性があるため、基本的にはInputStreamを返却値に指定する実装を推奨する。
ダウンロードするファイルのサイズが小さく、メモリに展開しても問題ない場合に限り、Resourceを返却値に指定した実装を検討するとよい。

レスポンスボディをInputStream型で取得する実装例

HTTP Service Clientsを利用して、レスポンスボディをInputStream型で受け取りダウンロードする場合の実装例を示す。

HTTP Service Clientsの定義

@GetExchange(value = "users/download")
InputStream download();  // (1)

項番

説明

(1)
返却値にInputStream型を指定する。

HTTP Service Clientsの呼び出し

インポート宣言

import org.springframework.util.FileCopyUtils;

メソッド

@Override
public void download() {
    try(InputStream in = userApi.download()) { // (1)
        File rcvFile = File.createTempFile("rcvFile", "zip");  // (2)
        FileCopyUtils.copy(in, new FileOutputStream(rcvFile));  // (3)

        // omitted
    };
}

項番

説明

(1)
HTTP Service Clientsのメソッドを呼び出し、ダウンロードを実行する。
(2)
出力するファイルを生成する。
(3)
FileCopyUtilsを使用して、(2)で生成したファイルにレスポンスボディを少しずつ書き出す。

レスポンスボディをResource型で取得する実装例

HTTP Service Clientsを利用して、レスポンスボディをResource型で受け取りダウンロードする場合の実装例を示す。
本実装では、ResourceHttpMessageConverterがレスポンスボディを一度byte配列として読み込むため、レスポンスボディ全体がメモリ上に展開される。
そのため、ダウンロードするファイルのサイズが小さい場合のみ、以下の方法で実装を行うこと。

HTTP Service Clientsの定義

@GetExchange(value = "users/download")
Resource download();  // (1)

項番

説明

(1)
返却値にResource型を指定する。

HTTP Service Clientsの呼び出し

@Override
public void download() {
    Resource resource = userApi.download();  // (1)

    // omitted
}

項番

説明

(1)
HTTP Service Clientsのメソッドを呼び出し、ダウンロードを実行する。
ダウンロード後、必要に応じてResourceを使用した処理を実装する。

5.2.5. How to extend(HTTP Service Clients)

本節では、HTTP Service Clients実装の拡張方法について説明する。


5.2.5.1. HTTP Service ClientsでのURIテンプレート変数値のフォーマット変換

HTTP Service Clientsを利用し、URIテンプレート変数の値をフォーマット変換する場合は、ConversionServiceを利用して実装する必要がある。

URIテンプレート変数値のフォーマット変換」と同様のことをHTTP Service Clientsで実現する場合に、UriBuilderではなくConversionServiceで変換処理を実装する理由を説明する。
HTTP Service Clientsを利用した場合もUriBuilderの適用は行われるため、カスタマイズしたUriBuilderも動作するが、HTTP Service Clientsで定義しているデータ型の変換にSpring Frameworkの型変換の仕組みであるConversionServiceが利用されており、そのConversionServiceUriBuilderより先に動作するため、意図した変換が行われない。
具体的には、UriBuilderより先にConversionServiceがURIテンプレート変数を文字列型に変換するため、ConversionServiceの後に動作するUriBuilderは期待するデータ型(LocalDate等)ではなく文字列型で変換処理が呼び出され、意図したフォーマットでの変換が行われない。
したがって、HTTP Service Clientsを利用する場合は、ConversionServiceを利用して変換処理を実装する必要がある。

以下は、HTTP Service ClientsでURIテンプレート変数の値(LocalDate)を、特定のフォーマット文字列に変換するConversionServiceの実装例である。
  • URIテンプレート:http://localhost:8080/api/order/{date}

  • URIテンプレート変数の値:LocalDate.of(2025, 1, 1);

  • 送信時のURL:http://localhost:8080/api/order/20250101


Bean定義ファイルの定義例(インターフェース単位で設定・登録する場合)

HttpServiceProxyFactoryを利用したインターフェース単位での設定方法は、以下の通りである。

  • ApplicationContextConfig.java

private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");

// (1)
@Bean("dateConversionService")
public ConversionService dateConversionService() {
    var conversionService = new DefaultConversionService();  // (2)
    conversionService.addConverter(LocalDate.class, String.class, d -> formatter.format(d));  // (3)
    return conversionService;
}

@Bean("userApi")
public UserApi userApi() {
    RestClientAdapter adapter = RestClientAdapter.create(restClient());
    HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter)
            .conversionService(dateConversionService()).build();  // (4)
    return factory.createClient(UserApi.class);
}

項番

説明

(1)
org.springframework.core.convert.ConversionServiceのBean定義を行う。
(2)
org.springframework.core.convert.support.DefaultConversionServiceのインスタンスを生成する。
(3)
DefaultConversionServiceaddConverterメソッドを使用して、Converterを登録する。
引数には、変換前の型、変換後の型、変換処理を設定する。
(4)
HttpServiceProxyFactory.BuilderconversionServiceメソッドを使用して、(1)のConversionServiceを登録する。

Bean定義ファイルの定義例(グループ単位で設定を集約して一括登録する場合)

@ImportHttpServicesアノテーションとHttpServiceGroupConfigurerを利用してグループ単位で一括で設定を行う場合は、以下の通りである。

  • HttpServiceClientsConfig.java

private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");

// (1)
@Bean("dateConversionService")
public ConversionService dateConversionService() {
    var conversionService = new DefaultConversionService();
    conversionService.addConverter(LocalDate.class, String.class, d -> formatter.format(d));
    return conversionService;
}

@Bean
@Order(0)
RestClientHttpServiceGroupConfigurer defaults() {
    return groups -> groups.forEachGroup((group, clientBuilder, factoryBuilder) -> {
        clientBuilder.baseUrl(url)
                    .requestFactory(new HttpComponentsClientHttpRequestFactory())
                    .defaultHeader("Default-Header", "TestHeader")
                    .requestInterceptor((request, body, execution) -> {
                        if (logger.isInfoEnabled()) {
                            var requestBody = new String(body, StandardCharsets.UTF_8);

                            logger.info("Request Header {}", request.getHeaders());
                            logger.info("Request Body {}", requestBody);
                        }

                        ClientHttpResponse response = execution.execute(request, body);

                        if (logger.isInfoEnabled()) {
                            logger.info("Response Header {}", response.getHeaders());
                            logger.info("Response Status Code {}", response.getStatusCode());
                        }
                        return response;
                    });

        factoryBuilder.conversionService(dateConversionService());  // (2)
    });
}

項番

説明

(1)
org.springframework.core.convert.ConversionServiceのBean定義を行う。
実装に関しては、インターフェース単位で設定・登録する場合と同様である。
(2)
引数で渡されるHttpServiceProxyFactory.BuilderconversionServiceメソッドを使用して、(1)のConversionServiceを登録する。

5.2.5.2. HTTP Service Clientsの共通処理

HTTP Service ClientsではexchangeAdapterDecoratorを利用することで、HTTP Service Clientsでの共通処理を実装することができる。
RestClientRestTemplateで利用するClientHttpRequestInterceptorと異なり、リクエストの送信前後ではなくHTTP Service Clientsのアダプタの実行前後に処理が実行される。
そのため、HTTP Service Clientsの例外処理や、返却値の加工などの共通処理を実装することができる。
Spring Frameworkでは上記の仕組みを利用して、404エラー(NOT_FOUND)の場合にもエラーとせずレスポンスボディをnullで返却する機能を提供している。

以下にて、exchangeAdapterDecoratorの実装について説明を行う。
実装例は、Spring Frameworkが提供するNotFoundRestClientAdapterDecoratorを利用した例となり、404エラー(NOT_FOUND)の場合にもエラーとせずレスポンスボディをnullで返却する設定である。

Bean定義ファイルの定義例(インターフェース単位で設定・登録する場合)

HttpServiceProxyFactoryを利用したインターフェース単位での設定方法は、以下の通りである。

  • ApplicationContextConfig.java

@Bean("userApi")
public UserApi userApi() {
    RestClientAdapter adapter = RestClientAdapter.create(restClient());
    HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter)
            .exchangeAdapterDecorator(NotFoundRestClientAdapterDecorator::new).build(); // (1)
    return factory.createClient(UserApi.class);
}

項番

説明

(1)
HttpServiceProxyFactory.BuilderexchangeAdapterDecoratorメソッドを使用して、org.springframework.web.client.support.NotFoundRestClientAdapterDecoratorのインスタンスを登録する。

Note

独自のexchangeAdapterDecoratorの定義

Spring Frameworkで提供しているorg.springframework.web.service.invoker.HttpExchangeAdapterDecoratorを継承した実装クラスを作成することで、独自のexchangeAdapterDecoratorを定義し設定することも可能である。


Bean定義ファイルの定義例(グループ単位で設定を集約して一括登録する場合)

@ImportHttpServicesアノテーションとHttpServiceGroupConfigurerを利用してグループ単位で一括で設定を行う場合は、以下の通りである。

  • HttpServiceClientsConfig.java

@Bean
@Order(0)
RestClientHttpServiceGroupConfigurer defaults() {
    return groups -> groups.forEachGroup((group, clientBuilder, factoryBuilder) -> {
        clientBuilder.baseUrl(url)
                    .requestFactory(new HttpComponentsClientHttpRequestFactory())
                    .defaultHeader("Default-Header", "TestHeader")
                    .requestInterceptor((request, body, execution) -> {
                        if (logger.isInfoEnabled()) {
                            var requestBody = new String(body, StandardCharsets.UTF_8);

                            logger.info("Request Header {}", request.getHeaders());
                            logger.info("Request Body {}", requestBody);
                        }

                        ClientHttpResponse response = execution.execute(request, body);

                        if (logger.isInfoEnabled()) {
                            logger.info("Response Header {}", response.getHeaders());
                            logger.info("Response Status Code {}", response.getStatusCode());
                        }
                        return response;
                    });

        factoryBuilder.exchangeAdapterDecorator(NotFoundRestClientAdapterDecorator::new);  // (1)
    });
}

項番

説明

(1)
引数で渡されるHttpServiceProxyFactory.BuilderexchangeAdapterDecoratorメソッドを使用して、org.springframework.web.client.support.NotFoundRestClientAdapterDecoratorのインスタンスを登録する。

Note

独自のexchangeAdapterDecoratorの定義

Spring Frameworkで提供しているorg.springframework.web.service.invoker.HttpExchangeAdapterDecoratorを継承した実装クラスを作成することで、独自のexchangeAdapterDecoratorを定義し設定することも可能である。


5.2.6. Appendix

5.2.6.1. RestTemplateにてURIテンプレートを扱う方法と実装例

RestTemplateにてURIテンプレートを扱うには、RestTemplateの呼び出すメソッドにより対応方法が異なるため、メソッド毎に説明を行う。
getForObjectメソッドとexchangeメソッドの違いについては、「データの取得(GETリクエスト送信)」を参照されたい。

getForObjectメソッドでの使用例

getForObjectメソッドでURLを直接渡す場合は、RestTemplateの内部でUriBuilderFactoryが使用されるため、URIテンプレートとURIテンプレート変数の値を使ったURL文字列の生成が自動で行われる。
したがって、本メソッドを使用した場合は、特に意識せずURIテンプレートとURIテンプレート変数の値を引数で指定することでRESTfulなURLを実現できる。

フィールド宣言部

@Value("${api.serverUrl}/api/users/{userId}") // (1)
String uriStr;

メソッド内部

User user = restTemplate.getForObject(uriStr, User.class, "0001"); // (2)

項番

説明

(1)
URIテンプレートの変数{userId}は、RestTemplateの使用時に指定の値に変換される。
(2)
URIテンプレートの変数1つ目がgetForObjectメソッドの第3引数に指定した値で置換され、『http://localhost:8080/api/users/0001』として処理される。

exchangeメソッドでの使用例

exchangeメソッドでRequestEntityを渡す場合は、RestTemplateにてUriBuilderFactoryは使用されないため、URIテンプレートとURIテンプレート変数の値を使ったURL文字列の生成が自動で行われない。
したがって、本メソッドを使用した場合は、UriComponentsBuilderを使用して明示的にURLを構築する必要がある。

フィールド宣言部

@Value("${api.serverUrl}/api/users/{action}") // (1)
String uriStr;

メソッド内部

URI targetUri = UriComponentsBuilder.fromUriString(uriStr).
        buildAndExpand("create").toUri(); //(2)

var user = new User();

// omitted

RequestEntity<User> requestEntity = RequestEntity
        .post(targetUri)
        .body(user);

ResponseEntity<User> responseEntity = restTemplate.exchange(requestEntity, User.class);

項番

説明

(1)
URIテンプレートの変数{action}は、RestTemplateの使用時に指定の値に変換される。
(2)
UriComponentsBuilderを使用することで、URIテンプレートの変数1つ目がbuildAndExpandの引数で指定した値に置換され、『http://localhost:8080/api/users/create』のURIが作成される。
詳細はUriComponentsBuilderのJavadocを参照されたい。

5.2.6.2. 非同期通信の利用について

非同期通信に関しては、Spring Framework 6.0からAsyncRestTemplateが削除されており、Spring Web Reactive APIのWebClientを使用するように案内されているため、非同期通信を行う場合はWebClientを使用する必要がある。
WebClientは、Spring WebFluxに含まれるRESTクライアント実装であり、非同期かつリアクティブプログラミングをサポートしたRESTクライアント実装である。
TERASOLUNA Server Framework for Java (5.x)ではAsyncRestTemplateの代替機能としてWebClientを案内しているだけであり、Spring Web Reactiveを完全にサポートしているわけではない点に注意されたい。

WebClientや、WebClientで使用されるReactor Nettyに関する詳細は以下を参照されたい。
WebClientに関する参考情報
Reactor Nettyに関する参考情報

5.2.6.2.1. 非同期通信のセットアップ

非同期通信の利用に関しては、以下の手順でセットアップを行う。

項番

手順

(1)
pom.xmlSpring WebFluxの依存関係を追加する。
(2)
WebClientのBean定義を行い、DIコンテナに登録する。
(3)
WebClientを利用するコンポーネントにて、(2)のBeanをインジェクションする。

5.2.6.2.1.1. 依存ライブラリ設定
WebClientを使用するためにpom.xmlに、Spring Frameworkのspring-webfluxライブラリを追加する。
マルチプロジェクト構成の場合は、domainプロジェクトのpom.xmlに追加する。
<dependencies>

    <!-- (1) -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webflux</artifactId>
    </dependency>
    <!-- (2) -->
    <dependency>
          <groupId>io.projectreactor.netty</groupId>
          <artifactId>reactor-netty</artifactId>
    </dependency>

</dependencies>

項番

説明

(1)
spring-webfluxライブラリをdependenciesに追加する。
(1)
reactor-nettyライブラリをdependenciesに追加する。

5.2.6.2.1.2. WebClientのBean定義
WebClientのBean定義を行い、DIコンテナに登録する。

Bean定義の実装例(WebClientConfig.java)

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient defaultWebClient() {
        // (1)
        return WebClient.builder().build();
    }

}

項番

説明

(1)
WebClient.BuilderbuildすることでWebClientを生成しBeanとして登録する。
WebClient.Builderはオプションを付けることでカスタマイズすることが可能である。
設定できるオプションは以下の通り。

Configuration

説明

uriBuilderFactory
ベースURLとして使用するUriBuilderFactoryをカスタマイズ。
defaultUriVariables
URIテンプレートを展開する際に使用するデフォルト値。
defaultHeader
リクエストのHeader。
defaultCookie
リクエストごとのCookie。
defaultRequest
リクエストをカスタマイズ。
filter
リクエスト時に使用されるFilter
exchangeStrategies
HTTP MessageのReader/Writerをカスタマイズ。
clientConnector
HTTP Client ライブラリを設定。

5.2.6.2.2. 非同期通信のデータ登録

非同期通信を利用したケースとして、同期処理の中でデータ登録のみを非同期で通信する実装例を説明する。
ユースケースとしては、処理を完了させるまでに時間がかかるREST APIへのアクセスを非同期で行うケースを想定したものである。

なお、本項では非同期通信の説明としてCompletableFutureを使用した実装にて説明を行っているが、MonoFluxのみを使用した実装も可能である。
MonoFluxの仕様については、以下を参照されたい。

CompletableFutureを利用したデータの登録

java.util.concurrent.CompletableFutureは、JDKが提供する非同期処理を行うためのクラスである。
Spring FrameworkのWebClientなどのリアクティブAPIと統合して利用する場合は、MonoFluxからの結果をバイパスするために利用される。
実際にはMonoFluxの処理が動作した後にCompletableFutureにデータを渡されており、CompletableFutureがリアクティブ処理を隠蔽しているため、従来型の非同期処理として実装を行える。
CompletableFutureの詳しい仕様は、「CompletableFutureのJavadoc」を参照されたい。

CompletableFuture<ResponseEntity<Void>> future =  // (1)
        webClient.post()  // (2)
                .uri(url)
                .bodyValue(users)  // (3)
                .retrieve()  // (4)
                .toBodilessEntity()  // (5)
                .toFuture();  // (6)

future.handle((r, t) -> {  // (7)
    if (t == null) {  // (8)
        // 成功のレスポンスが返ってきた場合の処理を実装
    } else {  // (9)
        // エラーが発生した場合の処理を実装
    }
    return null;
});

// omitted

項番

説明

(1)
返却値にCompletableFutureを指定し、非同期通信のレスポンスを受け取る。
上記の例では、レスポンスボディは不要なためCompletableFutureの型引数には、ResponseEntity<Void>を指定している。
(2)
postメソッドを使用してHTTPメソッドにPOSTを設定する。
(3)
RequestBodyUriSpecbodyValueメソッドを使用してリクエストボディを設定する。
(4)
RequestBodyUriSpecretrieveメソッドを使用してレスポンスのストリームを作成する。
(5)
ResponseSpectoBodilessEntityメソッドを使用して、ストリームで扱うデータ型を指定する。
上記の場合は、レスポンスボディを扱わないためMono<ResponseEntity<Void>>型のレスポンスのストリームを取得している。
(6)
MonotoFutureメソッドを使用して、CompletableFuture型に変換を行う。
このタイミングで、内部的にMonosubscribeが実行され、非同期通信が開始される。
(7)
CompletableFuturehandleメソッドに、処理結果に応じた処理を実装する。
handleメソッドは、正常終了した場合と異常終了した場合の両方で呼び出されるため、エラーが発生したかどうかは例外の有無で判断を行う。
上記の場合、handleメソッドの関数の引数には、ResponseEntityThrowableが渡されるため、第二引数のThrowableを参照してエラーの有無を判断している。

Tip

同期通信に変更したい場合は、handleメソッドからメソッドチェーンでgetメソッドを呼び出すことで処理結果の待ち合わせを行うことができる。

(8)
成功時の処理を実装する。
(9)
エラーが発生した場合の処理を実装する。

5.2.6.2.3. エラーハンドリング

非同期通信でのエラーハンドリングの方法について説明を行う。
非同期通信のエラーハンドリングは、大きく2つ考慮すべき点がある。
1つ目は、サーバとの通信結果でエラーが発生している場合、2つ目はレスポンスを受信した後にデータをストリーム化して処理している最中にエラーが発生している場合の2つがある。
それぞれのケースについて、以下で説明を行う。
5.2.6.2.3.1. サーバとの通信結果をハンドリングする方法
WebClientStatusHandlerは、HTTPステータスコードをもとにエラーの判定を行うためのインタフェースである。
したがって、エラー全体をハンドリングする仕組みではないため、その点は留意する必要がある。
RestClientと同様で、アプリケーション全体でエラーハンドリングを行う場合と、リクエスト毎でエラーハンドリングを行う場合の2つがあるため、それぞれについて説明を行う。

アプリケーション全体でエラーハンドリングを行う方法

アプリケーション全体でエラーハンドリングを行う場合は、WebClientを生成するタイミングでStatusHandlerを設定し、エラーの判定とエラー処理の実装を行う。

  • WebClientConfig.java

@Bean("webClient")
public WebClient webClient() {
    return WebClient.builder()
            .defaultStatusHandler(HttpStatusCode::isError, response -> {  // (1)
                // エラー時の処理を実装
                return Mono.error(new CustomException(response.statusCode().value()));
            }).build();
}

項番

説明

(1)
WebClient.BuilderdefaultStatusHandlerメソッドにPredicate<HttpStatusCode>と、Function<ClientResponse, Mono<? extends Throwable>>を設定する。
上記の実装例では、HttpStatusCodeがサーバエラー(5xx系)及びクライアントエラー(4xx系)の場合、独自例外にHTTPステータスコードを設定して返却するように実装している。
なお、ストリーム中にエラーを返却する場合は、Monoerrorメソッドを使用して返却する必要がある。

リクエスト毎にエラーハンドリングを行う方法

リクエスト毎にエラーハンドリングを行う場合は、非同期通信を行う際にResponseSpeconStatusメソッドを使用して、エラーの判定とエラー処理の実装を行う。

CompletableFuture<ResponseEntity<Void>> future =
        webClient.post()
                .uri(url)
                .bodyValue(users)
                .retrieve()
                .onStatus(HttpStatusCode::is5xxServerError, response -> { // (1)
                    // エラー時の処理を実装
                    return Mono.error(new CustomException(response.statusCode().value()));
                })
                .toBodilessEntity()
                .toFuture();

項番

説明

(1)
サーバエラー(5xx系)のHTTPステータスコードが返却された場合のエラーハンドリングを行う。
defaultStatusHandlerの実装と同様に、onStatusメソッドにPredicate<HttpStatusCode>Function<ClientResponse, Mono<? extends Throwable>>を設定する。
なお、onStatusメソッドの記載がある場合は、defaultStatusHandlerの実装より優先されて処理される。
したがって、上記の実装例ではサーバエラー(5xx系)のHTTPステータスコードが返却された場合にのみ適用され、クライアントエラー(4xx系)のHTTPステータスコードが返却された場合はdefaultStatusHandlerの実装が適用される。

5.2.6.2.3.2. ストリーム中やStatusHandlerで発生した例外をハンドリングする方法

最終的なエラーのハンドリングは、CompletableFuturehandleメソッドにてハンドリングを行う必要がある。

CompletableFuture<ResponseEntity<Void>> future =
        webClient.post()
                .uri(url)
                .bodyValue(users)
                .retrieve()
                .toBodilessEntity()
                .toFuture();

future.handle((r, t) -> {  // (1)
    if (t == null) {
        // 成功のレスポンスが返ってきた場合の処理を実装
    } else {
        // エラーが発生した場合の処理を実装
    }
    return null;
});

項番

説明

(1)
CompletableFuturehandleメソッドにエラー時の処理を実装する。
非同期で動作している特性上、画面へのエラー通知は行えないため、エラー通知や復帰処理等を行う場合はhandleメソッドにて実装を行う必要がある。

5.2.6.2.4. リクエスト送信前の共通処理の適用

org.springframework.web.reactive.function.client.ExchangeFilterFunctionを使用した共通処理の実装方法について説明を行う。

5.2.6.2.4.1. リクエスト送信前の共通処理の実装
org.springframework.web.reactive.function.client.ExchangeFilterFunctionを実装することで、サーバとの通信処理前に任意の処理を実行させることができる。
ここでは単純な例として、通信前のロギング処理の実装例を紹介する。

通信ログ出力の実装例

package com.example.webclient;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;

import reactor.core.publisher.Mono;

public class WebClientExchangeFilterFunction implements ExchangeFilterFunction { // (1)

    private static final Logger LOGGER = LoggerFactory.getLogger(
            WebClientExchangeFilterFunction.class);

    @Override
    public Mono<ClientResponse> filter(ClientRequest request,
            ExchangeFunction next) {
        LOGGER.info("External Request to {}", request.url()); // (2)
        return next.exchange(request); // (3)
    }

}

項番

説明

(1)
ExchangeFilterFunctionインタフェースを実装する。
(2)
非同期リクエストを送信する前に実行する処理を実装する。
上記の実装例では、通信先のURLを出力している。
(3)
次のExchangeFilterFunctionを呼び出す。

Bean定義の実装例(WebClientConfig.java)

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient loggingWebClient() {

        // @formatter:off
        WebClient webClient = WebClient.builder()
                .filter(new LoggingExchangeFilterFunction()) // (1)
                .build();
        // @formatter:on

        return webClient;
    }

項番

説明

(1)
ExchangeFilterFunctionの実装クラスをfilterに登録する。

Note

複数のFilterを登録したい場合は、filtersを使用してFilterを登録する。

WebClient webClient = WebClient.builder().filters(f -> {
    f.add(0, new FirstExchangeFilterFunction());
    f.add(1, new SecondExchangeFilterFunction());
    f.add(2, new ThirdExchangeFilterFunction());
}).build();

リストに登録された順番に処理されるようになる。