使用 RestClient 调用外部 API (Spring Boot 3.2+)

1. 概述

从 Spring Framework 6.1(Spring Boot 3.2 附带)开始,Spring 新增了 RestClient。这是一个同步阻塞型 HTTP 客户端,使用类似 WebClient 的 fluent API,但无需 Reactor。本文将指导你创建 RestClient、发送 GET/POST 请求、处理错误、配置超时、添加拦截器和编写测试。读完本文后,你可以在新项目中用 RestClient 替代 RestTemplate。

2. 安装

RestClient 位于 spring-web 模块。Web starter 已经包含该模块和用于 JSON 转换的 Jackson:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

本文中,我们调用一个假设的订单 API,位于 https://api.shop.example。我们使用 record 声明数据:

public record Order(Long id, String customerName, BigDecimal total, String status) {}

public record CreateOrderRequest(String customerName, BigDecimal total) {}

基础 URL 在 application.properties 中设置:

shop.api.base-url=https://api.shop.example

3. 创建 RestClient

3.1. 最快的方式

RestClient restClient = RestClient.create("https://api.shop.example");

这种方法适合于原型开发或脚本运行。在 Spring Boot 应用中,你应该使用 Boot 提供的 builder。

3.2. 使用 Spring Boot 的 RestClient.Builder

Spring Boot 自动配置了一个 RestClient.Builder bean。该 builder 已经预装了应用的 HttpMessageConverter,例如你自定义的 ObjectMapper。它还通过 Micrometer 支持可观测性。

@Component
public class OrderClient {

    private final RestClient restClient;

    public OrderClient(RestClient.Builder builder,
                       @Value("${shop.api.base-url}") String baseUrl) {
        this.restClient = builder
                .baseUrl(baseUrl)
                .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }
}

说明:

  • baseUrl(...):后续调用只需传递相对路径,如 /orders/42。
  • defaultHeader(...):该 header 被附加到每个请求。
  • build():创建 RestClient 实例。该实例是线程安全的,所以你可以创建一次后重复使用。

注意: RestClient.Builder bean 的作用域是 prototype。每次注入时,你都会获得一个新的 builder。因此,OrderClient 中的配置不会影响其他客户端。要为所有 builder 应用通用配置,请声明 RestClientCustomizer bean。

4. 发送 GET 请求

4.1. 获取 body 作为对象

public Order getOrder(Long id) {
    return restClient.get()
            .uri("/orders/{id}", id)
            .retrieve()
            .body(Order.class);
}
  • get():选择 HTTP 方法。
  • uri("/orders/{id}", id):变量 {id} 会被自动替换和编码。结果是 GET https://api.shop.example/orders/42。
  • retrieve():发送请求并准备读取响应。
  • body(Order.class):Jackson 将 JSON 转换为 Order record。

4.2. 获取状态和 header

当需要 status code 或 header 时,使用 toEntity:

ResponseEntity<Order> response = restClient.get()
        .uri("/orders/{id}", 42)
        .retrieve()
        .toEntity(Order.class);

System.out.println(response.getStatusCode());
System.out.println(response.getHeaders().getContentType());
System.out.println(response.getBody());
200 OK
application/json
Order[id=42, customerName=Lan, total=250000, status=PAID]

4.3. 获取列表和查询参数

泛型类型如 List<Order> 在运行时会被擦除。因此,你需要使用 ParameterizedTypeReference:

public List<Order> findOrders(String status, int page) {
    return restClient.get()
            .uri(uriBuilder -> uriBuilder
                    .path("/orders")
                    .queryParam("status", status)
                    .queryParam("page", page)
                    .build())
            .retrieve()
            .body(new ParameterizedTypeReference<List<Order>>() {});
}

调用 findOrders("PAID", 1) 生成请求 GET /orders?status=PAID&page=1。当你使用 uriBuilder 时,基础 URL 仍然被保留。

5. 发送 POST、PUT、DELETE 请求

public Order createOrder(CreateOrderRequest request) {
    return restClient.post()
            .uri("/orders")
            .contentType(MediaType.APPLICATION_JSON)
            .body(request)
            .retrieve()
            .body(Order.class);
}

public void cancelOrder(Long id) {
    restClient.delete()
            .uri("/orders/{id}", id)
            .retrieve()
            .toBodilessEntity();
}
  • body(request):对象通过声明的 contentType 被序列化为 JSON。
  • toBodilessEntity():当你不需要读取 body 时使用。该方法仍会触发请求并处理错误。

put() 和 patch() 的用法完全相同于 post()。

