Skip to content

Commit dc46d9d

Browse files
authored
🎨 #3640 【微信支付】使用HttpClient发送http请求时调整为使用连接池的形式
1 parent bc6fb7b commit dc46d9d

File tree

5 files changed

+388
-17
lines changed

5 files changed

+388
-17
lines changed

weixin-java-pay/CONNECTION_POOL.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# HTTP连接池功能说明
2+
3+
## 概述
4+
5+
`WxPayServiceApacheHttpImpl` 现在支持HTTP连接池功能,可以显著提高高并发场景下的性能表现。
6+
7+
## 主要改进
8+
9+
1. **连接复用**: 不再为每个请求创建新的HttpClient实例,而是复用连接池中的连接
10+
2. **性能提升**: 减少连接建立和销毁的开销,提高吞吐量
11+
3. **资源优化**: 合理控制并发连接数,避免资源浪费
12+
4. **SSL支持**: 同时支持普通HTTP和SSL连接的连接池
13+
14+
## 配置说明
15+
16+
### 默认配置
17+
```java
18+
WxPayConfig config = new WxPayConfig();
19+
// 默认配置:
20+
// maxConnTotal = 20 (最大连接数)
21+
// maxConnPerRoute = 10 (每个路由最大连接数)
22+
```
23+
24+
### 自定义配置
25+
```java
26+
WxPayConfig config = new WxPayConfig();
27+
config.setMaxConnTotal(50); // 设置最大连接数
28+
config.setMaxConnPerRoute(20); // 设置每个路由最大连接数
29+
```
30+
31+
## 使用方式
32+
33+
连接池功能是自动启用的,无需额外配置:
34+
35+
```java
36+
// 1. 配置微信支付
37+
WxPayConfig config = new WxPayConfig();
38+
config.setAppId("your-app-id");
39+
config.setMchId("your-mch-id");
40+
config.setMchKey("your-mch-key");
41+
42+
// 2. 创建支付服务(连接池自动启用)
43+
WxPayServiceApacheHttpImpl payService = new WxPayServiceApacheHttpImpl();
44+
payService.setConfig(config);
45+
46+
// 3. 正常使用,所有HTTP请求都会使用连接池
47+
WxPayUnifiedOrderResult result = payService.unifiedOrder(request);
48+
```
49+
50+
## 向后兼容性
51+
52+
- 此功能完全向后兼容,现有代码无需修改
53+
- 如果不设置连接池参数,将使用默认配置
54+
- 支持原有的HttpClientBuilderCustomizer自定义功能
55+
56+
## 注意事项
57+
58+
1. 连接池中的HttpClient实例会被复用,不要手动关闭
59+
2. SSL连接和普通连接使用不同的连接池
60+
3. 连接池参数建议根据实际并发量调整
61+
4. 代理配置仍然正常工作
62+
63+
## 性能建议
64+
65+
- 对于高并发应用,建议适当增加`maxConnTotal``maxConnPerRoute`
66+
- 监控连接池使用情况,避免连接数不足导致的阻塞
67+
- 在容器环境中,注意连接池配置与容器资源限制的平衡

weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,16 @@
1414
import lombok.extern.slf4j.Slf4j;
1515
import org.apache.commons.lang3.RegExUtils;
1616
import org.apache.commons.lang3.StringUtils;
17+
import org.apache.http.HttpHost;
18+
import org.apache.http.auth.AuthScope;
19+
import org.apache.http.auth.UsernamePasswordCredentials;
20+
import org.apache.http.client.CredentialsProvider;
21+
import org.apache.http.impl.client.BasicCredentialsProvider;
1722
import org.apache.http.impl.client.CloseableHttpClient;
23+
import org.apache.http.impl.client.HttpClients;
24+
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
25+
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
26+
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
1827
import org.apache.http.ssl.SSLContexts;
1928

