Skip to content

Implement HTTP connection pooling for WxPayServiceApacheHttpImpl #3648

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions weixin-java-pay/CONNECTION_POOL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# HTTP连接池功能说明

## 概述

`WxPayServiceApacheHttpImpl` 现在支持HTTP连接池功能,可以显著提高高并发场景下的性能表现。

## 主要改进

1. **连接复用**: 不再为每个请求创建新的HttpClient实例,而是复用连接池中的连接
2. **性能提升**: 减少连接建立和销毁的开销,提高吞吐量
3. **资源优化**: 合理控制并发连接数,避免资源浪费
4. **SSL支持**: 同时支持普通HTTP和SSL连接的连接池

## 配置说明

### 默认配置
```java
WxPayConfig config = new WxPayConfig();
// 默认配置:
// maxConnTotal = 20 (最大连接数)
// maxConnPerRoute = 10 (每个路由最大连接数)
```

### 自定义配置
```java
WxPayConfig config = new WxPayConfig();
config.setMaxConnTotal(50); // 设置最大连接数
config.setMaxConnPerRoute(20); // 设置每个路由最大连接数
```

## 使用方式

连接池功能是自动启用的,无需额外配置:

```java
// 1. 配置微信支付
WxPayConfig config = new WxPayConfig();
config.setAppId("your-app-id");
config.setMchId("your-mch-id");
config.setMchKey("your-mch-key");

// 2. 创建支付服务(连接池自动启用)
WxPayServiceApacheHttpImpl payService = new WxPayServiceApacheHttpImpl();
payService.setConfig(config);

// 3. 正常使用,所有HTTP请求都会使用连接池
WxPayUnifiedOrderResult result = payService.unifiedOrder(request);
```

## 向后兼容性

- 此功能完全向后兼容,现有代码无需修改
- 如果不设置连接池参数,将使用默认配置
- 支持原有的HttpClientBuilderCustomizer自定义功能

## 注意事项

1. 连接池中的HttpClient实例会被复用,不要手动关闭
2. SSL连接和普通连接使用不同的连接池
3. 连接池参数建议根据实际并发量调整
4. 代理配置仍然正常工作

## 性能建议

- 对于高并发应用,建议适当增加`maxConnTotal`和`maxConnPerRoute`
- 监控连接池使用情况,避免连接数不足导致的阻塞
- 在容器环境中,注意连接池配置与容器资源限制的平衡
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ public String toString() {
* </pre>
*/
@XStreamAlias("refund_recv_accout")
private String refundRecvAccout;
private String refundRecvAccount;

/**
* <pre>
Expand Down Expand Up @@ -324,7 +324,7 @@ public void loadXML(Document d) {
settlementRefundFee = readXmlInteger(d, "settlement_refund_fee");
refundStatus = readXmlString(d, "refund_status");
successTime = readXmlString(d, "success_time");
refundRecvAccout = readXmlString(d, "refund_recv_accout");
refundRecvAccount = readXmlString(d, "refund_recv_accout");
refundAccount = readXmlString(d, "refund_account");
refundRequestSource = readXmlString(d, "refund_request_source");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
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.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.SSLContext;
Expand Down Expand Up @@ -185,11 +194,32 @@ public class WxPayConfig {


private CloseableHttpClient apiV3HttpClient;

/**
* 用于普通支付接口的可复用HttpClient,使用连接池
*/
private CloseableHttpClient httpClient;

/**
* 用于需要SSL证书的支付接口的可复用HttpClient,使用连接池
*/
private CloseableHttpClient sslHttpClient;

/**
* 支持扩展httpClientBuilder
*/
private HttpClientBuilderCustomizer httpClientBuilderCustomizer;
private HttpClientBuilderCustomizer apiV3HttpClientBuilderCustomizer;

/**
* HTTP连接池最大连接数,默认20
*/
private int maxConnTotal = 20;

/**
* HTTP连接池每个路由的最大连接数,默认10
*/
private int maxConnPerRoute = 10;
/**
* 私钥信息
*/
Expand Down Expand Up @@ -498,4 +528,111 @@ private Object[] p12ToPem() {
return null;

}

/**
* 初始化使用连接池的HttpClient
*
* @return CloseableHttpClient
* @throws WxPayException 初始化异常
*/
public CloseableHttpClient initHttpClient() throws WxPayException {
if (this.httpClient != null) {
return this.httpClient;
}

// 创建连接池管理器
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(this.maxConnTotal);
connectionManager.setDefaultMaxPerRoute(this.maxConnPerRoute);

// 创建HttpClient构建器
org.apache.http.impl.client.HttpClientBuilder httpClientBuilder = HttpClients.custom()
.setConnectionManager(connectionManager);

// 配置代理
configureProxy(httpClientBuilder);

// 提供自定义httpClientBuilder的能力
Optional.ofNullable(httpClientBuilderCustomizer).ifPresent(e -> {
e.customize(httpClientBuilder);
});

this.httpClient = httpClientBuilder.build();
return this.httpClient;
}

/**
* 初始化使用连接池且支持SSL的HttpClient
*
* @return CloseableHttpClient
* @throws WxPayException 初始化异常
*/
public CloseableHttpClient initSslHttpClient() throws WxPayException {
if (this.sslHttpClient != null) {
return this.sslHttpClient;
}

// 初始化SSL上下文
SSLContext sslContext = this.getSslContext();
if (null == sslContext) {
sslContext = this.initSSLContext();
}

// 创建支持SSL的连接池管理器
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(this.maxConnTotal);
connectionManager.setDefaultMaxPerRoute(this.maxConnPerRoute);

// 创建HttpClient构建器,配置SSL
org.apache.http.impl.client.HttpClientBuilder httpClientBuilder = HttpClients.custom()
.setConnectionManager(connectionManager)
.setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier()));

