使用 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.Builderbean 的作用域是prototype。每次注入时,你都会获得一个新的 builder。因此,OrderClient中的配置不会影响其他客户端。要为所有 builder 应用通用配置,请声明RestClientCustomizerbean。
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 转换为Orderrecord。
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() 链。