REST クライアント

Spring Framework は、REST エンドポイントを呼び出すために次の選択肢を提供します。

  • RestClient — 流れるような API を備えた同期クライアント

  • WebClient — 流れるような API を備えたノンブロッキング、リアクティブクライアント

  • RestTemplate — テンプレートメソッド API を備えた同期クライアント。現在は RestClient に置き換えられて非推奨となっています

  • HTTP サービスクライアント  — 生成されたプロキシによってサポートされるアノテーション付きインターフェース

RestClient

RestClient は、リクエストを実行するためのスムーズな API を提供する同期 HTTP クライアントです。HTTP ライブラリの抽象化として機能し、HTTP リクエストとレスポンスのコンテンツを高水準 Java オブジェクトとの間で変換します。

RestClient を作成する

RestClient には静的な create ショートカットメソッドがあります。また、追加のオプションを備えた builder() も公開されています。

  • 使用する HTTP ライブラリを選択します。クライアントリクエストファクトリを参照してください

  • メッセージコンバーターを構成するには、HTTP メッセージ変換を参照してください。

  • baseUrl を設定する

  • デフォルトのリクエストヘッダー、Cookie、パス変数、API バージョンを設定する

  • ApiVersionInserter を構成する

  • インターセプターを登録する

  • レジスタリクエスト初期化子

RestClient は一度作成されると、複数のスレッドで安全に使用できます。

以下に、RestClient を作成または構築する方法を示します。

  • Java

  • Kotlin

RestClient defaultClient = RestClient.create();

RestClient customClient = RestClient.builder()
	.requestFactory(new HttpComponentsClientHttpRequestFactory())
	.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
	.baseUrl("https://example.com")
	.defaultUriVariables(Map.of("variable", "foo"))
	.defaultHeader("My-Header", "Foo")
	.defaultCookie("My-Cookie", "Bar")
	.defaultVersion("1.2")
	.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
	.requestInterceptor(myCustomInterceptor)
	.requestInitializer(myCustomInitializer)
	.build();
val defaultClient = RestClient.create()

val customClient = RestClient.builder()
	.requestFactory(HttpComponentsClientHttpRequestFactory())
	.messageConverters { converters -> converters.add(MyCustomMessageConverter()) }
	.baseUrl("https://example.com")
	.defaultUriVariables(mapOf("variable" to "foo"))
	.defaultHeader("My-Header", "Foo")
	.defaultCookie("My-Cookie", "Bar")
       .defaultVersion("1.2")
       .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
	.requestInterceptor(myCustomInterceptor)
	.requestInitializer(myCustomInitializer)
	.build()

RestClient を使用する

HTTP リクエストを実行するには、まず使用する HTTP メソッドを指定します。get()head()post() などの便利なメソッド、または method(HttpMethod) を使用してください。

リクエスト URL

次に、uri メソッドを使用してリクエスト URI を指定します。これはオプションであり、ビルダーを使用して baseUrl を設定した場合はこの手順を省略できます。URL は通常、String として指定され、オプションで URI テンプレート変数を使用できます。リクエストの実行方法を以下に示します。

  • Java

  • Kotlin

int id = 42;
restClient.get()
	.uri("https://example.com/orders/{id}", id)
	// ...
val id = 42
restClient.get()
	.uri("https://example.com/orders/{id}", id)
	// ...

関数は、リクエストパラメーターの指定など、追加の制御にも使用できます。

文字列 URL はデフォルトでエンコードされますが、カスタム uriBuilderFactory を使用してクライアントを構築することでこれを変更できます。URL は関数または java.net.URI として提供することもできますが、どちらもエンコードされません。URI の操作とエンコードの詳細については、URI リンクを参照してください。

リクエストのヘッダーと本文

