bigywater.tech
Spring · 52

RestClient、RestTemplate 与 HTTP Interface:连接池与超时

RestClient、RestTemplate、HTTP Interface 底层都用什么?默认有没有超时?连接池要不要自己配?为什么我设的 read-timeout 没生效?返回 404 是抛异常还是返回?这篇用一个会计数连接数的本地服务端,在 Spring Boot 4.0.8 上逐个测量。

0 → 522 ms
读超时:不配置时等 3051 ms,用 spring.http.clients.read-timeout=500ms 后 522 ms 就抛异常
1 / 20
顺序 20 次请求只建 1 个连接,20 线程并发建 20 个连接
5 个
加上 Apache HttpClient 后,30 个并发请求默认只用 5 个连接(总用时 1886 ms)

选了什么底层

实测 · 本机执行输出
  ClientHttpRequestFactoryBuilder.detect() 建出的工厂 = JdkClientHttpRequestFactory
  RestTemplateBuilder.build() 的工厂 = JdkClientHttpRequestFactory
  new RestTemplate() 的工厂 = SimpleClientHttpRequestFactory
  spring.http.clients.imperative.factory=simple 时:
  ClientHttpRequestFactoryBuilder.detect() 建出的工厂 = JdkClientHttpRequestFactory
  RestTemplateBuilder.build() 的工厂 = SimpleClientHttpsRequestFactory
  new RestTemplate() 的工厂 = SimpleClientHttpRequestFactory
  spring.http.clients.imperative.factory=simple 时 RestClient 的请求头:
  rc-boot 服务端看到的请求头 = {"User-Agent": "Java/21.0.12.1", "Accept": "*/*", "Connection": "keep-alive"}

有 Boot 配置时没有 Apache 就用 JDK HttpClient;new RestTemplate() 用 HttpURLConnection。

请求头

实测 · 本机执行输出
  rc-boot 服务端看到的请求头 = {"User-Agent": "Java-http-client/21.0.12.1", "Connection": "Upgrade, HTTP2-Settings", "Upgrade": "h2c", "HTTP2-Settings": "AAEAAEAAAAIAAAAAAAMAAAAAAAQBAAAAAAUAAEAAAAYABgAA", "Accept-Encoding": "gzip"}
  rc-create 服务端看到的请求头 = {"User-Agent": "Java-http-client/21.0.12.1", "Connection": "Upgrade, HTTP2-Settings", "Upgrade": "h2c", "HTTP2-Settings": "AAEAAEAAAAIAAAAAAAMAAAAAAAQBAAAAAAUAAEAAAAYABgAA", "Accept-Encoding": "gzip"}
  rt-boot 服务端看到的请求头 = {"User-Agent": "Java-http-client/21.0.12.1", "Accept": "text/plain, application/json, application/*+json, */*", "Connection": "Upgrade, HTTP2-Settings", "Upgrade": "h2c", "HTTP2-Settings": "AAEAAEAAAAIAAAAAAAMAAAAAAAQBAAAAAAUAAEAAAAYABgAA", "Accept-Encoding": "gzip"}
  rt-new 服务端看到的请求头 = {"User-Agent": "Java/21.0.12.1", "Accept": "text/plain, application/json, application/*+json, */*", "Connection": "keep-alive"}

JDK 客户端在明文 HTTP 上会发 Upgrade: h2c。

连接复用

实测 · 本机执行输出
  顺序 20 次 Boot 的 RestClient.Builder:服务端收到 20 个请求、1 个新连接
  并发 20 个线程(每个请求在服务端睡 300 ms)Boot 的 RestClient.Builder:服务端收到 20 个请求、20 个新连接;rc-boot par 20 次完成,用时 376 ms
  顺序 20 次 RestClient.create():服务端收到 20 个请求、1 个新连接
  并发 20 个线程(每个请求在服务端睡 300 ms)RestClient.create():服务端收到 20 个请求、20 个新连接;rc-create par 20 次完成,用时 387 ms
  顺序 20 次 RestTemplateBuilder:服务端收到 20 个请求、1 个新连接
  并发 20 个线程(每个请求在服务端睡 300 ms)RestTemplateBuilder:服务端收到 20 个请求、20 个新连接;rt-boot par 20 次完成,用时 382 ms
  顺序 20 次 new RestTemplate():服务端收到 20 个请求、1 个新连接
  并发 20 个线程(每个请求在服务端睡 300 ms)new RestTemplate():服务端收到 20 个请求、20 个新连接;rt-new par 20 次完成,用时 338 ms

keep-alive 自动复用,并发时没有默认上限。

读超时

