21 人が閲覧(直近 30 日) Spring

Spring RestClientの使い方:Boot 4.1での導入・エラー処理・テストとRestTemplateからの移行

Spring RestClientの使い方:Boot 4.1での導入・エラー処理・テストとRestTemplateからの移行

RestClientは、Spring Framework 6.1で追加された同期HTTPクライアントです。restClient.get().uri("/pets/{id}", 1).retrieve().body(Pet.class) のように、リクエストの組み立てから型変換までを1本のメソッドチェーンで書けます。動作はRestTemplateと同じブロッキングで、書き方はWebClientに近くなりました。

この記事が扱うのはJavaのインターフェース org.springframework.web.client.RestClient です。VS Codeの拡張機能「REST Client」やMicroProfileの @RestClient は別物なので、末尾のよくある質問で見分け方を書いています。コード例と数値は、2026年11月1日にSpring Boot 4.1.2(Spring Framework 7.0.10)とJDK 25で動かして確かめたものです。

まとめ:RestClientの要点とRestTemplate移行の判断

  • RestTemplateは7.0のリファレンスで非推奨になり、7.1でコードに @Deprecated(forRemoval = true) が付く。7.1.0-RC1でコンパイルすると、オプションなしで [removal] 警告が出る。削除予定は8.0
  • Spring Boot 4では spring-boot-starter-restclient を追加し、自動設定された RestClient.Builder を注入して使う
  • HTTPライブラリは追加しなければJDKの HttpClient。Apache HttpClient 5をクラスパスに足すとそちらに切り替わる
  • 4xx・5xxは既定で例外になる。タイムアウトは spring.http.clients.read-timeout、再試行はFramework 7.0の @Retryable(既定で初回+3回)
  • APIをインターフェースで定義するなら @HttpExchange と @ImportHttpServices。APIバージョンのプロパティ名は apiversion.default で、default-version と書くと効かない
  • テストは @RestClientTest(テスト用スターターは別)か MockRestServiceServer.bindTo(RestClient.Builder)

RestClient・RestTemplate・WebClientの使い分けと非推奨の時期

7.0は文書で非推奨、7.1でコンパイル警告

Spring Framework 7.0のリファレンスは、RestTemplateの節に「As of Spring Framework 7.0, RestTemplate is deprecated in favor of RestClient and will be removed in a future version」と書いています。ただし7.0.10のRestTemplateクラスには @Deprecated が付いておらず、ビルドしても警告は出ません。

注釈が付くのは7.1です。7.1.0-RC1のソースには @Deprecated(since = "7.1", forRemoval = true) と「For removal in 8.0」があります。new RestTemplate() の1行だけのクラスをjavacでコンパイルしたところ、7.0.10では警告0件、7.1.0-RC1では -Xlint を付けなくても「[removal] RestTemplate … has been deprecated and marked for removal」が2件出ました。forRemoval = true の警告は既定で表示されるためです。

GitHubのマイルストーンでは、7.1.0のリリース予定日は2026年11月19日です。CIで警告をエラーにしている(-Werror)プロジェクトは、7.1を取り込むSpring Bootへ上げた時点でビルドが止まります。

同期はRestClient、非同期とストリーミングはWebClient

クライアント 動作 追加された版 現在の位置づけ
RestClient 同期 6.1 同期HTTPの標準
RestTemplate 同期 3.0 7.0で非推奨・8.0で削除予定
WebClient 非同期・リアクティブ 5.0 非同期・ストリーミング用
HTTPサービスクライアント 宣言的(上の3つに委譲) 6.0 インターフェースで呼ぶ場合

呼び出し元のスレッドが待ってよいならRestClient、ノンブロッキングで多数の呼び出しを並べる・レスポンスを流しながら処理するならWebClientです。WebClient側の使い方はSpring WebFluxとは?MVCとの違い・Mono/Fluxの基本とSpring Boot 4.1での使い方で扱っています。

