文章

重新认识http连接池

重新认识http连接池

重新认识http连接池

最近借由公司的一个推荐工程项目,又对http连接池有了更深入的了解。

项目是使用http调用推荐的底层代码,调用的时候使用的是apache http 4.5.12 版本。 官网:https://hc.apache.org/httpcomponents-client-4.5.x/quickstart.html

线上问题

在生产环境中,当用户访问量激增时,我们的应用开始出现响应缓慢、甚至请求失败的情况。

经过排查发现,问题出在HTTP连接池上。什么意思呢?

用餐厅的比喻来理解

  • 连接池就像餐厅的餐具库房,里面放着可以重复使用的碗筷
    • 如果没有餐具库房,客人每次来用餐时,我们需要通过购买等手段先买来一套全新餐具,客户每次吃完,餐具就全部销毁
  • 当有客人来用餐时,服务员从库房取出干净的餐具
  • 客人用完餐后,餐具应该收回去清洗,然后放回库房供下次使用

我们遇到的问题

  1. 餐具用完了:库房里的干净餐具都被拿光了,新来的客人只能等待
  2. 用过的餐具没有及时回收:很多桌子用完餐后,餐具还堆在桌上没收走
  3. 恶性循环:餐具越积越多,可用的干净餐具越来越少,后面的客人就只能一直等

根本原因

我们使用的Apache HttpClient连接池就像一个管理不善的餐厅:

  • 没有设置”餐具最长使用时间”(连接存活时间)
  • 没有安排服务员定期收拾餐具(空闲连接清理)
  • 导致大量”脏餐具”长期占用空间,干净餐具不够用

问题影响

  • 客户体验差:用户请求响应缓慢,就像客人等不到餐具无法用餐
  • 服务器压力大:大量请求排队等待,就像餐厅门口排长队
  • 系统不稳定:可能导致服务崩溃,就像餐厅因为混乱而暂停营业

需要制定餐具管理制度(配置连接池回收策略),确保

  • 用完的餐具及时回收清洗
  • 定期检查和清理库房
  • 保持餐厅正常运转

解决方案

可以用两种方式设置自动回收策略

  1. 自带apache http client 自己的配置evictExpiredConnections和evictIdleConnections,不容易出错
  2. 手动添加IdleConnectionMonitorThread,容易出错,但也可以打印一些自定义日志观测数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
@Configuration
public class HttpClientConfig implements DisposableBean {

    private IdleConnectionMonitorThread monitorThread;
    private static final Logger log = LoggerFactory.getLogger(HttpClientConfig.class);

    /**
     * http连接管理器
     */
    @Bean
    public HttpClientConnectionManager poolingHttpClientConnectionManager() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        // 设置最大连接数
        connectionManager.setMaxTotal(500);
        // 设置每个路由的最大连接数
        connectionManager.setDefaultMaxPerRoute(100);
        // 设置连接存活检测时间(毫秒)3秒检查一次
        connectionManager.setValidateAfterInactivity(2000);