// 配置代理
configureProxy(httpClientBuilder);

// 提供自定义httpClientBuilder的能力
Optional.ofNullable(httpClientBuilderCustomizer).ifPresent(e -> {
e.customize(httpClientBuilder);
});

this.sslHttpClient = httpClientBuilder.build();
return this.sslHttpClient;
}

/**
* 配置HTTP代理
*/
private void configureProxy(org.apache.http.impl.client.HttpClientBuilder httpClientBuilder) {
if (StringUtils.isNotBlank(this.getHttpProxyHost()) && this.getHttpProxyPort() > 0) {
if (StringUtils.isEmpty(this.getHttpProxyUsername())) {
this.setHttpProxyUsername("whatever");
}

// 使用代理服务器 需要用户认证的代理服务器
CredentialsProvider provider = new BasicCredentialsProvider();
provider.setCredentials(new AuthScope(this.getHttpProxyHost(), this.getHttpProxyPort()),
new UsernamePasswordCredentials(this.getHttpProxyUsername(), this.getHttpProxyPassword()));
httpClientBuilder.setDefaultCredentialsProvider(provider)
.setProxy(new HttpHost(this.getHttpProxyHost(), this.getHttpProxyPort()));
}
}

/**
* 获取用于普通支付接口的HttpClient
*
* @return CloseableHttpClient
*/
public CloseableHttpClient getHttpClient() {
return httpClient;
}

