/**
- HTTP客户端工具类
- 特性:
- 连接池管理优化
- 空闲连接自动清理
- 自动重试机制
- 更安全的TLS配置
- 资源自动释放
*/
public class HttpClientUtil {
private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class);
public static final String METHOD_POST = "POST";
public static final String METHOD_GET = "GET";
public static final String DEFAULT_CHARSET = "UTF-8";
public static final String DEFAULT_CONTENT_TYPE = "application/json;charset=UTF-8";
// 超时配置
public static final int DEFAULT_CONNECT_TIMEOUT = 10000; // 10秒连接超时
public static final int DEFAULT_READ_TIMEOUT = 30000; // 30秒读取超时
public static final int DEFAULT_CONNECT_REQUEST_TIMEOUT = 10000; // 10秒请求超时
// 连接池配置
private static final int MAX_TOTAL = 200; // 最大连接数
private static final int MAX_PER_ROUTE = 100; // 每路由最大连接数
private static final int VALIDATE_AFTER_INACTIVITY = 5000; // 5秒空闲后验证
private static final int EVICT_IDLE_CONNECTION_TIME = 30; // 30秒空闲连接清理
private static final int CONNECTION_TTL = 60; // 连接存活时间(秒)
private static final int MAX_RETRIES = 3; // 最大重试次数
private static final RequestConfig requestConfig;
private static final PoolingHttpClientConnectionManager connectionManager;
private static final CloseableHttpClient httpClient;
private static final CloseableHttpClient httpsClient;
static {
// 初始化SSLContext
SSLContext sslContext = initSSLContext();
// 配置请求参数
requestConfig = RequestConfig.custom()
.setSocketTimeout(DEFAULT_READ_TIMEOUT)
.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT)
.setConnectionRequestTimeout(DEFAULT_CONNECT_REQUEST_TIMEOUT)
.build();
// 配置连接池
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", new SSLConnectionSocketFactory(
sslContext,
new String[]{"TLSv1.2", "TLSv1.3"}, // 安全协议
null,
NoopHostnameVerifier.INSTANCE))
.build();
connectionManager = new PoolingHttpClientConnectionManager(
socketFactoryRegistry,
null,
null,
null,
VALIDATE_AFTER_INACTIVITY,
TimeUnit.MILLISECONDS);
connectionManager.setMaxTotal(MAX_TOTAL);
connectionManager.setDefaultMaxPerRoute(MAX_PER_ROUTE);
// 创建HttpClient
HttpClientBuilder builder = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setConnectionManager(connectionManager)
.evictExpiredConnections()
.evictIdleConnections(EVICT_IDLE_CONNECTION_TIME, TimeUnit.SECONDS)
.setConnectionTimeToLive(CONNECTION_TTL, TimeUnit.SECONDS)
.disableAutomaticRetries()
.setRetryHandler((exception, executionCount, context) -> {
if (executionCount > MAX_RETRIES) {
return false;
}
if (exception instanceof org.apache.http.NoHttpResponseException) {
return true;
}
return false;
});
httpClient = builder.build();
httpsClient = builder.build();
}
private static SSLContext initSSLContext() {
try {
return new SSLContextBuilder()
.loadTrustMaterial(null, (X509Certificate[] chain, String authType) -> true)
.build();
} catch (KeyStoreException | NoSuchAlgorithmException | KeyManagementException ex) {
logger.error("Failed to initialize SSL context", ex);
throw new RuntimeException("Failed to initialize SSL context", ex);
}
}
private HttpClientUtil() {
// 私有构造器防止实例化
}
/**
* 执行GET请求
* @param url 请求URL
* @param headers 请求头
* @return 响应内容
*/
public static String get(String url, Map<String, String> headers) {
HttpGet request = new HttpGet(url);
try {
wrapHeader(request, headers);
return isHttps(url) ? execute(request, httpsClient) : execute(request, httpClient);
} catch (Exception e) {
logger.error("GET request failed: {}", e.getMessage(), e);
throw new RuntimeException("HTTP GET request failed", e);
} finally {
request.releaseConnection();
}
}
/**
* 执行POST请求(JSON body)
* @param url 请求URL
* @param body 请求体(JSON)
* @param headers 请求头
* @return 响应内容
*/
public static String postBody(String url, String body, Map<String, String> headers) {
HttpPost request = new HttpPost(url);
try {
wrapHeader(request, headers);
wrapStringEntity(request, body);
return isHttps(url) ? execute(request, httpsClient) : execute(request, httpClient);
} catch (Exception e) {
logger.error("POST request failed: {}", e.getMessage(), e);
throw new RuntimeException("HTTP POST request failed", e);
} finally {
request.releaseConnection();
}
}
/**
* 执行POST请求(form表单)
* @param url 请求URL
* @param params 表单参数
* @param headers 请求头
* @return 响应内容
*/
public static String postForm(String url, Map<String, String> params, Map<String, String> headers) {
HttpPost request = new HttpPost(url);
try {
wrapHeader(request, headers);
wrapFormEntity(request, params);
return isHttps(url) ? execute(request, httpsClient) : execute(request, httpClient);
} catch (Exception e) {
logger.error("POST form request failed: {}", e.getMessage(), e);
throw new RuntimeException("HTTP POST form request failed", e);
} finally {
request.releaseConnection();
}
}
/**
* 清理连接池中的无效连接
*/
public static void cleanup() {
connectionManager.closeExpiredConnections();
connectionManager.closeIdleConnections(0, TimeUnit.SECONDS);
logger.debug("HTTP connection pool cleaned up");
}
private static String execute(HttpRequestBase request, CloseableHttpClient httpClient) {
String respJson = null;
int retryCount = 0;
while (retryCount <= MAX_RETRIES) {
try (CloseableHttpResponse response = httpClient.execute(request)) {
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
HttpEntity httpEntity = response.getEntity();
respJson = EntityUtils.toString(httpEntity, DEFAULT_CHARSET);
EntityUtils.consume(httpEntity);
break;
}
} catch (IOException e) {
retryCount++;
if (retryCount > MAX_RETRIES) {
logger.error("Request failed after {} retries: {}", MAX_RETRIES, e.getMessage());
throw new RuntimeException("HTTP request failed after retries", e);
}
try {
// 指数退避
Thread.sleep((long) Math.pow(2, retryCount) * 100);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during retry", ie);
}
// 清理无效连接
cleanup();
}
}
return respJson;
}
private static void wrapHeader(HttpRequestBase request, Map<String, String> headers) {
if (headers != null) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
request.addHeader(entry.getKey(), entry.getValue());
}
}
}
private static void wrapStringEntity(HttpPost request, String body) {
if (body != null) {
StringEntity entity = new StringEntity(body, DEFAULT_CHARSET);
entity.setContentEncoding(DEFAULT_CHARSET);
entity.setContentType(DEFAULT_CONTENT_TYPE);
request.setEntity(entity);
}
}
private static void wrapFormEntity(HttpPost request, Map<String, String> params) throws UnsupportedEncodingException {
if (params != null) {
List<NameValuePair> nvps = new ArrayList<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
nvps.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
request.setEntity(new UrlEncodedFormEntity(nvps, DEFAULT_CHARSET));
}
}
private static boolean isHttps(String url) {
return StringUtils.startsWithIgnoreCase(url, "https:");
}
}