Spring Boot 4.1でRestClientを使い始める手順

spring-boot-starter-restclientの依存追加

Spring Boot 4.0でスターターが機能ごとに分かれ、ブロッキングのHTTPクライアントは spring-boot-starter-restclient に入りました。説明文は「Starter using Spring’s blocking HTTP clients (RestClient, RestTemplate and HTTP Service Clients)」で、Jacksonのスターターも一緒に入ります。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-restclient</artifactId>
</dependency>

Gradleなら implementation 'org.springframework.boot:spring-boot-starter-restclient' です。このアーティファクトはMaven Centralに4.0.0以降しかなく、Spring Boot 3.2以降の3.x系では spring-boot-starter-web にRestClientの自動設定が含まれます。3系から上げるときにどの依存が分かれたかはSpring Boot 4とは?最新バージョン4.1の変更点・新機能とSpring Boot 3との違い【2026年10月】にまとめています。

RestClient.Builderの注入とクライアント生成

Spring Bootは、メッセージコンバーターとHTTPライブラリを設定済みの RestClient.Builder をプロトタイプのbeanとして用意します。注入先ごとに新しいビルダーが渡るので、ベースURLを足しても他のクラスに影響しません。RestClient.create() で自前に作ると後述のタイムアウト設定やメトリクスが乗りません。

@Service
public class PetClient {

    public record Pet(long id, String name) {}

    private final RestClient restClient;

    public PetClient(RestClient.Builder builder) {
        this.restClient = builder
                .baseUrl("https://petstore.example.com")
                .defaultHeader("X-Client", "issoh-sample")
                .build();
    }

    public Pet findById(long id) {
        return restClient.get()
                .uri("/pets/{id}", id)
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .body(Pet.class);
    }
}

アプリ全体に共通のヘッダーやインターセプターを足すなら、org.springframework.boot.restclient.RestClientCustomizer をbeanにします。ビルダーがプロトタイプである理由はJavaのBuilderパターン実装|Lombok @Builder・record・Spring Bootでの使い方でも触れています。

HTTPライブラリの検出順と切り替え設定

RestClientは実際の通信をHTTPライブラリに任せます。Spring Bootのリファレンスによると、クラスパスを見て次の順で選びます。

優先順 ライブラリ imperative.factoryの値
1 Apache HttpClient 5 http-components
2 Jetty HttpClient jetty
3 Reactor Netty reactor
4 JDKのjava.net.http.HttpClient jdk
5 HttpURLConnection simple

スタブサーバーで受けたUser-Agentは、スターターだけのときが Java-http-client/25.0.4.1、httpclient5(5.6.5)を依存に足したときが Apache-HttpClient/5.6.5 (Java/25.0.4.1) でした。別のライブラリが推移的依存で入ると、コードを変えなくても通信部分が入れ替わります。Cookieの扱いも変わり、ApacheとJettyは受け取ったCookieを次のリクエストで送り返し、JDKのクライアントは無視します。

意図したライブラリに固定するには spring.http.clients.imperative.factory を指定します。

spring.http.clients.imperative.factory=jdk

よく見かける「PATCHが送れない」という注意は、5番目の simple(HttpURLConnection)の制約です。Spring Boot 4はJava 17以上が前提なので、通常のRestClientでは当たりません。

GET・POST・PUT・DELETEのリクエスト送信

retrieve()後のbody・toEntityによるレスポンス取得

get() や post() で始め、uri() でパスを、retrieve() で受け取り方を決めます。本文だけなら body(型)、ステータスコードやヘッダーも見るなら toEntity(型) です。List<Pet> のようなジェネリクス型は、クラスリテラルでは型情報が消えるため ParameterizedTypeReference で渡します。

public List<Pet> findAll() {
    return restClient.get()
            .uri("/pets")
            .retrieve()
            .body(new ParameterizedTypeReference<List<Pet>>() {});
}

