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