        this.monitorThread = new IdleConnectionMonitorThread(connectionManager);
        this.monitorThread.setDaemon(true);
        this.monitorThread.start();
        return connectionManager;
    }

    @Override
    public void destroy() throws Exception {
        if (this.monitorThread != null) {
            this.monitorThread.shutdown();
        }
    }

    /**
     * 后台线程定期主动清理连接池
     * 每10秒定期执行
     * - 关闭已过期的连接
     * - 关闭空闲超过30秒的连接
     */
    public static class IdleConnectionMonitorThread extends Thread {

        private final HttpClientConnectionManager connMgr;
        private volatile boolean shutdown;
        private final Object obj = new Object();

        public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
            super();
            this.connMgr = connMgr;
        }

        @Override
        public void run() {
            try {
                while (!shutdown) {
                    synchronized (obj) {
                        obj.wait(10000);
                        if (connMgr instanceof PoolingHttpClientConnectionManager) {
                            PoolingHttpClientConnectionManager poolingManager = (PoolingHttpClientConnectionManager) connMgr;
                            log.info("HttpClientConfig be连接池状态 最大连接数: {}, 可用连接: {}, 已租用连接: {} 挂起连接: {}",
                                    poolingManager.getTotalStats().getMax(),
                                    poolingManager.getTotalStats().getAvailable(),
                                    poolingManager.getTotalStats().getLeased(),
                                    poolingManager.getTotalStats().getPending());
                        }
                        connMgr.closeExpiredConnections();
                        connMgr.closeIdleConnections(15, TimeUnit.SECONDS);
                    }
                }
            } catch (InterruptedException ex) {
                log.error("IdleConnectionMonitorThread interrupted");
            }
        }

        public void shutdown() {
            shutdown = true;
            synchronized (obj) {
                obj.notifyAll();
            }
        }
    }


    /**
     * 请求配置
     */
    @Bean
    public RequestConfig requestConfig() {
        return RequestConfig.custom()
                // 从连接池获取连接的超时时间
                .setConnectionRequestTimeout(500)
                // 建立连接的超时时间
                .setConnectTimeout(500)
                // 数据传输的超时时间
                .setSocketTimeout(500)
                .build();
    }


    @Bean
    public CloseableHttpClient httpClient(HttpClientConnectionManager poolingHttpClientConnectionManager,
                                          RequestConfig requestConfig) {
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        // 设置http连接管理器
        httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
        // 设置请求配置
        httpClientBuilder.setDefaultRequestConfig(requestConfig);
        // 设置重试次数
        httpClientBuilder.setRetryHandler(new HttpCustomRetryHandler(3));

        //  // 创建HttpClient
        // httpClient = HttpClients.custom()
        //         .setConnectionManager(connectionManager)
        //         .setDefaultRequestConfig(requestConfig)
        //         .evictExpiredConnections()
        //         .evictIdleConnections(10, TimeUnit.SECONDS)
        //         .build();
        return httpClientBuilder.build();
    }
}

理论基础

长连接

想象一下,你每次向同一个朋友请教问题,每个问题你都需要重新拨号、等待接通、说”喂,你好,我是AAA,请问你是BBB吗”,聊完后说”再见”挂断。如果你要向这个朋友一次连续问10个问题,却需要重复10次这套动作,是不是非常浪费时间?

HTTP连接也是如此。在早期的HTTP/1.0时代,每次请求一份数据、图片或其他资源时,都要经历:

  1. 建立连接:客户端和服务器进行”三次握手”(就像拨号接通的过程)
  2. 发送请求和接收响应:传输实际的数据
  3. 关闭连接:进行”四次挥手”(就像礼貌地说再见挂电话)

这个过程有多耗时呢?建立和关闭连接的时间往往比传输数据本身还要长!比如:

  • 建立连接:50-100毫秒
  • 传输一个小文件:10毫秒
  • 关闭连接:50毫秒

一个网页通常需要加载HTML、CSS、JavaScript、图片等几十个文件,如果每个文件都要重新建立连接,累积的开销是巨大的。

解决方案就是HTTP连接池和长连接:就像保持电话线路不挂断,连续传输多个请求,这样就能:

  • 减少网络延迟
  • 降低服务器负载
  • 提升用户体验
  • 节省带宽资源

HTTP/1.1引入了持久连接(Keep-Alive),让这个想法成为现实。而连接池则是在应用层面管理这些长连接的技术。

连接池中连接的状态管理

继续用餐厅餐具的比喻,每个HTTP连接就像餐厅里的一套餐具,在整个生命周期中会经历不同的状态。

1
2
3
4
5
6
7
8
// Apache HttpClient 连接池状态
PoolStats totalStats = connectionManager.getTotalStats();

totalStats.getMax();        // 最大连接数 = 餐厅最大餐具容量
totalStats.getAvailable();  // 当前可用连接数 = 库房中现有的空闲餐具
totalStats.getLeased();     // 租用连接数 = 客人正在使用的餐具
totalStats.getPending();    // 等待连接的请求数 = 排队等餐具的客人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 客人要用餐的流程
public Connection getConnection() {
    if (totalStats.getAvailable() > 0) {
        // 库房有空闲餐具,直接取用
        return getFromPool();  // Available--,Leased++
    } else if (totalStats.getLeased() < totalStats.getMax()) {
        // 库房没有空闲餐具,但还没达到最大容量,创建新餐具
        return createNewConnection();  // Leased++
    } else {
        // 餐具全部用完,客人需要等待
        return waitForAvailableConnection();  // Pending++
    }
}