public ResponseEntity<Pet> findWithHeaders(long id) {
    return restClient.get().uri("/pets/{id}", id).retrieve().toEntity(Pet.class);
}

POST・PUTのボディ送信とDELETEによる削除

送信は contentType() と body() を挟むだけで、オブジェクトはJacksonでJSONに変換されます。本文の無いレスポンスは toBodilessEntity() で受けます。

public Pet create(Pet pet) {
    return restClient.post()
            .uri("/pets")
            .contentType(MediaType.APPLICATION_JSON)
            .body(pet)
            .retrieve()
            .body(Pet.class);
}

public void update(Pet pet) {
    restClient.put().uri("/pets/{id}", pet.id()).body(pet).retrieve().toBodilessEntity();
}

public void delete(long id) {
    restClient.delete().uri("/pets/{id}", id).retrieve().toBodilessEntity();
}

exchange()によるステータス別の戻り値制御

「404なら null、それ以外は本文を返す」のように、ステータスで戻り値を変えたいときは exchange() を使います。exchange() ではステータスハンドラーが働かないので、4xx・5xxも例外にならずラムダまで届きます。

public Pet findOrNull(long id) {
    return restClient.get().uri("/pets/{id}", id)
            .exchange((request, response) -> {
                if (response.getStatusCode().value() == 404) {
                    return null;
                }
                if (response.getStatusCode().isError()) {
                    throw new IllegalStateException("Unexpected HTTP status: " + response.getStatusCode());
                }
                return response.bodyTo(Pet.class);
            });
}

RestClientのエラー処理・タイムアウト・リトライ設定

既定で投げられる例外とonStatus

retrieve() の既定では、4xxと5xxが例外になります。スタブで確かめた型は、404が HttpClientErrorException.NotFound、500が HttpServerErrorException.InternalServerError でした。メッセージには 404 Not Found: "{"error":"not found"}" のようにレスポンス本文まで入ります。本文に個人情報やトークンが入るAPIでは、例外メッセージをそのままログに出さないよう注意します。

独自の例外に置き換えるなら、呼び出しごとに onStatus を挟みます。

public Pet find(long id) {
    return restClient.get().uri("/pets/{id}", id)
            .retrieve()
            .onStatus(status -> status.value() == 404, (request, response) -> {
                throw new PetNotFoundException("pet " + id + " not found");
            })
            .body(Pet.class);
}

ハンドラーの中で例外を投げないと、そのステータスはエラーとして扱われなくなります。404を握りつぶしたまま toEntity で受けると、ステータス404と本文がそのまま返りました(実測)。

defaultStatusHandlerとインターセプターによるエラー処理・ログの共通化

同じ変換を全呼び出しに入れるなら、ビルダーの defaultStatusHandler にまとめます。所要時間やステータスのログはインターセプターの担当です。どちらもビルダーに1回書けば、呼び出し側のコードは変わりません。

this.restClient = builder
        .baseUrl("https://petstore.example.com")
        .defaultStatusHandler(HttpStatusCode::is5xxServerError, (request, response) -> {
            throw new IllegalStateException("upstream error: " + response.getStatusCode());
        })
        .requestInterceptor((request, body, execution) -> {
            long start = System.nanoTime();
            var response = execution.execute(request, body);
            System.out.printf("%s %s -> %d (%d ms)%n", request.getMethod(), request.getURI().getHost(),
                    response.getStatusCode().value(), (System.nanoTime() - start) / 1_000_000);
            return response;
        })
        .build();

メトリクスだけが目的なら、Spring Bootが注入するビルダーにはObservationが組み込み済みなので、Actuatorを入れれば http.client.requests が記録されます。

spring.http.clientsによる接続・読み取りタイムアウト設定

Spring Boot 4では、HTTPクライアント共通の設定が spring.http.clients にまとまりました。接続・読み取りのタイムアウト、リダイレクト、Cookie、SSLバンドルを指定できます。

