diff --git a/weixin-java-pay/CONNECTION_POOL.md b/weixin-java-pay/CONNECTION_POOL.md new file mode 100644 index 0000000000..2b2569adcb --- /dev/null +++ b/weixin-java-pay/CONNECTION_POOL.md @@ -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` +- 监控连接池使用情况,避免连接数不足导致的阻塞 +- 在容器环境中,注意连接池配置与容器资源限制的平衡 \ No newline at end of file diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResult.java index ae86b8c854..8615a2e461 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResult.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResult.java @@ -273,7 +273,7 @@ public String toString() { * */ @XStreamAlias("refund_recv_accout") - private String refundRecvAccout; + private String refundRecvAccount; /** *
@@ -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");
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
index 96b6f1dd8f..01f9cd534f 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
@@ -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;
@@ -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;
/**
* 私钥信息
*/
@@ -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;
+ }
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
index 130a47a49f..977a2856fe 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
@@ -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()));
@@ -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();
}
@@ -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));
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResultTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResultTest.java
index 963afb2618..e7a22ee6cd 100644
--- a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResultTest.java
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/bean/notify/WxPayRefundNotifyResultTest.java
@@ -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;
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/ConnectionPoolUsageExampleTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/ConnectionPoolUsageExampleTest.java
new file mode 100644
index 0000000000..143743ffcf
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/ConnectionPoolUsageExampleTest.java
@@ -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");
+ }
+}
\ No newline at end of file
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImplConnectionPoolTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImplConnectionPoolTest.java
new file mode 100644
index 0000000000..393d601a69
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImplConnectionPoolTest.java
@@ -0,0 +1,86 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+/**
+ * 测试WxPayServiceApacheHttpImpl的连接池功能
+ */
+public class WxPayServiceApacheHttpImplConnectionPoolTest {
+
+ @Test
+ public void testHttpClientConnectionPool() throws Exception {
+ WxPayConfig config = new WxPayConfig();
+ config.setAppId("test-app-id");
+ config.setMchId("test-mch-id");
+ config.setMchKey("test-mch-key");
+
+ // 测试初始化连接池
+ CloseableHttpClient httpClient1 = config.initHttpClient();
+ Assert.assertNotNull(httpClient1, "HttpClient should not be null");
+
+ // 再次获取,应该返回同一个实例
+ CloseableHttpClient httpClient2 = config.getHttpClient();
+ Assert.assertSame(httpClient1, httpClient2, "Should return the same HttpClient instance");
+
+ // 验证连接池配置
+ WxPayServiceApacheHttpImpl service = new WxPayServiceApacheHttpImpl();
+ service.setConfig(config);
+
+ // 测试不使用SSL的情况下应该使用连接池
+ CloseableHttpClient clientForNonSSL = service.createHttpClient(false);
+ Assert.assertSame(httpClient1, clientForNonSSL, "Should use pooled client for non-SSL requests");
+ }
+
+ @Test
+ public void testSslHttpClientConnectionPool() throws Exception {
+ WxPayConfig config = new WxPayConfig();
+ config.setAppId("test-app-id");
+ config.setMchId("test-mch-id");
+ config.setMchKey("test-mch-key");
+
+ // 为了测试SSL客户端,我们需要设置一些基本的SSL配置
+ // 注意:在实际使用中需要提供真实的证书
+ try {
+ CloseableHttpClient sslClient1 = config.initSslHttpClient();
+ Assert.assertNotNull(sslClient1, "SSL HttpClient should not be null");
+
+ CloseableHttpClient sslClient2 = config.getSslHttpClient();
+ Assert.assertSame(sslClient1, sslClient2, "Should return the same SSL HttpClient instance");
+
+ WxPayServiceApacheHttpImpl service = new WxPayServiceApacheHttpImpl();
+ service.setConfig(config);
+
+ // 测试使用SSL的情况下应该使用SSL连接池
+ CloseableHttpClient clientForSSL = service.createHttpClient(true);
+ Assert.assertSame(sslClient1, clientForSSL, "Should use pooled SSL client for SSL requests");
+
+ } catch (WxPayException e) {
+ // SSL初始化失败是预期的,因为我们没有提供真实的证书
+ // 这里主要是测试代码路径是否正确
+ Assert.assertTrue(e.getMessage().contains("证书") || e.getMessage().contains("商户号"),
+ "Should fail with certificate or merchant ID related error");
+ }
+ }
+
+ @Test
+ public void testConnectionPoolConfiguration() throws Exception {
+ WxPayConfig config = new WxPayConfig();
+ config.setAppId("test-app-id");
+ config.setMchId("test-mch-id");
+ config.setMchKey("test-mch-key");
+ config.setMaxConnTotal(50);
+ config.setMaxConnPerRoute(20);
+
+ CloseableHttpClient httpClient = config.initHttpClient();
+ Assert.assertNotNull(httpClient, "HttpClient should not be null");
+
+ // 验证配置值是否正确设置
+ Assert.assertEquals(config.getMaxConnTotal(), 50, "Max total connections should be 50");
+ Assert.assertEquals(config.getMaxConnPerRoute(), 20, "Max connections per route should be 20");
+ }
+}
\ No newline at end of file