必要に応じて、リクエストヘッダーに header(String, String)headers(Consumer<HttpHeaders>、または便利なメソッド accept(MediaType…​)acceptCharset(Charset…​) などを追加することで、HTTP リクエストを操作できます。ボディ(POSTPUTPATCH)を含む可能性のある HTTP リクエストの場合は、追加のメソッド contentType(MediaType)contentLength(long) が利用可能です。クライアントが ApiVersionInserter で構成されている場合には、リクエストの API バージョンを設定できます。

リクエスト本文自体は、内部で HTTP メッセージ変換を使用する body(Object) によって設定できます。あるいは、ParameterizedTypeReference を使用してリクエスト本文を設定し、ジェネリクスを使用できるようにすることもできます。最後に、本体を OutputStream に書き込むコールバック関数に設定できます。

レスポンスの取得

リクエストが設定されると、retrieve() の後にメソッド呼び出しを連鎖させることでリクエストを送信できます。例: リストなどのパラメーター化された型の場合、レスポンス本体には retrieve().body(Class) または retrieve().body(ParameterizedTypeReference) を使用してアクセスできます。body メソッドは、レスポンスの内容をさまざまな型に変換します。たとえば、バイトは String に変換でき、JSON は Jackson を使用してオブジェクトに変換できます (HTTP メッセージ変換を参照)。

レスポンスは ResponseEntity に変換され、レスポンスヘッダーと本文にアクセスできるようになる。retrieve().toEntity(Class)

retrieve() を単独で呼び出しても何も実行されず、ResponseSpec が返されます。副作用が発生するには、アプリケーションは ResponseSpec でターミナル操作を呼び出す必要があります。レスポンスの消費がユースケースに関係ない場合は、retrieve().toBodilessEntity() を使用できます。

このサンプルは、RestClient を使用して単純な GET リクエストを実行する方法を示します。

  • Java

  • Kotlin

String result = restClient.get() (1)
	.uri("https://example.com") (2)
	.retrieve() (3)
	.body(String.class); (4)

System.out.println(result); (5)
1GET リクエストを設定する
2 接続先の URL を指定する
3 レスポンスを取得する
4 レスポンスを文字列に変換する
5 結果を出力する
val result= restClient.get() (1)
	.uri("https://example.com") (2)
	.retrieve() (3)
	.body<String>() (4)

println(result) (5)
1GET リクエストを設定する
2 接続先の URL を指定する
3 レスポンスを取得する
4 レスポンスを文字列に変換する
5 結果を出力する

レスポンスステータスコードとヘッダーへのアクセスは、ResponseEntity を通じて提供されます。

  • Java

  • Kotlin

ResponseEntity<String> result = restClient.get() (1)
	.uri("https://example.com") (1)
	.retrieve()
	.toEntity(String.class); (2)

System.out.println("Response status: " + result.getStatusCode()); (3)
System.out.println("Response headers: " + result.getHeaders()); (3)
System.out.println("Contents: " + result.getBody()); (3)
1 指定された URL に対する GET リクエストを設定します
2 レスポンスを ResponseEntity に変換します
3 結果を出力する
val result = restClient.get() (1)
	.uri("https://example.com") (1)
	.retrieve()
	.toEntity<String>() (2)

println("Response status: " + result.statusCode) (3)
println("Response headers: " + result.headers) (3)
println("Contents: " + result.body) (3)
1 指定された URL に対する GET リクエストを設定します
2 レスポンスを ResponseEntity に変換します
3 結果を出力する

RestClient は、Jackson ライブラリを使用して JSON をオブジェクトに変換できます。このサンプルでは URI 変数が使用されており、Accept ヘッダーが JSON に設定されていることに注意してください。

  • Java

  • Kotlin

int id = ...;
Pet pet = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id) (1)
	.accept(APPLICATION_JSON) (2)
	.retrieve()
	.body(Pet.class); (3)
1URI 変数の使用
2Accept ヘッダーを application/json に設定します
3JSON レスポンスを Pet ドメインオブジェクトに変換します
val id = ...
val pet = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id) (1)
	.accept(APPLICATION_JSON) (2)
	.retrieve()
	.body<Pet>() (3)
1URI 変数の使用
2Accept ヘッダーを application/json に設定します
3JSON レスポンスを Pet ドメインオブジェクトに変換します

次のサンプルでは、RestClient を使用して JSON を含む POST リクエストを実行します。JSON は再び Jackson を使用して変換されます。

  • Java

  • Kotlin

Pet pet = ... (1)
ResponseEntity<Void> response = restClient.post() (2)
	.uri("https://petclinic.example.com/pets/new") (2)
	.contentType(APPLICATION_JSON) (3)
	.body(pet) (4)
	.retrieve()
	.toBodilessEntity(); (5)
1Pet ドメインオブジェクトを作成する
2POST リクエストと接続先の URL を設定します
3Content-Type ヘッダーを application/json に設定します
4 リクエストボディとして pet を使用します
5 レスポンスを本文のないレスポンスエンティティに変換します。
val pet: Pet = ... (1)
val response = restClient.post() (2)
	.uri("https://petclinic.example.com/pets/new") (2)
	.contentType(APPLICATION_JSON) (3)
	.body(pet) (4)
	.retrieve()
	.toBodilessEntity() (5)
1Pet ドメインオブジェクトを作成する
2POST リクエストと接続先の URL を設定します
3Content-Type ヘッダーを application/json に設定します
4 リクエストボディとして pet を使用します
5 レスポンスを本文のないレスポンスエンティティに変換します。

エラー処理

デフォルトでは、RestClient は 4xx または 5xx ステータスコードを含むレスポンスを取得するときに RestClientException のサブクラスをスローします。この動作は onStatus を使用してオーバーライドできます。

  • Java

  • Kotlin

String result = restClient.get() (1)
	.uri("https://example.com/this-url-does-not-exist") (1)
	.retrieve()
	.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { (2)
		throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); (3)
	})
	.body(String.class);
1404 ステータスコードを返す URL の GET リクエストを作成する
2 すべての 4xx ステータスコードのステータスハンドラーをセットアップする
3 カスタム例外をスローする
val result = restClient.get() (1)
	.uri("https://example.com/this-url-does-not-exist") (1)
	.retrieve()
	.onStatus(HttpStatusCode::is4xxClientError) { _, response -> (2)
		throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } (3)
	.body<String>()
1404 ステータスコードを返す URL の GET リクエストを作成する
2 すべての 4xx ステータスコードのステータスハンドラーをセットアップする
3 カスタム例外をスローする

交換

