Skip to content
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

新增 apache http builder,支持 apache http client #315

Open
wants to merge 10 commits into
base: main
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
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,54 @@ JsapiService service = new JsapiService.Builder().httpclient(httpClient).build()

我们提供基于 [腾讯 Kona 国密套件](https://github.com/Tencent/TencentKonaSMSuite) 的国密扩展。文档请参考 [shangmi/README.md](shangmi/README.md)。

## 使用 ApacheHttpClient 发送 HTTP请求

SDK 支持使用 ApacheHttpClient 发送 HTTP 请求。建议使用 [ApacheHttpClientBuilder](https://github.com/wechatpay-apiv3/wechatpay-java/tree/main/core/src/main/java/com/wechat/pay/java/core/http/ApacheHttpClientBuilder.java) 创建 [ApacheHttpClientAdapter]https://github.com/wechatpay-apiv3/wechatpay-java/tree/main/core/src/main/java/com/wechat/pay/java/core/http/apache/ApacheHttpClientAdapter.java) 来发送 HTTP 请求,会自动生成签名和验证签名。

### 使用示例

发送请求步骤如下:

1. 初始化 `ApacheHttpClientAdapter`,建议使用 `ApacheHttpClientBuilder` 构建
2. 构建请求 `HttpRequest`
3. 调用 `httpClient.execute` 或者 `httpClient.get` 等方法来发送 HTTP 请求。`httpClient.execute` 支持发送 GET、PUT、POST、PATCH、DELETE 请求,也可以调用指定的 HTTP 方法发送请求。

[ApacheHttpClientAdapterTest](https://github.com/wechatpay-apiv3/wechatpay-java/blob/main/core/src/test/java/com/wechat/pay/java/core/http/ApacheHttpClientAdapterTest.java) 中演示了如何构造和发送 HTTP 请求。以下是简单的代码使用示例:

```java
// 初始化商户配置
Config config =
new RSAAutoCertificateConfig.Builder()
.merchantId(merchantId) // 商户号
.privateKeyFromPath(privateKeyPath) // 商户API私钥路径
.merchantSerialNumber(merchantSerialNumber) // 商户证书序列号
.apiV3Key(apiV3Key) // 商户APIV3密钥
.build();

// 方法一:使用默认创建的 ApacheHttpClient
HttpClient httpClient = new ApacheHttpClientBuilder().config(config).build();
// 方法二:使用业务已有的 customApacheHttpClient
HttpClient httpClient = new ApacheHttpClientBuilder().config(config).apacheHttpClient(customApacheHttpClient).build();

// 构造 HttpRequest,以 GET 为例
HttpRequest httpRequest =
new HttpRequest.Builder()
.httpMethod(HttpMethod.GET)
.url(requestPath)
.headers(headers)
.build();

// 发送请求 Response 为自定义的回包的类型
HttpResponse<ExampleResponse> httpResponse = httpClient.execute(httpRequest, ExampleResponse.class);
```

### 如何迁移

如果之前使用的是 [wechatpay-apache-httpclient](https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient),想要迁移到使用 SDK,可以按照以下步骤:

1. 修改初始化方式:可参考示例代码
2. 修改收发包请求:定义回包类 XXXResponse,构造 HttpRequest,可参考示例代码

## 常见问题

### 为什么收到应答中的证书序列号和发起请求的证书序列号不一致?
Expand Down
2 changes: 2 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jar {
dependencies {
implementation "com.google.code.gson:gson:${gsonVersion}"
implementation "com.squareup.okhttp3:okhttp:${okhttpVersion}"
implementation "org.apache.httpcomponents:httpmime:${apachehttpVersion}"
implementation "org.apache.httpcomponents:httpclient:${apachehttpVersion}"
implementation "org.slf4j:slf4j-api:${slf4jVersion}"

testImplementation "junit:junit:${junitVersion}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.wechat.pay.java.core.http;
EmmetZC marked this conversation as resolved.
Show resolved Hide resolved

import static java.util.Objects.requireNonNull;

import com.wechat.pay.java.core.Config;
import com.wechat.pay.java.core.auth.Credential;
import com.wechat.pay.java.core.auth.Validator;
import com.wechat.pay.java.core.http.apache.ApacheHttpClientAdapter;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;

/** 默认HttpClient构造器 */
public class ApacheHttpClientBuilder implements AbstractHttpClientBuilder<ApacheHttpClientBuilder> {

private Credential credential;
private Validator validator;

private CloseableHttpClient customizeApacheHttpClient;

static PoolingHttpClientConnectionManager apacheHttpClientConnectionManager =
new PoolingHttpClientConnectionManager();

private CloseableHttpClient initDefaultApacheHttpClient() {
return HttpClientBuilder.create()
.setConnectionManager(apacheHttpClientConnectionManager)
.setConnectionManagerShared(true)
.build();
}

/**
* 复制工厂,复制一个当前对象
*
* @return 对象的副本
*/
@Override
public ApacheHttpClientBuilder newInstance() {
ApacheHttpClientBuilder result = new ApacheHttpClientBuilder();
result.credential = this.credential;
result.validator = this.validator;
result.customizeApacheHttpClient = this.customizeApacheHttpClient;
return result;
}

/**
* 设置凭据生成器
*
* @param credential 凭据生成器
* @return apacheHttpClientBuilder
*/
@Override
public ApacheHttpClientBuilder credential(Credential credential) {
this.credential = credential;
return this;
}

/**
* 设置验证器
*
* @param validator 验证器
* @return apacheHttpClientBuilder
*/
@Override
public ApacheHttpClientBuilder validator(Validator validator) {
this.validator = validator;
return this;
}

/**
* 设置 appacheHttpClient,若没有设置,则使用默认创建的 appacheHttpClient
*
* @param apacheHttpClient 用户自定义的apacheHttpClient
* @return apacheHttpClientBuilder
*/
public ApacheHttpClientBuilder apacheHttpClient(CloseableHttpClient apacheHttpClient) {
this.customizeApacheHttpClient = apacheHttpClient;
return this;
}

public ApacheHttpClientBuilder config(Config config) {
requireNonNull(config);
this.credential = config.createCredential();
this.validator = config.createValidator();
return this;
}

/**
* 构建默认HttpClient
*
* @return httpClient
*/
@Override
public AbstractHttpClient build() {
requireNonNull(credential);
requireNonNull(validator);

CloseableHttpClient httpclient =
customizeApacheHttpClient == null
? initDefaultApacheHttpClient()
: customizeApacheHttpClient;
return new ApacheHttpClientAdapter(credential, validator, httpclient);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package com.wechat.pay.java.core.http.apache;

import static java.util.Objects.requireNonNull;
import static org.apache.http.entity.ContentType.APPLICATION_JSON;

import com.wechat.pay.java.core.auth.Credential;
import com.wechat.pay.java.core.auth.Validator;
import com.wechat.pay.java.core.exception.HttpException;
import com.wechat.pay.java.core.exception.MalformedMessageException;
import com.wechat.pay.java.core.exception.ServiceException;
import com.wechat.pay.java.core.http.AbstractHttpClient;
import com.wechat.pay.java.core.http.FileRequestBody;
import com.wechat.pay.java.core.http.HttpRequest;
import com.wechat.pay.java.core.http.JsonRequestBody;
import com.wechat.pay.java.core.http.OriginalResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ApacheHttpClientAdapter extends AbstractHttpClient {

private static final Logger logger = LoggerFactory.getLogger(ApacheHttpClientAdapter.class);
private static final String META_NAME = "meta";
private static final String FILE_NAME = "file";

private final CloseableHttpClient apacheHttpClient;

public ApacheHttpClientAdapter(
Credential credential, Validator validator, CloseableHttpClient client) {
super(credential, validator);
this.apacheHttpClient = requireNonNull(client);
}

@Override
protected String getHttpClientInfo() {
return "apachehttp/" + apacheHttpClient.getClass().getPackage().getImplementationVersion();
}

@Override
public OriginalResponse innerExecute(HttpRequest wechatPayRequest) {
try {
CloseableHttpResponse apacheHttpResponse =
apacheHttpClient.execute(buildApacheHttpRequest(wechatPayRequest));
return assembleOriginalResponse(wechatPayRequest, apacheHttpResponse);
} catch (IOException e) {
throw new HttpException(wechatPayRequest, e);
}
}

private org.apache.http.client.methods.HttpUriRequest buildApacheHttpRequest(
HttpRequest wechatPayRequest) {
String url = wechatPayRequest.getUrl().toString();
org.apache.http.client.methods.HttpUriRequest apacheHttpRequest;

switch (wechatPayRequest.getHttpMethod().name()) {
case "GET":
apacheHttpRequest = new HttpGet(url);
break;
case "POST":
apacheHttpRequest = new HttpPost(url);
((HttpPost) apacheHttpRequest).setEntity(buildApacheHttpEntity(wechatPayRequest.getBody()));
break;
case "PUT":
apacheHttpRequest = new HttpPut(url);
((HttpPut) apacheHttpRequest).setEntity(buildApacheHttpEntity(wechatPayRequest.getBody()));
break;
case "PATCH":
apacheHttpRequest = new HttpPatch(url);
((HttpPatch) apacheHttpRequest)
.setEntity(buildApacheHttpEntity(wechatPayRequest.getBody()));
break;
case "DELETE":
apacheHttpRequest = new HttpDelete(url);
break;
default:
throw new IllegalArgumentException(
"Unsupported HTTP method: " + wechatPayRequest.getHttpMethod().name());
}
Map<String, String> headers = wechatPayRequest.getHeaders().getHeaders();
headers.forEach(apacheHttpRequest::addHeader);
return apacheHttpRequest;
}

private HttpEntity buildApacheHttpEntity(
com.wechat.pay.java.core.http.RequestBody wechatPayRequestBody) {
// 处理空请求体的情况
if (wechatPayRequestBody == null) {
return new StringEntity("", "");
}

if (wechatPayRequestBody instanceof JsonRequestBody) {
return new StringEntity(
((JsonRequestBody) wechatPayRequestBody).getBody(),
ContentType.create(wechatPayRequestBody.getContentType()));
}
if (wechatPayRequestBody instanceof FileRequestBody) {
FileRequestBody fileRequestBody = (FileRequestBody) wechatPayRequestBody;
MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
entityBuilder.setMode(HttpMultipartMode.RFC6532);
entityBuilder.addTextBody(META_NAME, fileRequestBody.getMeta(), APPLICATION_JSON);
entityBuilder.addBinaryBody(
FILE_NAME,
fileRequestBody.getFile(),
ContentType.create(fileRequestBody.getContentType()),
fileRequestBody.getFileName());
return entityBuilder.build();
}
logger.error(
"When an http request is sent and the apache request body is constructed, the requestBody"
+ " parameter"
+ " type cannot be found,requestBody class name[{}]",
wechatPayRequestBody.getClass().getName());
return null;
}

private OriginalResponse assembleOriginalResponse(
HttpRequest wechatPayRequest, CloseableHttpResponse apacheHttpResponse) throws IOException {
Map<String, String> responseHeaders = assembleResponseHeader(apacheHttpResponse);
HttpEntity entity = apacheHttpResponse.getEntity();
try {
String responseBody = entity != null ? EntityUtils.toString(entity) : null;
return new OriginalResponse.Builder()
.request(wechatPayRequest)
.headers(responseHeaders)
.statusCode(apacheHttpResponse.getStatusLine().getStatusCode())
.contentType(entity != null ? entity.getContentType().getValue() : null)
.body(responseBody)
.build();
} catch (IOException e) {
throw new MalformedMessageException(
String.format(
"Assemble OriginalResponse,get responseBody failed.%nHttpRequest[%s]",
wechatPayRequest));
}
}

private Map<String, String> assembleResponseHeader(CloseableHttpResponse apacheHttpResponse) {
Map<String, String> responseHeaders = new ConcurrentHashMap<>();
Header[] headers = apacheHttpResponse.getAllHeaders();
for (Header header : headers) {
responseHeaders.put(header.getName(), header.getValue());
}
return responseHeaders;
}

@Override
protected InputStream innerDownload(HttpRequest httpRequest) {
try {
CloseableHttpResponse apacheHttpResponse =
apacheHttpClient.execute(buildApacheHttpRequest(httpRequest));
if (isInvalidHttpCode(apacheHttpResponse.getStatusLine().getStatusCode())) {
throw new ServiceException(
httpRequest, apacheHttpResponse.getStatusLine().getStatusCode(), "");
}
InputStream responseBodyStream = null;
if (apacheHttpResponse.getEntity() != null) {
responseBodyStream = apacheHttpResponse.getEntity().getContent();
}
return responseBodyStream;
} catch (IOException e) {
throw new HttpException(httpRequest, e);
}
}
}
Loading
Loading