http工具类(支持https,连接池和失败重试)

本文介绍了一种封装HTTP客户端的方法,包括GET和POST请求的支持,并详细解释了如何通过连接池管理和失败重试来提高HTTP请求的性能及稳定性。此外,还提供了处理HTTPS请求和自定义SSL证书验证的功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在实际项目中,经常会遇到调用外部(第三方)的接口,如果调用量较大的话,可能需要考虑连接池、失败重试、SSL证书等问题,以提升性能和稳定性。

以下代码是封装的小组件,供大家参考。

  • maven依赖
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.6</version>
        </dependency>
  • HttpUtil.java代码:

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.util.CollectionUtils;

import javax.exceptions.ServiceException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * http/https工具类。
 * 支持线程池管理、failRetry特性。
 */
public class HttpUtil {

    private static final String DEFAULT_CHARSET = "UTF-8";
    private static CloseableHttpClient httpClient;

    public static String get(final String url) throws Exception {
        return getExecute(url, null, getHttpClient(null), null);
    }

    public static String get(final String url, final Map<String, Object> paramMap) throws Exception {
        return getExecute(url, paramMap, getHttpClient(null), null);
    }

    public static String get(final String url, final Map<String, Object> paramMap, final Map<String, String> headers) throws Exception {
        return getExecute(url, paramMap, getHttpClient(null), headers);
    }

    /**
     * POST请求,支持失败重试。
     */
    public static String post(final String url, final Map<String, Object> paramMap) throws Exception {
        return postExecute(url, paramMap, new HashMap<>(), getHttpClient(null));
    }

    /**
     * POST请求,支持失败重试。
     */
    public static String postRetry(final String url, final Map<String, Object> paramMap, final Map<String, String> headers) throws Exception {
        return postExecute(url, paramMap, headers, getHttpClient(null));
    }

    /**
     * POST请求,支持失败重试。
     */
    public static String postRetry(final String url, final String paramStr,
                                   final CertInfo certInfo, final Map<String, String> headers) throws Exception {
        return postExecute(url, paramStr, headers, getHttpClient(certInfo));
    }

    /**
     * POST请求,支持失败重试。
     */
    public static String postRetry(final String url, final Map<String, Object> paramMap,
                                   final CertInfo certInfo, final Map<String, String> headers) throws Exception {
        return postExecute(url, paramMap, headers, getHttpClient(certInfo));
    }

