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);
    }
}
posted @ 2025-10-10 10:46  Soul-Q  阅读(2)  评论(0)    收藏  举报