注意: 如果你调用 retrieve() 而不接着调用 body(...)、toEntity(...) 或 toBodilessEntity(),请求将不会被发送。这是使用 DELETE 调用时的常见错误。

6. 错误处理

6.1. 默认行为

默认情况下,当响应具有错误状态时,retrieve() 会抛出异常:

  • 4xx:抛出 HttpClientErrorException,例如 HttpClientErrorException.NotFound。
  • 5xx:抛出 HttpServerErrorException。

连接错误或超时会抛出 ResourceAccessException。

6.2. 使用 onStatus 自定义

你通常希望将 HTTP 错误转换为业务异常:

public class OrderNotFoundException extends RuntimeException {
    public OrderNotFoundException(Long id) {
        super("未找到订单 " + id);
    }
}
public Order getOrder(Long id) {
    return restClient.get()
            .uri("/orders/{id}", id)
            .retrieve()
            .onStatus(status -> status.value() == 404, (request, response) -> {
                throw new OrderNotFoundException(id);
            })
            .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
                throw new IllegalStateException(
                        "Shop API 错误: " + response.getStatusCode());
            })
            .body(Order.class);
}

Handler 按声明顺序被检查。第一个与状态匹配的 handler 将被执行。如果没有 handler 匹配,则应用第 6.1 节中的默认行为。

6.3. 使用 exchange 获得完全控制

当需要读取错误 body 或根据状态分支处理时,使用 exchange:

public Optional<Order> findOrder(Long id) {
    return restClient.get()
            .uri("/orders/{id}", id)
            .exchange((request, response) -> {
                if (response.getStatusCode().value() == 404) {
                    return Optional.empty();
                }
                if (response.getStatusCode().isError()) {
                    throw new IllegalStateException("错误 " + response.getStatusCode());
                }
                return Optional.ofNullable(
                        response.bodyTo(Order.class));
            });
}

注意: 在 exchange 中,默认的 status handler 不会被应用。你必须自己检查状态。如果忘记,错误 body 可能被解析为 Order,其字段为 null。

7. 超时和底层 HTTP 客户端

默认情况下,RestClient 按照以下顺序选择 classpath 上可用的 HTTP 库:Apache HttpClient 5、Jetty、Reactor Netty、JDK 的 java.net.http.HttpClient,最后是 SimpleClientHttpRequestFactory。你应该始终设置超时。否则,一个缓慢的 API 可能会长时间占用你的线程。

@Configuration
public class ShopClientConfig {

    @Bean
    public RestClientCustomizer timeoutCustomizer() {
        return builder -> {
            var factory = new SimpleClientHttpRequestFactory();
            factory.setConnectTimeout(Duration.ofSeconds(2));
            factory.setReadTimeout(Duration.ofSeconds(5));
            builder.requestFactory(factory);
        };
    }
}

RestClientCustomizer 被应用于 Spring Boot 创建的每个 RestClient.Builder。如果只想为一个客户端设置超时,可以在该客户端的构造函数中调用 builder.requestFactory(factory)。

对于缓慢的 API,请求将在 5 秒后失败:

org.springframework.web.client.ResourceAccessException: I/O error on GET request
for "https://api.shop.example/orders/42": Read timed out

从 Spring Boot 3.4 开始,你也可以通过属性 spring.http.client.connect-timeout 和 spring.http.client.read-timeout 声明超时。

8. 拦截器:附加 token、记录日志

ClientHttpRequestInterceptor 允许你拦截每个请求。例如,以下代码附加 API key 并记录响应时间:

@Slf4j
public class ApiKeyInterceptor implements ClientHttpRequestInterceptor {

    private final String apiKey;

    public ApiKeyInterceptor(String apiKey) {
        this.apiKey = apiKey;
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {
        request.getHeaders().set("X-Api-Key", apiKey);
        long start = System.currentTimeMillis();
        ClientHttpResponse response = execution.execute(request, body);
        log.info("{} {} -> {} ({} ms)", request.getMethod(), request.getURI(),
                response.getStatusCode(), System.currentTimeMillis() - start);
        return response;
    }
}

你在构建客户端时注册拦截器:

this.restClient = builder
        .baseUrl(baseUrl)
        .requestInterceptor(new ApiKeyInterceptor(apiKey))
        .build();
INFO  ApiKeyInterceptor : GET https://api.shop.example/orders/42 -> 200 OK (87 ms)

注意: 不要在拦截器中读取 response.getBody() 用于日志记录。Stream 只能读取一次。如果必须读取,请用 BufferingClientHttpRequestFactory 包装请求工厂。

9. HTTP interface:通过 interface 声明客户端

与其自己编写每个调用,不如用 interface 描述 API。Spring 将基于 RestClient 生成实现:

public interface OrderApi {

