RestClient・WebClient・RestTemplate の違いは、「同期か非同期か」と「API の書き方」です。RestClient は同期でメソッドをつなげて書く形、WebClient はノンブロッキングでリアクティブな形、RestTemplate は同期でテンプレートメソッドを呼ぶ形で、Spring Framework 7.0 からは RestTemplate が非推奨になり RestClient が後継に位置づけられています。

この記事では、3つに HTTP Service Clients(@HttpExchange のインターフェース)を加えた4つの選択肢を比較し、RestTemplate のメソッドを RestClient でどう書くかの対応表、同じ処理を RestClient とインターフェースの両方で書いたコード全文、リクエストファクトリの一覧をまとめます。

確認環境:Spring Framework 7.0.9/Spring Boot 4.1.1 の公式リファレンスで確認(確認日 2026年10月7日)。Java 単体で再現できる部分は OpenJDK 21.0.10(Linux)で実行しています。

RestClient・WebClient・RestTemplateの早見表

選択肢 同期か非同期か 書き方 公式の位置づけ 向く場面
RestClient 同期 メソッドをつなげる RestTemplate の後継 新しく書く同期の呼び出し全般
WebClient ノンブロッキング メソッドをつなげる(Mono/Flux) 非同期・ストリーミング向け WebFlux のアプリ、ストリーミング
RestTemplate 同期 テンプレートメソッド 7.0 で非推奨 すでに動いている既存コード
HTTP Service Clients 土台のクライアントに従う インターフェースに注釈 RestClient などの上に載る宣言型 呼び出し先が多く、定義を一覧したい

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「RestClient・WebClient・RestTemplateの違い|移行対応表」

どれを選ぶかのフローチャート

flowchart TD
    A["外部APIを呼び出したい"] --> B{"ノンブロッキングや<br>ストリーミングが必要か"}
    B -- はい --> C["WebClient"]
    B -- いいえ --> D{"RestTemplate の<br>既存コードがあるか"}
    D -- はい --> E["そのまま動かしつつ<br>RestClient へ順に書き換える"]
    D -- いいえ --> F{"呼び出しをインターフェースで<br>宣言したいか"}
    F -- はい --> G["HTTP Service Clients<br>土台は RestClient"]
    F -- いいえ --> H["RestClient"]

判断の根拠は公式の記述です。Spring Framework のリファレンスは、非同期やストリーミングの用途ではリアクティブな WebClient を検討するよう案内しています。Spring Boot のリファレンスは、Spring WebFlux がクラスパスにあるなら WebClient の利用を勧め、RestTemplate については「移行したくない既存コードがあるとき」や「API に慣れているとき」に選ぶ場面があると説明しています。

RestTemplate の非推奨はどこまで決まっているか

Spring Framework 7.0.9 のリファレンスにある記述は次のとおりです。

  • Spring Framework 7.0 から、RestTemplate は RestClient を後継として非推奨
  • 将来のバージョンで削除される(どのバージョンかは、このページには書かれていません)
  • 移行には「Migrating to RestClient」の対応表を使う

Spring Boot 4.1.1 でも RestTemplateBuilder は引き続き自動設定されるので、既存の RestTemplate のコードはそのまま動かしながら、少しずつ書き換えられます。Spring Boot の世代ごとの OSS サポート期間は次のとおりです(spring.io の公開 API、確認日 2026年10月7日)。

世代 初回リリース OSS サポート終了
3.5.x 2025-05-31 2026-06-30
4.0.x 2025-11-30 2026-12-31
4.1.x 2026-06-30 2027-07-31

Spring Boot 4 で必要な依存関係

Spring Boot 4.0 の移行ガイドでは、HTTP クライアント用のスターターが分かれています。

使うもの スターター テスト用スターター
RestClient・RestTemplate spring-boot-starter-restclient spring-boot-starter-restclient-test
WebClient spring-boot-starter-webclient spring-boot-starter-webclient-test
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-restclient</artifactId>
</dependency>

RestTemplate から RestClient への移行対応表

公式リファレンスの「Migrating to RestClient」の表を、メソッドの系統ごとにまとめました。URL の渡し方は RestTemplate・RestClient のどちらも「文字列+可変長引数」「文字列+Map」「URI」の3通りで、uri(...) の引数にそのまま移せます。