库房管理制度:

  • 餐具清洗完放回库房后开始计时
  • 如果在库房里放置超过n分钟没人使用,就认为这套餐具”过期了”,需要直接丢弃
  • 如果HTTP连接在传输过程中突然断开(客人用餐时碗突然裂了)
  • 连接异常的处理,如果发生了一下异常,立即移除
    • 服务器主动关闭了正在使用的连接(服务员误收了客人还在用的餐具)
    • 连接因为网络延迟等原因超时(餐具放在某个角落太久,找不到了)
    • 连接状态异常,无法正常通信(餐具被污染,不能继续使用)

动手测试

请ai帮忙写了一小段测试代码来验证各种想法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
package com.http;

import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ConnectionPoolTimeoutException;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.pool.PoolStats;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * HttpClient连接池分析Demo
 * 演示HttpClientConnectionManager如何管理连接池
 */
public class HttpTestDemoV2 {
    
    // 连接池管理器
    private static PoolingHttpClientConnectionManager connectionManager;
    
    // HTTP客户端
    private static CloseableHttpClient httpClient;
    
    // 测试URL
    private static final String TEST_URL = "http://httpbin.org/delay/1";
    
    public static void main(String[] args) {
        try {
            // 初始化连接池
            initConnectionPool();
            
            // 演示各种场景
            System.out.println("=== HttpClient连接池分析Demo ===\n");
            
            // 1. 基本的GET请求分析
            demonstrateBasicGetRequest();
            
            // 2. 连接池状态监控
            demonstrateConnectionPoolMonitoring();
            
            // 3. 并发请求测试
            demonstrateConcurrentRequests();
            
            // 4. 连接复用测试
            demonstrateConnectionReuse();
            
            // 5. 连接超时和释放测试
//            demonstrateConnectionTimeout();

            // 等待15秒
            demonstrateConnectionTimeoutV2();


        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 清理资源
            cleanup();
        }
    }
    
    /**
     * 初始化连接池配置
     */
    private static void initConnectionPool() {
        System.out.println("1. 初始化连接池配置");
        
        // 创建连接池管理器
//        connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager = new PoolingHttpClientConnectionManager(
                10, TimeUnit.SECONDS  // 连接最多存活x秒
        );
        
        // 设置连接池参数
        connectionManager.setMaxTotal(20);              // 最大连接数
        connectionManager.setDefaultMaxPerRoute(10);    // 每个路由的最大连接数
        connectionManager.setValidateAfterInactivity(5000); // 5秒后验证连接



        // 创建请求配置
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(5000)  // 从连接池获取连接的超时时间
                .setConnectTimeout(5000)           // 建立连接的超时时间
                .setSocketTimeout(10000)           // 数据传输的超时时间
                .build();
        
        // 创建HttpClient
        httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                .evictExpiredConnections()
//                .evictIdleConnections(10, TimeUnit.SECONDS)
                .build();
        
