Spring Boot 4.0 RestClient 与 HTTP Interface 实战
前言
调用另一个 HTTP 服务时,最容易出现的代码是一个方法里同时负责拼 URL、设请求头、发送请求、处理状态码和解析 JSON。接口一多,这些重复代码会快速扩散。
Spring Framework 7 提供两种层次:
RestClient:显式构造请求并解析响应。- HTTP Interface:先声明 Java 接口,再由代理和
RestClient执行网络调用。
本章会把订单 API 声明成客户端接口,配置连接超时和读取超时,并用真实 HTTP 服务验证调用链。
配套源码:dvsusan/spring-boot4-demo。
最终结果
业务代码可以这样调用远端订单服务:
// 调用方只看到 Java 方法,不需要拼 URL 或序列化 JSON。
ApiResponse<OrderView> response = orderApiClient.findByOrderNo(orderNo);
OrderView order = response.data();调用方不需要拼路径、序列化 JSON 或手动关闭连接。代理内部仍然使用标准 RestClient 发起 HTTP 请求。

图 1:HTTP Interface 是声明层,RestClient 是执行层,异常和超时必须由调用方处理。
第 1 步:添加 RestClient Starter
Spring Boot 4 将 HTTP 客户端自动配置拆成独立 Starter:
<dependency>
<!-- 提供 RestClient.Builder 和客户端自动配置。 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>只引入 spring-web 也能手写 RestClient.builder(),但会失去 Boot 的自动配置、消息转换器、观测等集成。
第 2 步:声明订单客户端接口
创建:
demo-web/src/main/java/com/susan/training/client/OrderApiClient.java/**
* 声明式订单客户端,由代理负责生成 HTTP 请求。
*/
@HttpExchange("/api/v1/orders")
public interface OrderApiClient {
@PostExchange
ApiResponse<OrderView> create(@RequestBody CreateOrderRequest request);
@GetExchange("/{orderNo}")
ApiResponse<OrderView> findByOrderNo(@PathVariable String orderNo);
@PatchExchange("/{orderNo}")
ApiResponse<OrderView> update(
@PathVariable String orderNo,
@RequestBody CreateOrderRequest request
);
}这段声明和 Controller 很像,但执行方向相反:
- Controller 接收 HTTP 并调用本地 Service。
- HTTP Interface 接收 Java 调用并发送 HTTP。
接口返回 ApiResponse<OrderView>,说明客户端明确知道服务端响应契约。如果服务端结构变化,反序列化或测试会暴露问题。
第 3 步:配置地址和超时
创建:
demo-web/src/main/java/com/susan/training/client/OrderClientProperties.java@ConfigurationProperties(prefix = "demo.order-client")
public class OrderClientProperties {
// 远端服务基础地址,不包含具体订单路径。
private String baseUrl = "http://localhost:8080";
// 建立连接的最长等待时间。
private Duration connectTimeout = Duration.ofSeconds(2);
// 等待响应数据的最长时间。
private Duration readTimeout = Duration.ofSeconds(3);
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public Duration getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(Duration connectTimeout) {
this.connectTimeout = connectTimeout;
}
public Duration getReadTimeout() {
return readTimeout;
}
public void setReadTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
}
}公共配置:
demo:
order-client:
# 默认调用本机服务,部署时可通过环境变量替换。
base-url: ${DEMO_ORDER_CLIENT_BASE_URL:http://localhost:${server.port:8080}}
connect-timeout: 2s
read-timeout: 3s网络调用不能无限等待。连接超时控制“多久连不上就失败”,读取超时控制“连接成功后多久等不到响应就失败”。
第 4 步:创建 RestClient 代理工厂
创建:
demo-web/src/main/java/com/susan/training/client/OrderHttpClientFactory.java@Component
public class OrderHttpClientFactory {
public OrderApiClient create(
String baseUrl,
RestClient.Builder builder,
OrderClientProperties properties
) {
// 连接超时由 JDK HttpClient 控制。
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(properties.getConnectTimeout())
.build();
var requestFactory =
new JdkClientHttpRequestFactory(httpClient);
// 读取超时由请求工厂控制。
requestFactory.setReadTimeout(properties.getReadTimeout());
RestClient restClient = builder.clone()
.baseUrl(baseUrl)
.requestFactory(requestFactory)
.build();
HttpServiceProxyFactory proxyFactory =
HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build();
return proxyFactory.createClient(OrderApiClient.class);
}
}builder.clone() 很重要。Boot 注入的 Builder 可能被其他客户端共享,直接修改基础地址会让不同客户端互相污染。
第 5 步:注册客户端 Bean
创建:
demo-web/src/main/java/com/susan/training/config/OrderClientConfiguration.java// 把声明式客户端注册为可注入 Bean。
@Configuration
@EnableConfigurationProperties(OrderClientProperties.class)
public class OrderClientConfiguration {
@Bean
public OrderApiClient orderApiClient(
OrderClientProperties properties,
OrderHttpClientFactory factory,
RestClient.Builder builder
) {
return factory.create(
properties.getBaseUrl(),
builder,
properties
);
}
}其他 Service 现在可以直接构造器注入 OrderApiClient:
public class FulfillmentService {
private final OrderApiClient orderApiClient;
// 客户端由配置类创建,业务代码不负责网络基础设施。
public FulfillmentService(OrderApiClient orderApiClient) {
this.orderApiClient = orderApiClient;
}
public OrderView loadOrder(String orderNo) {
return orderApiClient.findByOrderNo(orderNo).data();
}
}上面是调用示例,不在当前仓库中新增另一个业务 Service,避免为了演示制造无用类。
第 6 步:用真实 HTTP 往返验证
测试位于:
demo-web/src/test/java/com/susan/training/client/OrderHttpClientIntegrationTest.java@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderHttpClientIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private RestClient.Builder restClientBuilder;
@Autowired
private OrderHttpClientFactory clientFactory;
@Autowired
private OrderClientProperties properties;
@Test
void httpInterfaceRoundTripsJson() {
// 测试不写死 8080,而是连接本次随机端口。
OrderApiClient client = clientFactory.create(
"http://localhost:" + port,
restClientBuilder,
properties
);
ApiResponse<OrderView> created = client.create(
new CreateOrderRequest("C9009", new BigDecimal("520.00"))
);
ApiResponse<OrderView> found =
client.findByOrderNo(created.data().orderNo());
assertThat(created.code()).isEqualTo("0");
assertThat(found.data().customerId()).isEqualTo("C9009");
assertThat(found.data().amount())
.isEqualByComparingTo("520.00");
}
}执行:
mvn -q -pl demo-web -am test \
-Dtest=OrderHttpClientIntegrationTest \
-Dsurefire.failIfNoSpecifiedTests=false这个测试启动真实 Tomcat、使用真实 TCP 请求并经过 JSON 编解码,不是用 Mockito 假装客户端成功。
第 7 步:什么时候用显式 RestClient
HTTP Interface 适合固定的服务端 API。临时请求、需要动态构造多个 URI 或需要直接读取流式响应时,显式 RestClient 更直接:
// 一次性请求可以直接使用显式 RestClient。
String body = restClient.get()
.uri("/api/v1/orders/{orderNo}", orderNo)
.retrieve()
.body(String.class);不要为了一次性请求也设计一个只有单个方法的接口。声明式客户端的价值来自稳定契约和重复调用。
常见问题排查
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
找不到 RestClient.Builder | 缺少 Boot 4 RestClient Starter | 引入 spring-boot-starter-restclient |
Connection refused | 服务没有启动或端口错误 | 检查 base-url 和监听端口 |
| 请求一直挂起 | 没有设置读取超时 | 配置连接与读取超时 |
| 4xx 直接抛异常 | RestClient 默认把错误状态当失败 | 按业务选择状态处理器或捕获异常 |
| 字段反序列化为空 | JSON 字段名与 DTO 不一致 | 对齐契约并检查 Jackson 名称 |
| 测试随机端口连到 8080 | 客户端写死地址 | 使用 @LocalServerPort 注入本次端口 |
本章小结
HTTP Interface 把远端 API 表达成 Java 接口,RestClient 负责真正执行。
一个可维护的客户端至少要有:
- 明确的服务端响应类型。
- 可配置的基础地址。
- 连接和读取超时。
- 独立 Bean,不把网络代码散落在业务方法里。
- 真实 HTTP 集成测试。
下一章会处理需要持续保持连接的场景,普通请求响应模型将换成 SSE 与 WebSocket。
练习题
为 OrderApiClient 增加按客户查询:
- 使用
@GetExchange和@RequestParam声明查询参数。 - 返回
ApiResponse<List<OrderView>>。 - 设置
limit=5。 - 在集成测试中确认只返回指定客户订单。
上一节:OpenAPI 文档与客户端代码生成
下一节:SSE 与 WebSocket 实时消息
相关阅读:
13 个企业级实战项目 · 源码 + 教程 + 答疑
简历包装指导AI Agent 实战高并发·微服务Offer 导向持续更新3000+ 同行交流1V1 答疑3 天包退
覆盖高并发、微服务、分布式与 AI Agent:100万QPS短链、企业智能知识库、商城微服务、SaaS点餐、秒杀、刷题小程序、代码生成器、AI Agent 等。券后 ¥129(原价 ¥159),不满意 3 天包退。
