云计算百科
云计算领域专业知识百科平台

RestClient.create()绕过超时与虚拟线程配置

/* 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() 后两者却像没写——官方文档其实早就点名了。

一、症状:超时与虚拟线程「写了等于没写」

联调或压测时,常见两类「配置明明开了、行为却像默认」的现象:

  • 读/连超时不生效:application.yml 已写 spring.http.client.connect-timeout、read-timeout,下游故意拖慢时,客户端仍长时间挂起,不像你设的秒级失败;
  • 虚拟线程开关「看不见」:已设 spring.threads.virtual.enabled=true(Java 21+),但对 JDK HttpClient 出站调用做 jstack / 线程观测时,仍大量看到平台线程形态的 HttpClient-* worker,而不是预期中的虚拟线程执行路径。
  • 这两类问题经常叠在一起:业务代码里随手写了 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 差在哪」。

    YAML 配置走 Builder 与 create() 绕过对照

    二、官方结论:create() 不走自动配置

    Boot 3.5 文档对 RestClient 写得很直白:

    Spring Boot 会为你创建并预配置一个原型(prototype) RestClient.Builder Bean。强烈建议在组件里注入它,再用它 build() 出 RestClient。该 Builder 会带上 HttpMessageConverters 以及合适的 ClientHttpRequestFactory。

    定制路径分三档(由窄到宽):

  • 注入自动配置的 RestClient.Builder,在局部改 baseUrl、header 等;
  • 声明 RestClientCustomizer,对所有注入点拿到的 Builder 做应用级增量定制;
  • 退回框架原 API:RestClient.create()——文档原话的意思是:此时不会应用自动配置,也不会应用 RestClientCustomizer。
  • 全局 HTTP 客户端属性(对走自动配置的工厂生效)包括:

    spring:
    http:
    client:
    connect-timeout: 2s
    read-timeout: 1s
    redirects: dont–follow
    # factory: jdk # 需要时显式指定

    记一句口诀:YAML / Customizer 只服务「注入 Builder 再 build」这条路;create() 是旁路。 手搓 JdkClientHttpRequestFactory 再塞进自建 RestClient,同样不经过 Boot 替你装配好的那条链。超时和虚线程相关的自动装配,也就谈不上「自动」了。

    还有三个容易判断错的边界:

  • 「我用了 RestClient.builder()」≠ 注入了 Boot 的 Builder。框架静态 RestClient.builder() 只是新建一个空白建造者;只有容器里那个预配置原型 Bean 才带着 converters、工厂与全局 spring.http.client.*。
  • classpath 上的 HTTP 库优先级(Apache → Jetty → Reactor Netty → JDK → Simple)只决定「自动配置会选谁」;一旦你 create() 或手搓工厂,这条检测链对你手上的实例无效。
  • 文档里的 Global HTTP Client Configuration(含 ClientHttpRequestFactoryBuilderCustomizer)同样作用在自动配置链上——旁路实例分享不到这些定制点。
  • 这三点连同「强烈建议注入 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」一节给的就是这条扩展点)。

    拆成两张账单看,不容易说错话:

    账单Boot 3.5 + 注入 BuilderBoot 3.5 + create()/手搓Boot 4.0 + 注入 Builder
    全局 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,否则超时同样会「写了没生效」。

    五、排障清单

    超时未生效与平台线程 Worker 的排障决策树

    按顺序查,避免一上来改 JVM 或下游:

  • 搜创建路径:全仓库搜 RestClient.create(、new JdkClientHttpRequestFactory、以及是否绕过注入、在工具类里静态 new;
  • 改官方路径:业务客户端改为注入 RestClient.Builder(或 clone 后再 baseUrl),确认 RestClientCustomizer 是否按预期执行(例如默认 Header 是否出现);
  • 核对全局属性:spring.http.client.connect-timeout / read-timeout / redirects / factory 是否被 profile / 环境变量覆盖;factory=jdk 时才进入 JDK 专用讨论;
  • 区分超时 vs VT:Builder 修复后超时已正常、仅 worker 仍是平台线程 → 查 Boot 大版本。3.5:自定义 executor / BuilderCustomizer;4.0+:确认已启用虚线程开关且仍走 Builder(参考 #46404,以及 Gateway #4150 的同类社区症状描述);
  • 观测手段:对比 create vs Builder 的失败耗时;jstack 看 HttpClient 相关线程是否仍为平台线程。不要用未经验证的「提升 N 倍」当结论;
  • Code Review 护栏:禁止在新代码里直接 RestClient.create() 访问需统一超时/观测的下游;测试里若用 create(),注明「故意旁路,不验证 Boot 超时」;
  • 静态工具类迁移:把 static RestClient 改成 Spring 管理的 Bean(或在 @PostConstruct / 配置类里用注入的 Builder 构建一次),避免类加载期就 create() 定终身;
  • 版本沟通:向团队同步时分开讲——「超时 / Customizer → 永远看是否注入 Builder」;「JDK VT 自动挂载 → 看是否 Boot 4.0+#46404」。混在一句话里最容易把 4.0 的修复误传到 3.5 升级说明里。
  • 六、小结

    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。这样排障成本最低,也不会在错误的客户端实例上调虚线程开关。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » RestClient.create()绕过超时与虚拟线程配置
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!