2029
import javax.net.ssl.SSLContext;
@@ -185,11 +194,32 @@ public class WxPayConfig {
185194

186195

187196
private CloseableHttpClient apiV3HttpClient;
197+
198+
/**
199+
* 用于普通支付接口的可复用HttpClient,使用连接池
200+
*/
201+
private CloseableHttpClient httpClient;
202+
203+
/**
204+
* 用于需要SSL证书的支付接口的可复用HttpClient,使用连接池
205+
*/
206+
private CloseableHttpClient sslHttpClient;
207+
188208
/**
189209
* 支持扩展httpClientBuilder
190210
*/
191211
private HttpClientBuilderCustomizer httpClientBuilderCustomizer;
192212
private HttpClientBuilderCustomizer apiV3HttpClientBuilderCustomizer;
213+
214+
/**
215+
* HTTP连接池最大连接数,默认20
216+
*/
217+
private int maxConnTotal = 20;
218+
219+
/**
220+
* HTTP连接池每个路由的最大连接数,默认10
221+
*/
222+
private int maxConnPerRoute = 10;
193223
/**
194224
* 私钥信息
195225
*/
@@ -498,4 +528,111 @@ private Object[] p12ToPem() {
498528
return null;
499529

500530
}
531+
532+
/**
533+
* 初始化使用连接池的HttpClient
534+
*
535+
* @return CloseableHttpClient
536+
* @throws WxPayException 初始化异常
537+
*/
538+
public CloseableHttpClient initHttpClient() throws WxPayException {
539+
if (this.httpClient != null) {
540+
return this.httpClient;
541+
}
542+
543+
// 创建连接池管理器
544+
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
545+
connectionManager.setMaxTotal(this.maxConnTotal);
546+
connectionManager.setDefaultMaxPerRoute(this.maxConnPerRoute);
547+
548+
// 创建HttpClient构建器
549+
org.apache.http.impl.client.HttpClientBuilder httpClientBuilder = HttpClients.custom()
550+
.setConnectionManager(connectionManager);
551+
552+
// 配置代理
553+
configureProxy(httpClientBuilder);
554+
555+
// 提供自定义httpClientBuilder的能力
556+
Optional.ofNullable(httpClientBuilderCustomizer).ifPresent(e -> {
557+
e.customize(httpClientBuilder);
558+
});
559+
560+
this.httpClient = httpClientBuilder.build();
561+
return this.httpClient;
562+
}
563+
564+
/**
565+
* 初始化使用连接池且支持SSL的HttpClient
566+
*
567+
* @return CloseableHttpClient
568+
* @throws WxPayException 初始化异常
569+
*/
570+
public CloseableHttpClient initSslHttpClient() throws WxPayException {
571+
if (this.sslHttpClient != null) {
572+
return this.sslHttpClient;
573+
}
574+
575+
// 初始化SSL上下文
576+
SSLContext sslContext = this.getSslContext();
577+
if (null == sslContext) {
578+
sslContext = this.initSSLContext();
579+
}
580+
581+
// 创建支持SSL的连接池管理器
582+
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
583+
connectionManager.setMaxTotal(this.maxConnTotal);
584+
connectionManager.setDefaultMaxPerRoute(this.maxConnPerRoute);
585+
586+
// 创建HttpClient构建器,配置SSL
587+
org.apache.http.impl.client.HttpClientBuilder httpClientBuilder = HttpClients.custom()
588+
.setConnectionManager(connectionManager)
589+
.setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier()));
590+
591+
// 配置代理
592+
configureProxy(httpClientBuilder);
593+
594+
// 提供自定义httpClientBuilder的能力
595+
Optional.ofNullable(httpClientBuilderCustomizer).ifPresent(e -> {
596+
e.customize(httpClientBuilder);
597+
});
598+
599+
this.sslHttpClient = httpClientBuilder.build();
600+
return this.sslHttpClient;
601+
}
602+
603+
/**
604+
* 配置HTTP代理
605+
*/
606+
private void configureProxy(org.apache.http.impl.client.HttpClientBuilder httpClientBuilder) {
607+
if (StringUtils.isNotBlank(this.getHttpProxyHost()) && this.getHttpProxyPort() > 0) {
608+
if (StringUtils.isEmpty(this.getHttpProxyUsername())) {
609+
this.setHttpProxyUsername("whatever");
610+
}
611+
612+
// 使用代理服务器 需要用户认证的代理服务器
613+
CredentialsProvider provider = new BasicCredentialsProvider();
614+
provider.setCredentials(new AuthScope(this.getHttpProxyHost(), this.getHttpProxyPort()),
615+
new UsernamePasswordCredentials(this.getHttpProxyUsername(), this.getHttpProxyPassword()));
616+
httpClientBuilder.setDefaultCredentialsProvider(provider)
617+
.setProxy(new HttpHost(this.getHttpProxyHost(), this.getHttpProxyPort()));
618+
}
619+
}
620+
621+
/**
622+
* 获取用于普通支付接口的HttpClient
623+
*
624+
* @return CloseableHttpClient
625+
*/
626+
public CloseableHttpClient getHttpClient() {
627+
return httpClient;
628+
}
629+
630+
/**
631+
* 获取用于SSL支付接口的HttpClient
632+
*
633+
* @return CloseableHttpClient
634+
*/
635+
public CloseableHttpClient getSslHttpClient() {
636+
return sslHttpClient;
637+
}
501638
}

weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ public class WxPayServiceApacheHttpImpl extends BaseWxPayServiceImpl {
5252
@Override
5353
public byte[] postForBytes(String url, String requestStr, boolean useKey) throws WxPayException {
5454
try {
55-
HttpClientBuilder httpClientBuilder = createHttpClientBuilder(useKey);
5655
HttpPost httpPost = this.createHttpPost(url, requestStr);
57-
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
58-
final byte[] bytes = httpClient.execute(httpPost, ByteArrayResponseHandler.INSTANCE);
59-
final String responseData = Base64.getEncoder().encodeToString(bytes);
60-
this.logRequestAndResponse(url, requestStr, responseData);
61-
wxApiData.set(new WxPayApiData(url, requestStr, responseData, null));
62-
return bytes;
63-
}
56+
CloseableHttpClient httpClient = this.createHttpClient(useKey);
57+
58+
// 使用连接池的客户端,不需要手动关闭
59+
final byte[] bytes = httpClient.execute(httpPost, ByteArrayResponseHandler.INSTANCE);
60+
final String responseData = Base64.getEncoder().encodeToString(bytes);
61+
this.logRequestAndResponse(url, requestStr, responseData);
62+
wxApiData.set(new WxPayApiData(url, requestStr, responseData, null));
63+
return bytes;
6464
} catch (Exception e) {
6565
this.logError(url, requestStr, e);
6666
wxApiData.set(new WxPayApiData(url, requestStr, null, e.getMessage()));
@@ -71,17 +71,17 @@ public byte[] postForBytes(String url, String requestStr, boolean useKey) throws
7171
@Override
7272
public String post(String url, String requestStr, boolean useKey) throws WxPayException {
7373
try {
74-
HttpClientBuilder httpClientBuilder = this.createHttpClientBuilder(useKey);
7574
HttpPost httpPost = this.createHttpPost(url, requestStr);
76-
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
77-
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
78-
String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
79-
this.logRequestAndResponse(url, requestStr, responseString);
80-
if (this.getConfig().isIfSaveApiData()) {
81-
wxApiData.set(new WxPayApiData(url, requestStr, responseString, null));
82-
}
83-
return responseString;
75+
CloseableHttpClient httpClient = this.createHttpClient(useKey);
76+
77+
// 使用连接池的客户端,不需要手动关闭
78+
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
79+
String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
80+
this.logRequestAndResponse(url, requestStr, responseString);
81+
if (this.getConfig().isIfSaveApiData()) {
82+
wxApiData.set(new WxPayApiData(url, requestStr, responseString, null));
8483
}
84+
return responseString;
8585
} finally {
8686
httpPost.releaseConnection();
8787
}
@@ -281,6 +281,26 @@ private CloseableHttpClient createApiV3HttpClient() throws WxPayException {
281281
return apiV3HttpClient;
282282
}
283283

284+
CloseableHttpClient createHttpClient(boolean useKey) throws WxPayException {
285+
if (useKey) {
286+
// 使用SSL连接池客户端
287+
CloseableHttpClient sslHttpClient = this.getConfig().getSslHttpClient();
288+
if (null == sslHttpClient) {
289+
this.getConfig().initSslHttpClient();
290+
sslHttpClient = this.getConfig().getSslHttpClient();
291+
}
292+
return sslHttpClient;
293+
} else {
294+
// 使用普通连接池客户端
295+
CloseableHttpClient httpClient = this.getConfig().getHttpClient();
296+
if (null == httpClient) {
297+
this.getConfig().initHttpClient();
298+
httpClient = this.getConfig().getHttpClient();
299+
}
300+
return httpClient;
301+
}
302+
}
303+
284304
private static StringEntity createEntry(String requestStr) {
285305
return new StringEntity(requestStr, ContentType.create(APPLICATION_JSON, StandardCharsets.UTF_8));
286306
//return new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.github.binarywang.wxpay.service.impl;
2+
3+
import com.github.binarywang.wxpay.config.WxPayConfig;
4+
import org.apache.http.impl.client.CloseableHttpClient;
5+
import org.testng.Assert;
6+
import org.testng.annotations.Test;
7+
8+
/**
9+
* 演示连接池功能的示例测试
10+
*/
11+
public class ConnectionPoolUsageExampleTest {
12+
13+
@Test
14+
public void demonstrateConnectionPoolUsage() throws Exception {
15+
// 1. 创建配置并设置连接池参数
16+
WxPayConfig config = new WxPayConfig();
17+
config.setAppId("wx123456789");
18+
config.setMchId("1234567890");
19+
config.setMchKey("32位商户密钥32位商户密钥32位商户密钥");
20+
21+
// 设置连接池参数(可选,有默认值)
22+
config.setMaxConnTotal(50); // 最大连接数,默认20
23+
config.setMaxConnPerRoute(20); // 每个路由最大连接数,默认10
24+
25+
// 2. 初始化连接池
26+
CloseableHttpClient pooledClient = config.initHttpClient();
27+
Assert.assertNotNull(pooledClient);
28+
29+
// 3. 创建支付服务实例
30+
WxPayServiceApacheHttpImpl payService = new WxPayServiceApacheHttpImpl();
31+
payService.setConfig(config);
32+
33+
// 4. 现在所有的HTTP请求都会使用连接池
34+
// 对于非SSL请求,会复用同一个HttpClient实例
35+
CloseableHttpClient client1 = payService.createHttpClient(false);
36+
CloseableHttpClient client2 = payService.createHttpClient(false);
37+
Assert.assertSame(client1, client2, "非SSL请求应该复用同一个客户端实例");
38+
39+
// 对于SSL请求,也会复用同一个SSL HttpClient实例(需要配置证书后)
40+
System.out.println("连接池配置成功!");
41+
System.out.println("最大连接数:" + config.getMaxConnTotal());
42+
System.out.println("每路由最大连接数:" + config.getMaxConnPerRoute());
43+
}
44+
45+
@Test
46+
public void demonstrateDefaultConfiguration() throws Exception {
47+
// 使用默认配置的示例
48+
WxPayConfig config = new WxPayConfig();
49+
config.setAppId("wx123456789");
50+
config.setMchId("1234567890");
51+
config.setMchKey("32位商户密钥32位商户密钥32位商户密钥");
52+
53+
// 不设置连接池参数,使用默认值
54+
CloseableHttpClient client = config.initHttpClient();
55+
Assert.assertNotNull(client);
56+
57+
// 验证默认配置
58+
Assert.assertEquals(config.getMaxConnTotal(), 20, "默认最大连接数应该是20");
59+
Assert.assertEquals(config.getMaxConnPerRoute(), 10, "默认每路由最大连接数应该是10");
60+
}
61+
}

0 commit comments

Comments
 (0)