RestTemplate のメソッド RestClient での書き方
getForObject(url, Class, ...) get().uri(url, ...).retrieve().body(Class)
getForEntity(url, Class, ...) get().uri(url, ...).retrieve().toEntity(Class)
headForHeaders(url, ...) head().uri(url, ...).retrieve().toBodilessEntity().getHeaders()
postForLocation(url, Object, ...) post().uri(url, ...).body(Object).retrieve().toBodilessEntity().getLocation()
postForObject(url, Object, Class, ...) post().uri(url, ...).body(Object).retrieve().body(Class)
postForEntity(url, Object, Class, ...) post().uri(url, ...).body(Object).retrieve().toEntity(Class)
put(url, Object, ...) put().uri(url, ...).body(Object).retrieve().toBodilessEntity()
patchForObject(url, Object, Class, ...) patch().uri(url, ...).body(Object).retrieve().body(Class)
delete(url, ...) delete().uri(url, ...).retrieve().toBodilessEntity()
optionsForAllow(url, ...) options().uri(url, ...).retrieve().toBodilessEntity().getAllow()
exchange(url, HttpMethod, HttpEntity, Class, ...) method(HttpMethod).uri(url, ...).headers(Consumer<HttpHeaders>).body(Object).retrieve().toEntity(Class)
exchange(url, HttpMethod, HttpEntity, ParameterizedTypeReference, ...) method(HttpMethod).uri(url, ...).headers(Consumer<HttpHeaders>).body(Object).retrieve().toEntity(ParameterizedTypeReference)
exchange(RequestEntity, Class) method(HttpMethod).uri(URI).headers(Consumer<HttpHeaders>).body(Object).retrieve().toEntity(Class)
exchange(RequestEntity, ParameterizedTypeReference) method(HttpMethod).uri(URI).headers(Consumer<HttpHeaders>).body(Object).retrieve().toEntity(ParameterizedTypeReference)
execute(url, HttpMethod, RequestCallback, ResponseExtractor, ...) method(HttpMethod).uri(url, ...).exchange(ExchangeFunction)

読み替えの要点は3つです。

RestTemplate で渡すもの RestClient での置き場所
戻り値の型(Class) 最後の body(Class) または toEntity(Class)
リクエスト本文(Object、HttpEntity の本文) body(Object)
HttpEntity のヘッダー headers(...)、accept(...)、contentType(...)

書き換えの例

// RestTemplate
Order order = restTemplate.getForObject("/orders/{id}", Order.class, id);
ResponseEntity<Order> created = restTemplate.postForEntity("/orders", newOrder, Order.class);
restTemplate.delete("/orders/{id}", id);
// RestClient
Order order = restClient.get().uri("/orders/{id}", id).retrieve().body(Order.class);
ResponseEntity<Order> created = restClient.post().uri("/orders").body(newOrder).retrieve().toEntity(Order.class);
restClient.delete().uri("/orders/{id}", id).retrieve().toBodilessEntity();

RestClient で書くコード全文

注文 API(/orders)を呼び出す例です。Spring Boot は RestClient.Builder の Bean を用意し、メッセージ変換と適切な ClientHttpRequestFactory を設定済みにしてくれるので、コンストラクターで RestClient.Builder を受け取って組み立てる形が公式の勧めです。

package com.example.demo.client;

public record Order(long id, String item, int quantity) {
}
package com.example.demo.client;

public record NewOrder(String item, int quantity) {
}
package com.example.demo.client;

import org.springframework.http.HttpStatusCode;

public class OrderApiException extends RuntimeException {

    private final HttpStatusCode statusCode;

    public OrderApiException(HttpStatusCode statusCode) {
        super("注文APIの呼び出しがエラーになりました: " + statusCode);
        this.statusCode = statusCode;
    }

    public HttpStatusCode getStatusCode() {
        return this.statusCode;
    }
}
package com.example.demo.client;

import static org.springframework.http.MediaType.APPLICATION_JSON;

import java.net.URI;
import java.util.List;
import java.util.Optional;