实测 · 本机执行输出
  不配置任何超时:
  RestClient 读到 slow-done,用时 3051 ms
  RestTemplate(Boot 构建)读到 slow-done,用时 3011 ms
  new RestTemplate() 读到 slow-done,用时 3013 ms
  spring.http.client.read-timeout=500ms(旧的属性名,没有 s):
  RestClient 读到 slow-done,用时 3046 ms
  RestTemplate(Boot 构建)读到 slow-done,用时 3010 ms
  new RestTemplate() 读到 slow-done,用时 3011 ms
  spring.http.clients.read-timeout=500ms:
  RestClient 异常 ResourceAccessException ← HttpTimeoutException,用时 522 ms
  RestTemplate(Boot 构建)异常 ResourceAccessException ← HttpTimeoutException,用时 514 ms
  new RestTemplate() 读到 slow-done,用时 3012 ms

默认没有;旧属性名静默无效;只对 Boot 构建的客户端生效。

连接超时

实测 · 本机执行输出
  spring.http.clients.connect-timeout=300ms:
  RestClient 连 10.255.255.1:81 异常 ResourceAccessException ← HttpConnectTimeoutException ← ConnectException,用时 331 ms
  不配置连接超时(最多等 20 秒):
  20 秒内没有返回,测试程序被强行终止(rc=-9)

不配置时连不可达地址会挂很久。

状态码

实测 · 本机执行输出
  retrieve().body(String) /s/200 → 返回 'body-of-200'
  retrieve().body(String) /s/204 → 返回 null
  retrieve().body(String) /s/404 → 抛 NotFound,状态 404,body='body-of-404'
  retrieve().body(String) /s/500 → 抛 InternalServerError,状态 500,body='body-of-500'
  exchange() /s/404 → 不抛,拿到 '404 body-of-404'
  onStatus 自定义 /s/404 → 抛 IllegalStateException: 自定义 4xx 处理:404 NOT_FOUND
  onStatus 吞掉 500 → toBodilessEntity 状态 500
  RestTemplate /s/404 → 抛 NotFound

4xx/5xx 默认抛异常,exchange 与 onStatus 可改。

HTTP Interface

实测 · 本机执行输出
  api.echo(x=a b, h=H1) → x=a b,X-Test=H1
  api.status(200) → body-of-200
  api.statusEntity(404) 通过 ResponseEntity 接 → 仍然会抛?
  api.statusEntity(404) 抛 NotFound
  api.status(500) 抛 InternalServerError 状态 500

声明式接口,异常行为与 RestClient 一致。

重定向

实测 · 本机执行输出
  默认:
  RestClient GET /redirect → 状态 200 body='ok'
  RestTemplate(Boot 构建)GET /redirect → 'ok'
  new RestTemplate() GET /redirect → 'ok'
  spring.http.clients.redirects=dont-follow:
  RestClient GET /redirect → 状态 302 body='null'
  RestTemplate(Boot 构建)GET /redirect → 'null'
  new RestTemplate() GET /redirect → 'ok'

默认跟随;dont-follow 只对 Boot 构建的客户端生效。

Apache 连接池

实测 · 本机执行输出
  detect() 建出的工厂 = HttpComponentsClientHttpRequestFactory
  Boot 的 RestClient.Builder(默认):30 个并发请求,成功 30,失败 {},总用时 1886 ms
      服务端收到 30 个请求、5 个新连接
  自己配的连接池(总 100、每个路由 50):30 个并发请求,成功 30,失败 {},总用时 361 ms
      服务端收到 30 个请求、30 个新连接
  连接池只有 2 个连接、等连接最多 200 ms:6 个并发请求,成功 2,失败 {ERR ResourceAccessException ← ConnectionRequestTimeoutException=4},总用时 642 ms
      服务端收到 2 个请求、2 个新连接

默认每个路由 5 个连接;池大小和等连接超时要显式配。

分步演示

连接池与超时
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 Spring Boot 4.0.8 的 SpringApplication(spring-boot-starter-restclient,非 Web 模式,JDK 21;最后一节加 org.apache.httpcomponents.client5:httpclient5);被调用的服务是本机的 Python http.server(HTTP/1.1,keep-alive),在服务端统计新连接数;每个场景单独启动一个 JVM;属性名的有效性靠实测效果判断(Boot 的配置元数据里同时有 spring.http.client.* 和 spring.http.clients.*)
    • 没有验证:Jetty、Reactor Netty 作为底层时的行为;HTTP/2 实际协商成功时的连接复用(服务端不支持 h2c);TLS 与 SSL bundle;重试与熔断(见 Cloud 与 @Retryable 的章节);连接池的空闲回收与存活检测;请求拦截器与日志;JDK HttpClient 的连接池参数(jdk.httpclient.* 系统属性);响应体很大时的流式读取;WebClient(见第 59 篇)
    京ICP备2026067502号-1京公网安备11010502063487号