/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
Spring Boot RestClient:create() 绕过自动配置,超时和虚拟线程都没生效
YAML 里配了 spring.http.client.* 超时,也开了虚拟线程,RestClient.create() 后两者却像没写——官方文档其实早就点名了。
一、症状:超时与虚拟线程「写了等于没写」
联调或压测时,常见两类「配置明明开了、行为却像默认」的现象:
这两类问题经常叠在一起:业务代码里随手写了 RestClient.create(),或自己 new JdkClientHttpRequestFactory(…),再指望 Boot 的全局 HTTP 客户端属性与虚线程开关自动贴上去。结果 YAML 和 Customizer 全被绕过,排障时却先去怀疑下游、网关,甚至 JVM 参数。
触发场景很日常,不用等出事故才撞上:
- 从 RestTemplate / 示例仓库抄来的 RestClient.create(baseUrl),评审时没人盯自动配置路径;
- 工具类写成 static final RestClient CLIENT = RestClient.create(…),生命周期完全脱离 Spring;
- 单测里用 create() 图省事,后来同一写法进了生产配置类;
- 明确要用 JDK 客户端时,手写 HttpClient.newHttpClient() + JdkClientHttpRequestFactory,以为「已经跟 yaml 里的 factory=jdk 对齐了」。
下面只按 Spring Boot 3.5 · Calling REST Services 的原文,对照 Framework / Boot 议题,讲清楚「谁生效、谁绕过、3.5 与 4.0 差在哪」。