import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class OrderClient {

    private final RestClient restClient;

    public OrderClient(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder
            .baseUrl("https://api.example.com")
            // 5xx はこのクライアント共通で独自の例外にする
            .defaultStatusHandler(HttpStatusCode::is5xxServerError, (request, response) -> {
                throw new OrderApiException(response.getStatusCode());
            })
            .build();
    }

    // GET:1件取得
    public Order get(long id) {
        return this.restClient.get()
            .uri("/orders/{id}", id)
            .accept(APPLICATION_JSON)
            .retrieve()
            .body(Order.class);
    }

    // GET:一覧(ジェネリクスつきの型で受け取る)
    public List<Order> list(int page) {
        return this.restClient.get()
            .uri("/orders?page={page}", page)
            .accept(APPLICATION_JSON)
            .retrieve()
            .body(new ParameterizedTypeReference<List<Order>>() { });
    }

    // POST:作成して Location ヘッダーを受け取る
    public URI create(NewOrder newOrder) {
        ResponseEntity<Void> response = this.restClient.post()
            .uri("/orders")
            .contentType(APPLICATION_JSON)
            .body(newOrder)
            .retrieve()
            .toBodilessEntity();
        return response.getHeaders().getLocation();
    }

    // エラー処理:4xx をこの呼び出しだけ独自の例外にする
    public Order getOrThrow(long id) {
        return this.restClient.get()
            .uri("/orders/{id}", id)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
                throw new OrderApiException(response.getStatusCode());
            })
            .body(Order.class);
    }

    // エラー処理:ステータスを自分で見て分岐する(404 は「無い」として扱う)
    public Optional<Order> find(long id) {
        return this.restClient.get()
            .uri("/orders/{id}", id)
            .accept(APPLICATION_JSON)
            .exchange((request, response) -> {
                if (response.getStatusCode().value() == 404) {
                    return Optional.<Order>empty();
                }
                if (response.getStatusCode().isError()) {
                    throw new OrderApiException(response.getStatusCode());
                }
                return Optional.ofNullable(response.bodyTo(Order.class));
            });
    }

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

RestClient.Builder は状態を持ち、変更はそのビルダーから後で作るクライアントすべてに反映されます。同じビルダーから複数のクライアントを作るときは builder.clone() で複製する方法がリファレンスで紹介されています。RestClient.create() を直接使うと、Spring Boot の自動設定や RestClientCustomizer は適用されません。

タイムアウトの設定

Spring Boot 4.1.1 では、HTTP クライアント共通のタイムアウトをプロパティで指定できます。

spring:
  http:
    clients:
      connect-timeout: 2s
      read-timeout: 1s
      redirects: dont-follow

クライアントごとに変えたいときは、ClientHttpRequestFactoryBuilder と HttpClientSettings でリクエストファクトリを組み立てて渡します。

package com.example.demo.client;

import java.time.Duration;

import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.boot.http.client.HttpClientSettings;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class ReportClient {

    private final RestClient restClient;

    public ReportClient(RestClient.Builder restClientBuilder) {
        // 集計に時間のかかる API だけ、読み取りタイムアウトを長くする
        HttpClientSettings settings = HttpClientSettings.defaults()
            .withReadTimeout(Duration.ofMinutes(2));
        ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder.detect().build(settings);
        this.restClient = restClientBuilder
            .baseUrl("https://report.example.com")
            .requestFactory(requestFactory)
            .build();
    }

    public String monthly(String month) {
        return this.restClient.get().uri("/reports/{month}", month).retrieve().body(String.class);
    }
}

RestTemplate を使い続ける箇所は、RestTemplateBuilder の connectTimeout(Duration)・readTimeout(Duration) で同じ指定ができます。

HTTP Service Clients(@HttpExchange)で同じ処理を書く

HTTP Service Clients は、呼び出しをインターフェースのメソッドとして宣言し、実体はプロキシが作る仕組みです。プロキシの中では RestClient・WebClient・RestTemplate のいずれかが動きます。先ほどの OrderClient と同じ内容を宣言すると、次のようになります。

package com.example.demo.client;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.service.annotation.DeleteExchange;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;

@HttpExchange(url = "/orders", accept = "application/json")
public interface OrderApi {