より高度なシナリオの場合、RestClient は、retrieve() の代わりに使用できる exchange() メソッドを介して、基礎となる HTTP リクエストおよびレスポンスへのアクセスを提供します。exchange() を使用する場合、ステータスハンドラーは適用されません。これは、交換関数によって完全なレスポンスへのアクセスがすでに提供されており、必要なエラー処理を実行できるためです。

  • Java

  • Kotlin

Pet result = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id)
	.accept(APPLICATION_JSON)
	.exchange((request, response) -> { (1)
		if (response.getStatusCode().is4xxClientError()) { (2)
			throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); (2)
		}
		else {
			Pet pet = convertResponse(response); (3)
			return pet;
		}
	});
1exchange はリクエストとレスポンスを提供します
2 レスポンスに 4xx ステータスコードがある場合に例外をスローする
3 レスポンスを Pet ドメインオブジェクトに変換する
val result = restClient.get()
	.uri("https://petclinic.example.com/pets/{id}", id)
	.accept(MediaType.APPLICATION_JSON)
	.exchange { request, response -> (1)
		if (response.getStatusCode().is4xxClientError()) { (2)
			throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) (2)
		} else {
			val pet: Pet = convertResponse(response) (3)
			pet
		}
	}
1exchange はリクエストとレスポンスを提供します
2 レスポンスに 4xx ステータスコードがある場合に例外をスローする
3 レスポンスを Pet ドメインオブジェクトに変換する

HTTP メッセージ変換

Jackson JSON ビュー

オブジェクトプロパティのサブセットのみを直列化するには、次の例に示すように、Jackson JSON ビュー (英語) を指定できます。

MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);

ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
	.contentType(APPLICATION_JSON)
	.body(value)
	.retrieve()
	.toBodilessEntity();

マルチパート

マルチパートデータを送信するには、値がパーツコンテンツ用の Object、ファイルパーツ用の Resource、ヘッダー付きパーツコンテンツ用の HttpEntity である MultiValueMap<String, Object> を提供する必要があります。例:

MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();

parts.add("fieldPart", "fieldValue");
parts.add("filePart", new FileSystemResource("...logo.png"));
parts.add("jsonPart", new Person("Jason"));

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(myBean, headers));

// send using RestClient.post or RestTemplate.postForEntity

ほとんどの場合、パーツごとに Content-Type を指定する必要はありません。コンテンツ型は、直列化するために選択された HttpMessageConverter に基づいて、または Resource の場合はファイル拡張子に基づいて自動的に決定されます。必要に応じて、MediaType に HttpEntity ラッパーを明示的に提供できます。

MultiValueMap の準備ができたら、RestClient.post().body(parts) (または RestTemplate.postForObject) を使用して、それを POST リクエストの本文として使用できます。

MultiValueMap に少なくとも 1 つの非 String 値が含まれている場合、Content-Type は FormHttpMessageConverter によって multipart/form-data に設定されます。MultiValueMap に String 値がある場合、Content-Type はデフォルトで application/x-www-form-urlencoded になります。必要に応じて、Content-Type を明示的に設定することもできます。

クライアントリクエストファクトリ

HTTP リクエストを実行するために、RestClient はクライアント HTTP ライブラリを使用します。これらのライブラリは、ClientRequestFactory インターフェースを介して適応されます。さまざまな実装が利用可能です。

  • Java の HttpClientJdkClientHttpRequestFactory 

  • HttpComponentsClientHttpRequestFactory は Apache HTTP コンポーネント HttpClient と一緒に使用します

  • Jetty の HttpClientJettyClientHttpRequestFactory 

  • ReactorNettyClientRequestFactory Reactor 用 Netty の HttpClient

  • 単純なデフォルトとしての SimpleClientHttpRequestFactory 

RestClient がビルドされたときにリクエストファクトリが指定されていない場合、クラスパスで使用可能な場合は Apache または Jetty HttpClient が使用されます。それ以外の場合、java.net.http モジュールがロードされると、Java の HttpClient が使用されます。最終的には、単純なデフォルトが使用されます。

SimpleClientHttpRequestFactory は、エラーを表すレスポンスのステータス (たとえば、401) にアクセスするときに例外を発生させる可能性があることに注意してください。これが課題になる場合は、代替のリクエストファクトリのいずれかを使用してください。

WebClient

WebClient は、HTTP リクエストを実行するためのノンブロッキングのリアクティブクライアントです。これは 5.0 で導入され、同期、非同期、ストリーミングのシナリオをサポートする RestTemplate の代替手段を提供します。

WebClient は以下をサポートします。

  • ノンブロッキング I/O

  • Reactive Streams バックプレッシャー

  • 少ないハードウェアリソースで高い同時実行性を実現

  • Java 8 ラムダを利用する関数型スタイルの流れるような API

  • 同期および非同期の相互作用

  • サーバーへのストリーミングまたはサーバーからのストリーミングダウン

詳細については、WebClient を参照してください。

RestTemplate

RestTemplate は、HTTP クライアントライブラリを介した高レベルの API を、従来の Spring テンプレートクラスの形式で提供します。これは、次のオーバーロードされたメソッドのグループを公開します。

Spring Framework 7.0 以降、RestTemplate は非推奨となり、RestClient が採用されます。将来のバージョンでは削除される予定です。「RestClient への移行」ガイドをご利用ください。非同期およびストリーミングのシナリオでは、リアクティブ型の WebClient をご検討ください。
表 1: RestTemplate メソッド
メソッドグループ 説明

