Spring Boot 4:spring.http.clients 一把梭,RestClient 与 WebClient 共用超时
升级 Boot 4 后别再各写一套 client / reactiveclient;全局超时与重定向收敛到 spring.http.clients,注入 Builder 才生效。
一、痛点:3.5 两套前缀,升到 4.0 先改名再统一
Boot 3.5 时代,命令式客户端看 spring.http.client.*,响应式再看 spring.http.reactiveclient.*。同一套「连超时 / 读超时 / 是否跟随重定向」往往要抄两遍,评审时还容易漏改一侧。升到 Boot 4.0 之后,Configuration Changelog 明确把这两套键标成弃用,并指向新的公共前缀 spring.http.clients.*。
落地时常碰到三类问题,还经常叠在一起:
- 旧键还留在 yaml:升版后以为「超时还在」,其实绑定目标已经换了;联调时下游故意拖慢,客户端却仍长时间挂起,排障先去怪网关。
- 工厂 / 连接器名字变了 :原来的
spring.http.client.factory要迁到spring.http.clients.imperative.factory,响应式侧则是spring.http.clients.reactive.connector;只改了超时、忘了改工厂键,classpath 上多个实现时仍可能选到你不想要的那一个。 - 仍用
RestClient.create()/WebClient.create():全局属性只服务自动配置链,旁路实例用不上。这点和 2026-09-26 那篇「create()绕过自动配置」互补,这里只提一句,旁路细节不再展开。
还有一类更隐蔽的误判:有人以为「服务端开了 API 版本协商,出站客户端会自动带上同一套策略」。Boot 4 文档写得很清楚:服务端 API 版本配置不会用来自动配置客户端;客户端若要版本协商,必须在 Builder 或 HTTP Service 分组上显式设置。这和「全局超时统一」是两条线,别混进同一次 yaml 改动的预期里。
下面按 Boot 4.0 · Calling REST Services 和上面的 Changelog,把改名对照、全局属性怎么落到 Builder、HTTP Service 分组如何覆盖讲清楚。文中不给延迟基准数据;文档没说旧键在 Boot 4 上还能绑定,这里也不做这个假设;出现的属性名都以文档和 Changelog 为准。

