Spring Framework 6.1 から追加された RestClient を試してみる

この記事は[NTTコムウェア Advent Calendar 2023](https://qiita.com/advent-calendar/2023/nttcomware) 13日目の記事です。

# はじめに
こんにちは、NTTコムウェアの田村です。
普段は[Macchinetta Framework](https://macchinetta.github.io/)、Springプロジェクトに関する社内からの問合せ対応や技術検証を行っています。
今回は Spring Framework 6.1 から新しく登場したRESTクライアントである RestClient について[公式ドキュメント](https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-restclient)を参照しつつ試してみました。
Spring Boot では 3.2 からRestClientをサポートしています。

:::note info
本記事では Spring Framework 6.1.1 をもとに説明しています。
6.1.1 では [RestClient による API 応答結果が no response body の場合、null ではなくエラーが返却される](https://github.com/spring-projects/spring-framework/issues/31719)ことが報告されています（ 6.1.2 で修正される予定）。
:::

# RestClient とは
REST API を呼び出すためのクライアントです。
Spring Framework では同期クライアントとして RestTemplate がありましたが、その後継として RestClient の利用が推奨されています。
※RestTemplate は Spring Framework 5.x からメンテナンスモードでした。
非同期およびストリーミングの場合はリアクティブクライアントである WebClient が推奨されています。

# RestClient の作成
RestClient を用意する単純な方法は create メソッドを実行することです。
```java
RestClient defaultClient = RestClient.create();
```
これだけだと何をしているか分からないためソースを見ていきます。
```java:RestClient.class
public interface RestClient {
	// 略
	/**
	 * Create a new {@code RestClient}.
	 * @see #create(String)
	 * @see #builder()
	 */
	static RestClient create() {
		return new DefaultRestClientBuilder().build();
	}
	// 略
 }
```
RestClientはインターフェースであり、create メソッドでDefaultRestClientBuilderのbuildメソッドを実行しています。
```java:DefaultRestClientBuilder.class
final class DefaultRestClientBuilder implements RestClient.Builder {
	// 略
	@Override
	public RestClient build() {
		ClientHttpRequestFactory requestFactory = initRequestFactory();
		UriBuilderFactory uriBuilderFactory = initUriBuilderFactory();
		HttpHeaders defaultHeaders = copyDefaultHeaders();
		List<HttpMessageConverter<?>> messageConverters = (this.messageConverters != null ?
				this.messageConverters : initMessageConverters());
		return new DefaultRestClient(requestFactory,
				this.interceptors, this.initializers, uriBuilderFactory,
				defaultHeaders,
				this.statusHandlers,
				messageConverters,
				this.observationRegistry,
				this.observationConvention,
				new DefaultRestClientBuilder(this)
				);
	}
 	// 略
}
```
DefaultRestClientBuilder はRestClient.Builder インターフェースの実装クラスです。
build メソッド内ではクライアントリクエストファクトリやヘッダー情報など RestClient に必要な情報を設定して、DefaultRestClient のインスタンスを生成しています。
デフォルトで設定される[クライアントリクエストファクトリ](#クライアントリクエストファクトリ)と[メッセージコンバーター](#http-メッセージ変換)については後述します。

これらの構成をカスタマイズしたい場合は下記公式ページのサンプルのように一度 builder メソッドで DefaultRestClientBuilder を呼び出し、変更を行ってから build メソッドを実行します。
```java
RestClient customClient = RestClient.builder()
  .requestFactory(new HttpComponentsClientHttpRequestFactory())
  .messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
  .baseUrl("https://example.com")
  .defaultUriVariables(Map.of("variable", "foo"))
  .defaultHeader("My-Header", "Foo")
  .requestInterceptor(myCustomInterceptor)
  .requestInitializer(myCustomInitializer)
  .build();
```


# RestClient を使用する
リクエストを実行する手順は主に以下になります。
- リクエストのメソッドを設定する
- 接続先の URL を指定する
- ヘッダー情報を設定する
- レスポンスを取得する
- レスポンスを型変換する

[RestClient の作成](#restclient-の作成)で接続先の URL やヘッダー情報を設定している場合、それらがデフォルト値として付与されます。
使用例を以下に示しますが、メソッドチェーンで簡単に使用することが可能です（簡単なのでほぼ公式サンプル通りです）。

## GET
String 型でbodyを受け取りたい場合
```java
String result = restClient.get() 
  .uri("https://example.com") 
  .retrieve() 
  .body(String.class); 
```
レスポンスエンティティを受け取りたい場合
```java
ResponseEntity<String> result = restClient.get() 
  .uri("https://example.com") 
  .retrieve()
  .toEntity(String.class); 
```
List 型で body を受け取りたい場合
```java
List<DemoModel> demoModels = restClient.get()
  .uri("https://example.com")
  .retrieve()
  .body(new ParameterizedTypeReference<>() {});
```
## POST
toBodilessEntity メソッドを利用することで body の無いレスポンスを取得することができます。
```java
ResponseEntity<Void> response = restClient.post() 
  .uri("https://petclinic.example.com/pets/new") 
  .contentType(APPLICATION_JSON) 
  .body(pet) 
  .retrieve()
  .toBodilessEntity(); 
```

## PUT
```java
ResponseEntity<Void> response = restClient.put()
  .uri("https://petclinic.example.com/pets/1")
  .contentType(APPLICATION_JSON)
  .body(pet)
  .retrieve()
  .toBodilessEntity();
```

## DELETE
```java
ResponseEntity<Void> response = restClient.delete()
  .uri("https://petclinic.example.com/pets/1")
  .retrieve()
  .toBodilessEntity();
```

## エラー処理をカスタマイズする
ステータスコード4xxのときに MyCustomRuntimeException を発生させる。
```java
String result = restClient.get() 
  .uri("https://example.com/this-url-does-not-exist") 
  .retrieve()
  .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { 
      throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); 
  })
  .body(String.class);
```
## レスポンスへの処理を独自実装する
retrieve メソッドの代わりに exchange メソッドを利用することで HTTP リクエストおよびレスポンスの処理を実装できます。
```java
Pet result = restClient.get()
  .uri("https://petclinic.example.com/pets/{id}", id)
  .accept(APPLICATION_JSON)
  .exchange((request, response) -> { 
    if (response.getStatusCode().is4xxClientError()) { 
      throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); 
    }
    else {
      Pet pet = convertResponse(response); 
      return pet;
    }
  });
```

# HTTP メッセージ変換
HTTP リクエストおよびレスポンスの body は HttpMessageConverter インターフェースを介して変換されます。
DefaultRestClientBuilder に対して messageConverters の指定をすることで追加ができます。
```java
RestClient customClient = RestClient.builder()
  .requestFactory(new HttpComponentsClientHttpRequestFactory())
  .messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
  .build();
  ```
メッセージコンバーターはデフォルトで以下のものが設定されます。
- ByteArrayHttpMessageConverter
- StringHttpMessageConverter
- ResourceHttpMessageConverter
- AllEncompassingFormHttpMessageConverter
- KotlinSerializationJsonHttpMessageConverter
    - クラスパスに kotlinx.serialization.json.Json が含まれる場合
- MappingJackson2HttpMessageConverter
    - クラスパスに com.fasterxml.jackson.databind.ObjectMapper が含まれる場合
- GsonHttpMessageConverter
    - クラスパスに com.google.gson.Gson が含まれる場合
- JsonbHttpMessageConverter
    - クラスパスに jakarta.json.bind.Jsonb が含まれる場合
- MappingJackson2SmileHttpMessageConverter
    - クラスパスに com.fasterxml.jackson.dataformat.smile.SmileFactory が含まれる場合
- MappingJackson2CborHttpMessageConverter
    - クラスパスに com.fasterxml.jackson.dataformat.cbor.CBORFactory が含まれる場合
```java
	private List<HttpMessageConverter<?>> initMessageConverters() {
		if (this.messageConverters == null) {
			this.messageConverters = new ArrayList<>();
			this.messageConverters.add(new ByteArrayHttpMessageConverter());
			this.messageConverters.add(new StringHttpMessageConverter());
			this.messageConverters.add(new ResourceHttpMessageConverter(false));
			this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());

			if (kotlinSerializationJsonPresent) {
				this.messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
			}
			if (jackson2Present) {
				this.messageConverters.add(new MappingJackson2HttpMessageConverter());
			}
			else if (gsonPresent) {
				this.messageConverters.add(new GsonHttpMessageConverter());
			}
			else if (jsonbPresent) {
				this.messageConverters.add(new JsonbHttpMessageConverter());
			}
			if (jackson2SmilePresent) {
				this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
			}
			if (jackson2CborPresent) {
				this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
			}
		}
		return this.messageConverters;
	}
```
# クライアントリクエストファクトリ
HTTP リクエストを実行するためのクライアント HTTP ライブラリを設定可能です。
DefaultRestClientBuilder に対して requestFactory の指定をすることで設定できます。
```java
RestClient customClient = RestClient.builder()
  .requestFactory(new HttpComponentsClientHttpRequestFactory())
  .build();
```
クライアントリクエストファクトリの優先度は以下の通りです。
- ユーザー指定
- HttpComponentsClientHttpRequestFactory
    - クラスパスに Apache HTTP Client が含まれる場合
- JettyClientHttpRequestFactory
    - クラスパスに Jetty HTTP Client が含まれる場合
- JdkClientHttpRequestFactory
    - クラスパスに Java HTTP Client が含まれる場合
- SimpleClientHttpRequestFactory
```java:DefaultRestClientBuilder.class
	private ClientHttpRequestFactory initRequestFactory() {
		if (this.requestFactory != null) {
			return this.requestFactory;
		}
		else if (httpComponentsClientPresent) {
			return new HttpComponentsClientHttpRequestFactory();
		}
		else if (jettyClientPresent) {
			return new JettyClientHttpRequestFactory();
		}
		else if (jdkClientPresent) {
			// java.net.http module might not be loaded, so we can't default to the JDK HttpClient
			return new JdkClientHttpRequestFactory();
		}
		else {
			return new SimpleClientHttpRequestFactory();
		}
	}
```
SimpleClientHttpRequestFactory は RestTemplate でもクライアントリクエストファクトリを指定しない場合に使われていたデフォルト設定です。

# RestTemplate からの移行
カスタマイズした RestTemplate を RestClient に移行したい場合は create メソッドもしくは builder メソッドに RestTemplate を引数として渡すことで設定を引き継いだ RestClient を作成することができます。
```java
RestClient defaultClient = RestClient.create(restTemplate);

RestClient customClient = RestClient.builder(restTemplate)
  // 略
  .build();
```
引き継がれる項目は以下の`restTemplate.getXXX()`で取得可能なものに限られるため、その他に独自実装を加えている場合はご注意ください。
```java:DefaultRestClientBuilder.class
	public DefaultRestClientBuilder(RestTemplate restTemplate) {
		Assert.notNull(restTemplate, "RestTemplate must not be null");

		if (restTemplate.getUriTemplateHandler() instanceof UriBuilderFactory builderFactory) {
			this.uriBuilderFactory = builderFactory;
		}
		this.statusHandlers = new ArrayList<>();
		this.statusHandlers.add(StatusHandler.fromErrorHandler(restTemplate.getErrorHandler()));

		this.requestFactory = restTemplate.getRequestFactory();
		this.messageConverters = new ArrayList<>(restTemplate.getMessageConverters());

		if (!CollectionUtils.isEmpty(restTemplate.getInterceptors())) {
			this.interceptors = new ArrayList<>(restTemplate.getInterceptors());
		}
		if (!CollectionUtils.isEmpty(restTemplate.getClientHttpRequestInitializers())) {
			this.initializers = new ArrayList<>(restTemplate.getClientHttpRequestInitializers());
		}
		this.observationRegistry = restTemplate.getObservationRegistry();
		this.observationConvention = restTemplate.getObservationConvention();
	}
```

また、RestTemplate のメソッドと RestClient のメソッドの対応表が[公式ページ](https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#_migrating_from_resttemplate_to_restclient)で公開されています。

# 感想
RestClient を試してみましたが、RestTemplate よりもメソッドチェーンで直感的に操作ができるようになったと感じました。
WebClient を利用している人にとってもクラスの構成が似ているので扱いやすいかと思います。
RestTemplate 自体はまだ非推奨にはなっていませんが、Spring Framework 6.1 以降を利用する場合は同期クライアントとして RestClient を採用していきたいと思います。

---
記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。