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

目次


5.2.1. Overview

本節では、Spring Frameworkが提供するorg.springframework.web.client.RestTemplateを使用してRESTful Web Service(REST API)を呼び出す実装方法について説明する。


5.2.1.1. RestTemplateとは

RestTemplateは、REST API(Web API)を呼び出すためのメソッドを提供するクラスであり、Spring Frameworkが提供するHTTPクライアントである。

具体的な実装方法の説明を行う前に、RestTemplateがどのようにREST API(Web API)にアクセスしているかを説明する。

Constitution of RestTemplate
項番 コンポーネント 説明
(1)
アプリケーション
RestTemplateのメソッドを呼び出して、REST API(Web API)の呼び出し依頼を行う。
(2)
RestTemplate
HttpMessageConverterを使用して、Javaオブジェクトをリクエストボディに設定する電文(JSON等)に変換する。
(3)

ClientHttpRequestFactoryからClientHttpRequestを取得して、電文(JSON等)の送信依頼を行う。
(4)
ClientHttpRequest
電文(JSON等)をリクエストボディに設定して、REST API(Web API)にHTTP経由でリクエストを行う。
(5)
RestTemplate
ResponseErrorHandlerを使用して、HTTP通信のエラー判定及びエラー処理を行う。
(6)
ResponseErrorHandler
ClientHttpResponseからレスポンスデータを取得して、エラー判定及びエラー処理を行う。
(7)
RestTemplate
HttpMessageConverterを使用して、レスポンスボディに設定されている電文(JSON等)をJavaオブジェクトに変換する。
(8)

REST API(Web API)の呼び出し結果(Javaオブジェクト)をアプリケーションへ返却する。

Note

非同期処理への対応

REST APIからの応答を別スレッドで処理したい場合(非同期で処理したい場合)は、RestTemplateの代わりにorg.springframework.web.reactive.function.client.WebClientを使用する。

非同期処理の実装例については、非同期リクエストを参照されたい。


5.2.1.1.1. HttpMessageConverter

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

RestTemplateを使用した場合、デフォルトで以下のHttpMessageConverterの実装クラスが登録される。

