Apache HttpClient5 之顺丰快递
Apache HttpClient5 之顺丰快递
概要
-
由于顺丰Java Sdk使用Apache HttpClient版本较低,且不支持maven引用——需要手工导入maven,
以下使用Java 17、Apache HttpClient5进行重构、加密算法保留与顺丰Sdk一致。 -
Java 17:代码运行环境是Java 17
-
maven:Apache HttpClient5 具体版本如下
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>5.3.5</version>
</dependency>
代码
- HttpClientConfig:配置连接池、请求级别、连接资源等配置
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.ProxySelector;
import java.time.Duration;
@Configuration
public class HttpClientConfig {
// 连接池最大连接数 - 控制整个连接池允许的最大并发连接数
private final static int MAX_TOTAL = 100;
// 每个路由(目标主机)的默认最大连接数 - 限制到单个主机的并发连接数
private final static int DEFAULT_MAX_PER_ROUTE = 50;
// 连接建立超时时间(毫秒)- 建立TCP连接的最大等待时间
private final static Duration CONNECT_TIMEOUT = Duration.ofMillis(5000);
// 从连接池获取连接的超时时间(秒)- 等待连接池分配连接的最大时间
private final static Duration CONNECTION_REQUEST_TIMEOUT = Duration.ofSeconds(2);
// 响应超时时间(秒)- 等待服务器响应的最大时间
private final static Duration RESPONSE_TIMEOUT = Duration.ofSeconds(30);
// Socket超时时间(毫秒)- 两次数据包之间的最大间隔时间
private final static Duration SOCKET_TIMEOUT = Duration.ofMillis(10000);
// 空闲连接最大存活时间(毫秒)- 超过此时间的空闲连接将被驱逐
private final static Duration IDLE_EVICT_TIME = Duration.ofMillis(120000);
/**
* 配置HTTP连接池管理器 (核心Bean)
* 使用@Bean使其由Spring容器管理单例生命周期
* Spring容器关闭时自动调用close()释放连接
*/
@Bean(destroyMethod = "close")
public HttpClientConnectionManager httpClientConnectionManager() throws Exception {
// 创建SSL socket工厂
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
SSLContexts.custom().build(),
new String[]{"TLSv1.2", "TLSv1.3"}, // 支持的TLS协议版本
null, // 支持的密码套件(null表示使用默认值)
new DefaultHostnameVerifier()); // 主机名验证器
// 配置默认连接配置
ConnectionConfig connectionConfig = ConnectionConfig.custom()
.setConnectTimeout(Timeout.of(CONNECT_TIMEOUT)) // 连接建立超时
.setSocketTimeout(Timeout.of(SOCKET_TIMEOUT)) // Socket读写超时
.build();
// 创建连接池管理器(HTTP 连接会自动使用默认的普通 Socket 工厂(PlainConnectionSocketFactory),无需显式设置)
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(sslSocketFactory) // 设置SSL socket工厂
.setDefaultConnectionConfig(connectionConfig) // 设置默认连接配置
.build();
// 设置连接池参数
connectionManager.setMaxTotal(MAX_TOTAL); // 设置整个连接池的最大连接数
connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_PER_ROUTE); // 设置每个路由的默认最大连接数
return connectionManager;
}
/**
* 配置请求级别的参数
* 主要设置从连接池获取连接的超时时间和响应超时时间
*/
@Bean
public RequestConfig requestConfig() {
return RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.of(CONNECTION_REQUEST_TIMEOUT))
// 从连接池获取连接的超时时间
.setResponseTimeout(Timeout.of(RESPONSE_TIMEOUT))
// 等待服务器响应的超时时间
.build();
}
/**
* 创建并配置HttpClient实例
* 使用@Bean注解使其成为Spring管理的Bean
* destroyMethod = "close"确保Spring容器关闭时自动关闭HttpClient释放资源
*/
@Bean(destroyMethod = "close")
public CloseableHttpClient httpClient(HttpClientConnectionManager manager, RequestConfig reqConfig) {
return HttpClients.custom()
.setConnectionManager(manager)
// 设置连接池
.setDefaultRequestConfig(reqConfig)
// 设置请求配置
.setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()))
// 使用系统默认代理
.setRetryStrategy(new DefaultHttpRequestRetryStrategy(3, TimeValue.ofSeconds(1)))
// 使用内置的重试策略
.evictExpiredConnections()
// 驱逐过期连接
.evictIdleConnections(TimeValue.of(IDLE_EVICT_TIME))
// 驱逐空闲连接
.build();
}
}
- HttpClientUtil
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class HttpClientUtil {
private final RequestConfig requestConfig;
private final HttpClient httpClient;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").withZone(ZoneId.systemDefault());
public String post(String url, StringEntity entity) {
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(entity);
return invoke(httpClient, httpPost);
}
public String post(String url, List<NameValuePair> parameters) {
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8));
return invoke(httpClient, httpPost);
}
public String postSFAPI(String url, String xml, String verifyCode) {
List<NameValuePair> parameters = List.of(
new BasicNameValuePair("xml", xml),
new BasicNameValuePair("verifyCode", verifyCode));
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8));
return invoke(httpClient, httpPost);
}
public String get(String url) {
HttpGet get = new HttpGet(url);
return invoke(httpClient, get);
}
public String delete(String url) {
HttpDelete delete = new HttpDelete(url);
return invoke(httpClient, delete);
}
public String post(String url, Map<String, String> params) {
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
httpPost.addHeader("appCode", params.get("partnerID"));
httpPost.addHeader("timestamp", FORMATTER.format(Instant.now()));
httpPost.addHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.withCharset(StandardCharsets.UTF_8).toString());
List<NameValuePair> paramsList = params.entrySet().stream()
.map(i->new BasicNameValuePair(i.getKey(),i.getValue()))
.collect(Collectors.toList());
httpPost.setEntity(new UrlEncodedFormEntity(paramsList, StandardCharsets.UTF_8));
return invoke(httpClient, httpPost);
}
public static String invoke(HttpClient httpclient, HttpUriRequest httpRequest) {
try {
// 创建一个响应处理器
HttpClientResponseHandler<String> responseHandler = resp -> {
int statusCode = resp.getCode();
HttpEntity entity = resp.getEntity();
String body = entity != null ? EntityUtils.toString(entity) : "";
if (statusCode >= 200 && statusCode < 300) {
return body;
}else if (statusCode >= 400 && statusCode < 500) {
// 客户端错误
log.warn("客户端错误 {}: {}", statusCode, body);
throw new RuntimeException("客户端错误: " + statusCode);
} else if (statusCode >= 500) {
// 服务器错误
log.error("服务器错误 {}: {}", statusCode, body);
throw new RuntimeException("服务器错误: " + statusCode);
} else {
// 其他状态码
log.info("非标准状态码 {}: {}", statusCode, body);
return body;
}
};
return httpclient.execute(httpRequest, responseHandler);
} catch (IOException | RuntimeException e) {
log.error("HttpClient请求执行失败: {} {}", httpRequest.getMethod(), httpRequest.getRequestUri(), e);
throw new RuntimeException("HTTP请求执行失败", e);
}
}
}
- VerifyCodeUtil
import lombok.AllArgsConstructor;
import org.apache.commons.codec.binary.Base64;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@AllArgsConstructor
public class VerifyCodeUtil {
public static String md5EncryptAndBase64(String str) {
return encodeBase64(md5Encrypt(str));
}
private static byte[] md5Encrypt(String encryptStr) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(encryptStr.getBytes(StandardCharsets.UTF_8));
return md5.digest();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static String encodeBase64(byte[] b) {
return (new Base64()).encodeAsString(b);
}
}