    // GET /orders/{id}
    @GetExchange("/{id}")
    Order get(@PathVariable long id);

    // GET /orders?page=1
    @GetExchange
    List<Order> list(@RequestParam int page);

    // POST /orders(ステータスとヘッダーを受け取る)
    @PostExchange(contentType = "application/json")
    ResponseEntity<Void> create(@RequestBody NewOrder newOrder);

    // DELETE /orders/{id}
    @DeleteExchange("/{id}")
    void delete(@PathVariable long id);
}

プロキシを自分で組み立てる(エラー処理つき)

エラー処理は、土台のクライアントに設定します。既定では 4xx・5xx のステータスで例外が投げられ、変えたいときは defaultStatusHandler を登録します。

package com.example.demo.client;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@Configuration(proxyBeanMethods = false)
public class OrderApiConfig {

    @Bean
    OrderApi orderApi(RestClient.Builder restClientBuilder) {
        RestClient restClient = restClientBuilder
            .baseUrl("https://api.example.com")
            .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> {
                throw new OrderApiException(response.getStatusCode());
            })
            .build();
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        return factory.createClient(OrderApi.class);
    }
}
package com.example.demo.client;

import java.net.URI;

import org.springframework.stereotype.Service;

@Service
public class OrderService {

    private final OrderApi orderApi;

    public OrderService(OrderApi orderApi) {
        this.orderApi = orderApi;
    }

    public URI reorder(long id) {
        Order order = this.orderApi.get(id);
        return this.orderApi.create(new NewOrder(order.item(), order.quantity()))
            .getHeaders().getLocation();
    }
}

グループとプロパティで設定する(URL・タイムアウト)

Spring Boot 4.1.1 では、@ImportHttpServices でインターフェースを取り込み、グループ名ごとに URL やタイムアウトをプロパティで指定できます。グループを指定しない場合は default という名前のグループに入ります。

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.service.registry.ImportHttpServices;

@SpringBootApplication
@ImportHttpServices(group = "orders", basePackages = "com.example.demo.client")
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
spring:
  http:
    clients:
      connect-timeout: 1s            # すべての HTTP クライアントの既定
    serviceclient:
      orders:
        base-url: "https://api.example.com"
        connect-timeout: 2s          # orders グループだけ上書き
        read-timeout: 2s

spring.http.serviceclient.<グループ名> の下では、ベース URL、既定のヘッダー、API バージョンの設定、リダイレクト、接続・読み取りタイムアウト、SSL バンドルを指定できます。上の「自分で組み立てる」方法と、この「グループで取り込む」方法は、同じインターフェースに対してどちらか一方を使います。

メソッドの引数と戻り値の一覧

@HttpExchange のメソッドで使える引数です(Spring Framework 7.0.9 リファレンス)。

引数 役割
URI リクエストの URL を動的に指定(注釈の url より優先)
UriBuilderFactory URI テンプレートの展開方法とベース URL を差し替える
HttpMethod HTTP メソッドを動的に指定
@RequestHeader リクエストヘッダーを追加(単一値、Collection、Map など)
@PathVariable URL のプレースホルダーに入れる値
@RequestAttribute リクエスト属性(RestClient と WebClient のみ)
@RequestBody リクエスト本文
@RequestParam クエリパラメーター。content-type がフォーム形式なら本文に入る
@RequestPart マルチパートの1パート(文字列、Resource、オブジェクトなど)
MultipartFile MultipartFile をパートとして送る
@CookieValue Cookie を追加

RestClient・RestTemplate を土台にしたときに使える戻り値です。

戻り値 内容
void リクエストを実行するだけ
HttpHeaders レスポンスヘッダー
<T> 本文を宣言した型に変換
ResponseEntity<Void> ステータスとヘッダー
ResponseEntity<T> ステータス、ヘッダー、変換した本文

WebClient を土台にすると、これらに加えて Mono<T>・Flux<T>・Mono<ResponseEntity<T>> などのリアクティブな戻り値も使えます。

インターフェースから呼び出しが作られる仕組みを Java 21 で確かめる