    private static synchronized CloseableHttpClient getHttpClient(final CertInfo certInfo) {
        if (null == certInfo && null != httpClient) {
            return httpClient;
        } else {
            //https请求暂不走httpClient缓存,因为每次请求可能使用不同的证书。
            RequestConfig config = RequestConfig.custom()
                    .setSocketTimeout(Constants.HTTP_READ_TIMEOUT_MS)
                    .setConnectTimeout(Constants.HTTP_CONNECTION_TIMEOUT_MS)
                    .build();
            HttpClientConnectionManager connManager = getPoolConnManager(certInfo);
            HttpRequestRetryHandler retryHandler = RetryHandler.getHandler(Constants.DEFAULT_HTTP_RETRY_MAX_TIMES, Constants.DEFAULT_HTTP_RETRY_INTERVAL_MILLIS);

            httpClient = HttpClients.custom()
                    .setConnectionManager(connManager)
                    .setDefaultRequestConfig(config)
                    .setRetryHandler(retryHandler)
                    .build();

            //jvm关闭时,释放连接
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                try {
                    httpClient.close();
                } catch (IOException e) {
                    //catched
                }
            }));

            return httpClient;
        }
    }

    /**
     * 获取 连接管理器实例 。
     * 支持http/https和连接池。
     */
    private static HttpClientConnectionManager getPoolConnManager(final CertInfo certInfo) {
        Registry<ConnectionSocketFactory> registry;
        if (null != certInfo) {
            certInfo.validate();

            try {
                // 证书
                char[] password = certInfo.getPassword().toCharArray();
                InputStream certStream = certInfo.getCertStream();
                KeyStore ks = KeyStore.getInstance("PKCS12");
                ks.load(certStream, password);

                // 实例化密钥库 & 初始化密钥工厂
                KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
                kmf.init(ks, password);

                // 创建 SSLContext
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());

                SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
                        sslContext,
                        new String[]{"TLSv1"},
                        null,
                        new DefaultHostnameVerifier());

                registry = RegistryBuilder.<ConnectionSocketFactory>create()
                        .register("https", sslConnectionSocketFactory)
                        .register("http", PlainConnectionSocketFactory.getSocketFactory())
                        .build();

            } catch (Exception e) {
                throw new ServiceException(ErrorCodeEnums.InternalServerError, "创建 SSLContext 失败,证书初始化失败");
            }

        } else {
            registry = RegistryBuilder.<ConnectionSocketFactory>create()
                    .register("http", PlainConnectionSocketFactory.getSocketFactory())
                    .register("https", SSLConnectionSocketFactory.getSocketFactory())
                    .build();
        }
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
        //http连接池: 最大连接数,默认20
        connectionManager.setMaxTotal(Constants.HTTP_POOL_MAX_TOTAL);
        //http连接池: 每个路由的并发连接,默认2
        connectionManager.setDefaultMaxPerRoute(Constants.HTTP_POOL_MAX_PER_ROUTE);
        return connectionManager;
    }

    /**
     * 执行POST请求。
     */
    private static String postExecute(String url, String data, Map<String, String> headers,
                                      CloseableHttpClient closeableHttpClient) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        if (!CollectionUtils.isEmpty(headers)) {
            headers.forEach(httpPost::addHeader);
        }
        StringEntity postEntity = new StringEntity(data, DEFAULT_CHARSET);
        httpPost.setEntity(postEntity);
        HttpResponse httpResponse = closeableHttpClient.execute(httpPost);

        int httpStatus = httpResponse.getStatusLine().getStatusCode();
        if (HttpStatus.SC_OK != httpStatus) {
            System.err.println(url + "接口调用失败,返回状态码:" + httpStatus);
        }

        HttpEntity httpEntity = httpResponse.getEntity();
        return EntityUtils.toString(httpEntity, DEFAULT_CHARSET);
    }

    private static String postExecute(String url, Map<String, Object> params, Map<String, String> headers,
                                      CloseableHttpClient closeableHttpClient) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        if (headers != null && !headers.isEmpty()) {
            headers.forEach(httpPost::addHeader);
        }
        List<NameValuePair> pairList = new ArrayList<>(params.size());
        for (Map.Entry<String, Object> entry : params.entrySet()) {
            NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry.getValue().toString());
            pairList.add(pair);
        }
        httpPost.setEntity(new UrlEncodedFormEntity(pairList, DEFAULT_CHARSET));
        HttpResponse httpResponse = closeableHttpClient.execute(httpPost);

        int httpStatus = httpResponse.getStatusLine().getStatusCode();
        if (HttpStatus.SC_OK != httpStatus) {
            System.err.println(url + "接口调用失败,返回状态码:" + httpStatus);
        }

        HttpEntity httpEntity = httpResponse.getEntity();
        return EntityUtils.toString(httpEntity, DEFAULT_CHARSET);
    }

    private static String getExecute(String url, Map<String, Object> params, CloseableHttpClient closeableHttpClient, Map<String, String> headers) throws IOException {
        CloseableHttpResponse response;

        if (!CollectionUtils.isEmpty(params)) {
            StringBuilder realUrl = new StringBuilder().append(url).append("?");
            for (String key : params.keySet()) {
                realUrl.append(key).append("=").append(params.get(key)).append("&");
            }
            url = realUrl.substring(0, realUrl.length() - 1);
        }

        HttpGet httpGet = new HttpGet(url);
        if (!CollectionUtils.isEmpty(headers)) {
            headers.forEach(httpGet::addHeader);
        }
        response = closeableHttpClient.execute(httpGet);

        int httpStatus = response.getStatusLine().getStatusCode();
        if (HttpStatus.SC_OK != httpStatus) {
            System.err.println(url + "接口调用失败,返回状态码:" + httpStatus);
        }
        return EntityUtils.toString(response.getEntity(), DEFAULT_CHARSET);
    }
}


  • Constants 常量类:
public class Constants {
    // --------------------- http配置相关 start ------------------
    /**
     * HTTP连接超时时间。
     */
    public static final int HTTP_CONNECTION_TIMEOUT_MS = 3000;

    /**
     * http read timeout
     */
    public static final int HTTP_READ_TIMEOUT_MS = 25000;