    @GetExchange("/orders/{id}")
    Order getOrder(@PathVariable Long id);

    @PostExchange("/orders")
    Order createOrder(@RequestBody CreateOrderRequest request);
}
@Configuration
public class OrderApiConfig {

    @Bean
    public OrderApi orderApi(RestClient.Builder builder,
                             @Value("${shop.api.base-url}") String baseUrl) {
        RestClient restClient = builder.baseUrl(baseUrl).build();
        return HttpServiceProxyFactory
                .builderFor(RestClientAdapter.create(restClient))
                .build()
                .createClient(OrderApi.class);
    }
}

现在你只需要注入 OrderApi 并调用 orderApi.getOrder(42L)。当 API 有许多端点时,这种方式更简洁。超时、拦截器和状态处理程序仍在底层 RestClient 上配置。

10. 使用 @RestClientTest 编写测试

从 Spring Boot 3.2 开始,@RestClientTest 支持 RestClient。此注解仅初始化与 HTTP 客户端相关的 bean。它还将 MockRestServiceServer 连接到 RestClient.Builder,因此没有真实请求被发送到外部。

我们用第 6.2 节中带有 onStatus 的 getOrder 版本来测试 OrderClient:

@RestClientTest(components = OrderClient.class,
        properties = "shop.api.base-url=https://api.shop.example")
class OrderClientTest {

    @Autowired
    private OrderClient orderClient;

    @Autowired
    private MockRestServiceServer server;

    @Test
    void givenExistingOrder_whenGetOrder_thenReturnsOrder() {
        server.expect(requestTo("https://api.shop.example/orders/42"))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withSuccess("""
                        {"id":42,"customerName":"Lan","total":250000,"status":"PAID"}
                        """, MediaType.APPLICATION_JSON));

        Order order = orderClient.getOrder(42L);

        assertThat(order.id()).isEqualTo(42L);
        assertThat(order.customerName()).isEqualTo("Lan");
        assertThat(order.total()).isEqualByComparingTo("250000");
        server.verify();
    }

    @Test
    void givenMissingOrder_whenGetOrder_thenThrowsOrderNotFound() {
        server.expect(requestTo("https://api.shop.example/orders/99"))
                .andRespond(withStatus(HttpStatus.NOT_FOUND));

        assertThatThrownBy(() -> orderClient.getOrder(99L))
                .isInstanceOf(OrderNotFoundException.class)
                .hasMessage("未找到订单 99");
    }
}

所需的静态导入位于 MockRestRequestMatchers 和 MockRestResponseCreators 中,分别在 org.springframework.test.web.client.match 和 org.springframework.test.web.client.response 包中。

  • server.expect(...):声明测试预期的请求。
  • andRespond(...):为该请求返回模拟响应。
  • server.verify():检查所有声明的请求是否确实被发送。

注意: MockRestServiceServer 仅在客户端由 Spring 注入的 RestClient.Builder 创建时工作。如果你在类内部调用 RestClient.create(...),测试将发送真实请求到网络。

11. 何时不应该使用 RestClient

  • WebFlux(响应式)应用程序: RestClient 阻塞线程。如果在反应式管道中调用它,事件循环将被阻塞。使用 WebClient。
  • 需要流式传输大数据或非阻塞的服务器发送事件: WebClient 更合适。
  • 旧代码与 RestTemplate 运行良好: 你不必重写。RestTemplate 仍然受支持,只是不会获得新功能。你可以使用 RestClient.create(restTemplate) 逐步迁移,同时保持现有配置。

如果应用程序在 Java 21 上使用虚拟线程运行(spring.threads.virtual.enabled=true),RestClient 的阻塞模型几乎没有可扩展性缺点。代码仍然简单易调试。

12. 总结

在本文中,你使用 RestClient 通过 fluent 和易读的 API 调用外部 API。我们经历了以下步骤:从 Spring Boot 的 builder 创建客户端、发送 GET/POST、使用 onStatus 或 exchange 将 HTTP 错误转换为业务异常、设置超时、添加拦截器和通过 HTTP interface 声明客户端。最后,你使用 @RestClientTest 和 MockRestServiceServer 编写测试而无需调用实际网络。

对于遵循 servlet 模型的 Spring Boot 3.2+ 项目,RestClient 应该是 RestTemplate 的默认替代品。记住两件事:始终设置超时,并始终以读取响应的方法结束 retrieve() 链。

spring-bootrestclienthttprest-apitesting