        System.out.println("连接池初始化完成:");
        System.out.println("- 最大连接数: " + connectionManager.getMaxTotal());
        System.out.println("- 每个路由最大连接数: " + connectionManager.getDefaultMaxPerRoute());
        System.out.println();
    }
    
    /**
     * 演示基本的GET请求和连接管理
     */
    private static void demonstrateBasicGetRequest() throws IOException {
        System.out.println("2. 基本GET请求分析");
        
        // 请求前的连接池状态
        printConnectionPoolStats("请求前");
        
        HttpGet httpGet = new HttpGet(TEST_URL);
        
        System.out.println("发起GET请求: " + TEST_URL);
        long startTime = System.currentTimeMillis();
        
        try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
            // 请求执行中的连接池状态
            printConnectionPoolStats("请求执行中");
            
            HttpEntity entity = response.getEntity();
            String result = EntityUtils.toString(entity);
            
            long endTime = System.currentTimeMillis();
            System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
            System.out.println("响应状态: " + response.getStatusLine().getStatusCode());
            System.out.println("响应长度: " + result.length() + " 字符");
            
            // 消费完实体,连接会被释放回连接池
            EntityUtils.consume(entity);
            
        } catch (Exception e) {
            System.out.println("请求失败: " + e.getMessage());
        }
        
        // 请求完成后的连接池状态
        printConnectionPoolStats("请求完成后");
        System.out.println();
    }
    
    /**
     * 演示连接池状态监控
     */
    private static void demonstrateConnectionPoolMonitoring() {
        System.out.println("3. 连接池状态详细监控");
        
        // 启动一个线程定期打印连接池状态
        Thread monitorThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                printDetailedConnectionStats();
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    break;
                }
            }
        });

        monitorThread.start();

        // 同时进行一些HTTP请求
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 3; i++) {
            final int requestId = i + 1;
            executor.submit(() -> {
                try {
                    HttpGet httpGet = new HttpGet(TEST_URL);
                    try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
                        System.out.println("请求" + requestId + "完成: " + response.getStatusLine().getStatusCode());
                        EntityUtils.consume(response.getEntity());
                    }
                } catch (Exception e) {
                    System.out.println("请求" + requestId + "失败: " + e.getMessage());
                }
            });
        }

        executor.shutdown();
        try {
            executor.awaitTermination(15, TimeUnit.SECONDS);
            monitorThread.interrupt();
            monitorThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println();
    }
    
    /**
     * 演示并发请求对连接池的影响
     */
    private static void demonstrateConcurrentRequests() {
        System.out.println("4. 并发请求测试 (10个并发请求)");
        
        int concurrentRequests = 10;
        CountDownLatch latch = new CountDownLatch(concurrentRequests);
        ExecutorService executor = Executors.newFixedThreadPool(concurrentRequests);
        
        long startTime = System.currentTimeMillis();
        
        for (int i = 0; i < concurrentRequests; i++) {
            final int requestId = i + 1;
            executor.submit(() -> {
                try {
                    System.out.println("线程" + Thread.currentThread().getName() + " 开始请求" + requestId);
                    
                    HttpGet httpGet = new HttpGet(TEST_URL);
                    try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
                        System.out.println("请求" + requestId + " 完成,状态码: " + response.getStatusLine().getStatusCode());
                        EntityUtils.consume(response.getEntity());
                    }
                } catch (ConnectionPoolTimeoutException e) {
                    System.out.println("请求" + requestId + " 连接池超时: " + e.getMessage());
                } catch (Exception e) {
                    System.out.println("请求" + requestId + " 失败: " + e.getMessage());
                } finally {
                    latch.countDown();
                }
            });
        }
        
        try {
            latch.await(30, TimeUnit.SECONDS);
            long endTime = System.currentTimeMillis();
            System.out.println("所有并发请求完成,总耗时: " + (endTime - startTime) + "ms");
            
            // 最终的连接池状态
            printConnectionPoolStats("并发请求完成后");
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        executor.shutdown();
        System.out.println();
    }
    
    /**
     * 演示连接复用机制
     */
    private static void demonstrateConnectionReuse() throws IOException {
        System.out.println("5. 连接复用测试");
        
        String sameHostUrl = "http://httpbin.org/get";
        
        // 连续发送多个请求到同一个主机
        for (int i = 1; i <= 3; i++) {
            System.out.println("发送第" + i + "个请求到同一主机...");
            
            printConnectionPoolStats("请求" + i + "前");
            long startTime = System.currentTimeMillis();
            HttpGet httpGet = new HttpGet(sameHostUrl + "?request=" + i);
            try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
                long endTime = System.currentTimeMillis();
                System.out.println("请求" + i + "响应: " + response.getStatusLine().getStatusCode() + " 耗时:" + (endTime - startTime) + "ms");
                EntityUtils.consume(response.getEntity());
            }
            
            printConnectionPoolStats("请求" + i + "后");
            
            // 短暂等待观察连接状态
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        
        System.out.println();
    }
    
    /**
     * 演示连接超时和释放
     */
    private static void demonstrateConnectionTimeout() {
        System.out.println("6. 连接超时和释放测试");
        
        // 创建一个会超时的客户端配置
        RequestConfig timeoutConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(1000)  // 1秒超时
                .setConnectTimeout(1000)
                .setSocketTimeout(2000)
                .build();
        
        // 使用新的超时配置发起请求
        HttpGet httpGet = new HttpGet("http://httpbin.org/delay/5"); // 服务端延迟5秒响应
        httpGet.setConfig(timeoutConfig);
        
        try {
            System.out.println("发起一个会超时的请求...");
            printConnectionPoolStats("超时请求前");
            
            try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
                EntityUtils.consume(response.getEntity());
            }
        } catch (Exception e) {
            System.out.println("请求超时异常: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        }
        
        printConnectionPoolStats("超时请求后");
        
        // 测试连接清理
        System.out.println("清理过期连接...");
        connectionManager.closeExpiredConnections();
        connectionManager.closeIdleConnections(30, TimeUnit.SECONDS);
        
        printConnectionPoolStats("清理后");
        System.out.println();
    }


    private static void demonstrateConnectionTimeoutV2() {
        System.out.println("6. 连接超时和释放测试");

        // 创建一个会超时的客户端配置
        RequestConfig timeoutConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(1000)  // 1秒超时
                .setConnectTimeout(1000)
                .setSocketTimeout(2000)
                .build();

        // 使用新的超时配置发起请求
        HttpGet httpGet = new HttpGet("http://httpbin.org/delay/5"); // 服务端延迟5秒响应
        httpGet.setConfig(timeoutConfig);

        try {
            System.out.println("发起一个会超时的请求...");
            printConnectionPoolStats("超时请求前");

            try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
                EntityUtils.consume(response.getEntity());
            }
        } catch (Exception e) {
            System.out.println("请求超时异常: " + e.getClass().getSimpleName() + " - " + e.getMessage());
        }

        printConnectionPoolStats("超时请求后");

        // 等待15秒
        System.out.println("等待15秒...");
        try {
            Thread.sleep(15000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        printConnectionPoolStats("等待15秒后");

        // 测试连接清理
//        System.out.println("清理过期连接...");
//        connectionManager.closeExpiredConnections();
//        connectionManager.closeIdleConnections(10, TimeUnit.SECONDS);

        printConnectionPoolStats("清理后");
        System.out.println();
    }
    
    /**
     * 打印连接池基本状态
     */
    private static void printConnectionPoolStats(String phase) {
        PoolStats totalStats = connectionManager.getTotalStats();
        System.out.println("[" + phase + "] 连接池状态 - " +
                "总连接: " + totalStats.getLeased() + "/" + totalStats.getMax() + 
                ", 可用: " + totalStats.getAvailable() + 
                ", 挂起: " + totalStats.getPending());
    }
    
    /**
     * 打印详细的连接池状态
     */
    private static void printDetailedConnectionStats() {
        PoolStats totalStats = connectionManager.getTotalStats();
        
        System.out.println("=== 连接池详细状态 ===");
        System.out.println("总体统计:");
        System.out.println("  - 最大连接数: " + totalStats.getMax());
        System.out.println("  - 已租用连接: " + totalStats.getLeased());
        System.out.println("  - 可用连接: " + totalStats.getAvailable());
        System.out.println("  - 挂起请求: " + totalStats.getPending());
        
        // 打印路由统计(如果有的话)
        try {
            System.out.println("路由统计: " + connectionManager.getRoutes().size() + " 个路由");
        } catch (Exception e) {
            // 某些版本可能不支持这个方法
        }
        
        System.out.println("当前时间: " + System.currentTimeMillis());
        System.out.println("==================");
    }
    
    /**
     * 清理资源
     */
    private static void cleanup() {
        System.out.println("7. 清理资源");
        
        try {
            if (httpClient != null) {
                httpClient.close();
                System.out.println("HttpClient已关闭");
            }
            
            if (connectionManager != null) {
                connectionManager.shutdown();
                System.out.println("连接管理器已关闭");
            }
            
        } catch (IOException e) {
            System.out.println("清理资源时出错: " + e.getMessage());
        }
        
        System.out.println("资源清理完成");
    }
}

本文由作者按照 CC BY 4.0 进行授权

热门标签