spring.http.clients.connect-timeout=2s
spring.http.clients.read-timeout=5s

読み取りタイムアウトを1秒にして3秒かかるエンドポイントを呼ぶと、約1.15秒で ResourceAccessException が投げられ、原因(cause)はJDKの java.net.http.HttpTimeoutException でした。

@Retryableによる再試行回数と待機時間の設定

Spring Framework 7.0から、再試行の仕組みが本体に入りました。org.springframework.resilience.annotation.Retryable をメソッドに付け、設定クラスに @EnableResilientMethods を付けます。対象クラスはSpringのbeanとして登録し、別のbeanから注入されたプロキシ経由で呼び出します。同じインスタンス内の自己呼び出しには再試行が適用されません。

@Retryable
public String call() {
    return restClient.get().uri("/flaky").retrieve().body(String.class);
}

既定は maxRetries=3・delay=1000ms です。常に503を返すエンドポイントで試すと、呼び出しは初回を含めて4回、全体で約3.4秒かかり、最後に HttpServerErrorException.ServiceUnavailable が投げられました。Spring Retryの同名アノテーションは maxAttempts で初回を回数に含めるので、importを取り違えると回数が1回ずれます。

この例では再試行を副作用のないGETに限定します。ただし、既定の@Retryableはすべての例外を対象にするため、本番ではincludesやpredicateで一時的な障害に絞り、認証失敗や存在しないリソースへのアクセスを繰り返さないようにします。POSTで決済や登録を呼ぶAPIを再試行すると、タイムアウトしただけで実は処理が済んでいた場合に二重登録になります。相手側の障害が続くときに呼び出しを止める仕組みはcircuit breakerの実装:Java・C#・Go・Node.jsの設定コードと閾値の決め方で扱っています。

HTTPサービスクライアント(@HttpExchange)のインターフェース定義と呼び出し

@ImportHttpServicesの登録とグループ単位の設定

呼び出し先が多いなら、APIをJavaのインターフェースで定義し、実装をSpringに作らせる方法があります。メソッドに @GetExchange などを付け、Spring Framework 7.0の @ImportHttpServices で登録します。グループ名を付けると、ベースURLやタイムアウトをグループ単位のプロパティで渡せます。

@SpringBootApplication
@ImportHttpServices(group = "pets", types = PetService.class)
public class App { }

public interface PetService {
    @GetExchange("/pets/{id}")
    Pet get(@PathVariable long id);
}
spring.http.serviceclient.pets.base-url=https://petstore.example.com
spring.http.serviceclient.pets.read-timeout=2s

PetService を注入すれば pets.get(1) で呼べ、例外やタイムアウトの挙動は前章と同じです。OpenAPIの定義があるAPIなら、インターフェースを手で書かずに生成する手もあります(OpenAPI GeneratorでJavaコードを生成する方法|openapi-generator-cliの導入とlibrary選択・Spring Boot)。

APIバージョンのヘッダー送信とapiversion.defaultの設定名

Spring Framework 7.0で、クライアント側にもAPIバージョンの指定が入りました。ビルダーで書くなら defaultApiVersion("1.2") と apiVersionInserter(ApiVersionInserter.useHeader("API-Version")) です。HTTPサービスクライアントでは、プロパティで同じことができます。

spring.http.serviceclient.pets.apiversion.default=1.2
spring.http.serviceclient.pets.apiversion.insert.header=API-Version