getForObject

GET を介して表現を取得します。

getForEntity

GET を使用して ResponseEntity (つまり、ステータス、ヘッダー、本文)を取得します。

headForHeaders

HEAD を使用して、リソースのすべてのヘッダーを取得します。

postForLocation

POST を使用して新しいリソースを作成し、レスポンスから Location ヘッダーを返します。

postForObject

POST を使用して新しいリソースを作成し、レスポンスから表現を返します。

postForEntity

POST を使用して新しいリソースを作成し、レスポンスから表現を返します。

put

PUT を使用してリソースを作成または更新します。

patchForObject

PATCH を使用してリソースを更新し、レスポンスから表現を返します。JDK HttpURLConnection は PATCH をサポートしていませんが、Apache HttpComponents などはサポートしていることに注意してください。

delete

DELETE を使用して、指定された URI のリソースを削除します。

optionsForAllow

ALLOW を使用して、リソースに許可された HTTP メソッドを取得します。

exchange

必要に応じて柔軟性を高める、前述のメソッドのより一般化された(そしてあまり独自ではない)バージョン。RequestEntity (HTTP メソッド、URL、ヘッダー、本文を入力として含む)を受け入れ、ResponseEntity を返します。

これらのメソッドでは、Class の代わりに ParameterizedTypeReference を使用して、ジェネリクスでレスポンス型を指定できます。

execute

コールバックインターフェースを介したリクエストの準備とレスポンスの抽出を完全に制御して、リクエストを実行する最も一般的な方法。

初期化

RestTemplate は、RestClient と同じ HTTP ライブラリ抽象化を使用します。デフォルトでは SimpleClientHttpRequestFactory が使用されますが、これはコンストラクターを介して変更できます。クライアントリクエストファクトリを参照してください。

RestTemplate は、メトリクスとトレースを生成するために、可観測性のためにインストルメント化できます。RestTemplate 可観測性のサポートセクションを参照してください。

本文

RestTemplate メソッドに渡されるオブジェクトと RestTemplate メソッドから返されるオブジェクトは、HttpMessageConverter を使用して HTTP メッセージに変換されます。HTTP メッセージ変換を参照してください。

RestClient への移行

アプリケーションは段階的に RestClient を導入できます。まず API の利用に重点を置き、次にインフラストラクチャのセットアップに着手します。以下の手順を検討してください。

  1. 既存の RestTemplate インスタンスから、RestClient restClient = RestClient.create(restTemplate) のように 1 つ以上の RestClient を作成します。まずはリクエストの発行に重点を置き、アプリケーション内のコンポーネントごとに RestTemplate の使用を段階的に置き換えます。対応する API については、以下の表を参照してください。

  2. すべてのクライアントリクエストが RestClient インスタンスを経由したら、RestClient.Builder を使用して既存の RestTemplate インスタンスの作成を複製できます。RestTemplate と RestClient は同じインフラストラクチャを共有しているため、カスタム ClientHttpRequestFactory または ClientHttpRequestInterceptor をセットアップで再利用できます。RestClient  ビルダー API を参照してください。

クラスパス上に他のライブラリがない場合、RestClient は最新の JDK である HttpClient を搭載した JdkClientHttpRequestFactory を選択しますが、RestTemplate は HttpURLConnection を使用する SimpleClientHttpRequestFactory を選択します。これにより、HTTP レベルでの実行時における微妙な動作の違いが説明できます。

次の表は、RestTemplate メソッドの RestClient 相当を示しています。

表 2: RestTemplate のメソッドと RestClient の対応
RestTemplate メソッド RestClient 同等

getForObject(String, Class, Object…​)

get() .uri(String, Object…​) .retrieve() .body(Class)

getForObject(String, Class, Map)

get() .uri(String, Map) .retrieve() .body(Class)

getForObject(URI, Class)

get() .uri(URI) .retrieve() .body(Class)

getForEntity(String, Class, Object…​)

get() .uri(String, Object…​) .retrieve() .toEntity(Class)

getForEntity(String, Class, Map)

get() .uri(String, Map) .retrieve() .toEntity(Class)

getForEntity(URI, Class)

get() .uri(URI) .retrieve() .toEntity(Class)

headForHeaders(String, Object…​)

head() .uri(String, Object…​) .retrieve() .toBodilessEntity() .getHeaders()

headForHeaders(String, Map)

head() .uri(String, Map) .retrieve() .toBodilessEntity() .getHeaders()

headForHeaders(URI)

head() .uri(URI) .retrieve() .toBodilessEntity() .getHeaders()

postForLocation(String, Object, Object…​)

post() .uri(String, Object…​) .body(Object).retrieve() .toBodilessEntity() .getLocation()

postForLocation(String, Object, Map)

post() .uri(String, Map) .body(Object) .retrieve() .toBodilessEntity() .getLocation()

postForLocation(URI, Object)

post() .uri(URI) .body(Object) .retrieve() .toBodilessEntity() .getLocation()

postForObject(String, Object, Class, Object…​)

post() .uri(String, Object…​) .body(Object) .retrieve() .body(Class)

postForObject(String, Object, Class, Map)

post() .uri(String, Map) .body(Object) .retrieve() .body(Class)

