Java中Apache HttpClient与Java 11 HttpClient循环请求时触发IOException的问题排查及解决
问题分析与解决方案
看起来你遇到的是典型的连接池未复用 + 服务端连接数限制导致的连接重置问题,而且104这个固定数字也能佐证这一点——先给你拆解原因,再给你具体的代码修复方案:
为什么会出现固定104次请求后报错?
你的两段代码有一个共同的核心问题:每次请求都新建一个HttpClient实例,而不是复用全局的单例。
每次新建HttpClient都会创建新的TCP连接,当这些请求完成后,连接会进入TIME_WAIT状态(TCP协议的特性,防止迟到的数据包干扰新连接)。当短时间内创建的连接数量达到服务端的并发连接限制(或者客户端本地的临时端口耗尽,不过104这个数更偏向服务端的阈值设置),服务端就会主动重置后续的连接,也就是你看到的Connection reset/connection reset by peer错误。
而手动请求时,你是单个连接,远没达到服务端的阈值,所以能正常响应;当一批错误请求后,之前的连接逐渐从TIME_WAIT状态释放,服务端的连接数又回到阈值以下,所以请求又能正常进行了。
解决方案:复用HttpClient + 连接池优化
1. Apache HttpClient 修复方案
Apache HttpClient是线程安全的,应该全局复用一个实例,并且使用连接池来管理连接,避免频繁创建销毁连接:
// 全局单例的HttpClient和连接池 private static CloseableHttpClient httpClient; static { CredentialsProvider provider = new BasicCredentialsProvider(); provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(env.getProperty("user"), env.getProperty("password"))); // 配置连接池 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); // 设置最大总连接数 connectionManager.setMaxTotal(200); // 设置每个路由的最大连接数(针对目标服务的并发连接数) connectionManager.setDefaultMaxPerRoute(50); httpClient = HttpClientBuilder.create() .setDefaultCredentialsProvider(provider) .setConnectionManager(connectionManager) // 配置连接存活时间,复用连接 .setKeepAliveStrategy((response, context) -> 60000) // 60秒 .build(); } public String ApacheHTTP_getIceCatSpecifications(Integer Id) { String list = ""; // 原代码ArrayList<String>定义为String是笔误,这里对应调整 HttpGet httpGet = new HttpGet("http://somesite/" + Id + ".xml"); try (CloseableHttpResponse response = httpClient.execute(httpGet)) { final HttpEntity entity = response.getEntity(); if (entity != null) { try (InputStream inputStream = entity.getContent()) { // 从inputStream读取数据到list } } } catch (ClientProtocolException e) { log.error("协议错误", e); } catch (IOException e) { log.error("IO异常,产品ID:{}", Id, e); // 针对连接重置异常添加重试逻辑 if (e.getCause() instanceof SocketException && e.getMessage().contains("Connection reset")) { try { Thread.sleep(1000); // 间隔1秒重试一次 return ApacheHTTP_getIceCatSpecifications(Id); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } return list; }
2. Java 11 HttpClient 修复方案
Java 11的HttpClient同样是线程安全的,全局复用一个实例即可,同时可以配置连接池和超时参数:
// 全局单例的HttpClient private static HttpClient httpClient; static { httpClient = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_1_1) .authenticator(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication( env.getProperty("user"), env.getProperty("password").toCharArray()); } }) .connectTimeout(Duration.ofSeconds(10)) // 连接超时 .build(); } public List<String> getSpecifications_JAVA11(Integer Id) { List<String> specificationList = new ArrayList<>(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://somesite/" + Id + ".xml")) .timeout(Duration.ofSeconds(15)) // 请求超时 .build(); try { HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); try (InputStream inputStream = response.body()) { // 从inputStream读取数据到specificationList } } catch (IOException e) { e.printStackTrace(); // 针对连接重置异常添加重试 if (e.getMessage().contains("connection reset by peer")) { try { Thread.sleep(1000); // 间隔1秒重试 return getSpecifications_JAVA11(Id); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(); } return specificationList; }
额外优化建议
- 控制重试次数:不要无限重试,建议设置最多2-3次重试,避免给服务端造成不必要的压力。
- 确认服务端限制:如果有条件联系外部服务的维护方,可以确认他们的连接数限制,调整客户端的连接池参数(比如
DefaultMaxPerRoute)来匹配。 - 监控连接状态:添加日志记录连接的复用情况,方便后续排查类似问题。
内容的提问来源于stack exchange,提问作者DwnBrd




