package cn.keking.utils; import cn.keking.config.ConfigConstants; import cn.keking.model.FileAttribute; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; 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.ssl.DefaultClientTlsStrategy; import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.pool.PoolConcurrencyPolicy; import org.apache.hc.core5.pool.PoolReusePolicy; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.StringUtils; import org.springframework.web.client.RequestCallback; import org.springframework.web.client.RestTemplate; import javax.net.ssl.SSLContext; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * HTTP请求工具类,统一处理HTTP请求逻辑 * 优化版本:支持连接复用,减少开销 */ public class HttpRequestUtils { private static final Logger logger = LoggerFactory.getLogger(HttpRequestUtils.class); private static final ObjectMapper mapper = new ObjectMapper(); // 连接池管理器(静态变量,全局共享) private static volatile PoolingHttpClientConnectionManager connectionManager; // 用于缓存不同配置的HttpClient实例 private static final Map httpClientCache = new ConcurrentHashMap<>(); // 用于缓存不同配置的RestTemplate实例 private static final Map restTemplateCache = new ConcurrentHashMap<>(); // 默认连接池配置 private static final int DEFAULT_MAX_TOTAL = 200; // 最大连接数 private static final int DEFAULT_MAX_PER_ROUTE = 50; // 每个路由最大连接数 /** * 判断是否为客户端中断连接的异常 */ public static boolean isClientAbortException(Throwable e) { if (e == null) { return false; } // 检查异常链 Throwable cause = e; while (cause != null) { // 检查异常消息 if (cause instanceof IOException) { String message = cause.getMessage(); if (message != null && ( message.contains("你的主机中的软件中止了一个已建立的连接") || message.contains("Broken pipe") || message.contains("Connection reset by peer") || message.contains("ClientAbortException"))) { return true; } } // 检查异常类型 String className = cause.getClass().getName(); if (className.contains("ClientAbortException") || className.contains("AbortedException") || className.contains("AsyncRequestNotUsableException")) { return true; } cause = cause.getCause(); } return false; } /** * 初始化连接池管理器(懒加载) */ private static PoolingHttpClientConnectionManager getConnectionManager() throws Exception { if (connectionManager == null) { synchronized (HttpRequestUtils.class) { if (connectionManager == null) { // 创建连接池管理器 PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create(); // 如果配置忽略SSL,使用自定义TLS策略 if (ConfigConstants.isIgnoreSSL()) { SSLContext sslContext = SslUtils.createIgnoreVerifySSL(); DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy( sslContext, NoopHostnameVerifier.INSTANCE); builder.setTlsSocketStrategy(tlsStrategy); } // 设置连接池参数 builder.setPoolConcurrencyPolicy(PoolConcurrencyPolicy.LAX) .setConnPoolPolicy(PoolReusePolicy.LIFO) .setMaxConnTotal(DEFAULT_MAX_TOTAL) .setMaxConnPerRoute(DEFAULT_MAX_PER_ROUTE); // 设置Socket配置 SocketConfig socketConfig = SocketConfig.custom() .setTcpNoDelay(true) .setSoKeepAlive(true) .setSoReuseAddress(true) .setSoTimeout(Timeout.ofSeconds(30)) .build(); builder.setDefaultSocketConfig(socketConfig); // 设置连接配置 ConnectionConfig connectionConfig = ConnectionConfig.custom() .setConnectTimeout(Timeout.ofSeconds(10)) .setSocketTimeout(Timeout.ofSeconds(30)) .setTimeToLive(TimeValue.ofMinutes(5)) .build(); builder.setDefaultConnectionConfig(connectionConfig); connectionManager = builder.build(); // 启动空闲连接清理线程 startIdleConnectionMonitor(); logger.info("HTTP连接池管理器初始化完成,最大连接数:{},每个路由最大连接数:{}", DEFAULT_MAX_TOTAL, DEFAULT_MAX_PER_ROUTE); } } } return connectionManager; } /** * 启动空闲连接监控线程 */ private static void startIdleConnectionMonitor() { Thread monitorThread = new Thread(() -> { try { while (!Thread.currentThread().isInterrupted()) { synchronized (HttpRequestUtils.class) { Thread.sleep(30000); // 每30秒检查一次 if (connectionManager != null) { // 关闭过期的连接 connectionManager.closeExpired(); // 关闭空闲超过30秒的连接 connectionManager.closeIdle(TimeValue.ofSeconds(30)); // 可选:打印连接池状态 if (logger.isDebugEnabled()) { logger.debug("连接池状态:最大连接数={}, 每个路由最大连接数={}", connectionManager.getMaxTotal(), connectionManager.getDefaultMaxPerRoute()); } } } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); logger.info("连接池监控线程被中断"); } catch (Exception e) { logger.error("连接池监控异常", e); } }); monitorThread.setDaemon(true); monitorThread.setName("http-connection-monitor"); monitorThread.start(); } /** * 创建根据配置定制的HttpClient(支持复用) */ public static CloseableHttpClient createConfiguredHttpClient() throws Exception { String cacheKey = buildHttpClientCacheKey(); // 尝试从缓存获取 CloseableHttpClient cachedClient = httpClientCache.get(cacheKey); if (cachedClient != null) { // HttpClient 5.x 没有 isClosed() 方法,我们需要通过其他方式判断 // 暂时假设缓存的客户端都是可用的,如果有问题会在使用时报错 return cachedClient; } // 创建新的HttpClient synchronized (httpClientCache) { // 双重检查 cachedClient = httpClientCache.get(cacheKey); if (cachedClient != null) { return cachedClient; } // 构建HttpClientBuilder HttpClientBuilder httpClientBuilder = HttpClients.custom() .setConnectionManager(getConnectionManager()) .setConnectionManagerShared(true); // 共享连接管理器 // 使用SslUtils配置HttpClientBuilder CloseableHttpClient httpClient = SslUtils.configureHttpClientBuilder( httpClientBuilder, ConfigConstants.isIgnoreSSL(), ConfigConstants.isEnableRedirect() ).build(); // 缓存HttpClient httpClientCache.put(cacheKey, httpClient); logger.debug("创建并缓存新的HttpClient实例,缓存键:{}", cacheKey); return httpClient; } } /** * 构建HttpClient缓存键 */ private static String buildHttpClientCacheKey() { return String.format("ignoreSSL_%s_enableRedirect_%s", ConfigConstants.isIgnoreSSL(), ConfigConstants.isEnableRedirect()); } /** * 获取缓存的RestTemplate(减少对象创建) */ private static RestTemplate getCachedRestTemplate(CloseableHttpClient httpClient) { String cacheKey = "restTemplate_" + System.identityHashCode(httpClient); return restTemplateCache.computeIfAbsent(cacheKey, key -> { HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); factory.setHttpClient(httpClient); // 设置连接超时和读取超时 factory.setConnectTimeout(30000); factory.setReadTimeout(30000); RestTemplate restTemplate = new RestTemplate(); restTemplate.setRequestFactory(factory); logger.debug("创建并缓存新的RestTemplate实例,缓存键:{}", cacheKey); return restTemplate; }); } /** * 执行HTTP请求(使用连接池) */ public static void executeHttpRequest(java.net.URL url, CloseableHttpClient httpClient, FileAttribute fileAttribute, FileResponseHandler handler) throws Exception { // 获取缓存的RestTemplate RestTemplate restTemplate = getCachedRestTemplate(httpClient); String finalUrlStr = url.toString(); RequestCallback requestCallback = createRequestCallback(finalUrlStr, fileAttribute); try { restTemplate.execute(url.toURI(), HttpMethod.GET, requestCallback, response -> { FileResponseWrapper wrapper = new FileResponseWrapper(); wrapper.setInputStream(response.getBody()); wrapper.setContentType(WebUtils.headersType(response)); try { handler.handleResponse(wrapper); } catch (Exception e) { // 如果是客户端中断连接,不再记录为错误 if (isClientAbortException(e)) { if (logger.isDebugEnabled()) { logger.debug("客户端中断连接,可能用户取消了下载,URL: {}", url); } } else { logger.error("处理文件响应时出错", e); } try { throw e; } catch (Exception ex) { throw new RuntimeException(ex); } } return null; }); } catch (Exception e) { // 如果是客户端中断连接,不再记录为错误 if (isClientAbortException(e)) { if (logger.isDebugEnabled()) { logger.debug("客户端中断连接,URL: {}", url); } throw e; // 重新抛出,让调用者处理 } // 如果是SSL证书错误,给出建议 if (e.getMessage() != null && (e.getMessage().contains("SSL") || e.getMessage().contains("证书") || e.getMessage().contains("certificate")) && !ConfigConstants.isIgnoreSSL()) { logger.warn("SSL证书验证失败,建议启用SSL忽略功能或检查证书"); } throw e; } // 注意:不再关闭HttpClient,由连接池管理 } /** * 创建请求回调 */ private static RequestCallback createRequestCallback(String finalUrlStr, FileAttribute fileAttribute) { return request -> { request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)); WebUtils.applyBasicAuthHeaders(request.getHeaders(), finalUrlStr); // 添加Keep-Alive头 request.getHeaders().set("Connection", "keep-alive"); request.getHeaders().set("Keep-Alive", "timeout=60"); String proxyAuthorization = fileAttribute.getKkProxyAuthorization(); if (StringUtils.hasText(proxyAuthorization)) { Map proxyAuthorizationMap = mapper.readValue( proxyAuthorization, TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class) ); proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value)); } }; } /** * 清理资源(在应用关闭时调用) */ public static void shutdown() { logger.info("开始清理HTTP连接池资源..."); // 关闭所有缓存的HttpClient httpClientCache.values().forEach(client -> { try { client.close(); } catch (Exception e) { logger.warn("关闭HttpClient失败", e); } }); httpClientCache.clear(); // 关闭连接池管理器 if (connectionManager != null) { try { connectionManager.close(); logger.info("HTTP连接池管理器已关闭"); } catch (Exception e) { logger.warn("关闭连接池管理器失败", e); } connectionManager = null; } // 清空RestTemplate缓存 restTemplateCache.clear(); logger.info("HTTP连接池资源清理完成"); } /** * 文件响应处理器接口 */ public interface FileResponseHandler { void handleResponse(FileResponseWrapper responseWrapper) throws Exception; } /** * 文件响应包装器 */ public static class FileResponseWrapper { private InputStream inputStream; private String contentType; private boolean hasError; public InputStream getInputStream() { return inputStream; } public void setInputStream(InputStream inputStream) { this.inputStream = inputStream; } public String getContentType() { return contentType; } public void setContentType(String contentType) { this.contentType = contentType; } public boolean isHasError() { return hasError; } public void setHasError(boolean hasError) { this.hasError = hasError; } } }