「インターフェースに注釈を書くだけで呼び出せる」動きは、Java の動的プロキシで説明できます。Spring を使わず、JDK の java.net.http.HttpClient と com.sun.net.httpserver.HttpServer だけで、GET・POST・404・タイムアウトの往復と、プロキシによる呼び出しの組み立てを確かめました(Spring の実装そのものではなく、考え方を小さく再現したものです)。

import com.sun.net.httpserver.HttpServer;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;

/** GET・POST・404・タイムアウトの往復と、インターフェースから呼び出しを組み立てる仕組みを JDK だけで確かめる。 */
public class HttpRoundTrip {

    @Retention(RetentionPolicy.RUNTIME)
    @interface Get { String value(); }

    /** HTTP Service Clients と同じ発想の、宣言だけのインターフェース。 */
    interface OrderApi {
        @Get("/orders/{id}")
        String getOrder(long id);
    }

    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 10392), 0);
        server.createContext("/orders", ex -> {
            String path = ex.getRequestURI().getPath();
            String method = ex.getRequestMethod();
            int status;
            String body;
            if (method.equals("POST")) {
                String received = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
                ex.getResponseHeaders().set("Location", "/orders/2");
                status = 201;
                body = "{\"id\":2,\"received\":" + received + "}";
            } else if (path.equals("/orders/1")) {
                status = 200;
                body = "{\"id\":1,\"item\":\"ノート\",\"quantity\":3}";
            } else {
                status = 404;
                body = "{\"title\":\"Not Found\",\"status\":404,\"detail\":\"order not found\"}";
            }
            byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
            ex.getResponseHeaders().set("Content-Type", "application/json");
            ex.sendResponseHeaders(status, bytes.length);
            ex.getResponseBody().write(bytes);
            ex.close();
        });
        server.createContext("/slow", ex -> {
            try { Thread.sleep(3000); } catch (InterruptedException ignored) { }
            ex.sendResponseHeaders(204, -1);
            ex.close();
        });
        server.start();

        String base = "http://127.0.0.1:10392";
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(2))   // 接続タイムアウト
                .build();

        // 1. GET
        HttpResponse<String> get = client.send(
                HttpRequest.newBuilder(URI.create(base + "/orders/1")).GET().build(),
                HttpResponse.BodyHandlers.ofString());
        System.out.println("GET  /orders/1   -> " + get.statusCode() + " " + get.body());

        // 2. POST
        HttpResponse<String> post = client.send(
                HttpRequest.newBuilder(URI.create(base + "/orders"))
                        .header("Content-Type", "application/json")
                        .POST(HttpRequest.BodyPublishers.ofString("{\"item\":\"ペン\",\"quantity\":10}"))
                        .build(),
                HttpResponse.BodyHandlers.ofString());
        System.out.println("POST /orders     -> " + post.statusCode()
                + " Location=" + post.headers().firstValue("Location").orElse("") + " " + post.body());

        // 3. 404:JDK の HttpClient は例外にせず、ステータスをそのまま返す
        HttpResponse<String> notFound = client.send(
                HttpRequest.newBuilder(URI.create(base + "/orders/999")).GET().build(),
                HttpResponse.BodyHandlers.ofString());
        System.out.println("GET  /orders/999 -> " + notFound.statusCode() + " " + notFound.body());

        // 4. 読み取りタイムアウト(1秒)を付けて、3秒かかる応答を呼ぶ
        long start = System.nanoTime();
        try {
            client.send(HttpRequest.newBuilder(URI.create(base + "/slow"))
                            .timeout(Duration.ofSeconds(1)).GET().build(),
                    HttpResponse.BodyHandlers.discarding());
        } catch (HttpTimeoutException e) {
            long ms = (System.nanoTime() - start) / 1_000_000;
            System.out.println("GET  /slow       -> " + e.getClass().getName() + ": " + e.getMessage()
                    + "(約" + (ms / 100) / 10.0 + "秒で打ち切り)");
        }

        // 5. インターフェースの宣言から呼び出しを組み立てる(動的プロキシ)
        OrderApi api = (OrderApi) Proxy.newProxyInstance(
                OrderApi.class.getClassLoader(), new Class<?>[] {OrderApi.class},
                (proxy, method, methodArgs) -> {
                    String path = method.getAnnotation(Get.class).value()
                            .replace("{id}", String.valueOf(methodArgs[0]));
                    System.out.println("  proxy: " + method.getName() + "(" + methodArgs[0] + ") を GET " + path + " に変換");
                    return client.send(HttpRequest.newBuilder(URI.create(base + path)).GET().build(),
                            HttpResponse.BodyHandlers.ofString()).body();
                });
        System.out.println("api.getOrder(1)  -> " + api.getOrder(1));
        System.out.println("実装クラス       -> " + api.getClass().getName());

        server.stop(0);
    }
}

