Java 项目对接多个外部 HTTP 服务时,常常在业务代码中重复拼 URL、设置请求头、解析 JSON、处理状态码。Spring Framework 的 HTTP Service Client 允许使用接口注解声明请求,再由代理执行实际 HTTP 调用。本文用商品服务示例解释它和 RestClient 的关系、如何配置、怎样测试,以及从旧客户端迁移时的边界。
技术栈:Java 17+、Spring Framework 7、Spring Boot 4.x、RestClient、JUnit 5
一、先分清四种 HTTP 客户端
HTTP Interface 并非 Spring Framework 7 才首次提供的功能;本文讨论其在 Spring Framework 7 中的工程化使用方式,而非当天新发布的特性。
| 技术 | 调用模型 | 主要用途 |
|---|---|---|
RestClient |
同步、流式 API | 常规阻塞 HTTP 请求 |
WebClient |
响应式、非阻塞 | Reactor 链路 |
| HTTP Interface | 注解接口与动态代理 | 统一声明外部服务契约 |
RestTemplate |
传统同步模板 | 旧代码维护与迁移 |
HTTP Interface 不是新的传输协议,也不是自动熔断或重试框架。它通过底层 HTTP 客户端适配器发起请求,主要价值在于减少重复的调用代码、集中接口契约。
二、从普通 RestClient 代码开始
假设第三方商品接口:
http
GET /api/products/42
Accept: application/json
返回:
json
{
"id": 42,
"name": "Keyboard",
"price": 199.00
}
定义响应类型:
java
public record ProductDto(
Long id,
String name,
BigDecimal price
) {
}
普通调用:
java
@Service
public class ProductGateway {
private final RestClient client;
public ProductGateway(RestClient.Builder builder) {
this.client = builder
.baseUrl("https://catalog.example.invalid")
.build();
}
public ProductDto find(long id) {
return client.get()
.uri("/api/products/{id}", id)
.retrieve()
.body(ProductDto.class);
}
}
example.invalid 是故意不可访问的示例域名。实际项目需要替换为真实下游地址。
只有一两个接口时,这种直接使用 RestClient 的写法已经很清楚。外部接口数量变多,才更有理由引入声明式接口。
三、声明 HTTP Interface
java
@HttpExchange(
url = "/api/products",
accept = "application/json"
)
public interface ProductHttpApi {
@GetExchange("/{id}")
ProductDto getProduct(@PathVariable long id);
@GetExchange
List<ProductDto> search(
@RequestParam String keyword,
@RequestParam int page
);
}
这里的接口不需要手写实现类。Spring 使用代理把 Java 方法调用转换为 HTTP 请求。
常用注解包括:
@HttpExchange:声明通用路径、请求头和交换信息。@GetExchange/@PostExchange:声明 HTTP 方法。@PathVariable:绑定路径变量。@RequestParam:绑定查询参数。@RequestBody:绑定请求体。
写入接口也能这样定义:
java
public record CreateProductRequest(
String name,
BigDecimal price
) {
}
@HttpExchange("/api/products")
public interface ProductWriteApi {
@PostExchange
ProductDto create(
@RequestBody CreateProductRequest request
);
}
但声明式接口不意味着 POST 自动幂等。订单创建、支付等动作仍要依靠业务请求 ID 和下游幂等设计。
四、手动创建代理:RestClientAdapter + HttpServiceProxyFactory
java
@Configuration
public class CatalogClientConfiguration {
@Bean
ProductHttpApi productHttpApi(RestClient.Builder builder) {
RestClient client = builder
.baseUrl("https://catalog.example.invalid")
.build();
RestClientAdapter adapter =
RestClientAdapter.create(client);
HttpServiceProxyFactory factory =
HttpServiceProxyFactory
.builderFor(adapter)
.build();
return factory.createClient(ProductHttpApi.class);
}
}
这里有两个层次:
RestClient负责网络请求、序列化和状态码处理。HttpServiceProxyFactory负责根据接口方法构造调用。
真实运行前需要补齐相关 import、模型转换器、网络配置和测试环境;本次没有独立编译这段示例。
不要在每次业务请求中重复创建代理。 更适合将代理注册为 Spring Bean,统一配置基础地址、鉴权和超时。
五、多个下游如何管理配置?
例如接入商品、库存、用户三个外部服务,建议分别定义客户端配置:
yaml
clients:
catalog:
base-url: https://catalog.example.invalid
inventory:
base-url: https://inventory.example.invalid
用配置属性对象承载地址:
java
@ConfigurationProperties(prefix = "clients.catalog")
public record CatalogClientProperties(String baseUrl) {
}
再注入客户端工厂:
java
@Bean
ProductHttpApi productHttpApi(
RestClient.Builder builder,
CatalogClientProperties props
) {
RestClient client = builder
.baseUrl(props.baseUrl())
.build();
return HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(client))
.build()
.createClient(ProductHttpApi.class);
}
需要通过 @ConfigurationPropertiesScan 或 @EnableConfigurationProperties 注册属性类,否则无法保证 Bean 自动存在。
Spring Framework 7 文档也介绍了多 HTTP Interface 客户端注册能力,适合接口数量较多的项目。但具体注册 DSL、扫描能力和 Boot 自动配置随版本而异,不能假设所有项目自动扫描所有接口。
六、超时和连接池不能靠代理自动解决
以 JDK HTTP Client 为例:
java
HttpClient jdkClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
JdkClientHttpRequestFactory requestFactory =
new JdkClientHttpRequestFactory(jdkClient);
requestFactory.setReadTimeout(Duration.ofSeconds(5));
RestClient client = RestClient.builder()
.requestFactory(requestFactory)
.baseUrl("https://catalog.example.invalid")
.build();
连接超时、读取超时、业务总超时不是同一个概念。底层客户端的连接复用、超时和取消语义也可能不同,需要针对所用版本测试。
还要考虑:
- 并发上限:下游变慢时不能耗尽本服务线程。
- 请求总预算:不能无限等待重试。
- 连接复用:避免每个请求都创建新的底层连接资源。
- 调用链取消:用户取消请求不一定立刻取消底层网络调用。
- 观测指标:按下游区分错误率、延迟与超时次数。
七、404、429、500 的处理不能相同
RestClient.retrieve() 默认会对 4xx、5xx 进行状态码错误处理。HTTP Interface 代理会受底层客户端配置影响。
例如可以集中处理 5xx:
java
RestClient client = RestClient.builder()
.baseUrl("https://catalog.example.invalid")
.defaultStatusHandler(
HttpStatusCode::is5xxServerError,
(request, response) -> {
throw new IllegalStateException(
"catalog unavailable");
})
.build();
真实项目应定义业务异常类型和错误码,不要把下游响应中的敏感字段直接返回给前端。
| 场景 | 处理建议 |
|---|---|
| GET 临时网络错误 | 可考虑有限重试 |
| 429 | 尊重 Retry-After 与限流预算 |
| 400 | 优先修复请求参数 |
| 401 / 403 | 排查认证和权限 |
| POST 创建订单 | 先确认幂等能力 |
| 超时但可能执行成功 | 查询下游结果,避免盲目重发 |
HTTP 方法语义是重试判断的重要依据,但还要结合真实业务契约。不能因为用了接口代理就自动重试所有请求。
八、如何测试 HTTP Interface?
可以使用 Spring Test 的 MockRestServiceServer 绑定 RestClient.Builder,验证路径、方法和 JSON 解析:
java
RestClient.Builder builder = RestClient.builder()
.baseUrl("https://catalog.example.invalid");
MockRestServiceServer server =
MockRestServiceServer.bindTo(builder).build();
RestClient client = builder.build();
ProductHttpApi api = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(client))
.build()
.createClient(ProductHttpApi.class);
server.expect(requestTo(
"https://catalog.example.invalid/api/products/42"))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess("""
{"id":42,"name":"Keyboard","price":199.00}
""", MediaType.APPLICATION_JSON));
ProductDto result = api.getProduct(42);
assertEquals(42L, result.id());
server.verify();
上面是测试方法主体片段,需要 JUnit、spring-test 和静态导入;未在本次环境独立编译。
建议补充的契约测试:
- 路径变量、查询参数编码是否正确。
- 请求体字段和 Content-Type 是否符合契约。
- 404、429、500 是否转换成预期异常。
- JSON 缺失字段是否触发业务校验。
- 超时是否能在预算内返回。
- 鉴权头是否正确传递且不会写入敏感日志。
Mock 测试不会验证真实 DNS、TLS、网关、负载均衡和下游接口版本;上线前仍需要测试环境集成验证。
九、从 RestTemplate 或 Feign 迁移,怎么选?
不要为了追新一次性重写全部 HTTP 调用。先盘点下游接口:
- 是否同步还是响应式。
- 是否依赖服务发现和负载均衡。
- 是否有特殊拦截器、鉴权和请求签名。
- 是否使用重试、熔断、指标和链路追踪。
- 是否需要流式下载、文件上传或特殊编码。
建议先选择一个只读、接口简单的下游,抽出 HTTP Interface,补齐 Mock 与集成测试,核对错误码、超时、请求头和序列化行为,再逐步迁移。
如果原项目使用 Spring Cloud OpenFeign 的生态能力,不能只比较注解数量。迁移时需要按实际 Spring Cloud 版本核对服务发现、负载均衡、拦截器和容错方案。
十、常见问题
HTTP Interface 是 Spring 7 才有的吗?
不是。它在更早的 Spring Framework 版本中已经存在。本文关注的是 Spring 7 生态下的使用与治理。
HTTP Interface 能代替 RestClient 吗?
它提供声明式调用抽象,底层仍需要 RestClient、WebClient 等适配器。
会自动处理认证和重试吗?
不会。应通过底层客户端、拦截器和业务策略实现,避免泄露凭据或错误地跨租户复用认证信息。
可以在 WebFlux 项目中使用吗?
可以评估基于 WebClient 的适配方式,但不能在响应式事件循环里随意执行阻塞调用。
所有 Feign 接口都值得迁移吗?
不一定。要比较现有生态能力、迁移成本和可测试性,避免为了减少几行代码而引入运行风险。
总结
Spring HTTP Interface 的价值是集中声明 HTTP 契约,减少重复调用代码,并统一客户端配置。它不是自动提升性能或自动提供熔断的工具。
小型项目继续直接使用 RestClient 完全合理;当外部接口数量较多时,声明式接口与 Spring 7 的客户端注册能力值得评估。