二、改名对照:从 client / reactiveclient 到 clients
Changelog 和 OpenRewrite 的 Boot 4 properties 配方给出的核心映射,整理成一张表:
| Boot 3.5(弃用方向) | Boot 4.0 |
|---|---|
spring.http.client.*(全局超时等) |
spring.http.clients.* |
spring.http.client.factory |
spring.http.clients.imperative.factory |
spring.http.reactiveclient.* |
并入 spring.http.clients.*,连接器用 spring.http.clients.reactive.connector |
绑定类在 Boot 4 侧是 HttpClientsProperties,前缀就是 spring.http.clients。迁配置时优先整段替换前缀,再核对工厂 / 连接器键名;不要假设「旧键写着还能悄悄生效」。若仓库里同时存在 application.yml、profile 专属文件和配置中心远端片段,三处都要搜一遍,避免「本地改了、远端仍是旧前缀」。
Boot 3.5 下用 spring.http.client.* 配了超时、RestClient.create() 却不生效的排查过程,见本账号 9/26 那篇《RestClient.create()绕过超时与虚拟线程配置》;本篇接着讲 4.0 的新前缀怎么迁。
文档给的全局示例很直接,对所有走自动配置的 HTTP 客户端生效:
yaml
spring:
http:
clients:
connect-timeout: 2s
read-timeout: 1s
redirects: dont-follow
这些公共设置对应文档里的 HttpClientSettings:连接超时、读超时、重定向策略,以及需要时的 SSL bundle。需要指定底层实现时,命令式与响应式分开挑:
yaml
spring:
http:
clients:
connect-timeout: 2s
read-timeout: 1s
redirects: dont-follow
imperative:
factory: jetty # RestClient / RestTemplate 一侧
reactive:
connector: jetty # WebClient 一侧
命令式侧 classpath 探测顺序(未显式指定时)是:Apache HttpClient → Jetty → Reactor Netty → JDK HttpClient → Simple。响应式侧则是 Reactor Netty → Jetty RS → Apache → JDK。显式写 imperative.factory / reactive.connector,只为在「classpath 上有多个实现」时锁死选择,属性前缀还是同一套。多模块工程里尤其容易「传递依赖悄悄带进另一套客户端」,升版后建议在启动日志或 Actuator 条件报告里确认最终选中的实现。
OpenRewrite 的 SpringBootProperties_4_0 一类配方可以批量改键名,适合当 PR 的第一步。改完仍要用一次真实出站调用确认超时与重定向:配方改的是属性树,改不了你代码里的 create() 旁路。
三、生效路径:注入原型 Builder,别用 create()
Boot 会为你准备原型(prototype) RestClient.Builder 与 WebClient.Builder。官方建议在组件里注入它们,再 baseUrl(...).build()。只有这条路才会带上消息转换、合适的 ClientHttpRequestFactory / ClientHttpConnector,以及上面的 spring.http.clients.*。
java
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class OrderRemoteClient {
private final RestClient restClient;
public OrderRemoteClient(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.baseUrl("https://example.org").build();
}
public String fetchOrder(String id) {
return this.restClient.get()
.uri("/orders/{id}", id)
.retrieve()
.body(String.class);
}
}
响应式同理,注入 WebClient.Builder 再 build;需要多个 baseUrl 时,可先 builder.clone() 再分别定制,避免在同一个有状态 Builder 上改完 A 客户端又污染 B 客户端。若需要应用级增量定制,声明 RestClientCustomizer / WebClientCustomizer;更细的底层定制可用 ClientHttpRequestFactoryBuilderCustomizer 或自建 ClientHttpRequestFactoryBuilder / ClientHttpConnectorBuilder Bean。声明了自建 Builder,自动配置会退让,适合插自定义 ProxySelector、细调连接池这类场景。
文档原意要记牢:RestClient.create() / WebClient.create() 不走自动配置,也不应用对应 Customizer。 静态 RestClient.builder() 同样只是空白建造者,不等于容器里那个预配置原型。升版迁属性时,若业务代码仍在旁路建客户端,yaml 改得再漂亮也贴不上去。先改注入路径,再谈超时数字。工具类里 static final RestClient CLIENT = RestClient.create(...) 是最典型的漏网之鱼:生命周期完全脱离 Spring,评审 yaml 时根本看不见。
RestTemplate 老代码若暂时不迁到 RestClient,仍走自动配置的 RestTemplateBuilder;文档同样提示可以改全局 HTTP 客户端配置。所以 spring.http.clients 的覆盖面是「所有走自动配置的 HTTP 客户端」,不只新 API。迁移批次上可以「先统一属性前缀与 Builder 注入,再逐步把 RestTemplate 换成 RestClient」,两步不必绑死在同一个 PR。

