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


5.2.1. Overview

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

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

RestTemplateを利用したクライアント実装」にて説明を行っているが、RestTemplateはSpring Frameworkにて将来的に廃止されることが予定されており、RestTemplateの移行先としてRestClientが紹介されている。
そのため、5.11.x以降の TERASOLUNA Server Framework for Java (5.x) においては、RestClientによる同期通信の実装を推奨している。
RestClientを用いた設定や利用方法については、TERASOLUNA Server Framework for Java (5.x) の5.11.x以降のガイドラインに記載しているため、そちらを参照されたい。

5.2.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にて詳しく記載されているので参照されたい。
また、上述の通り TERASOLUNA Server Framework for Java (5.x) 5.11.x以降からRestClientにおけるRESTクライアントの実現方法について記載しているので、必要に応じて参照してほしい。
以下に、RestTemplateを利用したクライアントアプリケーションが、どのようにREST APIを公開しているサーバサイドアプリケーションにアクセスし、RESTによる通信を実現するかを示す。
RestTemplate内で利用されるクラスの詳細な説明は、「RestTemplateの構成要素と利用上のポイント」にてまとめて説明を行っているのでこちらを参照されたい。
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.2. RestTemplateの構成要素と利用上のポイント

本項では、RestTemplateで利用される構成要素の概要を示し、利用上、有益と思われるいくつかの内容について記載する。

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定義を行うことで実現できる。
UriBuilderFactoryの具体的なカスタマイズの例は、「URL生成処理をカスタマイズする方法(UriBuilderFactory)」を参照されたい。

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

RestTemplateは、サーバとの通信処理を以下の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の実装クラスについて

RestTemplateで使用されるClientHttpRequestFactoryのデフォルト実装クラスには、SimpleClientHttpRequestFactoryが設定されている。
デフォルトで設定されるSimpleClientHttpRequestFactoryは、コネクションプールやプロキシ認証等の機能を持たず、他のClientHttpRequestFactoryの実装と比較すると機能が限定されている。
そのため、TERASOLUNA Server Framework for Java (5.x) では、ClientHttpRequestFactoryの実装クラスとしては、高機能な通信設定が行えるHttpComponentsClientHttpRequestFactoryの利用を推奨する。
なお、RestTemplateClientHttpRequestFactoryの実装を指定しない場合、SimpleClientHttpRequestFactoryが自動設定されるため、推奨設定を適用するにはHttpComponentsClientHttpRequestFactoryをRestTemplateに明示的に指定する必要がある。
以下が、推奨設定のコード例である。
  • ApplicationContextConfig.java

@Bean("restTemplate")
public RestTemplate restTemplate() {
    return new RestTemplate(new HttpComponentsClientHttpRequestFactory());
}

ClientHttpRequestFactoryの設定について

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

Note

Content-Lengthヘッダについて

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

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

    • ApplicationContextConfig.java

    @Bean("restTemplate")
    public RestTemplate restTemplate() {
        return new RestTemplate(new BufferingClientHttpRequestFactory(new HttpComponentsClientHttpRequestFactory()));  // (1)
    }
    

    項番

    説明

    (1)
    BufferingClientHttpRequestFactoryのコンストラクタにHttpComponentsClientHttpRequestFactoryを設定しインスタンスを生成する。
    生成したBufferingClientHttpRequestFactoryRestTemplateに設定することで、バッファリングが有効になる。

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

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

ClientHttpRequestInitializerの利用が適しているケースには、リクエストヘッダにカスタムヘッダを一括で設定したい場合などがある。
リクエストヘッダの一括設定の方法については、「リクエストヘッダの設定」を参照されたい。

