【SpringBoot 3.2で登場!】RestClientの使い方

SpringBoot 3.2でRestClientが登場しました!

ブロッキングなHTTPリクエストをWebClientライクに実装できるもので、SpringBoot 3.2以降で新しくWebMvcを実装する場合は、RestClientを利用するのがよさそうです。

この記事では、そんなRestClientについて利用方法をご紹介します!

依存ライブラリ

動作を確認したbuild.gradleの抜粋です

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.0-RC1'
	id 'io.spring.dependency-management' version '1.1.3'
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	// RestClient単体で利用したい場合
  // implementation 'org.springframework.boot:spring-boot-starter-web'

  // 細かい設定をするためにHttpClient5も依存に追加
	implementation 'org.apache.httpcomponents.client5:httpclient5'
}

共通の設定

まずはどのアプリケーションでも共通で意識するであろう設定の実装方法をまとめておきます。

@Configuration
public class RestClientAutoConfiguration {
    @Value("${restclient.base-url}")
    private String baseUrl;

    @Bean
    RestClient customRestClient(
            ObservationRegistry observationRegistry // メトリクス取得のためのBean
    ) {
        var connectionConfig = ConnectionConfig.custom()
                // TTLを設定
                .setTimeToLive(TimeValue.of(59, TimeUnit.SECONDS))
                // コネクションタイムアウト値を設定
                .setConnectTimeout(Timeout.of(1, TimeUnit.SECONDS))
                // ソケットタイムアウト値を設定(レスポンスタイムアウトと同義)
                .setSocketTimeout(Timeout.of(5, TimeUnit.SECONDS))
                .build();

        // PoolingHttpClientConnectionManagerを使うことでコネクションがプールされて
        // リクエストごとにコネクションを確立する必要がなくなる
        var connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
                .setDefaultConnectionConfig(connectionConfig)
                // 全ルート合算の最大接続数
                .setMaxConnTotal(100)
                // ルート(基本的にはドメイン)ごとの最大接続数
                // !!! デフォルトが「5」で高負荷には耐えられない設定値なので注意 !!!
                .setMaxConnPerRoute(100)
                .build();

        var httpClient = HttpClientBuilder.create()
                .setConnectionManager(connectionManager)
                .build();

        var requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
        return RestClient.builder()
                .baseUrl(baseUrl)
                .observationRegistry(observationRegistry) // メトリクス取得設定
                .requestFactory(requestFactory)
                .defaultStatusHandler(new DefaultResponseErrorHandler())
                .build();
    }

クラス単体テスト

RestClientを依存にもつクラスの単体テストのサンプルです。

MockRestServiceServerを使用することでRestClientをMock化します。

    @Test
    void test() {
        var restClientBuilder = RestClient.builder();

        MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restClientBuilder).build();
        mockServer.expect(requestTo("/greeting"))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withSuccess().body("Hello World!"));

        var restClient = restClientBuilder.build();
        var sampleClient = new SampleClient(restClient);
        var actual = sampleClient.greeting();
        var expected = "Hello World!";
        assertThat(actual).isEqualTo(expected);

        mockServer.verify();
    }

    // 単体テスト対象(HTTPリクエストを送るクライアントクラス)
    @RequiredArgsConstructor
    public static class SampleClient {
        private final RestClient restClient;

        public String greeting() {
            return restClient.get()
                    .uri("/greeting")
                    .retrieve()
                    .body(String.class);
        }
    }

GETリクエスト

GETメソッドでクエリパラメータを指定する実装サンプルです。

    public String getSample() {
        return restClient.get() // HTTPメソッドを指定
                .uri("/greeting?name={name}", Map.of("name", "hirabay")) // パスとクエリパラメータを指定
                .retrieve() // HTTPリクエストを実行
                .body(String.class); // レスポンスを受け取る型を指定
    }

toEntity()メソッドを使用するとレスポンスBody以外の情報を扱うこともできます。
※これはGETに限らず他のHTTPメソッドでも共通

    public String getEntitySample() {
        var responseEntity = restClient.get() // HTTPメソッドを指定
                .uri("/greeting?name={name}", Map.of("name", "hirabay")) // パスとクエリパラメータを指定
                .retrieve() // HTTPリクエストを実行
                .toEntity(String.class); // レスポンスを受け取る型を指定

        HttpStatusCode httpStatusCode = responseEntity.getStatusCode();
        // ステータスコードを利用した処理

        HttpHeaders httpHeaders = responseEntity.getHeaders();
        // レスポンスヘッダを利用した処理
        
        return responseEntity.getBody();
    }