postForObject(URI, Object, Class)

post() .uri(URI) .body(Object) .retrieve() .body(Class)

postForEntity(String, Object, Class, Object…​)

post() .uri(String, Object…​) .body(Object) .retrieve() .toEntity(Class)

postForEntity(String, Object, Class, Map)

post() .uri(String, Map) .body(Object) .retrieve() .toEntity(Class)

postForEntity(URI, Object, Class)

post() .uri(URI) .body(Object) .retrieve() .toEntity(Class)

put(String, Object, Object…​)

put() .uri(String, Object…​) .body(Object) .retrieve() .toBodilessEntity()

put(String, Object, Map)

put() .uri(String, Map) .body(Object) .retrieve() .toBodilessEntity()

put(URI, Object)

put() .uri(URI) .body(Object) .retrieve() .toBodilessEntity()

patchForObject(String, Object, Class, Object…​)

patch() .uri(String, Object…​) .body(Object) .retrieve() .body(Class)

patchForObject(String, Object, Class, Map)

patch() .uri(String, Map) .body(Object) .retrieve() .body(Class)

patchForObject(URI, Object, Class)

patch() .uri(URI) .body(Object) .retrieve() .body(Class)

delete(String, Object…​)

delete() .uri(String, Object…​) .retrieve() .toBodilessEntity()

delete(String, Map)

delete() .uri(String, Map) .retrieve() .toBodilessEntity()

delete(URI)

delete() .uri(URI) .retrieve() .toBodilessEntity()

optionsForAllow(String, Object…​)

options() .uri(String, Object…​) .retrieve() .toBodilessEntity() .getAllow()

optionsForAllow(String, Map)

options() .uri(String, Map) .retrieve() .toBodilessEntity() .getAllow()

optionsForAllow(URI)

options() .uri(URI) .retrieve() .toBodilessEntity() .getAllow()

exchange(String, HttpMethod, HttpEntity, Class, Object…​)

method(HttpMethod) .uri(String, Object…​) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1]

exchange(String, HttpMethod, HttpEntity, Class, Map)

method(HttpMethod) .uri(String, Map) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1]

exchange(URI, HttpMethod, HttpEntity, Class)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [1]

exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Object…​)

method(HttpMethod) .uri(String, Object…​) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1]

exchange(String, HttpMethod, HttpEntity, ParameterizedTypeReference, Map)

method(HttpMethod) .uri(String, Map) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1]

exchange(URI, HttpMethod, HttpEntity, ParameterizedTypeReference)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [1]

exchange(RequestEntity, Class)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(Class) [2]

exchange(RequestEntity, ParameterizedTypeReference)

method(HttpMethod) .uri(URI) .headers(Consumer<HttpHeaders>) .body(Object) .retrieve() .toEntity(ParameterizedTypeReference) [2]

execute(String, HttpMethod, RequestCallback, ResponseExtractor, Object…​)

method(HttpMethod) .uri(String, Object…​) .exchange(ExchangeFunction)

execute(String, HttpMethod, RequestCallback, ResponseExtractor, Map)

method(HttpMethod) .uri(String, Map) .exchange(ExchangeFunction)

execute(URI, HttpMethod, RequestCallback, ResponseExtractor)

method(HttpMethod) .uri(URI) .exchange(ExchangeFunction)

RestClient インスタンスと RestTemplate インスタンスは、例外のスローに関して同じ動作を共有します(RestClientException 型が階層の最上位にあります)。RestTemplate が "4xx" レスポンスステータスに対して一貫して HttpClientErrorException をスローするのに対し、RestClient はカスタム「ステータスハンドラー」によってより柔軟に対応します。

HTTP サービスクライアント

@HttpExchange メソッドを持つ Java インターフェースとして HTTP サービスを定義し、HttpServiceProxyFactory を使用してクライアントプロキシを作成し、RestClientWebClientRestTemplate を介して HTTP 経由でリモートにアクセスできるようにします。サーバー側では、@Controller クラスで同じインターフェースを実装し、@HttpExchange コントローラーメソッドによるリクエストを処理できます。

まず、Java インターフェースを作成します。

public interface RepositoryService {

	@GetExchange("/repos/{owner}/{repo}")
	Repository getRepository(@PathVariable String owner, @PathVariable String repo);

	// more HTTP exchange methods...

}

オプションで、型レベルで @HttpExchange を使用して、すべてのメソッドに共通の属性を宣言します。

@HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json")
public interface RepositoryService {

	@GetExchange
	Repository getRepository(@PathVariable String owner, @PathVariable String repo);

	@PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
	void updateRepository(@PathVariable String owner, @PathVariable String repo,
			@RequestParam String name, @RequestParam String description, @RequestParam String homepage);

}

次に、クライアントを構成して HttpServiceProxyFactory を作成します。

// Using RestClient...

RestClient restClient = RestClient.create("...");
RestClientAdapter adapter = RestClientAdapter.create(restClient);

// or WebClient...

WebClient webClient = WebClient.create("...");
WebClientAdapter adapter = WebClientAdapter.create(webClient);

// or RestTemplate...

RestTemplate restTemplate = new RestTemplate();
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);

HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

これで、クライアントプロキシを作成する準備が整いました。