キー名に注意が要ります。設定クラスのフィールド名は defaultVersion ですが、ApiversionPropertiesのソースでは @Name("default") で名前が付け替えられています。Boot 4.1.2で apiversion.default-version=1.2 と書くと、エラーも出ずにヘッダーが送られませんでした。apiversion.default=1.2 に直すと API-Version: 1.2 が届きます。IDEの補完が default-version を出すという報告(spring-boot issue #48611)もあります。メソッド単位で @GetExchange(url = "/pets/{id}", version = "2.0") と書いた版は、どちらのキーでも送られました。

InetAddressFilterによる接続先制限とBoot 4.1のSSRF対策

ユーザーが入力したURLやWebhookの宛先をRestClientで呼ぶと、社内ネットワークやクラウドのメタデータ(169.254.169.254)へリクエストを向けられるSSRFの危険があります。Spring Boot 4.1.0で、HTTPクライアントが接続できるIPアドレスを絞る InetAddressFilter が追加されました(リファレンスの該当節)。4.0系にはありません。

@Bean
InetAddressFilter httpClientInetAddressFilter() {
    return InetAddressFilter.externalAddresses();
}

externalAddresses() は、ループバック・プライベート範囲・リンクローカル(169.254.0.0/16)などRFC 6890の特殊用途アドレスを除いた宛先だけを通します。このbeanを置いて localhost のスタブを呼ぶと、送信前に FilteredHostException: Filtered host 'localhost' で止まりました。beanにすると自動設定されたすべてのHTTPクライアントに効くので、社内APIも呼ぶアプリでは使えません。その場合は外部URLを呼ぶクライアントだけに、HttpClientSettings 経由でフィルターを渡します。

フィルターはIPアドレスで判定するため、名前解決をプロキシが行う構成では効きません。プロキシを使う場合は、プロキシサーバー側で宛先を制限します。

RestTemplateからRestClientへ移行する手順と判断基準

移行は RestClient.builder(restTemplate) から始めます。既存のRestTemplateに設定したリクエストファクトリー、インターセプター、メッセージコンバーターを引き継いだRestClientができるので、認証やプロキシの設定を作り直さずに呼び出し部分から置き換えられます。

RestTemplate RestClient
getForObject(url, Pet.class) get().uri(url).retrieve().body(Pet.class)
getForEntity(url, Pet.class) get().uri(url).retrieve().toEntity(Pet.class)
postForEntity(url, req, Void.class) post().uri(url).body(req).retrieve().toBodilessEntity()
exchange(url, method, entity, 型) method(m).uri(url).body(b).retrieve().toEntity(型)
setErrorHandler(handler) defaultStatusHandler(handler)

exchange の HttpEntity に載せていたヘッダーは、headers(h -> h.addAll(entity.getHeaders())) で引き継ぎます。表の b は entity.getBody() で、本文の無いリクエストでは body() を省きます。

7.0系のうちは警告も出ないので、動いているコードを一斉に書き換える必要はありません。急ぐのは、CIで警告をエラーにしているプロジェクトです。共通のHTTP呼び出しをまとめたクラスから順に、テストを書いてから置き換えます。8.0がどのSpring Bootに載るかはサポート期限と一緒に見ておきます(Spring Bootバージョン一覧とサポート期限(EOL)【2026年10月】最新版は4.1、4.0の無償サポートは12月末終了)。

外部API連携が多く、どこにRestTemplateが残っているか洗い出すところから手が回らない場合は、API開発・システム連携の支援も行っています。

MockRestServiceServerと@RestClientTestによるHTTPクライアントテスト

@RestClientTestによる正常系・404応答のスライステスト

Spring Boot 4では、テスト用スターター spring-boot-starter-restclient-test が分かれています。これを test スコープで足すと、@RestClientTest が対象のクラスと MockRestServiceServer に、RestClientとJackson(JSON変換)の自動設定だけを加えた軽いコンテキストを起動します。アノテーションのパッケージは org.springframework.boot.restclient.test.autoconfigure に変わりました。

@RestClientTest(PetClient.class)
class PetClientTest {

    @Autowired
    PetClient petClient;

    @Autowired
    MockRestServiceServer server;

    @Test
    void findById() {
        server.expect(requestTo("https://petstore.example.com/pets/1"))
                .andRespond(withSuccess("{\"id\":1,\"name\":\"Pochi\"}", MediaType.APPLICATION_JSON));

        assertThat(petClient.findById(1).name()).isEqualTo("Pochi");
    }

    @Test
    void notFoundThrows() {
        server.expect(requestTo("https://petstore.example.com/pets/9")).andRespond(withResourceNotFound());

        assertThatThrownBy(() -> petClient.findById(9)).isInstanceOf(HttpClientErrorException.NotFound.class);
    }
}

この記事の PetClient に対して、上の2件と exchange() で404を null にする1件を mvn test で流し、3件とも通ることを確かめました。

bindToによるSpring Boot非依存のモック設定

Bootのコンテキストを立てずに済ませたいなら、Spring Framework 6.1で追加された MockRestServiceServer.bindTo(RestClient.Builder) を使います。ビルダーにモックを結び付けてから、テスト対象に渡します。

RestClient.Builder builder = RestClient.builder();
MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
server.expect(requestTo("https://petstore.example.com/pets/2"))
        .andRespond(withSuccess("{\"id\":2,\"name\":\"Tama\"}", MediaType.APPLICATION_JSON));

PetClient client = new PetClient(builder);
assertThat(client.findById(2).name()).isEqualTo("Tama");
server.verify();

verify() を呼ばないと、期待したリクエストが1回も飛ばなくてもテストが通ります。名前の似た RestTestClient(Framework 7.0)は、自分が公開するエンドポイントをMockMvcや起動済みサーバーに対して叩くテスト用で、外部APIのモックには使いません。

よくある質問

VS Codeの「REST Client」や@RestClientアノテーションはSpringのRestClientと同じですか?

別物です。VS Codeの拡張機能「REST Client」は、エディターに書いたHTTPリクエストを送って結果を見るツールです。@RestClient はMicroProfile Rest Client(Quarkusなど)の注入用アノテーションで、Springにはありません。SpringのRestClientは org.springframework.web.client パッケージのインターフェースで、アノテーションではなくビルダーから作ります。

RestTemplateはいつ使えなくなりますか?

削除予定は8.0です。7.0系では警告も出ず、7.1からコンパイル時に [removal] 警告が出ます。警告をエラーにしていなければ、7.1でもビルドは通ります。

Spring Boot 3でRestClientを使うにはどうしますか?

Spring Boot 3.2以降なら spring-boot-starter-web を入れるだけで、RestClient.Builder が注入できます。spring-boot-starter-restclient は4.0.0以降にしかないので、3系のpomに書くと依存解決で失敗します。

RestClientで複数のAPIを並行して呼べますか?

RestClient自体に非同期のAPIはなく、1回の呼び出しは結果が返るまでスレッドを止めます。並行させるなら、Java 21以降の仮想スレッドで複数の呼び出しを同時に走らせるか、WebClientに切り替えます。

Basic認証のヘッダーはどう付けますか?

全リクエストで同じ資格情報を使うなら、ビルダーで defaultHeaders(h -> h.setBasicAuth("user", "secret")) を指定します。呼び出しごとに変えるなら、リクエスト側で headers(h -> h.setBasicAuth(user, password)) を挟みます。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.11 テックブログ Microsoft 2026年11月の月例更新:Outlookの悪用済みCVE-2026-124194と修正版の確認方法
  2. 2026.11.11 テックブログ JANISの不正アクセス:最大11,652名の医療機関情報と情報窃取プログラムの検知設定
  3. 2026.11.11 テックブログ SMBC日興証券の不正アクセス:持株会会員最大5.6万人の範囲と受託システムの分離設計
  4. 2026.01.21 コラム 税務調査の録音は禁止?判例・裁決と国税局マニュアルで見る、断られたときの対応
  5. 2026.11.09 テックブログ ステップゴルフ不正アクセス:口座情報の対象確認と偽連絡・引き落としの注意点

RELATED POSTS 関連記事

目次