java -Dstdout.encoding=UTF-8 HttpRoundTrip.java の実行結果です。

GET  /orders/1   -> 200 {"id":1,"item":"ノート","quantity":3}
POST /orders     -> 201 Location=/orders/2 {"id":2,"received":{"item":"ペン","quantity":10}}
GET  /orders/999 -> 404 {"title":"Not Found","status":404,"detail":"order not found"}
GET  /slow       -> java.net.http.HttpTimeoutException: request timed out(約1.0秒で打ち切り)
  proxy: getOrder(1) を GET /orders/1 に変換
api.getOrder(1)  -> {"id":1,"item":"ノート","quantity":3}
実装クラス       -> $Proxy0

ここから読み取れることは3つあります。

  • JDK の HttpClient は 404 を例外にせずステータスとして返します。 Spring のクライアントは既定で 4xx・5xx を例外にするので、「無いことも正常」という API では onStatus や exchange で扱いを決めておくと、呼び出し側のコードが素直になります。
  • タイムアウトは、応答を待つ時間に上限を付ける設定です。3秒かかる応答を1秒で打ち切れています。
  • インターフェースの実装クラスは実行時に作られるプロキシ($Proxy0)で、注釈の内容から URL を組み立てて HTTP クライアントに渡しています。@HttpExchange のインターフェースも、宣言と通信処理を分ける点は同じです。

ClientHttpRequestFactory 5種と自動選択の順序

RestClient と RestTemplate は、実際の通信を ClientHttpRequestFactory に任せます。Spring Framework 7.0.9 で用意されている実装は次の5つです。

実装クラス 使う HTTP クライアント
JdkClientHttpRequestFactory Java 標準の HttpClient
HttpComponentsClientHttpRequestFactory Apache HTTP Components の HttpClient
JettyClientHttpRequestFactory Jetty の HttpClient
ReactorNettyClientRequestFactory Reactor Netty の HttpClient
SimpleClientHttpRequestFactory シンプルな既定の実装

リクエストファクトリを指定しないときの選ばれ方は、Spring Framework 単体(RestClient を直接作る場合)と Spring Boot の自動検出で少し違います。

順位 Framework 単体 Spring Boot 4.1.1
1 Apache または Jetty(クラスパスにあれば) Apache HttpClient
2 Java の HttpClient(java.net.http モジュールがあれば) Jetty HttpClient
3 シンプルな既定の実装 Reactor Netty HttpClient
4 — JDK クライアント(java.net.http.HttpClient)
5 — シンプルな JDK クライアント(java.net.HttpURLConnection)

Spring Boot では、自動検出の結果を変えたいときにプロパティで指定できます。

spring:
  http:
    clients:
      imperative:
        factory: jetty

HTTP ライブラリの内部まで調整したい場合は、ClientHttpRequestFactoryBuilder の Bean を自分で定義すると、自動設定がそちらに譲ります。WebClient 側は別の仕組み(ClientHttpConnector)で、Spring Boot の検出順は Reactor Netty、Jetty、Apache HttpClient、JDK HttpClient です。

WebClient と RestTemplate の最小コード

比較のため、Spring Boot のリファレンスにある形で同じ GET を並べます。

// WebClient:戻り値は Mono。ビルダーは Spring Boot が用意する
import reactor.core.publisher.Mono;

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

@Service
public class OrderReactiveClient {

    private final WebClient webClient;

    public OrderReactiveClient(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.baseUrl("https://api.example.com").build();
    }

