Spring Boot 里用 Apache HttpClient 5 做连接池,升级到 5.7-beta1 后编译器开始抱怨 evictIdleConnections。官方建议改成配置 keep-alive / ConnectionConfig,再开 evictExpiredConnections。按字面只换这一处,空闲连接会继续堆在池里。

实测环境:Java 21.0.12.1、Spring Boot 4.1.2、HttpClient 5.7-beta1。对端是同进程 Tomcat,响应头带 Keep-Alive: timeout=60。

谁会撞上

RestTemplate / 自建 CloseableHttpClient,并且显式开过 evictIdleConnections(…) 的服务。升到 5.7 系列后,这个 API 打了 @Deprecated,Javadoc 指向 keep-alive 配置和 evictExpiredConnections()。

编译期就能看见警告:

[WARNING] ... DemoApplication.java:[99,21] evictIdleConnections(org.apache.hc.core5.util.TimeValue)
in org.apache.hc.client5.http.impl.classic.HttpClientBuilder has been deprecated

反射确认:evictIdleConnections.deprecated=true。

现场:四种配法对照

同一个 /ping,池上限 4。请求结束后看 PoolStats.available,再睡 2.5 秒。关键输出如下。

=== HC 5.7-beta1 idle eviction probe ===
java=21.0.12.1
httpclient5=5.7-beta1
boot=4.1.2
evictIdleConnections.deprecated=true
evictorSleep.onlyExpired=1 MINUTES
evictorSleep.idleMax1s=1 SECONDS
serverHeaders Keep-Alive=timeout=60; Connection=keep-alive
A_OLD_evictIdle_1s afterRequest available=1 leased=0 pending=0
A_OLD_evictIdle_1s afterIdle2.5s available=0 leased=0 pending=0
B_FOOTGUN_onlyExpired afterRequest available=1 leased=0 pending=0
B_FOOTGUN_onlyExpired afterIdle2.5s available=1 leased=0 pending=0
B_FOOTGUN_onlyExpired afterManualCloseExpired available=1 leased=0 pending=0
C_FIXED_timeToLive1s_closeExpired afterRequest available=1 leased=0 pending=0
C_FIXED_timeToLive1s_closeExpired afterIdle2.5s_beforeManual available=1 leased=0 pending=0
C_FIXED_timeToLive1s_closeExpired afterManualCloseExpired available=0 leased=0 pending=0
D_idleTimeout1s_on_lease afterIdle2.5s_stillInPool available=1 leased=0 pending=0
D_idleTimeout1s_on_lease afterReuse_ms=4 available=1 leased=0 pending=0

怎么读这组数

A 仍调用已弃用的 evictIdleConnections(1s)。后台线程睡眠间隔约 1 秒,2.5 秒后 available 从 1 变成 0。旧行为还在,只是编译告警。

B 按迁移提示只留 evictExpiredConnections(),其它不动。2.5 秒后 available 仍是 1;立刻再调一次 closeExpired() 还是 1。原因有两层:

  • Tomcat 回了 Keep-Alive: timeout=60。DefaultConnectionKeepAliveStrategy 优先吃响应头里的 timeout,RequestConfig.setConnectionKeepAlive(1s) 被盖掉,池条目的过期点按 60 秒算。
  • 只开 expired 时,内置 IdleConnectionEvictor 的睡眠默认是 1 分钟(有 idle max 时才按 idle/10,且至少 1 秒)。短时间窗口里后台线程根本没起来干活。

C 在连接管理器上设 ConnectionConfig.timeToLive(1s),再 evictExpiredConnections()。2.5 秒后池里还有(后台仍按 1 分钟睡),但手动 closeExpired() 后 available=0。closeIfExpired 会额外按 timeToLive 丢连接,不受 Keep-Alive 头牵着走。

D 只设 ConnectionConfig.idleTimeout(1s)。空闲 2.5 秒后连接仍在池里(available=1);下次租借时才会按 idleTimeout 丢掉再新建。它管的是「租的时候要不要复用」,不是「后台主动清池」。

旧写法与推荐骨架

旧代码常见这样写:

PoolingHttpClientConnectionManager cm =
    PoolingHttpClientConnectionManagerBuilder.create()
        .setMaxConnTotal(200)
        .setMaxConnPerRoute(50)
        .build();

CloseableHttpClient client = HttpClients.custom()
    .setConnectionManager(cm)
    .evictIdleConnections(TimeValue.ofSeconds(30)) // 5.7 起 Deprecated
    .build();

5.7 更稳妥的方向是把生命周期写进 ConnectionConfig,后台只清「已过期」:

cm.setDefaultConnectionConfig(ConnectionConfig.custom()
    .setTimeToLive(TimeValue.ofMinutes(5))
    .setIdleTimeout(Timeout.ofSeconds(30))
    .setValidateAfterInactivity(TimeValue.ofSeconds(2))
    .build());

CloseableHttpClient client = HttpClients.custom()
    .setConnectionManager(cm)
    .setDefaultRequestConfig(RequestConfig.custom()
        .setConnectionKeepAlive(TimeValue.ofSeconds(30))
        .build())
    .evictExpiredConnections()
    .build();

注意:对端如果下发更长的 Keep-Alive: timeout=…,单靠 RequestConfig 的 keep-alive 挡不住。timeToLive / 租借时的 idleTimeout 才是你这边能钉死的上限。需要更密的清池节奏时,别指望「只开 expired」那条默认 1 分钟睡眠,要么接受这个周期,要么自己调度 closeExpired() / closeIdle()。

连带断点

  • 共享连接管理器(setConnectionManagerShared(true))时,builder 上的 evict* 根本不生效,清池要自己管。
  • 5.7 对 HTTP/2 禁用了 keep-alive strategy(和 RFC 9113 冲突)。走 H2 多路复用时,更要靠 ConnectionConfig 的 TTL / idleTimeout,以及 H2 session 池上的同类参数。
  • Fluent Facade 在 5.7 被标成大概率最后一轮,官方推 Jakarta REST 客户端。本文只覆盖 classic 池化这条升级坑。

升级时怎么选

还在 5.6.x、且 idle 驱逐跑得好好的:可以先不跟 5.7-beta;等 GA 再迁,并准备改配置而不是只删一行 API。

必须上 5.7:删 evictIdleConnections 的同时补 timeToLive 和 idleTimeout,保留或加上 evictExpiredConnections。上线后看池的 available 是否在业务空闲后回落;如果对端 Keep-Alive 很长,只改 RequestConfig 会以为迁好了,其实连接还在。

建议:把「请求后 available」「空闲 N 秒后 available」「手动 closeExpired 后 available」三组数写进回归,避免以后再被弃用提示带进同类坑。

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。