POSTリクエスト

post()をメソッドを使うのと、bodyメソッドでbodyを指定すればOK!

@RequiredArgsConstructor
class PostMethodClient {
    private final RestClient restClient;

    public String postSample() {
        var person = new Person("hirabay");

        return restClient.post() // HTTPメソッドを指定
                .uri("/greeting") // パスを指定
                .body(person)
                .retrieve() // HTTPリクエストを実行
                .body(String.class); // レスポンスを受け取る型を指定
    }

    public record Person (String name) {}

}

class PostMethodTest {
    @Test
    void test() {
        var restClientBuilder = RestClient.builder();

        MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restClientBuilder).build();
        mockServer.expect(requestTo("/greeting"))
                .andExpect(method(HttpMethod.POST))
                // jsonをまるまる検証する場合
                .andExpect(content().json("""
                        {
                            "name": "hirabay"
                        }
                        """))
                // jsonの特定のフィールドを検証する場合
                .andExpect(jsonPath("$.name").value("hirabay"))
                .andRespond(withSuccess().body("Hello World!"));

        var restClient = restClientBuilder.build();
        var postMethodClient = new PostMethodClient(restClient);
        var actual = postMethodClient.postSample();
        var expected = "Hello World!";
        assertThat(actual).isEqualTo(expected);

        mockServer.verify();
    }
}

エラー時の挙動

エラー時は、発生要因ごとに例外がスローされるので、try-catchで処理するのと知識がなくても楽に実装できるかなと思います。

        try {
            restClient.get()
                    .uri("/greeting")
                    .retrieve()
                    .body(String.class);
        } catch (HttpClientErrorException httpClientErrorException) {
            // 4xx系のHTTPステータスが帰ってきた場合の処理
        } catch (HttpServerErrorException httpServerErrorException) {
            // 5xx系のHTTPステータスが帰ってきた場合の処理
        } catch (HttpStatusCodeException httpStatusCodeException) {
            // HTTPステータスがあるエラーが帰ってきた場合の処理
            // HttpClientErrorException + HttpServerErrorExceptionのイメージ
            var statusCode = httpStatusCodeException.getStatusCode(); // ステータスコードの取得
            var errorBoyd = httpStatusCodeException.getResponseBodyAs(String.class); // レスポンスBodyの取得
        } catch (ResourceAccessException resourceAccessException) {
            // 通信エラー系の処理(タイムアウト、証明書エラー等)
        }

WebClientライクに書こうとするとonStatus()メソッドを使用するとエラー時の挙動を制御することができます。

        restClient.get()
                .uri("/greeting")
                .retrieve()
                .onStatus(HttpStatusCode::isError, (request, response) -> {
                    // エラー時の処理
                    // 例)一般的な例外や独自の業務例外をスローする
                    throw new RuntimeException();
                })
                .body(String.class);

もしくはResponseErrorHandlerを設定する方法もあります。

ResponseErrorHandlerを利用する場合は、ライブラリ側で用意されているDefaultResponseErrorHandlerを拡張すると実装が楽かなと思います(bodyを取得するメソッドとかが揃っていたりするので)

        restClient.get()
                .uri("/greeting")
                .retrieve()
                // HTTPステータスエラー系を全てエラー扱いせず2xxと同じようにレスポンス取得する例
                .onStatus(new DefaultResponseErrorHandler() {
                    @Override
                    public boolean hasError(HttpStatusCode statusCode) {
                        return false;
                    }
                })
                .body(String.class);

共通処理の実装(interceptor)

interceptorを使用することで例えばヘッダを付与する等の処理を使いまわせるようになります。

以下はヘッダを追加する例です。

public class SampleClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution exec) throws IOException {
        request.getHeaders().set("sample", "sample value");
        return exec.execute(request, body);
    }
}
var restClientBuilder = RestClient.builder()
        // 単純な追加の場合
        .requestInterceptor(new SampleClientHttpRequestInterceptor("sample value"))
        // 既存のinterceptorをクリアしたり、順序を調整したい場合
        .requestInterceptors(interceptors -> {
            interceptors.clear();
            interceptors.add(new SampleClientHttpRequestInterceptor("sample value"));
        });

RestTemplateからの移行

すでにRestTemplateを使用しているケースで、RestClientを使ってみたい場合は、
RestTemplateをベースにRestClientを生成することができます!

RestTemplate restTemplate = new RestTemplateBuilder()
        .build();

RestClient restClient = RestClient.builder(restTemplate) // builderに既存のrestTemplateを指定するだけ
        .build();

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です