二、官方结论:create() 不走自动配置
Boot 3.5 文档对 RestClient 写得很直白:
Spring Boot 会为你创建并预配置一个原型(prototype) RestClient.Builder Bean。强烈建议在组件里注入它,再用它 build() 出 RestClient。该 Builder 会带上 HttpMessageConverters 以及合适的 ClientHttpRequestFactory。
定制路径分三档(由窄到宽):
全局 HTTP 客户端属性(对走自动配置的工厂生效)包括:
spring:
http:
client:
connect-timeout: 2s
read-timeout: 1s
redirects: dont–follow
# factory: jdk # 需要时显式指定
记一句口诀:YAML / Customizer 只服务「注入 Builder 再 build」这条路;create() 是旁路。 手搓 JdkClientHttpRequestFactory 再塞进自建 RestClient,同样不经过 Boot 替你装配好的那条链。超时和虚线程相关的自动装配,也就谈不上「自动」了。
还有三个容易判断错的边界:
这三点连同「强烈建议注入 Builder」一起看:yaml 多半没写错,问题出在根本没走进 Boot 替你铺好的那条路。
三、最小复现:Builder vs create()
依赖用 spring-boot-starter-web(或至少带上 RestClient 所需的 web 客户端栈)即可。先写一份「看起来很完整」的配置:
spring:
threads:
virtual:
enabled: true
http:
client:
connect-timeout: 2s
read-timeout: 1s
factory: jdk
再对比两个 Bean:一个走官方 Builder,一个静态 create()。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
@Configuration(proxyBeanMethods = false)
public class RestClientCompareConfig {
/** 官方路径:继承自动配置的 converters / request factory / http.client.* */
@Bean
RestClient configuredClient(RestClient.Builder builder) {
return builder.baseUrl("http://127.0.0.1:18080").build();
}
/** 旁路:文档明确 —— 无自动配置、无 RestClientCustomizer */
@Bean
RestClient rawCreateClient() {
return RestClient.create("http://127.0.0.1:18080");
}
}
本地用一个故意 sleep 超过 1s 的下游(任意 stub 即可)分别调用:
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.ResourceAccessException;
@Configuration(proxyBeanMethods = false)
class ProbeConfig {
@Bean
ApplicationRunner probe(RestClient configuredClient, RestClient rawCreateClient) {
return args -> {
call("builder", configuredClient);
call("create", rawCreateClient);
};
}
private static void call(String tag, RestClient client) {
long t0 = System.nanoTime();
try {
client.get().uri("/slow").retrieve().toBodilessEntity();
System.out.println(tag + " OK ms=" + (System.nanoTime() – t0) / 1_000_000);
} catch (ResourceAccessException ex) {
System.out.println(tag + " TIMEOUT/IO ms="
+ (System.nanoTime() – t0) / 1_000_000
+ " -> " + ex.getClass().getSimpleName());
}
}
}
预期现象(看趋势,别盯固定毫秒数):
- configuredClient:在 Boot 已把 read-timeout: 1s 接到自动配置的 ClientHttpRequestFactory 时,慢接口更容易在约 1s 量级以 I/O / 超时类异常失败;
- rawCreateClient:未走自动配置,不会因为你写了 spring.http.client.read-timeout 就自动带上同样的工厂设置——表现为「更像默认、更拖」。
复现时注意:下游 sleep 要明显大于 read-timeout(例如 sleep 5s、超时 1s),并在同一进程内先后打两次,免得把「网络抖动」误读成配置差异。如果 classpath 上还有 Apache HttpClient 等更高优先级实现,而 yaml 未写 factory: jdk,自动配置可能根本没落到 JDK。这时先固定 spring.http.client.factory,再看虚线程 worker 形态,不然两边比的不是同一条客户端栈。
若你还手写了:
import java.net.http.HttpClient;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
RestClient handBuilt = RestClient.builder()
.requestFactory(new JdkClientHttpRequestFactory(HttpClient.newHttpClient()))
.baseUrl("http://127.0.0.1:18080")
.build();
这里用的是自己 new 的 JDK HttpClient / 工厂,同样不会自动吃到 Boot 根据 spring.http.client.* 与(在支持版本上)虚线程开关装配好的那份工厂。正确做法还是:注入 Boot 的 RestClient.Builder,或通过文档给出的 ClientHttpRequestFactoryBuilder / ClientHttpRequestFactoryBuilderCustomizer 参与自动配置链。
可选的应用级 Customizer(只对 Builder 路径生效):
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
@Configuration(proxyBeanMethods = false)
public class RestClientCustomizerConfig {
@Bean
RestClientCustomizer demoCustomizer() {
return builder -> builder.defaultHeader(HttpHeaders.USER_AGENT, "demo-boot-restclient");
}
}
对 RestClient.create() 出来的实例,这个 Customizer 不会被调用——文档就是这么规定的,别当偶发 bug 查。
四、JDK HttpClient 与虚拟线程:3.5 vs 4.0
超时问题用「改注入 Builder」通常就能对齐。虚拟线程要再拆一层:谁在跑 JDK HttpClient 的阻塞适配 / worker。
依据链(按议题归属,勿混版本):
| Framework #34393 | RestClient 包装各 HTTP 库;JDK HttpClient 需要显式 executor 才会按虚线程模型跑 |
| Boot #45348 | 希望「开启 spring.threads.virtual.enabled 时,自动配置的 JDK 工厂也挂上 VT executor」;后续由 PR 落地 |
| Boot PR #46404 | 合入 Spring Boot 4.0:对自动配置侧的 JDK ClientHttpRequestFactory / Builder 路径补齐虚线程 executor |
| Gateway #4150 | 社区报告:出站 JDK HttpClient worker 池忽略 spring.threads.virtual.enabled、仍见平台线程;维护者讨论指向 Boot 侧修复 / 显式 virtual executor;这里只作症状归因参考,不当自测基准 |
还停在 Spring Boot 3.5 的项目,说法要收着点:
- 不要声称 PR #46404「已经在 3.5 里生效」;
- 3.5 上注入 RestClient.Builder,仍然能拿到:消息转换器 + 合适的请求工厂 + spring.http.client.* 超时/重定向等;
- JDK 客户端的 VT executor 自动挂载,要以 Boot 4.0 + #46404 为准;3.5 若必须让 JDK HttpClient 跑在虚线程上,需要自行通过 HttpClient.Builder.executor(…)、或 ClientHttpRequestFactoryBuilderCustomizer / 自定义 ClientHttpRequestFactoryBuilder 参与装配(文档「Global HTTP Client Configuration」一节给的就是这条扩展点)。
拆成两张账单看,不容易说错话:
| 全局 HTTP 客户端超时 | spring.http.client.*,走自动配置应生效 | 不生效 | 前缀改为 spring.http.clients.*,应生效 |
| RestClientCustomizer | 应用 | 不应用 | 应用 |
| JDK HttpClient VT executor | 需自挂 / 自定义 | 默认平台线程池倾向 | #46404 自动挂载(仍须 Builder) |
Gateway #4150 里维护者讨论把「出站 JDK worker 仍是平台线程」指向 Boot 侧修复或显式 virtual executor——这与上表一致:开关开了 ≠ 所有 HTTP 客户端实现都自动吃到。引用该 issue 时请标明「社区报告的症状」,不要改写成「我们压测提升了若干倍」。
示意(3.5 自制 executor,仅演示方向,按你们底座微调):
import java.net.http.HttpClient;
import java.util.concurrent.Executors;
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class JdkVtClientConfig {
@Bean
ClientHttpRequestFactoryBuilder<?> jdkWithVirtualExecutor() {
return ClientHttpRequestFactoryBuilder.jdk()
.withHttpClientCustomizer(builder ->
builder.executor(Executors.newVirtualThreadPerTaskExecutor()));
}
}
升级到 Boot 4.0 后:在 spring.threads.virtual.enabled=true 且走自动配置 Builder 时,JDK 工厂侧的 VT executor 由 #46404 补齐;但 RestClient.create() 依然绕过自动配置——版本升级解决不了「你根本没走 Builder」这个问题。另外,4.x 文档里全局属性前缀已改成 spring.http.clients.*(多了个 s),升级时记得一并迁移 YAML,否则超时同样会「写了没生效」。
五、排障清单

按顺序查,避免一上来改 JVM 或下游:
六、小结
RestClient.create()(以及手搓 JDK 请求工厂)会完整绕过 Spring Boot 的 RestClient 自动配置与 RestClientCustomizer,所以 spring.http.client.* 超时「写了也不生效」。官方给的修法是注入原型 RestClient.Builder,再写一遍 YAML 没用。虚拟线程方面:Framework 侧要求 JDK HttpClient 显式 executor;Boot 4.0 的 PR #46404 把自动配置 Builder 路径补上;3.5 请明确说「Builder 已覆盖 converters + 工厂 + http.client 超时,JDK VT 自动挂载需自定义或升级」,切勿把 4.0 的修复提前安到 3.5 头上。
落地顺序建议:先全库消灭旁路 create() / 手搓工厂 → 用慢下游核对超时 → 再按大版本处理 JDK VT。这样排障成本最低,也不会在错误的客户端实例上调虚线程开关。
网硕互联帮助中心





评论前必须登录!
注册