四、HTTP Service 分组:全局底线 + serviceclient 覆盖
Boot 4 还加强了基于 @HttpExchange / @GetExchange 的 HTTP Service 接口客户端。用 @ImportHttpServices(group = "echo", basePackages = "...") 把接口挂到命名分组后,可用 spring.http.serviceclient.<group>.* 按组配置 base-url 和连接/读超时等(文档示例给的就是这几项)。硬编码绝对 URL 在注解里通常不适合生产;分组 + 属性才是可按环境切换的做法。
文档示例的语义是:全局 spring.http.clients 先给所有自动配置客户端定底线;分组键只覆盖该组需要差异化的项。
yaml
spring:
http:
clients:
connect-timeout: 1s
serviceclient:
echo:
base-url: "https://echo.zuplo.io"
connect-timeout: 2s
read-timeout: 2s
上面这段里,未单独覆盖的客户端仍吃全局的 connect-timeout: 1s;echo 组则把自己的连/读超时抬到 2s,并绑定 base URL。未指定 group 时,接口会落到名为 default 的分组。同一包内接口要挂到不同组时,可重复使用 @ImportHttpServices,用 types 精确列出接口类,避免错误的包扫描把无关客户端扫进同一组。
需要比属性更灵活的定制时,可声明 RestClientHttpServiceGroupConfigurer(或 WebClient 对应的 WebClientHttpServiceGroupConfigurer),由自动配置应用到各组 Builder,比如按组名注入鉴权头。再往上还有 Framework 的 AbstractHttpServiceRegistrar 可编程注册;无论用注解还是 Registrar,Boot 侧的属性与 Customizer 支持保持一致。
建议把「全局底线」定得偏紧(fail-fast),再对少数确实更慢的下游用 serviceclient 做差量放宽。反过来把全局超时开得很大、再指望个别组收紧,新增分组时很容易忘了覆盖,又回到「看起来有配置、实际很宽松」。
联调时可以用一个故意延迟超过读超时的下游 stub,分别走「注入 Builder 的 Bean」与「误留的 create() Bean」各打一枪:前者应在接近 read-timeout 的时间窗失败,后者往往继续挂起。这个对照不需要压测平台,却能直接证明「属性前缀迁对了」和「代码路径走对了」是两件必须同时成立的事。升版 PR 里把对照步骤写进测试说明,比只贴一张 Changelog 截图更有说服力。
五、迁移清单(可直接贴进 PR 描述)
- 全文检索旧前缀 :
spring.http.client.、spring.http.reactiveclient.,按 Changelog 改成spring.http.clients.,并把factory/ 响应式连接器键迁到imperative.factory/reactive.connector。配置中心、测试application-*.yml、示例仓库一并扫。 - 核对注入点 :业务 Bean 是否注入原型
RestClient.Builder/WebClient.Builder;扫掉生产路径上的create()、静态builder()自建,以及「工具类里 static final 客户端」。 - 先设全局底线 :
connect-timeout/read-timeout/redirects用文档示例量级做 fail-fast,再按下游 SLA 微调;不要指望「旧键残留 + 旁路 create」还能保护你。 - 有 HTTP Service 接口时 :给分组起名,用
spring.http.serviceclient.<group>.*覆盖 base-url 与差异超时,避免把绝对 URL 写死在注解里;多组时用重复注解 +types精确导入。 - 可选自动化:OpenRewrite Boot 4 properties 配方可批量改键名,但改完仍要用一次真实出站调用确认超时与重定向行为;有条件的话在集成测试里对故意延迟的 stub 断言失败时间上限。
- 与 09-26 篇分工 :那篇讲「为什么
create()让超时和虚线程像没写」;这篇讲「Boot 4 属性树长什么样、怎么从 3.5 迁过来」。评审时两篇对照,少踩重复坑。 - 别顺手引入未文档化的键 :以 Changelog 与 rest-client 文档为准;别凭记忆拼
spring.mvc.api-version.*之类与客户端无关的前缀,也别把 Undertow 当成 Boot 4 的常规嵌入式选项写进客户端选型。
团队协作上,可以把「HTTP 客户端配置」拆成两张检查表:一张给平台组(全局 spring.http.clients 底线、默认工厂 / 连接器、是否启用 dont-follow),一张给业务组(是否注入 Builder、分组是否声明、serviceclient 覆盖是否只放差异项)。两张表都勾完再合入主干,能减少「平台改了前缀、业务仍 create」的交叉失误。若使用配置中心,升版窗口内同时发布旧键删除与新键写入,避免滚动期间一半实例吃新前缀、一半实例仍指向已无绑定源的旧键。
Boot 4 把 RestClient / WebClient 的公共旋钮收进 spring.http.clients;改名是表,注入 Builder 才是里。 旧前缀别留念,旁路 create() 别指望,分组覆盖用 serviceclient 做差量即可。升版 checklist 里把「属性前缀 + 注入路径」当成同一项验收,比只改 yaml 更不容易复发。