5.2.1.2.4. メッセージ変換(HttpMessageConverter

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

Spring Frameworkは主要なメディアタイプ向けのHttpMessageConverter実装を提供しており、RestTemplateが使用するHttpMessageConverterは、RestTemplateの生成時に登録され、リクエスト/レスポンスのボディ変換に利用される。
自動で登録される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.MappingJackson2HttpMessageConverterによる変換処理を例に説明を行う。
RestTemplateの送受信で使用するJavaBeanのフィールド名は、JSONのプロパティ名と一致していることを前提とし、特別なマッピング等は行っていない。

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

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

ClientHttpRequestInterceptorの動作仕様

ClientHttpRequestInterceptorは複数適用することができ、RestTemplateに登録した順番でチェーン実行される。
これはサーブレットフィルタの動作によく似ており、最後に実行されるチェーン先として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. エラーハンドリング(ResponseErrorHandler

Spring Frameworkでは、RestTemplateによるサーバ通信時のエラーに対応するためのエラーハンドラとしてResponseErrorHandlerを提供している。
RestTemplateでは、ResponseErrorHandlerのデフォルト実装としてDefaultResponseErrorHandlerが設定されており、デフォルトでHTTPステータスコードに応じたエラーハンドリングが実装されている。
デフォルトで使用されるエラーハンドラー実装はHTTPステータスコードに応じて、以下のようなハンドリングを行う。
エラーハンドラーの動作仕様

項番

HTTPステータスコード

動作仕様

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

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

Spring Frameworkでは、RestTemplateでサーバからのレスポンスを取得したあとの、ボディの抽出や変換などの処理(レスポンス取得後処理)を実現する仕組みとして、 ResponseExtractorを提供している。
RestTemplateで利用されるResponseExtractorの動作仕様は以下の通り。
Operation of ResponseExtractor

ResponseExtractorの動作

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

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


5.2.2. How to use(同期通信)

本節では、RestTemplateを使用した、同期通信を行うRESTクライアントの実装方法について説明する。

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

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

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

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


5.2.2.1.1. 依存ライブラリ設定

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定義

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

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

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

@Bean("restTemplate")
public RestTemplate restTemplate() {
    return new RestTemplate(new HttpComponentsClientHttpRequestFactory()); // (1)
}

項番

説明

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

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


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

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

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

@Service
public class AccountServiceImpl implements AccountService {

    @Inject
    RestTemplate restTemplate;

    // 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の設定は、DefaultUriBuilderFactorybaseUrlを設定して、RestTemplateに適用する必要がある。

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

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

@Bean("restTemplate")
public RestTemplate restTemplate() {
    var restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(url));  // (2)
    return restTemplate;
}

項番

説明

(1)
プロパティファイルから基準となるURLを取得し設定を行う。
(2)
DefaultUriBuilderFactoryのコンストラクタに(1)のURLを設定してインスタンスを生成する。
RestTemplatesetUriTemplateHandlerメソッドを使用して、生成したDefaultUriBuilderFactoryのインスタンスを設定する。

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

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

@Inject
private RestTemplate restTemplate;

public User getUser(String id) {
    return restTemplate.getForObject(uri, User.class, id);
}

項番

説明

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

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

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

@Inject
private RestTemplate restTemplate;

public User getUser(String id) {
    return restTemplate.getForObject(uri, User.class, id);
}

項番

説明

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

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

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

項番

用途

RestTemplateのメソッド

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

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

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

getForObjectメソッドの使用例

フィールド宣言部

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

メソッド内部

User user = restTemplate.getForObject(uri, User.class); // (1)

項番

説明

(1)
getForObjectメソッドを使用した場合は、戻り値はレスポンスボディの値になる。
レスポンスボディのデータはHttpMessageConverterによって第2引数に指定したJavaクラスへ変換された後、返却される。

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

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

getForEntityメソッドの使用例

インポート宣言

import org.springframework.http.ResponseEntity;

フィールド宣言部

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

メソッド内部

ResponseEntity<User> responseEntity =
        restTemplate.getForEntity(uri, User.class); // (1)
HttpStatusCode statusCode = responseEntity.getStatusCode(); // (2)
HttpHeaders header = responseEntity.getHeaders(); // (3)
User user = responseEntity.getBody(); // (4)

項番

説明

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

Note

ResponseEntityとは

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

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


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

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

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

getForObjectメソッドでは、コレクション形式のデータをレスポンスの型として指定できないため、exchangeメソッドを使用してリクエストを送信する必要がある。
exchangeメソッドの使い方については、「リクエスト送信単位でリクエストヘッダを設定する方法」にて説明を行っているのでこちらを参照されたい。
ResponseEntity<List<User>> responseEntity = //(1)
    restTemplate.exchange(requestEntity, new ParameterizedTypeReference<List<User>>(){}); //(2)

List<User> userList = responseEntity.getBody();//(3)

項番

説明

(1)
ResponseEntityの型パラメータにList<レスポンスデータの型>を指定する。
(2)
exchangeメソッドの第二引数にorg.springframework.core.ParameterizedTypeReferenceのインスタンスを指定し、型パラメータにList<レスポンスデータの型>を指定する。
(3)
getBodyメソッドで、レスポンスボディのデータを取得する。

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

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

項番

用途

RestTemplateのメソッド

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

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

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

postForObjectメソッドの使用例

フィールド宣言部

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

メソッド内部

var user = new User();

// omitted

User user = restTemplate.postForObject(uri, user, User.class); // (1)

項番

説明

(1)
postForObjectメソッドは、簡易にPOSTリクエストを実装できる。
第二引数には、HttpMessageConverterによってリクエストボディに変換されるJavaオブジェクトを設定する。
postForObjectメソッドを使用した場合は、戻り値はレスポンスボディの値になる。

5.2.2.5. エラーハンドリング

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

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

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

フィールド宣言部

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

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

メソッド内部

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

        User user = restTemplate.getForObject(uri, 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をキャッチしてリトライ処理を行っている。

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

エラーハンドラを拡張し、HTTPステータスコードでハンドリングする方法について説明を行う。
エラーハンドラの拡張用途は、HTTPステータスコードでハンドリングする方法以外にもあるため、本項で説明するのは一例である。
RestTemplateでは、ResponseErrorHandlerインタフェースの実装クラスをRestTemplateに設定することで、独自のエラー処理を行うことができる。

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

エラーハンドラの実装クラスの作成例

import java.io.IOException;
import java.net.URI;

import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.DefaultResponseErrorHandler;

public class CustomErrorHandler extends DefaultResponseErrorHandler {  // (1)

    // (2)
    @Override
    public void handleError(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
        // Don't throw Exception.
    }
}

項番

説明

(1)
ResponseErrorHandlerインタフェースの実装クラスを作成する。
上記の例では、エラーハンドラのデフォルト実装クラスであるDefaultResponseErrorHandlerを拡張している。
(2)
handleErrorメソッドには、拡張元であるDefaultResponseErrorHandlerhasErrorメソッドでエラーと判断されたHTTPステータスコードに対するエラー処理を実装する。
DefaultResponseErrorHandlerhasErrorメソッドがエラーとみなすHTTPステータスコードは、サーバエラー(5xx系)及びクライアントエラー(4xx系)が対象である。
詳細は「DefaultResponseErrorHandlerのJavadoc」を参照されたい。

上記の例では、サーバエラー(5xx系)及びクライアントエラー(4xx系)が発生した場合でも例外を発生させないことで、RestTemplateの呼び出し側でResponseEntityを受け取れるようにしている。

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

// (1)
@Bean("customErrorHandler")
public CustomErrorHandler customErrorHandler() {
    return new CustomErrorHandler();
}

@Bean("restTemplate")
public RestTemplate restTemplate() {
    var restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    restTemplate.setErrorHandler(customErrorHandler());  // (2)
    return restTemplate;
}

項番

説明

(1)
ResponseErrorHandlerの実装クラスのBean定義を行う。
(2)
RestTemplatesetErrorHandlerメソッドに、(1)で生成したResponseErrorHandlerの実装クラスのBeanを設定する。

Note

Spring Frameworkが提供するNoOpResponseErrorHandlerについて

サーバエラー及びクライアントエラーが発生した場合でも例外を発生させずにResponseEntityを返却する実装であれば、同様の動作を行うNoOpResponseErrorHandlerResponseErrorHandlerの実装クラスとしてSpring Frameworkから提供されているため、こちらで代替することも可能である。

  • ApplicationContextConfig.java

    // (1)
    @Bean("noOpResponseErrorHandler")
    public NoOpResponseErrorHandler noOpResponseErrorHandler() {
        return new NoOpResponseErrorHandler();
    }
    
    @Bean("restTemplate")
    public RestTemplate restTemplate() {
        var restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
        restTemplate.setErrorHandler(noOpResponseErrorHandler()); // (2)
        return restTemplate;
    }
    

    項番

    説明

    (1)
    org.springframework.web.client.NoOpResponseErrorHandlerのBean定義を行う。
    (2)
    RestTemplatesetErrorHandlerメソッドにNoOpResponseErrorHandlerのBeanを設定する。

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

int retryCount = 0;
while (true) {

    responseEntity = restTemplate.exchange(requestEntity, 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.6. リクエストヘッダの設定

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

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

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

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

ClientHttpRequestInitializerの実装例

// (1)
public class CustomHeaderInitializer implements ClientHttpRequestInitializer {

    // (2)
    @Override
    public void initialize(ClientHttpRequest request) {
        request.getHeaders().add("Custom-Header", "CustomHeaderValue"); // (3)
    }
}

項番

説明

(1)
ClientHttpRequestInitializerの実装クラスを作成する。
(2)
initializeメソッドにClientHttpRequestの初期化処理を実装する。
上記の例では、リクエストヘッダにカスタムヘッダを付与している。
(3)
リクエストヘッダの名前と値を設定する。
上記の例では、Custom-HeaderというキーでCustomHeaderValueという値を設定している。

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

// (1)
@Bean("customHeaderInitializer")
public CustomHeaderInitializer customHeaderInitializer() {
    return new CustomHeaderInitializer();
}

@Bean("restTemplate")
public RestTemplate restTemplate() {
    var restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    restTemplate.setClientHttpRequestInitializers(List.of(customHeaderInitializer())); // (2)
    return restTemplate;
}

項番

説明

(1)
ClientHttpRequestInitializerのBean定義を行う。
(2)
RestTemplatesetClientHttpRequestInitializersメソッドにClientHttpRequestInitializerのBeanを設定する。
setClientHttpRequestInitializersメソッドは、List型で引数が定義されているため、複数のClientHttpRequestInitializerを設定することができる。

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

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

リクエスト送信単位でヘッダの設定を行う場合は、RequestEntityのメソッドを使用してHTTPヘッダを設定する。
Content-TypeAcceptといった代表的なHTTPヘッダについては、RequestEntityが提供するBuilderに専用の設定メソッド(contentTypeaccept)が用意されているため、これを利用すると良い。
設定用のメソッドが無いヘッダは、RequestEntity.HeadersBuilderheaderメソッドを使用して設定する。
詳細はRequestEntityのJavadocを参照されたい。
本ガイドラインでは、以下のHTTPヘッダについて設定方法を示す。

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

Content-Typeヘッダの設定例

インポート宣言

import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;

フィールド宣言部

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

メソッド内部

var user = new User();

// omitted

RequestEntity<User> requestEntity = RequestEntity
        .post(uri) // (1)
        .contentType(MediaType.APPLICATION_JSON) // (2)
        .body(user); // (3)

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

User user = responseEntity.getBody();

項番

説明

(1)
RequestEntitypostメソッドを使用し、POSTリクエスト用のリクエストビルダを生成する。
パラメータにURIを設定する。
必要に応じて、リクエストビルダのメソッドを使用し、リクエストヘッダやリクエストボディを設定する。
(2)
RequestEntity.BodyBuildercontentTypeメソッドを使用し、Content-Typeヘッダの値を指定する。
上記の実装例では、送信時のデータ形式がJSONであることを示す「application/json」を設定している。
(3)
RequestEntity.BodyBuilderbodyメソッドにて、リクエストボディを設定し、RequestEntityオブジェクトを作成する。
ボディの設定が不要の場合は、buildメソッドを使用してRequestEntityオブジェクトを作成する。
(4)
exchangeメソッドを使用し、リクエストを送信する。
引数には、(3)で生成したRequestEntityと、レスポンスデータの型を指定する。
レスポンスはResponseEntity<T>で返却され、型パラメータは引数で指定したレスポンスデータの型となる。

Note

RequestEntityとは

RequestEntityはHTTPリクエストを表すクラスで、接続URI、HTTPメソッド、リクエストヘッダ、リクエストボディを設定することができる。

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


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

Acceptヘッダの設定例

メソッド内部

RequestEntity<Void> requestEntity = RequestEntity
        .get(uri)
        .accept(MediaType.APPLICATION_JSON) // (1)
        .build(); // (2)

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

User user = responseEntity.getBody();

項番

説明

(1)
RequestEntity.HeadersBuilderacceptメソッドを使用して、Acceptヘッダの値を設定する。
上記の実装例では、取得可能なデータ形式がJSONであることを示す「application/json」を設定している。
(2)
RequestEntity.HeadersBuilderbuildメソッドを使用し、RequestEntityオブジェクトを作成する。

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

カスタムヘッダの実装例

メソッド内部

RequestEntity<Void> requestEntity = RequestEntity
        .get(uri)
        .header("Custom-Header", "CustomValue") // (1)
        .build();

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

User user = responseEntity.getBody();

項番

説明

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

5.2.2.7. 認証要求の設定

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

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

ClientHttpRequestInitializerの実装例

// (1)
public class CustomHeaderInitializer implements ClientHttpRequestInitializer {

    private String username;

    private String password;

    // (2)
    public CustomHeaderInitializer(String basicAuthUsername, String basicAuthPassword) {
        this.username = basicAuthUsername;
        this.password = basicAuthPassword;
    }

    // (3)
    @Override
    public void initialize(ClientHttpRequest request) {
        String plainCredentials = username + ":" + password;  // (4)
        String base64Credentials = Base64.getEncoder().encodeToString(plainCredentials.getBytes(StandardCharsets.UTF_8));  // (5)
        request.getHeaders().add("Authorization", "Basic " + base64Credentials);  // (6)
    }
}

項番

説明

(1)
ClientHttpRequestInitializerの実装クラスを作成する。
(2)
Basic認証で利用するユーザ名とパスワードを設定するコンストラクタを定義する。
(3)
initializeメソッドにClientHttpRequestの初期化処理を実装する。
上記の例では、リクエストヘッダにAuthorizationヘッダを付与している。
(4)
ユーザ名とパスワードを「”:“ 」でつなげる。
(5)
(4)をバイト配列に変換し、Java標準のjava.util.Base64を使用してBase64エンコードを行う。
(6)
AuthorizationヘッダにBasic認証の資格情報を設定する。
上記ではBasic認証の例としているため、認証スキームとしてBasicを指定して認証情報を設定している。

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

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

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

// (3)
@Bean("customHeaderInitializer")
public CustomHeaderInitializer customHeaderInitializer() {
    return new CustomHeaderInitializer(basicAuthUsername, basicAuthPassword);
}

@Bean("restTemplate")
public RestTemplate restTemplate() {
    var restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    restTemplate.setClientHttpRequestInitializers(List.of(customHeaderInitializer())); // (4)
    return restTemplate;
}

項番

説明

(1)
Basic認証で利用するユーザ名を設定する。
(2)
Basic認証で利用するパスワードを設定する。
(3)
ClientHttpRequestInitializerのBean定義を行う。
ClientHttpRequestInitializerの実装クラスのコンストラクタに、Basic認証で利用するユーザ名とパスワードを渡す。
(4)
RestTemplatesetClientHttpRequestInitializersメソッドにClientHttpRequestInitializerのBeanを設定する。

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を使用した定義例である。

Bean定義ファイルの定義例

  • 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("timeoutRestTemplate")
public RestTemplate timeoutRestTemplate() {
    return new RestTemplate(clientHttpRequestFactory()); // (8)
}

項番

説明

(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)
RestTemplateのコンストラクタに、生成した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)

RequestEntity<MultiValueMap<String, Object>> requestEntity = RequestEntity
        .post(uri)
        .contentType(MediaType.MULTIPART_FORM_DATA)  //(3)
        .body(multiPartBody);  //(4)

項番

説明

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

インポート宣言

import org.springframework.util.FileCopyUtils;

メソッド内部

// (1)
final ResponseExtractor<File> responseExtractor =
        new ResponseExtractor<File>() {

    // (2)
    @Override
    public File extractData(ClientHttpResponse response)
            throws IOException {

        File rcvFile = File.createTempFile("rcvFile", "zip");  // (3)
        FileCopyUtils.copy(response.getBody(), new FileOutputStream(rcvFile));  // (4)
        return rcvFile;  // (5)
    }

};

// (6)
File rcvFile = this.restTemplate.execute(targetUri,
        HttpMethod.GET, null, responseExtractor);

// omitted

項番

説明

(1)
レスポンス取得後処理をカスタマイズするため、ResponseExtractorの実装を作成する。
(2)
extractDataメソッドを実装し、レスポンス取得後処理を定義する。
(3)
出力するファイルを生成する。
(4)
レスポンスボディをInputStreamで読み込み、FileCopyUtilsを使用して、(3)で生成したファイルにレスポンスボディを少しずつ書き出す。
(5)
ファイル作成後、作成したファイルを返却する。

Note

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

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

final ResponseExtractor<ResponseEntity<File>> responseExtractor =
        new ResponseExtractor<ResponseEntity<File>>() {

    @Override
    public ResponseEntity<File> extractData(ClientHttpResponse response)
            throws IOException {

        File rcvFile = File.createTempFile("rcvFile", "zip");
        FileCopyUtils.copy(response.getBody(), new FileOutputStream(rcvFile));
        return ResponseEntity.status(response.getStatusCode())
                .headers(response.getHeaders()).body(rcvFile);
    }

};
(6)
RestTemplate.executeメソッドを使用してファイルのダウンロードを行い、出力したファイルを取得する。

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

以下は、レスポンスボディをbyte配列で一括取得する実装例である。
ダウンロードするファイルのサイズが小さい場合のみ、以下の方法で実装を行うこと。
byte[] downloadContent =
        restTemplate.getForObject(uri, 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(同期通信)」だけでは実現できない動作や設定をカスタマイズしたい場合に必要な内容である。

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

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("restTemplate")
public RestTemplate restTemplate() {
    var restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    restTemplate.setUriTemplateHandler(customUriBuilderFactory()); // (2)
    return restTemplate;
}

項番

説明

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

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

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

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

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

FactoryBeanの実装例

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を、RestTemplateに設定する。

  • ApplicationContextConfig.java

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

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

// omitted

@Bean("httpsRestTemplate")
public RestTemplate httpsRestTemplate() throws Exception {
    return new RestTemplate(httpsRequestFactoryBean().getObject()); // (1)
}

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

項番

説明

(1)
作成したRequestFactoryBeanRestTemplateのコンストラクタに指定する。
RequestFactoryBeanには、キーストアファイルのファイル名とパスワードを渡す。

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

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


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

サーバへアクセスする際にHTTP Proxyサーバを経由する必要がある場合は、システムプロパティやJVM起動引数、またはRestTemplateのBean定義にてHTTP Proxyサーバの設定が必要である。
システムプロパティやJVM起動引数に設定した場合、アプリケーション全体に影響を与えてしまうため、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("proxyRestTemplate")
public RestTemplate proxyRestTemplate() {
    return new RestTemplate(httpComponentsClientHttpRequestFactory()); // (9)
}

項番

説明

(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)
RestTemplateのBean定義を行う。
(9)
RestTemplateのコンストラクタの引数に、(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("proxyRestTemplate")
public RestTemplate proxyRestTemplate() {
    return new RestTemplate(httpComponentsClientHttpRequestFactory());
}

項番

説明

(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("restTemplate")
public RestTemplate restTemplate() {
    var bean = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    List<HttpMessageConverter<?>> converters = new ArrayList<>();
    // omitted: Default HttpMessageConverters registration
    converters.add(customHttpMessageConverter()); // (2)
    bean.setMessageConverters(converters); // (3)
    return bean;
}

項番

説明

(1)
カスタムメッセージコンバータのBeanを定義する。
(2)
Listに(1)で定義したBeanを追加する。
カスタムメッセージコンバータ以外のHttpMessageConverterの登録は省略しているが、必要に応じて登録を行うこと。
(3)
RestTemplatesetMessageConvertersメソッドを使用して、HttpMessageConverterのリストを設定する。

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

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

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

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

通信ログ出力の実装例

ClientHttpRequestInterceptorの実装

package com.example.restclient;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

public class LoggingInterceptor implements ClientHttpRequestInterceptor { // (1)

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

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
            ClientHttpRequestExecution execution) throws IOException {

        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)
    }

}

項番

説明

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

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

// (1)
@Bean("loggingInterceptor")
public LoggingInterceptor loggingInterceptor() {
    return new LoggingInterceptor();
}

@Bean("restTemplate")
public RestTemplate restTemplate() {
    var bean = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    bean.setInterceptors(List.of(loggingInterceptor())); // (2)
    return bean;
}

項番

説明

(1)
ClientHttpRequestInterceptorの実装クラスのBean定義を行う。
(2)
RestTemplatesetInterceptorsメソッドにClientHttpRequestInterceptorListを設定する。

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

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

Bean定義ファイルの定義例

  • ApplicationContextConfig.java

@Bean("loggingInterceptor")
public LoggingInterceptor loggingInterceptor() {
    return new LoggingInterceptor();
}

@Bean("customInterceptor")
public CustomInterceptor customInterceptor() {
    return new CustomInterceptor();
}

@Bean("restTemplate")
public RestTemplate restTemplate() {
    var bean = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    bean.setInterceptors(List.of(loggingInterceptor(), customInterceptor()));  // (1)
    return bean;
}

項番

説明

(1)
RestTemplatesetInterceptorsメソッドにClientHttpRequestInterceptorListを設定する。
Listに追加した順番でClientHttpRequestInterceptorのチェーンが実行される。
上記の例では、リクエスト送信前はloggingInterceptor、customInterceptorの順に実行され、レスポンス受信後はcustomInterceptor、loggingInterceptorの順に実行される。

5.2.4. Appendix

5.2.4.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.4.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.4.2.1. 非同期通信のセットアップ

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

項番

手順

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

5.2.4.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.4.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.4.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.4.2.3. エラーハンドリング

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

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

5.2.4.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();

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