RepositoryService service = factory.createClient(RepositoryService.class);
// Use service methods for remote calls...

HTTP サービスクライアントは、HTTP 経由でリモートにアクセスするための強力で表現力豊かな選択肢です。これにより、REST API の仕組み、クライアントアプリケーションに関連する部分、作成すべき入出力型、必要なエンドポイントメソッドシグネチャー、必要な Javadoc などに関する知識を 1 つのチームで共有できます。作成された Java API はガイドとして機能し、すぐに使用できます。

メソッドパラメーター

@HttpExchange メソッドは、次の入力による柔軟なメソッドシグネチャーをサポートします。

メソッドパラメーター 説明

URI

アノテーションの url 属性をオーバーライドして、リクエストの URL を動的に設定します。

UriBuilderFactory

URI テンプレートと URI 変数を展開するための UriBuilderFactory を提供します。実際には、基礎となるクライアントの UriBuilderFactory (およびそのベース URL) を置き換えます。

HttpMethod

アノテーションの method 属性をオーバーライドして、リクエストの HTTP メソッドを動的に設定します

@RequestHeader

リクエストヘッダーまたは複数のヘッダーを追加します。引数は単一の値、Collection<?> または Map<String, ?>MultiValueMap<String, ?> のいずれかです。文字列以外の値では型変換がサポートされています。ヘッダー値が追加され、すでに追加されているヘッダー値は上書きされません。

@PathVariable

リクエスト URL にプレースホルダーを展開するための変数を追加します。引数は、複数の変数を持つ Map<String, ?>、または個別の値の場合があります。文字列以外の値では型変換がサポートされています。

@RequestAttribute

リクエスト属性として追加する Object を指定します。RestClient と WebClient でのみサポートされます。

@RequestBody

リクエストの本文を、直列化するオブジェクトとして、または Reactive Streams Publisher (MonoFlux など)、構成済みの ReactiveAdapterRegistry でサポートされるその他の非同期型として提供します。

@RequestParam

リクエストパラメーターまたは複数のパラメーターを追加します。引数は、複数のパラメーターを持つ Map<String, ?> または MultiValueMap<String, ?>、値の Collection<?>、または個々の値の場合があります。文字列以外の値の型変換がサポートされています。

"content-type" が "application/x-www-form-urlencoded" に設定されている場合、リクエストパラメーターはリクエスト本文にエンコードされます。それ以外の場合は、URL クエリパラメーターとして追加されます。

@RequestPart

リクエスト部分を追加します。リクエスト部分は、文字列 (フォームフィールド)、Resource (ファイル部分)、オブジェクト (JSON などの形式でエンコードされるエンティティ)、HttpEntity (部分コンテンツとヘッダー)、上記のいずれかの Spring Part、または Reactive Streams Publisher になります。

MultipartFile

MultipartFile からのリクエストパーツを追加します。これは通常、アップロードされたファイルを表す Spring MVC コントローラーで使用されます。

@CookieValue

1 つまたは複数の Cookie を追加します。引数は、複数の Cookie を持つ Map<String, ?> または MultiValueMap<String, ?>、値の Collection<?>、または個々の値です。文字列以外の値の型変換がサポートされています。

required 属性 (パラメーターアノテーションで使用可能な場合) が false に設定されていない限り、またはパラメーターが MethodParameter#isOptional (Javadoc) によって決定されたオプションとしてマークされていない限り、メソッドパラメーターを null にすることはできません。

RestClientAdapter は、OutputStream に書き込むことによってリクエスト本体を送信できるようにする、StreamingHttpOutputMessage.Body 型のメソッドパラメーターの追加サポートを提供します。

カスタム引数

カスタム HttpServiceArgumentResolver を設定できます。以下のインターフェース例では、カスタム Search メソッドパラメーター型を使用しています。

  • Java

  • Kotlin

public interface RepositoryService {

	@GetExchange("/repos/search")
	List<Repository> searchRepository(Search search);

}
interface RepositoryService {

	@GetExchange("/repos/search")
	fun searchRepository(search: Search): List<Repository>

}

カスタム引数リゾルバーは次のように実装できます。

  • Java

  • Kotlin

static class SearchQueryArgumentResolver implements HttpServiceArgumentResolver {
	@Override
	public boolean resolve(Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
		if (parameter.getParameterType().equals(Search.class)) {
			Search search = (Search) argument;
			requestValues.addRequestParameter("owner", search.owner());
			requestValues.addRequestParameter("language", search.language());
			requestValues.addRequestParameter("query", search.query());
			return true;
		}
		return false;
	}
}
class SearchQueryArgumentResolver : HttpServiceArgumentResolver {
	override fun resolve(
		argument: Any?,
		parameter: MethodParameter,
		requestValues: HttpRequestValues.Builder
	): Boolean {
		if (parameter.getParameterType() == Search::class.java) {
			val search = argument as Search
			requestValues.addRequestParameter("owner", search.owner)
				.addRequestParameter("language", search.language)
				.addRequestParameter("query", search.query)
			return true
		}
		return false
	}
}

カスタム引数リゾルバーを構成するには:

  • Java

  • Kotlin

RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory
		.builderFor(adapter)
		.customArgumentResolver(new SearchQueryArgumentResolver())
		.build();