    /**
     * http连接池: 最大连接数,默认20
     **/
    public static final int HTTP_POOL_MAX_TOTAL = 32;

    /**
     * http连接池: 每个路由的并发连接,默认2
     **/
    public static final int HTTP_POOL_MAX_PER_ROUTE = 16;

    /**
     * http失败重试:  默认最大重试次数。
     */
    public static final int DEFAULT_HTTP_RETRY_MAX_TIMES = 2;

    /**
     * http失败重试: 默认每次的间隔时间。默认为1秒。
     */
    public static final Integer DEFAULT_HTTP_RETRY_INTERVAL_MILLIS = 1000;
    
    // ---------------------  http配置相关  end ------------------
}
  • ErrorCodeEnums.java类:
public enum ErrorCodeEnums {
    InternalServerError(500, "服务器内部错误"),
}
  • SSL证书类CertInfo.java:

import org.springframework.util.Assert;

import java.io.InputStream;
/**
 * 发起HTTPS/SSL使用的证书。
 *
 * 
 */
public class CertInfo {
    //证书密码(口令)
    private String password;

    //证书文件
    private InputStream certStream;

    public String getPassword() {
        return password;
    }

    public CertInfo setPassword(String password) {
        this.password = password;
        return this;
    }

    public InputStream getCertStream() {
        return certStream;
    }

    public CertInfo setCertStream(InputStream certStream) {
        this.certStream = certStream;
        return this;
    }

    public void validate() {
        Assert.hasLength(password, "password不能为空");
        Assert.notNull(certStream, "certStream不能为空");
    }
}

  • HTTP重试策略类:
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;

/**
 * HTTP重试策略。
 *
 * @author machunlin
 * @date 2018/4/26
 */
public class RetryHandler {
    private static final Logger LOGGER = LogManager.getLogger(RetryHandler.class);

    /**
     * 自定义HTTP重试策略。
     *
     * @param maxRetryTimes  最大重试次数
     * @param intervalMillis 重试间隔时间,毫秒
     * @return
     */
    public static HttpRequestRetryHandler getHandler(final Integer maxRetryTimes, final Integer intervalMillis) {
        if (maxRetryTimes == null || maxRetryTimes <= 0) {
            throw new IllegalArgumentException("maxRetryTimes参数不合法:" + maxRetryTimes);
        }

        return (exception, executionCount, context) -> {
            if (executionCount > maxRetryTimes) {
                LOGGER.error(" 已超过最大重试次数。 当前为第{}次重试,最大重试次数为{}", executionCount, maxRetryTimes, exception);
                return false;
            }

            try {
                long interval = (null != intervalMillis) ? intervalMillis :  Constants.DEFAULT_HTTP_RETRY_INTERVAL_MILLIS;
                long nextInterval = interval * executionCount;
                Thread.sleep(nextInterval);
            } catch (InterruptedException e) {
                //声明线程状态为"可中断的"。
                Thread.currentThread().interrupt();
            }

            if (exception instanceof UnknownHostException) {
                LOGGER.error("目标服务器不可达, Unknown host", exception);
                return false;
            }
            if (exception instanceof ConnectTimeoutException) {
                LOGGER.error("连接被拒绝,ConnectTimeout ", exception);
                return true;
            }

            if (exception instanceof InterruptedIOException) {
                LOGGER.error("连接超时, Timeout", exception);
                return true;
            }

            if (exception instanceof IOException) {
                LOGGER.error("网络异常, IOException", exception);
                return true;
            }

            if (exception instanceof SSLHandshakeException) {
                LOGGER.error("SSL握手异常,SSLHandshakeException ", exception);
                return false;
            }
            if (exception instanceof SSLException) {
                LOGGER.error("SSLException exception", exception);
                return false;
            }

            if (exception instanceof NoHttpResponseException) {
                // 如果服务器丢掉了连接,那么就重试
                return true;
            }

            if (exception instanceof Exception) {
                return true;
            }

            HttpClientContext clientContext = HttpClientContext.adapt(context);
            HttpRequest request = clientContext.getRequest();
            if (!(request instanceof HttpEntityEnclosingRequest)) {
                // 如果请求是幂等的,就再次尝试
                return true;
            }

            LOGGER.error("HttpRequestRetryHandler, return false");
            return false;
        };
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值