/**
* 获取用于SSL支付接口的HttpClient
*
* @return CloseableHttpClient
*/
public CloseableHttpClient getSslHttpClient() {
return sslHttpClient;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ public class WxPayServiceApacheHttpImpl extends BaseWxPayServiceImpl {
@Override
public byte[] postForBytes(String url, String requestStr, boolean useKey) throws WxPayException {
try {
HttpClientBuilder httpClientBuilder = createHttpClientBuilder(useKey);
HttpPost httpPost = this.createHttpPost(url, requestStr);
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
final byte[] bytes = httpClient.execute(httpPost, ByteArrayResponseHandler.INSTANCE);
final String responseData = Base64.getEncoder().encodeToString(bytes);
this.logRequestAndResponse(url, requestStr, responseData);
wxApiData.set(new WxPayApiData(url, requestStr, responseData, null));
return bytes;
}
CloseableHttpClient httpClient = this.createHttpClient(useKey);

// 使用连接池的客户端,不需要手动关闭
final byte[] bytes = httpClient.execute(httpPost, ByteArrayResponseHandler.INSTANCE);
final String responseData = Base64.getEncoder().encodeToString(bytes);
this.logRequestAndResponse(url, requestStr, responseData);
wxApiData.set(new WxPayApiData(url, requestStr, responseData, null));
return bytes;
} catch (Exception e) {
this.logError(url, requestStr, e);
wxApiData.set(new WxPayApiData(url, requestStr, null, e.getMessage()));
Expand All @@ -71,17 +71,17 @@ public byte[] postForBytes(String url, String requestStr, boolean useKey) throws
@Override
public String post(String url, String requestStr, boolean useKey) throws WxPayException {
try {
HttpClientBuilder httpClientBuilder = this.createHttpClientBuilder(useKey);
HttpPost httpPost = this.createHttpPost(url, requestStr);
try (CloseableHttpClient httpClient = httpClientBuilder.build()) {
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
this.logRequestAndResponse(url, requestStr, responseString);
if (this.getConfig().isIfSaveApiData()) {
wxApiData.set(new WxPayApiData(url, requestStr, responseString, null));
}
return responseString;
CloseableHttpClient httpClient = this.createHttpClient(useKey);

// 使用连接池的客户端,不需要手动关闭
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
this.logRequestAndResponse(url, requestStr, responseString);
if (this.getConfig().isIfSaveApiData()) {
wxApiData.set(new WxPayApiData(url, requestStr, responseString, null));
}
return responseString;
} finally {
httpPost.releaseConnection();
}
Expand Down Expand Up @@ -281,6 +281,26 @@ private CloseableHttpClient createApiV3HttpClient() throws WxPayException {
return apiV3HttpClient;
}

CloseableHttpClient createHttpClient(boolean useKey) throws WxPayException {
if (useKey) {
// 使用SSL连接池客户端
CloseableHttpClient sslHttpClient = this.getConfig().getSslHttpClient();
if (null == sslHttpClient) {
this.getConfig().initSslHttpClient();
sslHttpClient = this.getConfig().getSslHttpClient();
}
return sslHttpClient;
} else {
// 使用普通连接池客户端
CloseableHttpClient httpClient = this.getConfig().getHttpClient();
if (null == httpClient) {
this.getConfig().initHttpClient();
httpClient = this.getConfig().getHttpClient();
}
return httpClient;
}
}

private static StringEntity createEntry(String requestStr) {
return new StringEntity(requestStr, ContentType.create(APPLICATION_JSON, StandardCharsets.UTF_8));
//return new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public void testFromXMLFastMode() throws WxPayException {
refundNotifyResult.loadReqInfo(xmlDecryptedReqInfo);
assertEquals(refundNotifyResult.getReqInfo().getRefundFee().intValue(), 15);
assertEquals(refundNotifyResult.getReqInfo().getRefundStatus(), "SUCCESS");
assertEquals(refundNotifyResult.getReqInfo().getRefundRecvAccout(), "用户零钱");
assertEquals(refundNotifyResult.getReqInfo().getRefundRecvAccount(), "用户零钱");
System.out.println(refundNotifyResult);
} finally {
XmlConfig.fastMode = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.github.binarywang.wxpay.service.impl;

import com.github.binarywang.wxpay.config.WxPayConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.testng.Assert;
import org.testng.annotations.Test;

/**
* 演示连接池功能的示例测试
*/
public class ConnectionPoolUsageExampleTest {

@Test
public void demonstrateConnectionPoolUsage() throws Exception {
// 1. 创建配置并设置连接池参数
WxPayConfig config = new WxPayConfig();
config.setAppId("wx123456789");
config.setMchId("1234567890");
config.setMchKey("32位商户密钥32位商户密钥32位商户密钥");

// 设置连接池参数(可选,有默认值)
config.setMaxConnTotal(50); // 最大连接数,默认20
config.setMaxConnPerRoute(20); // 每个路由最大连接数,默认10

// 2. 初始化连接池
CloseableHttpClient pooledClient = config.initHttpClient();
Assert.assertNotNull(pooledClient);

// 3. 创建支付服务实例
WxPayServiceApacheHttpImpl payService = new WxPayServiceApacheHttpImpl();
payService.setConfig(config);

// 4. 现在所有的HTTP请求都会使用连接池
// 对于非SSL请求,会复用同一个HttpClient实例
CloseableHttpClient client1 = payService.createHttpClient(false);
CloseableHttpClient client2 = payService.createHttpClient(false);
Assert.assertSame(client1, client2, "非SSL请求应该复用同一个客户端实例");

// 对于SSL请求,也会复用同一个SSL HttpClient实例(需要配置证书后)
System.out.println("连接池配置成功!");
System.out.println("最大连接数:" + config.getMaxConnTotal());
System.out.println("每路由最大连接数:" + config.getMaxConnPerRoute());
}

@Test
public void demonstrateDefaultConfiguration() throws Exception {
// 使用默认配置的示例
WxPayConfig config = new WxPayConfig();
config.setAppId("wx123456789");
config.setMchId("1234567890");
config.setMchKey("32位商户密钥32位商户密钥32位商户密钥");

// 不设置连接池参数,使用默认值
CloseableHttpClient client = config.initHttpClient();
Assert.assertNotNull(client);

// 验证默认配置
Assert.assertEquals(config.getMaxConnTotal(), 20, "默认最大连接数应该是20");
Assert.assertEquals(config.getMaxConnPerRoute(), 10, "默认每路由最大连接数应该是10");
}
}
Loading