RepositoryService repositoryService = factory.createClient(RepositoryService.class);

Search search = Search.create()
		.owner("spring-projects")
		.language("java")
		.query("rest")
		.build();
List<Repository> repositories = repositoryService.searchRepository(search);
val restClient = RestClient.builder().baseUrl("https://api.github.com/").build()
val adapter = RestClientAdapter.create(restClient)
val factory = HttpServiceProxyFactory
	.builderFor(adapter)
	.customArgumentResolver(SearchQueryArgumentResolver())
	.build()
val repositoryService = factory.createClient<RepositoryService>(RepositoryService::class.java)

val search = Search(owner = "spring-projects", language = "java", query = "rest")
val repositories = repositoryService.searchRepository(search)
デフォルトでは、RequestEntity はメソッドパラメーターとしてサポートされていませんが、代わりにリクエストの個々の部分に対してよりきめ細かいメソッドパラメーターの使用が推奨されています。

戻り値

サポートされる戻り値は、基礎となるクライアントによって異なります。

RestClient や RestTemplate などの HttpExchangeAdapter に適応したクライアントは、同期戻り値をサポートします。

メソッドの戻り値 説明

void

与えられたリクエストを実行します。

HttpHeaders

指定されたリクエストを実行し、レスポンスヘッダーを返します。

<T>

指定されたリクエストを実行し、レスポンスの内容を宣言された戻り値の型にデコードします。

ResponseEntity<Void>

指定されたリクエストを実行し、ステータスとヘッダーを含む ResponseEntity を返します。

ResponseEntity<T>

指定されたリクエストを実行し、レスポンスコンテンツを宣言された戻り値の型にデコードし、ステータス、ヘッダー、デコードされた本文を含む ResponseEntity を返します。

WebClient などの ReactorHttpExchangeAdapter に適応したクライアントは、上記のすべてとリアクティブなバリアントをサポートします。以下の表は Reactor 型を示していますが、ReactiveAdapterRegistry でサポートされている他のリアクティブ型も使用できます。

メソッドの戻り値 説明

Mono<Void>

指定されたリクエストを実行し、レスポンスコンテンツがある場合はそれを解放します。

Mono<HttpHeaders>

指定されたリクエストを実行し、レスポンスコンテンツがある場合はそれを解放して、レスポンスヘッダーを返します。

Mono<T>

指定されたリクエストを実行し、レスポンスの内容を宣言された戻り値の型にデコードします。

Flux<T>

指定されたリクエストを実行し、レスポンスコンテンツを宣言された要素型のストリームにデコードします。

Mono<ResponseEntity<Void>>

指定されたリクエストを実行し、レスポンスコンテンツがある場合はそれを解放して、ステータスとヘッダーを含む ResponseEntity を返します。

Mono<ResponseEntity<T>>

指定されたリクエストを実行し、レスポンスコンテンツを宣言された戻り値の型にデコードし、ステータス、ヘッダー、デコードされた本文を含む ResponseEntity を返します。

Mono<ResponseEntity<Flux<T>>

指定されたリクエストを実行し、レスポンスコンテンツを宣言された要素型のストリームにデコードし、ステータス、ヘッダー、デコードされたレスポンス本文ストリームを含む ResponseEntity を返します。

デフォルトでは、ReactorHttpExchangeAdapter による同期戻り値のタイムアウトは、基礎となる HTTP クライアントの構成方法によって異なります。アダプターレベルで blockTimeout 値を設定することもできますが、下位レベルで動作し、より詳細な制御を提供する、基盤となる HTTP クライアントのタイムアウト設定に依存することをお勧めします。

RestClientAdapter は、生のレスポンス本体コンテンツへのアクセスを提供する InputStream または ResponseEntity<InputStream> 型の戻り値の追加サポートを提供します。

エラー処理

HTTP サービスクライアントプロキシのエラー処理をカスタマイズするには、必要に応じて基盤となるクライアントを設定できます。デフォルトでは、クライアントは 4xx および 5xx の HTTP ステータスコードに対して例外を発生させます。これをカスタマイズするには、クライアントを介して実行されるすべてのレスポンスに適用されるレスポンスステータスハンドラーを以下のように登録します。

// For RestClient
RestClient restClient = RestClient.builder()
		.defaultStatusHandler(HttpStatusCode::isError, (request, response) -> ...)
		.build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);

// or for WebClient...
WebClient webClient = WebClient.builder()
		.defaultStatusHandler(HttpStatusCode::isError, resp -> ...)
		.build();
WebClientAdapter adapter = WebClientAdapter.create(webClient);

// or for RestTemplate...
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(myErrorHandler);

RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);

HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

詳細およびエラーステータスコードの抑制などのオプションについては、各クライアントのリファレンスドキュメント、および RestClient.Builder または WebClient.Builder の defaultStatusHandler の Javadoc、および RestTemplate の setErrorHandler を参照してください。

アダプターの装飾

HttpExchangeAdapter と ReactorHttpExchangeAdapter は、HTTP インターフェースクライアントインフラストラクチャと、基盤となるクライアントの呼び出しの詳細を分離する契約です。RestClientWebClientRestTemplate にはアダプター実装があります。