デフォルトで登録されるHttpMessageConverter
項番 クラス名 説明 サポート型
(1)
org.springframework.http.converter.
ByteArrayHttpMessageConverter
「HTTPボディ(テキスト or バイナリデータ)⇔バイト配列」変換用のクラス。
デフォルトですべてのメディアタイプ(*/*)をサポートする。
byte[]
(2)
org.springframework.http.converter.
StringHttpMessageConverter
「HTTPボディ(テキスト)⇔文字列」変換用のクラス。
デフォルトですべてのテキストメディアタイプ(text/*)をサポートする。
String
(3)
org.springframework.http.converter.
ResourceHttpMessageConverter
「HTTPボディ(バイナリデータ)⇔Springのリソースオブジェクト」変換用のクラス。
デフォルトですべてのメディアタイプ(*/*)をサポートする。
Resource[1]
(4)
org.springframework.http.converter.xml.
SourceHttpMessageConverter
「HTTPボディ(XML)⇔XMLソースオブジェクト」変換用のクラス。
デフォルトでXML用のメディアタイプ(text/xml,application/xml,application/*-xml)をサポートする。
Source[2]
(5)
org.springframework.http.converter.support.
AllEncompassingFormHttpMessageConverter
「HTTPボディ⇔MultiValueMapオブジェクト」変換用のクラス。
FormHttpMessageConverterの拡張クラスで、multipartのパートデータとしてXMLとJSONへの変換がサポートされている。
デフォルトでフォームデータ用のメディアタイプ(application/x-www-form-urlencoded,multipart/form-data)をサポートする。
  • メディアタイプがapplication/x-www-form-urlencodedの場合、MultiValueMap<String, String>として読込/書込される。
  • メディアタイプがmultipart/form-dataの場合、MultiValueMap<String, Object>として書込され、ObjectAllEncompassingFormHttpMessageConverter内に別途設定されるHttpMessageConveterで変換される。(注意: Note 参照)
デフォルトで登録されるパートデータ変換用のHttpMessageConveterは、AllEncompassingFormHttpMessageConverterFormHttpMessageConverterのソースを参照されたい。なお、任意のHttpMessageConverterを登録することもできる。
MultiValueMap[3]

Note

AllEncompassingFormHttpMessageConverterのメディアタイプがmultipart/form-dataの場合について

メディアタイプがmultipart/form-dataの場合、「MultiValueMapオブジェクト から HTTPボディ」への変換は可能だが、「HTTPボディ から MultiValueMapオブジェクト」への変換は現状サポートされていない。よって、「HTTPボディ から MultiValueMapオブジェクト」への変換を行いたい場合は、独自に実装する必要がある。

依存ライブラリがクラスパス上に存在する場合に登録されるHttpMessageConverter
項番 クラス名 説明 サポート型
(6)
org.springframework.http.converter.feed.
AtomFeedHttpMessageConverter
「HTTPボディ(Atom)⇔Atomフィードオブジェクト」変換用のクラス。
デフォルトでATOM用のメディアタイプ(application/atom+xml)をサポートする。
(ROMEがクラスパスに存在する場合に登録される)
Feed[4]
(7)
org.springframework.http.converter.feed.
RssChannelHttpMessageConverter
「HTTPボディ(RSS)⇔Rssチャネルオブジェクト」変換用のクラス。
デフォルトでRSS用のメディアタイプ(application/rss+xml)をサポートする。
(ROMEがクラスパスに存在する場合に登録される)
Channel[5]
(8)
org.springframework.http.converter.json.
MappingJackson2HttpMessageConverter
「HTTPボディ(JSON)⇔JavaBean」変換用のクラス。
デフォルトでJSON用のメディアタイプ(application/json,application/*+json)をサポートする。
(Jackson2がクラスパスに存在する場合に登録される)
Object(JavaBean)
Map
(9)
org.springframework.http.converter.xml.
MappingJackson2XmlHttpMessageConverter
「HTTPボディ(XML)⇔JavaBean」変換用のクラス。
デフォルトでXML用のメディアタイプ(text/xml,application/xml,application/*-xml)をサポートする。
(Jackson-dataformat-xmlがクラスパスに存在する場合に登録される)
Object(JavaBean)
Map
(10)
org.springframework.http.converter.xml.
Jaxb2RootElementHttpMessageConverter
「HTTPボディ(XML)⇔JavaBean」変換用のクラス。
デフォルトでXML用のメディアタイプ(text/xml,application/xml,application/*-xml)をサポートする。
(JAXBがクラスパスに存在する場合に登録される)

Note

Java SE 17環境にてJAXBをクラスパスに登録するにはJAXBの削除を参照されたい。

Object(JavaBean)
(11)
org.springframework.http.converter.json.
GsonHttpMessageConverter
「HTTPボディ(JSON)⇔JavaBean」変換用のクラス。
デフォルトでJSON用のメディアタイプ(application/json,application/*+json)をサポートする。
(Gsonがクラスパスに存在する場合に登録される)
Object(JavaBean)
Map
[1]org.springframework.core.ioパッケージのクラス
[2]jakarta.xml.transformパッケージのクラス
[3]org.springframework.utilパッケージのクラス
[4]com.rometools.rome.feed.atomパッケージのクラス
[5]com.rometools.rome.feed.rssパッケージのクラス

5.2.1.1.2. ClientHttpRequestFactory

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

  • org.springframework.http.client.ClientHttpRequestFactory
  • org.springframework.http.client.ClientHttpRequest
  • org.springframework.http.client.ClientHttpResponse

この3つのインタフェースのうち、開発者が意識するのはClientHttpRequestFactoryである。ClientHttpRequestFactoryは、サーバとの通信処理を行うクラス(ClientHttpRequestClientHttpResponseインタフェースの実装クラス)を解決する役割を担っている。

なお、Spring Frameworkが提供している主なClientHttpRequestFactoryの実装クラスは以下の通りである。

Spring Frameworkが提供している主なClientHttpRequestFactoryの実装クラス
項番 クラス名 説明
(1)
org.springframework.http.client.
SimpleClientHttpRequestFactory
Java SE標準のHttpURLConnectionのAPIを使用して通信処理(同期、非同期)を行うための実装クラス。(デフォルトで使用される実装クラス)
(2)
org.springframework.http.client.
HttpComponentsClientHttpRequestFactory
Apache HttpComponents HttpClientのAPIを使用して同期型の通信処理を行うための実装クラス。(HttpClient 5.2以上が必要)
(3)
org.springframework.http.client.
OkHttpClientHttpRequestFactory
Square OkHttpのAPIを使用して通信処理(同期、非同期)を行うための実装クラス。

Note

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

RestTemplateが使用するデフォルト実装はSimpleClientHttpRequestFactoryであり、本ガイドラインでもSimpleClientHttpRequestFactoryを使用した際の実装例となっている。Java SEのHttpURLConnectionで要件が満たせない場合は、Netty、Apache Http Componentsなどのライブラリの利用を検討されたい。


5.2.1.1.3. ResponseErrorHandler

RestTemplateは、サーバとの通信エラーのハンドリングをorg.springframework.web.client.ResponseErrorHandlerインタフェースに委譲することで実現している。

ResponseErrorHandlerには、

  • エラー判定を行うメソッド(hasError)
  • エラー処理を行うメソッド(handleError)

が定義されており、Spring Frameworkはデフォルト実装としてorg.springframework.web.client.DefaultResponseErrorHandlerを提供している。

DefaultResponseErrorHandlerは、サーバから応答されたHTTPステータスコードの値によって以下のようなエラー処理を行う。

  • レスポンスコードが正常系(2xx)の場合は、エラー処理は行わない。
  • レスポンスコードがクライアントエラー系(4xx)の場合は、org.springframework.web.client.HttpClientErrorExceptionを発生させる。
  • レスポンスコードがサーバエラー系(5xx)の場合は、org.springframework.web.client.HttpServerErrorExceptionを発生させる。
  • レスポンスコードが未定義のコード(ユーザ定義のカスタムコード)の場合は、org.springframework.web.client.UnknownHttpStatusCodeExceptionを発生させる。

Note

エラー時のレスポンスデータの取得方法

エラー時のレスポンスデータ(HTTPステータスコード、レスポンスヘッダ、レスポンスボディなど)は、例外クラスのgetterメソッドを呼び出すことで取得することができる。


5.2.1.1.4. ClientHttpRequestInterceptor

org.springframework.http.client.ClientHttpRequestInterceptorは、サーバとの通信の前後に共通的な処理を実装するためのインタフェースである。

ClientHttpRequestInterceptorを使用すると、

  • サーバとの通信ログ
  • 認証ヘッダの設定

といった共通的な処理をRestTemplateに適用することができる。

Note

ClientHttpRequestInterceptorの動作仕様

ClientHttpRequestInterceptorは複数適用することができ、指定した順番でチェーン実行される。これはサーブレットフィルタの動作によく似ており、最後に実行されるチェーン先としてClientHttpRequestによるHTTP通信処理が登録されている。例えば、ある条件に一致した際にサーバとの通信処理をキャンセルしたいという要件があった場合は、チェーン先を呼びださなければよい。

この仕組みを活用すると、

  • サーバとの通信の閉塞
  • 通信処理のリトライ

といった処理を適用することもできる。


5.2.2. How to use

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

Note

RestTemplateがサポートするHTTPメソッドについて

本ガイドラインでは、GETメソッドとPOSTメソッドを使用したクライアント処理の実装例のみを紹介するが、RestTemplateは他のHTTPメソッド(PUT, PATCH, DELETE, HEAD, OPTIONSなど)もサポートしており、同じような要領で使用することができる。

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


5.2.2.1. RestTemplateのセットアップ

RestTemplateを使用する場合は、RestTemplateをDIコンテナに登録し、RestTemplateを利用するコンポーネントにインジェクションする。


5.2.2.1.1. 依存ライブラリ設定

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

    <!-- (1) -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
    </dependency>

</dependencies>

Note

上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。

上記の依存ライブラリはterasoluna-gfw-parentが依存しているSpring Bootで管理されている。

項番 説明
(1)
Spring Frameworkのspring-webライブラリをdependenciesに追加する。

5.2.2.1.2. RestTemplateのbean定義

RestTemplateのbean定義を行い、DIコンテナに登録する。

bean定義ファイル(applicationContext.xml)の定義例

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate" /> <!-- (1) -->
項番 説明
(1)
RestTemplateをデフォルト設定のまま利用する場合は、デフォルトコンストラクタを使用してbeanを登録する。

Note

RestTemplateのカスタマイズ方法

HTTP通信処理をカスタマイズする場合は、以下のようなbean定義となる。

<bean id="clientHttpRequestFactory"
    class="org.springframework.http.client.SimpleClientHttpRequestFactory"> <!-- (1) -->
    <!-- Set properties for customize a http communication (omit on this sample) -->
</bean>

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <constructor-arg ref="clientHttpRequestFactory" /> <!-- (2) -->
</bean>
項番 説明
(1)
ClientHttpRequestFactoryのbean定義を行う。
本ガイドラインではタイムアウトの設定をカスタマイズする方法を紹介している。
詳細は通信タイムアウトの設定を参照されたい。
(2)
ClientHttpRequestFactoryを引数に指定するコンストラクタを使用してbeanを登録する。

なお、HttpMessageConverterResponseErrorHandlerClientHttpRequestInterceptorのカスタマイズ方法については、

を参照されたい。


5.2.2.1.3. RestTemplateの利用

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

RestTemplateのインジェクション例

@Service
public class AccountServiceImpl implements AccountService {

    @Inject
    RestTemplate restTemplate;

    // omitted

}

5.2.2.2. GETリクエストの送信

RestTemplateは、GETリクエストを行うためのメソッドを複数提供している。

  • 通常はgetForObjectメソッド又はgetForEntityメソッドを使用する。
  • 任意のヘッダを設定するなどリクエストに細かい設定を行いたい場合は、org.springframework.http.RequestEntityexchangeメソッドを使用する。

5.2.2.2.1. getForObjectメソッドを使用した実装

レスポンスボディのみ取得できればよい場合は、getForObjectメソッドを使用する。

getForObjectメソッドの使用例

フィールド宣言部

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

メソッド内部

User user = restTemplate.getForObject(uri, User.class); // (1)
項番 説明
(1)
getForObjectメソッドを使用した場合は、戻り値はレスポンスボディの値になる。
レスポンスボディのデータはHttpMessageConverterによって第2引数に指定したJavaクラスへ変換された後、返却される。

5.2.2.2.2. getForEntityメソッドを使用した実装

HTTPステータスコード、レスポンスヘッダ、レスポンスボディを取得する必要がある場合は、getForEntityメソッドを使用する。

getForEntityメソッドの使用例

ResponseEntity<User> responseEntity =
        restTemplate.getForEntity(uri, User.class); // (1)
HttpStatus statusCode = responseEntity.getStatusCode(); // (2)
HttpHeaders header = responseEntity.getHeaders(); // (3)
User user = responseEntity.getBody(); // (4)
項番 説明
(1)
getForEntityメソッドを使用した場合は、戻り値はorg.springframework.http.ResponseEntityとなる。
(2)
HTTPステータスコードはgetStatusCodeメソッドを用いて取得する。
(3)
レスポンスヘッダはgetHeadersメソッドを用いて取得する。
(4)
レスポンスボディはgetBodyメソッドを用いて取得する。

Note

ResponseEntityとは

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

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


5.2.2.2.3. exchangeメソッドを使用した実装

リクエストヘッダを指定する必要がある場合は、org.springframework.http.RequestEntityを生成しexchangeメソッドを使用する。

exchangeメソッドの使用例

import部

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

フィールド宣言部

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

メソッド内部

RequestEntity requestEntity = RequestEntity
        .get(uri)//(1)
        .build();//(2)

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

User user = responseEntity.getBody();//(4)
項番 説明
(1)
RequestEntitygetメソッドを使用し、GETリクエスト用のリクエストビルダを生成する。
パラメータにURIを設定する。
(2)
RequestEntity.HeadersBuilderbuildメソッドを使用し、RequestEntityオブジェクトを作成する。
(3)
exchangeメソッドを使用し、リクエストを送信する。第二引数に、レスポンスデータの型を指定する。
レスポンスは、ResponseEntity<T>になる。型パラメータに、レスポンスデータの型を設定する。
(4)
getBodyメソッドを使用し、レスポンスボディのデータを取得する。

Note

RequestEntityとは

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

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

なお、リクエストヘッダの設定方法については、リクエストヘッダの設定を参照されたい。


5.2.2.3. POSTリクエストの送信

RestTemplateは、POSTリクエストを行うためのメソッドを複数提供している。

  • 通常は、postForObjectpostForEntityを使用する。
  • 任意のヘッダを設定するなどリクエストに細かい設定を行いたい場合は、RequestEntityexchangeメソッドを使用する。

5.2.2.3.1. postForObjectメソッドを使用した実装

POSTした結果としてレスポンスボディのみ取得できればよい場合は、postForObjectメソッドを使用する。

postForObjectメソッドの使用例

User user = new User();

// omitted

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

5.2.2.3.2. postForEntityメソッドを使用した実装

POSTした結果としてHTTPステータスコード、レスポンスヘッダ、レスポンスボディを取得する必要がある場合は、postForEntityメソッドを使用する。

postForEntityメソッドの使用例

User user = new User();

// omitted

ResponseEntity<User> responseEntity =
        restTemplate.postForEntity(uri, user, User.class); // (1)
項番 説明
(1)
postForEntityメソッドもgetForObjectメソッドと同様に簡易にPOSTリクエストを実装できる。
postForEntityメソッドを使用した場合は、戻り値はResponseEntityとなる。
レスポンスボディの値は、ResponseEntityから取得する。

5.2.2.3.3. exchangeメソッドを使用した実装

リクエストヘッダを指定する必要がある場合は、RequestEntityを生成しexchangeメソッドを使用する。

exchangeメソッドの使用例

import部

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

フィールド宣言部

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

メソッド内部

User user = new User();

// omitted

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

ResponseEntity<User> responseEntity =
        restTemplate.exchange(requestEntity, User.class);//(4)
項番 説明
(1)
RequestEntityを使用して、リクエストを生成する。型パラメータに、リクエストボディに設定するデータの型を指定する。
(2)
postメソッドを使用し、POSTリクエスト用のリクエストビルダを生成する。パラメータにURIを設定する。
(3)
RequestEntity.BodyBuilderbodyメソッドを使用し、RequestEntityオブジェクトを作成する。
パラメータにリクエストボディに変換するJavaオブジェクトを設定する。
(4)
exchangeメソッドを使用し、リクエストを送信する。

Note

リクエストヘッダの設定方法

リクエストヘッダの設定方法については、リクエストヘッダの設定を参照されたい。


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

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

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

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.5. リクエストヘッダの設定

RequestEntityexchangeメソッドを使用すると、RequestEntityのメソッドを使用して特定のヘッダ及び任意のヘッダを設定することができる。
詳細はRequestEntityのJavadocを参照されたい。
本ガイドラインでは、

について説明する。


5.2.2.5.1. Content-Typeヘッダの設定

サーバへデータを送信する場合は、通常Content-Typeヘッダの指定が必要となる。

Content-Typeヘッダの設定例

User user = new User();

// omitted

RequestEntity<User> requestEntity = RequestEntity
        .post(uri)
        .contentType(MediaType.APPLICATION_JSON) // (1)
        .body(user);
項番 説明
(1)
RequestEntity.BodyBuildercontentTypeメソッドを使用し、Context-Typeヘッダの値を指定する。
上記の実装例では、データ形式がJSONであることを示す「application/json」を設定している。

5.2.2.5.2. Acceptヘッダの設定

サーバから取得するデータの形式を指定する場合は、Acceptヘッダの指定が必要となる。
サーバが複数のデータ形式のレスポンスをサポートしていない場合は、Acceptヘッダを明示的に指定しなくてもよいケースもある。

Acceptヘッダの設定例

User user = new User();

// omitted

RequestEntity<User> requestEntity = RequestEntity
        .post(uri)
        .accept(MediaType.APPLICATION_JSON) // (1)
        .body(user);
項番 説明
(1)
RequestEntity.HeadersBuilderacceptメソッドを使用して、Acceptヘッダの値を設定する。
上記の実装例では、取得可能なデータ形式がJSONであることを示す「application/json」を設定している。

5.2.2.5.3. 任意のリクエストヘッダの設定

サーバへアクセスするために、リクエストヘッダの設定が必要になるケースがある。

任意ヘッダの設定例

User user = new User();

// omitted

RequestEntity<User> requestEntity = RequestEntity
        .post(uri)
        .header("Authorization", "Basic " + base64Credentials) // (1)
        .body(user);
項番 説明
(1)
RequestEntity.HeadersBuilderheaderメソッドを使用してリクエストヘッダの名前と値を設定する。
上記の実装例では、AuthorizationヘッダにBasic認証に必要な資格情報を設定している。

5.2.2.6. エラーハンドリング

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

RestTemplateのデフォルト実装(DefaultResponseErrorHandler)では、

  • レスポンスコードがクライアントエラー系(4xx)の場合は、HttpClientErrorException
  • レスポンスコードがサーバエラー系(5xx)の場合は、HttpServerErrorException
  • レスポンスコードが未定義のコード(ユーザ定義のカスタムコード)の場合は、UnknownHttpStatusCodeException

が発生するため、必要に応じてこれらの例外をハンドリングする必要がある。

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

Note

以下の実装例は、サーバエラーが発生した際の例外ハンドリングの一例である。

アプリケーションの要件に応じて適切な例外ハンドリングを行うこと。

フィールド宣言部

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

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

メソッド内部

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

        responseEntity = restTemplate.exchange(requestEntity, String.class);

        if (logger.isInfoEnabled()) {
            logger.info("Success({}) ", responseEntity.getStatusCode());
        }

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

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

        // omitted
    }

}
項番 説明
(1)
例外をキャッチしてエラー処理を行う。サーバエラー(500系)の場合、HttpServerErrorExceptionをキャッチする。

5.2.2.6.2. ResponseEntityの返却(エラーハンドラの拡張)

org.springframework.web.client.ResponseErrorHandlerインタフェースの実装クラスをRestTemplateに設定することで、独自のエラー処理を行うことができる。

以下の例では、サーバエラー及びクライアントエラーが発生した場合でもResponseEntityを返すようにエラーハンドラを拡張している。

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

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

public class CustomErrorHandler extends DefaultResponseErrorHandler { // (1)

    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        //Don't throw Exception.
    }

}
項番 説明
(1)
ResponseErrorHandlerインタフェースの実装クラスを作成する。
上記の作成例では、デフォルトのエラーハンドラの実装クラスであるDefaultResponseErrorHandlerを拡張し、
サーバエラー及びクライアントエラーが発生した際に例外を発生させずにResponseEntityが返るようにしている。

bean定義ファイル(applicationContext.xml)の定義例

<bean id="customErrorHandler" class="com.example.restclient.CustomErrorHandler" /> <!-- (1) -->

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <property name="errorHandler" ref="customErrorHandler" /><!-- (2) -->
</bean>
項番 説明
(1)
ResponseErrorHandlerの実装クラスのbean定義を行う。
(2)
errorHandlerプロパティにResponseErrorHandlerの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.7. 通信タイムアウトの設定

サーバとの通信に対してタイムアウト時間を指定したい場合は、以下のようなbean定義を行う。

bean定義ファイル(applicationContext.xml)の定義例

<bean id="clientHttpRequestFactory"
      class="org.springframework.http.client.SimpleClientHttpRequestFactory">
    <property name="connectTimeout" value="${api.connectTimeout: 2000}" /><!-- (1) -->
    <property name="readTimeout" value="${api.readTimeout: 2000}" /><!-- (2) -->
</bean>

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <constructor-arg ref="clientHttpRequestFactory" />
</bean>
項番 説明
(1)
connectTimeoutプロパティにサーバとの接続タイムアウト時間(ミリ秒)を設定する。
タイムアウト発生時はorg.springframework.web.client.ResourceAccessExceptionが発生する。
(2)
readTimeoutプロパティにレスポンスデータの読み込みタイムアウト時間(ミリ秒)を設定する。
タイムアウト発生時はResourceAccessExceptionが発生する。

Note

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

ResourceAccessExceptionは起因例外をラップしており、接続タイムアウト及び読み込みタイムアウト発生時の起因例外は共にjava.net.SocketTimeoutExceptionである。デフォルト実装(SimpleClientHttpRequestFactory)を使用した場合は、どちらのタイムアウトが発生したかを例外クラスの種類で区別できないという点を補足しておく。

なお、他のHttpRequestFactoryを使用した場合の動作は未検証であるため、起因例外が上記と異なる可能性がある。他のHttpRequestFactoryを使用する場合は、タイムアウト時に発生する例外を把握した上で適切な例外ハンドリングを行うこと。


5.2.2.8. SSL自己署名証明書の使用

テスト環境などでSSL自己署名証明書を使用する場合は、以下のように実装する。

FactoryBeanの実装例

RestTemplateのBean定義で、コンストラクタの引数に渡す org.springframework.http.client.ClientHttpRequestFactoryを作成するための org.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.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.SSLConnectionSocketFactoryBuilder;
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 { // (16)

    private String keyStoreFileName;

    private char[] keyStorePassword;

    private HttpComponentsClientHttpRequestFactory factory; // (16)

    @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)
                .setSSLSocketFactory( // (3)
                        SSLConnectionSocketFactoryBuilder.create()
                        .setSslContext(sslContext)
                        .setTlsVersions(TLS.V_1_3, TLS.V_1_2)
                        .build())
                .setDefaultSocketConfig( // (4)
                        SocketConfig.custom()
                        .setSoTimeout(Timeout.ofMinutes(1L))
                        .build())
                .setMaxConnTotal(1) // (5)
                .setMaxConnPerRoute(1) // (6)
                .setPoolConcurrencyPolicy(PoolConcurrencyPolicy.STRICT) // (7)
                .setConnPoolPolicy(PoolReusePolicy.LIFO) // (8)
                .setConnectionTimeToLive(TimeValue.ofMinutes(1L)) // (9)
                .build();
        // @formatter:on

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

        // (15)
        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;
    }

    // (16)
    @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)
Socket Configのデフォルト値を設定する。
Socketから提供されるInputStreamread()のブロック時間がSoTimeoutを越えた場合、通常、java.net.SocketTimeoutExceptionが発生するが、TCPコネクション確立後、SSLハンドシェイクを行っている間にこのタイムアウトが起きた場合には、org.apache.hc.client5.http.ConnectTimeoutExceptionが発生する。

Note

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

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

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

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

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

という具合に使い分けること。なお、SoTimeoutのデフォルト値は3分となっている。

(5)
最大合計接続数を設定する。
最大合計接続数を超える場合、後続処理はコネクションの取得を待機する。
(6)
宛先(ホスト名 + ポート番号 及び スキーマ定義)ごとの最大接続数を設定する。
最大接続数を超える場合、後続処理はコネクションの取得を待機する。
(7)
(8)
(9)
コネクションが接続されてから切断されるまでの最大生存時間を設定する。
使用状況にかかわらず、ConnectionTimeToLiveを越えた場合にコネクションを破棄する。
(10)
作成したSSLコンテキストを利用する org.apache.hc.client5.http.impl.classic.CloseableHttpClientを作成する。
(11)
(2)で作成したHttpClientConnectionManagerを設定する。
(12)
新しい接続が確立されるまでのタイムアウトのデフォルト値を設定する。
接続の確立にConnectTimeout以上かかった場合、org.apache.hc.client5.http.HttpHostConnectExceptionが発生する。
(13)
レスポンスタイムアウトのデフォルト値を設定する。
レスポンス返却にResponseTimeout以上かかった場合、java.net.SocketTimeoutExceptionが発生する。
(14)
(15)
作成したHttpClientを利用するClientHttpRequestFactoryを作成する。
(16)
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のバージョンを指定するような実装を検討されたい。

HttpClientおよび HttpClientBuilderを使用するためには、Apache HttpComponents HttpClient のライブラリが必要となる。
以下を pom.xmlに追加し、Apache HttpComponents HttpClient を依存ライブラリに追加する。
  • pom.xml

    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
    </dependency>
    

    Note

    上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。

    上記の依存ライブラリはterasoluna-gfw-parentが依存しているSpring Bootで管理されている。

bean定義ファイル(applicationContext.xml)の定義例

SSL自己署名証明書を使用したSSL通信を行う RestTemplateを定義する。

<bean id="httpsRestTemplate" class="org.springframework.web.client.RestTemplate">
    <constructor-arg>
        <bean class="com.example.restclient.RequestFactoryBean"> <!-- (1) -->
            <property name="keyStoreFileName" value="${rscl.keystore.filename}" />
            <property name="keyStorePassword" value="${rscl.keystore.password}" />
        </bean>
    </constructor-arg>
</bean>
項番 説明
(1)
作成した RequestFactoryBeanRestTemplateのコンストラクタに指定する。
RequestFactoryBeanには、キーストアファイルのファイル名とパスワードを渡す。

RestTemplateの使用方法

RestTemplateの使い方については、SSL自己署名証明書を使用しない場合と同じである。


5.2.2.9. Basic認証

サーバがBasic認証を要求する場合は、以下のように実装する。このとき、Java標準のjava.util.Base64を使用する。

Basic認証の実装例

フィールド宣言部

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

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

メソッド内部

String plainCredentials = username + ":" + password; // (1)
String base64Credentials = Base64.getEncoder()
        .encodeToString(plainCredentials.getBytes(StandardCharsets.UTF_8)); // (2)

RequestEntity requestEntity = RequestEntity
      .get(uri)
      .header("Authorization", "Basic " + base64Credentials) // (3)
      .build();
項番 説明
(1)
ユーザ名とパスワードを「”:” 」でつなげる。
(2)
(1)をバイト配列に変換して、Base64エンコードする。
(3)
AuthorizationヘッダをBasic認証の資格情報を設定する。

Note

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


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

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

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

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に設定する。
(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.11. ファイルダウンロード

RestTeamplateを使用してファイルダウンロードを行う場合は、以下のように実装する。

ファイルダウンロードの実装例(ファイルサイズが小さい場合)

RequestEntity requestEntity = RequestEntity
        .get(uri)
        .build();

ResponseEntity<byte[]> responseEntity =
        restTemplate.exchange(requestEntity, byte[].class);//(1)

byte[] downloadContent = responseEntity.getBody();//(2)
項番 説明
(1)
ダウンロードファイルを指定したデータ型で扱う。ここでは、バイト配列を指定。
(2)
レスポンスボディからダウンロードしたファイルのデータを取得する。

Warning

サイズの大きいファイルをダウンロードする際の注意点

サイズの大きなファイルをデフォルトで登録されているHttpMessageConverterを使用して byte配列で取得すると、 java.lang.OutOfMemoryErrorが発生する可能性がある。そのため、サイズの大きなファイルをダウンロードしたい場合は、レスポンスから InputStreamを取得して、ダウンロードデータを少しずつファイルに書き出す必要がある。

ファイルダウンロードの実装例(ファイルサイズが大きい場合)

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

    // (2)
    @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);
    }

};

// (3)
ResponseEntity<File> responseEntity = this.restTemplate.execute(targetUri,
        HttpMethod.GET, null, responseExtractor);
if (HttpStatus.OK.equals(responseEntity.getStatusCode())) {
    File getFile = responseEntity.getBody();

    // omitted

}
項番 説明
(1)
RestTemplate#executeで取得されたレスポンスから、RestTemplate#executeの戻り値を作成するための処理を作成する。
(2)
レスポンスボディ(InputStream)からデータを読込み、ファイルを作成する。
作成したファイルとHTTPヘッダ、ステータスコードを ResponseEntity<File>に格納して返却する。
(3)
RestTemplate#executeを使用して、ファイルのダウンロードを行う。

ファイルダウンロードの実装例(ファイルサイズが大きい場合(ResponseEntityを使わない例))

ステータスコードの判定やHTTPヘッダの参照等が不要な場合は、 以下のようにResponseEntityではなく Fileを返却すればよい。

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

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

        File rcvFile = File.createTempFile("rcvFile", "zip");

        FileCopyUtils.copy(response.getBody(), new FileOutputStream(
                rcvFile));

        return rcvFile;
    }

};

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

5.2.2.12. RESTfulなURL(URIテンプレート)を扱う方法と実装例

RESTfulなURLを扱うには、URIテンプレートを使用して実装を行えばよい。

getForObjectメソッドでの使用例

フィールド宣言部

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

メソッド内部

User user = restTemplate.getForObject(uriStr, User.class, "0001"); // (2)
項番 説明
(1)
URIテンプレートの変数{userId}は、RestTeamplateの使用時に指定の値に変換される。
(2)
URIテンプレートの変数1つ目がgetForObjectメソッドの第3引数に指定した値で置換され、『http://localhost:8080/api/users/0001』として処理される。

exchangeメソッドでの使用例

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

メソッド内部

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

User user = new User();

// omitted

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

ResponseEntity<User> responseEntity = restTemplate.exchange(requestEntity, User.class);
項番 説明
(1)
URIテンプレートの変数{action}は、RestTeamplateの使用時に指定の値に変換される。
(2)
UriComponentsBuilderを使用することで、URIテンプレートの変数1つ目がbuildAndExpandの引数で指定した値に置換され、『http://localhost:8080/api/users/create』のURIが作成される。
詳細はUriComponentsBuilderのJavadocを参照されたい。

5.2.3. How to extend

本節では、RestTemplateの拡張方法について説明する。


5.2.3.1. 任意のHttpMessageConverterを登録する方法

デフォルトで登録されている HttpMessageConverterで電文変換の要件を満たせない場合は、任意のHttpMessageConverterを登録することができる。ただし、デフォルトで登録されていたHttpMessageConverterは削除されるので、必要なHttpMessageConverterをすべて個別に登録する必要がある。

bean定義ファイル(applicationContext.xml)の定義例

<bean id="jaxb2CollectionHttpMessageConverter"
      class="org.springframework.http.converter.xml.Jaxb2CollectionHttpMessageConverter" /> <!-- (1) -->

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <property name="messageConverters"> <!-- (2) -->
        <list>
            <ref bean="jaxb2CollectionHttpMessageConverter" />
        </list>
    </property>
</bean>
項番 説明
(1)
登録するHttpMessageConverterの実装クラスをbean定義する。
(2)
messageConvertersプロパティに登録するHttpMessageConverterのbeanをインジェクションする。

5.2.3.2. 共通処理の適用(ClientHttpRequestInterceptor

ClientHttpRequestInterceptorを使用することで、サーバとの通信処理の前後に任意の処理を実行させることができる。
ここでは、ロギング処理と、Basic認証用のリクエストヘッダ設定処理を適用する方法を紹介する。

5.2.3.2.1. ロギング処理

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

通信ログ出力の実装例

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()) {
            String requestBody = new String(body, StandardCharsets.UTF_8);

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

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

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

        return response; // (6)
    }

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

bean定義ファイル(applicationContext.xml)の定義例

<!-- (1) -->
<bean id="loggingInterceptor" class="com.example.restclient.LoggingInterceptor" />
項番 説明
(1)
ClientHttpRequestInterceptorの実装クラスのbean定義を行う。

5.2.3.2.2. Basic認証用のリクエストヘッダ設定処理

サーバにアクセスするためにBasic認証用のリクエストヘッダを設定する必要がある場合は、以下のようなbean定義を行う。

bean定義ファイル(applicationContext.xml)の定義例

<!-- (1) -->
<bean id="basicAuthInterceptor" class="org.springframework.http.client.support.BasicAuthorizationInterceptor">
    <constructor-arg name="username" value="${api.auth.username}" /><!-- (2) -->
    <constructor-arg name="password" value="${api.auth.password}" /><!-- (3) -->
</bean>
項番 説明
(1)
ClientHttpRequestInterceptorインタフェースを実装したBasicAuthorizationInterceptorのbean定義を行う。
(2)
コンストラクタ引数のusernameにユーザ名を設定する。
(3)
コンストラクタ引数のpasswordにパスワードを設定する。

5.2.3.2.3. ClientHttpRequestInterceptorの適用

RestTemplateに作成したClientHttpRequestInterceptorを適用する場合は、以下のようなbean定義を行う。

bean定義ファイル(applicationContext.xml)の定義例

<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <property name="interceptors"><!-- (1) -->
        <list>
            <ref bean="basicAuthInterceptor" />
            <ref bean="loggingInterceptor" />
        </list>
    </property>
</bean>
項番 説明
(1)
interceptorsプロパティにClientHttpRequestInterceptorのbeanをインジェクションする。
複数のbeanをインジェクションした場合は、リストの先頭から順にチェーン実行される。
上記の例だと、BasicAuthorizationInterceptor -> LoggingInterceptor -> ClientHttpRequest の順番でリクエスト前の処理が実行される。(レスポンス後の処理は順番が逆転する)

5.2.3.3. 非同期リクエスト

非同期リクエストを行う場合は、org.springframework.web.reactive.function.client.WebClientを使用する。

Note

Spring Framework 6.0からAsyncRestTemplateは削除されており、非同期リクエストはSpring Web Reactive APIのWebClientを使用するように案内されている。

TERASOLUNA Server Framework for Java (5.x)ではAsyncRestTemplateの代替機能としてWebClientを案内しているだけであり、Spring Web Reactiveをサポートしているわけではない点に注意されたい。


5.2.3.3.1. WebClientのセットアップ

WebClientを使用する場合は、WebClientをDIコンテナに登録し、WebClientを利用するコンポーネントにインジェクションする。

5.2.3.3.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に追加する。

Note

上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。

上記の依存ライブラリはterasoluna-gfw-parentが依存しているSpring Bootで管理されている。


5.2.3.3.2. WebClientのbean定義

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

Note

本ガイドラインのBean定義は基本的にXML-based configurationを用いているが、WebClientのBean定義に関してはJava-based configurationを用いている。

WebClientがBuilderパターンを使用していることに加え、Spring Frameworkが有用なXML DLSを提供していないためJava-based configurationで実装している。Java-based configurationで定義するクラスは、コンポーネントスキャンが有効となるパッケージ配下に配置されたい。詳しくはJava-based configurationを参照されたい。

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.3.3.3. 非同期リクエストの実装

非同期リクエストの実装例

フィールド宣言部

@Inject
WecClient webClient;

メソッド内部

CompletableFuture<ResponseEntity<User>> responseEntity = this.webClient
        .get() // (1)
        .uri(uri) // (1)
        .retrieve() // (2)
        .toEntity(User.class) // (2)
        .doOnSuccess(r -> { // (3)
            // omitted
        })
        .doOnError(t -> { // (3)
            // omitted
        })
        .toFuture(); // (4)
項番 説明
(1)
WebClientの各メソッドを使用して、非同期リクエストを送信する。
上記の実装例では、uriに対しGETメソッドでHTTPリクエストを送信している。
使用できるメソッドについてはWebClientのJavadocを参照されたい。
(2)
処理結果の取得方法を定義する。
この例では、User.classでレスポンスを受け取れるよう定義している。
(3)
成功のレスポンスが返ってきた場合の処理をdoOnSuccessに実装する。
エラーが発生した場合の処理をdoOnErrorに実装する。
(4)
非同期処理の結果をCompletableFutureでラップしたResponseEntity<User>として受け取る。

Note

WebClientの返却値について

Reactive APIが使用する戻り値の型は、単数の場合はreactor.core.publisher.Mono、複数の場合はreactor.core.publisher.Fluxが一般的に用いられるが、この例ではCompletableFutureを返却するようにしている。

Tip

block()を使用し、同期的に処理させることも可能である。その場合の戻り値の型はResponseEntity<User>となる。


5.2.3.3.4. カスタマイズ

WebClient.Builderをカスタマイズし、WebClientにデフォルトで設定する方法を説明する。
ここでの説明はあくまで一例であるため、業務要件に合わせ適宜カスタマイズされたい。

5.2.3.3.4.1. コネクションプールの設定
コネクションプールの設定を行う方法を説明する。

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

@Configuration
public class WebClientConfig {

  // omitted

  @Bean
  public WebClient connectionWebClient() {

      // @formatter:off
      ConnectionProvider connectionProvider = ConnectionProvider.builder(this.connectionName) // (1)
              .maxConnections(this.maxConnections) // (2)
              .pendingAcquireMaxCount(this.pendingAcquireMaxCount) // (3)
              .pendingAcquireTimeout(Duration.ofSeconds(this.pendingAcquireTimeout)) // (4)
              .build();
      // @formatter:on

      HttpClient httpClient = HttpClient.create(connectionProvider); // (5)

      ReactorClientHttpConnector httpConnector = new ReactorClientHttpConnector(httpClient); // (6)

      // @formatter:off
      WebClient webClient = WebClient.builder()
              .clientConnector(httpConnector) // (7)
              .build();
      // @formatter:on

      return webClient;
  }
項番 説明
(1)
reactor.netty.resources.ConnectionProviderのbuilderを作成する。
(2)
builderにMaxコネクション数を設定する。
設定しない場合は、デフォルト値2 * プロセッサ数が使用される。
(3)
builderにMaxプール数を設定する。
-1”を設定した場合は無制限となる。
(4)
TimeoutException がスローされるまでの最大時間(ms)を設定する。
設定しない場合は、デフォルト45秒が使用される。
-1”を設定した場合は無制限となる。
(5)
(1)~(4)で生成したreactor.netty.resources.ConnectionProviderを設定したreactor.netty.http.client.HttpClientを生成する。
(6)
(5)で定義したHttpClientを設定したorg.springframework.http.client.reactive.ReactorClientHttpConnectorのbeanを登録する。
(7)
(6)で定義したReactorClientHttpConnectorを設定したWebClientのbeanを登録する。

5.2.3.3.4.2. フィルターの設定
org.springframework.web.reactive.function.client.ExchangeFilterFunctionを実装することで、サーバとの通信処理前に任意の処理を実行させることができる。
ここでは単純な例として、通信前のロギング処理の実装例を紹介する。なお、サーバとの通信処理後に任意の処理を実施したい場合は、doOnSuccessに実装すればよい。

通信ログ出力の実装例

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

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


5.2.4. Appendix

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

サーバへアクセスする際にHTTP Proxyサーバを経由する必要がある場合は、システムプロパティやJVM起動引数、またはRestTemplateのBean定義にてHTTP Proxyサーバの設定が必要となる。
システムプロパティやJVM起動引数に設定した場合、アプリケーション全体に影響を与えてしまうため、RestTemplate毎にHTTP Proxyサーバの設定を行う例を紹介する。
RestTemplate毎のHTTP Proxyサーバの設定は、ClientHttpRequestFactoryインタフェースのデフォルト実装であるSimpleClientHttpRequestFactoryに付与することが可能である。ただし、SimpleClientHttpRequestFactoryでは資格情報を設定することはできないため、Proxy認証を行う場合はHttpComponentsClientHttpRequestFactoryを使用する。
HttpComponentsClientHttpRequestFactoryは、Apache HttpComponents HttpClientを用いてリクエストを生成するClientHttpRequestFactoryインタフェースの実装クラスである。

5.2.4.1.1. SimpleClientHttpRequestFactoryを使用したHTTP Proxyサーバの設定方法

資格情報が不要なHTTP Proxyサーバの接続先の指定については、RestTemplateがデフォルトで使用するSimpleClientHttpRequestFactoryで指定することが可能である。

Bean定義ファイル

<!-- (1) -->
<bean id="inetSocketAddress" class="java.net.InetSocketAddress" >
    <constructor-arg name="hostname" value="${rscl.http.proxyHost}" />    <!-- (2) -->
    <constructor-arg name="port" value="${rscl.http.proxyPort}" />    <!-- (3) -->
</bean>

<!-- (4) -->
<bean id="simpleClientRestTemplate" class="org.springframework.web.client.RestTemplate" >
    <constructor-arg>
        <!-- (5) -->
        <bean class="org.springframework.http.client.SimpleClientHttpRequestFactory">
            <!-- (6) -->
            <property name="proxy">
                <bean class="java.net.Proxy" >
                    <!-- (7) -->
                    <constructor-arg name="type">
                        <util:constant static-field="java.net.Proxy.Type.HTTP"/>
                    </constructor-arg>
                    <constructor-arg name="sa" ref="inetSocketAddress"/>
                </bean>
            </property>
        </bean>
    </constructor-arg>
</bean>
項番 説明
(1)
java.net.InetSocketAddressにHTTP Proxyサーバの設定を行う。
(2)
InetSocketAddressのコンストラクタ引数のhostnameに、プロパティファイルに設定されたキーrscl.http.proxyHostの値をHTTP Proxyサーバのホスト名として設定する。
(3)
InetSocketAddressのコンストラクタ引数のportに、プロパティファイルに設定されたキーrscl.http.proxyPortの値をHTTP Proxyサーバのポート番号として設定する。
(4)
RestTemplateのBean定義を行う。
(5)
RestTemplateのコンストラクタの引数に、SimpleClientHttpRequestFactoryを設定する。
(6)
SimpleClientHttpRequestFactoryproxyプロパティにjava.net.Proxyを設定する。
(7)
Proxyのコンストラクタの引数に、java.net.Proxy.Type.HTTPと生成したInetSocketAddressを設定する。

5.2.4.1.2. HttpComponentsClientHttpRequestFactoryを使用したHTTP Proxyサーバの設定方法

5.2.4.1.2.1. HTTP Proxyサーバの指定方法

資格情報が必要なHTTP Proxyサーバの接続先の指定は、RestTemplateに対して、HttpComponentsClientHttpRequestFactoryを使用し指定する。

pom.xml

<!-- (1) -->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
</dependency>
項番 説明
(1)
HttpComponentsClientHttpRequestFactory内で使用するApache HTTP Clientを使用するために、Apache HttpComponents Clientpom.xmlの依存ライブラリに追加する。

Note

上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。

上記の依存ライブラリはterasoluna-gfw-parentが依存しているSpring Bootで管理されている。

Bean定義ファイル

<!-- (1) -->
<bean id="proxyHttpClientBuilder" class="org.apache.hc.client5.http.impl.classic.HttpClientBuilder" factory-method="create">
    <!-- (2) -->
    <property name="proxy">
        <bean class="org.apache.hc.core5.http.HttpHost">
            <constructor-arg name="hostname" value="${rscl.http.proxyHost}" />
            <constructor-arg name="port" value="${rscl.http.proxyPort}" />
        </bean>
    </property>
</bean>

<!-- (5) -->
<bean id="proxyRestTemplate" class="org.springframework.web.client.RestTemplate" >
    <constructor-arg>
        <!-- (6) -->
        <bean class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">
            <!-- (7) -->
            <constructor-arg>
                <bean factory-bean="proxyHttpClientBuilder" factory-method="build" />
            </constructor-arg>
        </bean>
    </constructor-arg>
</bean>
項番 説明
(1)
org.apache.hc.client5.http.impl.classic.HttpClientBuilderを使用し、org.apache.http.client5.HttpClientの設定を行う。
(2)
HttpClientBuilderproxyプロパティに、HTTP Proxyサーバの設定を行ったorg.apache.http.HttpHostを設定する。
(3)
HttpHostのコンストラクタ引数のhostnameに、プロパティファイルに設定されたキーrscl.http.proxyHostの値をHTTP Proxyサーバのホスト名として設定する。
(4)
HttpHostのコンストラクタ引数のportに、プロパティファイルに設定されたキーrscl.http.proxyPortの値をHTTP Proxyサーバのポート番号として設定する。
(5)
RestTemplateのBean定義を行う。
(6)
RestTemplateのコンストラクタの引数に、HttpComponentsClientHttpRequestFactoryを設定する。
(7)
HttpComponentsClientHttpRequestFactoryのコンストラクタの引数に、HttpClientBuilderから生成したHttpClientを設定する。

5.2.4.1.2.2. HTTP 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)
        AuthScope authScope = new AuthScope(this.host, this.port);

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

        // (8)
        BasicCredentialsProvider 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定義ファイル

<bean id="proxyHttpClientBuilder" class="org.apache.hc.client5.http.impl.classic.HttpClientBuilder" factory-method="create">
    <!-- (1) -->
    <property name="defaultCredentialsProvider">
        <bean class="com.example.restclient.BasicCredentialsProviderFactoryBean" />
    </property>
    <property name="proxy">
        <bean id="proxyHost" class="org.apache.hc.core5.http.HttpHost">
            <constructor-arg name="hostname" value="${rscl.http.proxyHost}" />
            <constructor-arg name="port" value="${rscl.http.proxyPort}" />
        </bean>
    </property>
</bean>

<bean id="proxyRestTemplate" class="org.springframework.web.client.RestTemplate">
    <constructor-arg>
        <bean class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">
            <constructor-arg>
                <bean factory-bean="proxyHttpClientBuilder" factory-method="build" />
            </constructor-arg>
        </bean>
    </constructor-arg>
</bean>
項番 説明
(1)
HttpClientBuilderdefaultCredentialsProviderプロパティに、BasicCredentialsProviderを設定する。
BasicCredentialsProviderは、FactoryBeanを実装したBasicCredentialsProviderFactoryBeanを使用しBeanを作成する。