Spring 生态下,RestTemplate 默认是没有连接池的(它用的是 JDK 的 HttpURLConnection),如果不配置,每一次请求都会新建连接,这对一天的飞书长周期调用是灾难性的。
我们需要将 RestTemplate 的底层 HTTP 客户端替换为支持池化的实现(Apache HttpClient 或 OkHttp )。下面我以 Apache HttpClient 为例(企业级最稳定),展示如何将"池化思想"完整注入。
第一步:引入依赖(Maven/Gradle)
<!-- Apache HttpClient 连接池实现 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
第二步:构建带池化的 RestTemplate(核心配置类)
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.TimeUnit;
@Configuration
public class RestTemplatePoolConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate(clientHttpRequestFactory());
}
private ClientHttpRequestFactory clientHttpRequestFactory() {
// 1. 创建连接池管理器(核心)
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
// 【关键配置】最大总连接数(应对一天内的高并发突发流量)
connectionManager.setMaxTotal(200);
// 【关键配置】每个路由(即每个域名:open.feishu.cn)的默认最大连接数
connectionManager.setDefaultMaxPerRoute(50);
// 【可选】针对飞书单独设置更精细的路由限制
// HttpHost feishuHost = new HttpHost("open.feishu.cn", 443, "https");
// connectionManager.setMaxPerRoute(new HttpRoute(feishuHost), 80);
// 2. 构建 HttpClient
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
// 【池化关键】启用连接池的存活探测(清理僵尸连接)
builder.evictExpiredConnections();
builder.evictIdleConnections(30, TimeUnit.SECONDS); // 空闲超过30秒的连接会被回收
// 3. 包装成 RestTemplate 可识别的 Factory
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(builder.build());
// 【超时配置】防止某个慢请求占住连接池资源不放
factory.setConnectionRequestTimeout(5000); // 从连接池获取连接的超时时间(池满时等待)
factory.setConnectTimeout(3000); // 建立TCP连接的超时
factory.setReadTimeout(10000); // 读取飞书响应数据的超时
return factory;
}
}
第三步:结合"令牌池/工作池"使用(完整调用示例)
光有连接池还不够,必须配合**信号量(Semaphore)**控制并发数,否则即使连接池有200个连接,也会因为飞书限流(429)产生大量无效重试,浪费资源。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
@Service
public class FeishuApiService {
@Autowired
private RestTemplate restTemplate; // 使用上面配置好的带池化的实例
// 工作池(信号量):控制同时只有30个请求在飞书上运行
private final Semaphore semaphore = new Semaphore(30);
// 模拟获取飞书用户信息
public String fetchUser(String userId) {
// 尝试获取信号量,如果池满则阻塞等待(最多等3秒,避免线程永久挂起)
try {
if (!semaphore.tryAcquire(3, TimeUnit.SECONDS)) {
throw new RuntimeException("系统繁忙,请稍后重试");
}
// 1. 获取Token(假设从缓存/令牌池取)
String token = getCachedTenantAccessToken();
// 2. 构造请求
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + token);
HttpEntity<String> entity = new HttpEntity<>(headers);
// 3. 发起HTTP请求(连接直接从池中复用)
String url = "https://open.feishu.cn/open-apis/contact/v3/users/" + userId;
ResponseEntity<String> response = restTemplate.exchange(
url,
HttpMethod.GET,
entity,
String.class
);
// 4. 处理飞书限流(429)------ 释放信号量前处理
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody();
} else if (response.getStatusCode().value() == 429) {
// 触发限流,让当前线程休眠退避,不占用信号量
TimeUnit.SECONDS.sleep(2);
// 可以配合重试模板(Spring Retry)重新发起
return retryWithBackoff(userId);
}
return response.getBody();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
// 【务必释放信号量】无论成功还是异常,都要释放工作池的容量
semaphore.release();
}
}
private String getCachedTenantAccessToken() {
// 这里使用本地缓存(如 Caffeine),过期前5分钟异步刷新
return "fake_token";
}
}
第四步:针对一天的"数据池"优化(批量聚合)
如果你的场景是拉取10000个用户详情,不要 循环调用上面的 fetchUser。用批量接口 + 分批提交:
// 数据池思想:每50个ID攒一批,发起一次批量请求
public List<String> fetchUsersBatch(List<String> userIds) {
List<String> results = new ArrayList<>();
// 将用户ID分成每50个一组(飞书批量接口通常限制最大100)
List<List<String>> partitions = Lists.partition(userIds, 50);
for (List<String> batch : partitions) {
// 使用信号量控制并发批次(避免同时发太多批次)
semaphore.acquire();
try {
// 调用飞书的批量查询接口(如果有)
// POST https://open.feishu.cn/open-apis/contact/v3/users/batch_get
String response = restTemplate.postForObject(url, batch, String.class);
results.add(response);
} finally {
semaphore.release();
}
}
return results;
}
关键参数调优指南(针对一天的长周期)
| 参数 | 推荐值 | 理由 |
|---|---|---|
maxTotal |
200 | 即便有突发流量,200个连接足够;过大反而占用系统文件描述符。 |
defaultMaxPerRoute |
50 | 飞书单个域名下,50个并发连接已经能跑满大部分API的QPS限额。 |
evictIdleConnections |
30秒 | 飞书服务端可能主动断开空闲连接,提前清理避免拿到死连接报错。 |
connectionRequestTimeout |
5秒 | 如果连接池被占满,5秒内拿不到连接就快速失败,避免线程积压。 |
Semaphore(信号量) |
20~30 | 比连接数略小,提前在业务层限流,降低触发飞书429的概率。 |
与"令牌池"联动的最佳实践
把 RestTemplate 的 连接池 和 令牌桶限流器 结合:
// 使用 Guava RateLimiter 做令牌池(每秒放行20个请求)
private final RateLimiter rateLimiter = RateLimiter.create(20.0);
// 在调用 RestTemplate 之前
rateLimiter.acquire(); // 阻塞直到获取到令牌
ResponseEntity<String> response = restTemplate.exchange(...);
这样你就有三层防护:
-
连接池 → 复用TCP连接,降低CPU。
-
信号量 → 控制最大并发,防止内存溢出。
-
令牌桶 → 平滑QPS,避免触发飞书限流。
避坑提醒
-
不要 在每次请求时
new RestTemplate(),必须单例注入,否则连接池不生效。 -
如果你的服务是微服务架构且使用 Spring Cloud Feign ,它底层默认也支持 HttpClient 池化,配置方式类似(设置
feign.httpclient.enabled=true)。 -
一天的长周期运行,建议监控连接池状态:
PoolingHttpClientConnectionManager cm = ...;
int leased = cm.getTotalStats().getLeased(); // 当前正在使用的连接数
int pending = cm.getTotalStats().getPending(); // 等待获取连接的线程数