場合によっては、HttpServiceProxyFactory.Builder で設定可能なデコレータを介してクライアント呼び出しをインターセプトすると便利なことがあります。例: 組み込みのデコレータを適用して 404 例外を抑制し、NOT_FOUND と null 本体を含む ResponseEntity を返すことができます。

// For RestClient
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(restCqlientAdapter)
		.exchangeAdapterDecorator(NotFoundRestClientAdapterDecorator::new)
		.build();

// or for WebClient...
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builderFor(webClientAdapter)
		.exchangeAdapterDecorator(NotFoundWebClientAdapterDecorator::new)
		.build();

HTTP サービスグループ

HttpServiceProxyFactory を使ってクライアントプロキシを作成するのは簡単ですが、Bean として宣言すると設定の繰り返しが発生します。また、ターゲットホストが複数ある場合、複数のクライアントを設定する必要があり、さらに多くのクライアントプロキシ Bean を作成する必要が生じる可能性があります。

大規模なインターフェースクライアントの運用を容易にするため、Spring Framework は専用の構成サポートを提供します。これにより、アプリケーションは HTTP サービスをグループごとに識別し、各グループに合わせてクライアントをカスタマイズすることに集中できます。一方、フレームワークはクライアントプロキシのレジストリを透過的に作成し、各プロキシを Bean として宣言します。

HTTP サービスグループとは、プロキシを作成するために同じクライアント設定と HttpServiceProxyFactory インスタンスを共有するインターフェースのセットです。通常はホストごとに 1 つのグループですが、基盤となるクライアントの設定が異なる場合に備えて、同じターゲットホストに複数のグループを作成することもできます。

HTTP サービスグループを宣言する 1 つの方法は、次に示すように、@Configuration クラスの @ImportHttpServices アノテーションを使用することです。

@Configuration
@ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) (1)
@ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) (2)
public class ClientConfig {
}
1 グループ "echo" のインターフェースを手動でリストします
2 ベースパッケージのグループ "greeting" のインターフェースを検出する

HTTP サービスレジストラを作成してインポートすることで、プログラムでグループを宣言することもできます。

public class MyHttpServiceRegistrar extends AbstractHttpServiceRegistrar { (1)

	@Override
	protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
		registry.forGroup("echo").register(EchoServiceA.class, EchoServiceB.class); (2)
		registry.forGroup("greeting").detectInBasePackages(GreetServiceA.class); (3)
	}
}

@Configuration
@Import(MyHttpServiceRegistrar.class) (4)
public class ClientConfig {
}
1AbstractHttpServiceRegistrar の拡張クラスを作成する
2 グループ "echo" のインターフェースを手動でリストします
3 ベースパッケージのグループ "greeting" のインターフェースを検出する
4 レジストラをインポートする
@ImportHttpService アノテーションとプログラムレジストラを自由に組み合わせることができ、インポートを複数の構成クラスに分散させることもできます。すべてのインポートは、共通の HttpServiceProxyRegistry インスタンスに連携して貢献します。

HTTP サービスグループを宣言したら、HttpServiceGroupConfigurer Bean を追加して、各グループのクライアントをカスタマイズします。例:

@Configuration
@ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class})
@ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class)
public class ClientConfig {

	@Bean
	public RestClientHttpServiceGroupConfigurer groupConfigurer() {
		return groups -> {
			// configure client for group "echo"
			groups.filterByName("echo").forEachClient((group, clientBuilder) -> ...);

			// configure the clients for all groups
			groups.forEachClient((group, clientBuilder) -> ...);

			// configure client and proxy factory for each group
			groups.forEachGroup((group, clientBuilder, factoryBuilder) -> ...);
		};
	}
}
Spring Boot は、HttpServiceGroupConfigurer を使用して HTTP サービスグループによるクライアントプロパティのサポートを追加し、Spring Security を使用して OAuth サポートを追加し、Spring Cloud を使用して負荷分散を追加します。

上記の結果、各クライアントプロキシは、型別に便利にオートワイヤーできる Bean として利用できるようになります。

@RestController
public class EchoController {

	private final EchoService echoService;

	public EchoController(EchoService echoService) {
		this.echoService = echoService;
	}

	// ...
}

ただし、同じ型のクライアントプロキシが複数存在する場合(たとえば、複数のグループに同じインターフェースがある場合)、その型の一意の Bean は存在せず、型のみでオートワイヤーすることはできません。このような場合は、すべてのプロキシを保持する HttpServiceProxyRegistry を直接操作し、グループごとに必要なプロキシを取得できます。

@RestController
public class EchoController {

	private final EchoService echoService1;

	private final EchoService echoService2;

	public EchoController(HttpServiceProxyRegistry registry) {
		this.echoService1 = registry.getClient("echo1", EchoService.class); (1)
		this.echoService2 = registry.getClient("echo2", EchoService.class); (2)
	}

	// ...
}
1 グループ "echo1" の EchoService クライアントプロキシにアクセスする
2 グループ "echo2" の EchoService クライアントプロキシにアクセスする

1HttpEntity ヘッダーと本体は、headers(Consumer<HttpHeaders>) および body(Object) 経由で RestClient に提供される必要があります。
2RequestEntity メソッド、URI、ヘッダー、本文は、method(HttpMethod)uri(URI)headers(Consumer<HttpHeaders>)body(Object) 経由で RestClient に提供する必要があります。