    public Mono<Order> get(long id) {
        return this.webClient.get().uri("/orders/{id}", id).retrieve().bodyToMono(Order.class);
    }
}
// RestTemplate:RestTemplateBuilder から作る(Spring Boot は RestTemplate そのものの Bean は用意しない)
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class OrderLegacyClient {

    private final RestTemplate restTemplate;

    public OrderLegacyClient(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder.build();
    }

    public Order get(long id) {
        return this.restTemplate.getForObject("https://api.example.com/orders/{id}", Order.class, id);
    }
}

Spring Boot 4 では RestTemplateBuilder のパッケージが org.springframework.boot.restclient になっています。

移行を進める順番

手順 やること
1 spring-boot-starter-restclient を依存関係に入れる
2 新しく書く呼び出しは RestClient(またはインターフェース)で書く
3 既存の RestTemplate は、対応表を見ながら呼び出し単位で書き換える
4 HttpEntity で渡していたヘッダーと本文を headers(...) と body(...) に分ける
5 エラー処理を onStatus/defaultStatusHandler に移す
6 タイムアウトを spring.http.clients.* に寄せる

RestTemplate のインスタンスをそのまま土台にして @HttpExchange のインターフェースを使うこともできます(RestTemplateAdapter)。呼び出しの宣言を先にインターフェースへ移し、土台を後から RestClient に替える進め方も取れます。

API を提供する側(コントローラーの書き方や入力チェック)は、関連記事「Spring BootでREST APIを作る|入力チェック・エラー処理・テストまで」で扱っています。

よくある質問

Q. RestTemplate は今すぐ書き換える必要がありますか? A. Spring Framework 7.0 で非推奨になり、将来のバージョンで削除されると案内されています。Spring Boot 4.1.1 でも RestTemplateBuilder は使えるので、新しく書く部分から RestClient にし、既存の部分は対応表を使って順に書き換える進め方ができます。

Q. Spring MVC のアプリで WebClient を使ってもよいですか? A. 使えます。Spring Boot のリファレンスは WebFlux がクラスパスにある場合に WebClient を勧めています。同期の呼び出しだけなら、RestClient のほうが戻り値をそのまま受け取れて書きやすくなります。

Q. RestClient と HTTP Service Clients はどう使い分けますか? A. 呼び出しが数か所なら RestClient を直接書くのが手軽です。呼び出し先の API が多く、メソッドの一覧として定義を見渡したいときは @HttpExchange のインターフェースが向いています。中で動くのは RestClient なので、タイムアウトやエラー処理の考え方は共通です。

Q. タイムアウトはどこで決めるのがよいですか? A. 実際の通信は自動検出された HTTP クライアントが行うので、値は設定で明示しておくのがおすすめです。spring.http.clients.connect-timeout と read-timeout を書いておくと、どのクライアントが選ばれても同じ値で動かせます。

まとめ

  • RestClient は同期・WebClient はノンブロッキング・RestTemplate は 7.0 で非推奨。HTTP Service Clients はそれらの上に載る宣言型
  • RestTemplate は「将来のバージョンで削除」と案内されている。新規は RestClient、既存は対応表で順に書き換える
  • 読み替えの基本:戻り値の型は body(Class)/toEntity(Class)、本文は body(Object)、ヘッダーは headers(...)
  • Spring Boot 4 のスターターは spring-boot-starter-restclient(WebClient は spring-boot-starter-webclient)
  • エラー処理は onStatus・defaultStatusHandler・exchange、タイムアウトは spring.http.clients.* で指定
  • @HttpExchange のインターフェースは @ImportHttpServices とグループのプロパティで URL・タイムアウトを設定できる
  • リクエストファクトリは5種。Spring Boot の検出順は Apache、Jetty、Reactor Netty、JDK、シンプルな JDK クライアント

参考・出典

確認日はいずれも 2026年10月7日です。

  • Spring Framework 7.0.9 リファレンス「REST Clients」 https://docs.spring.io/spring-framework/reference/integration/rest-clients.html
  • Spring Boot 4.1.1 リファレンス「Calling REST Services」 https://docs.spring.io/spring-boot/reference/io/rest-client.html
  • Spring Boot 4.0 Migration Guide https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide
  • Spring Boot 世代別サポート期間 https://api.spring.io/projects/spring-boot/generations

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「RestClient・WebClient・RestTemplateの違い|移行対応表」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?