HttpClientUtil

最近公司有个项目,没有使用OpenFeign或Dubbo这种微服务远程调用框架,使用的是原生的Apache HttpClient。项目中有个定时任务,需要多次调用其他服务对结果进行汇总,为了保证汇总结果的正确性,就需要确保每次远程调用都是成功的。这个有点类似于数据库中的事务,不知道有没有专业的技术用语,这里姑且叫做远程调用事务。

那么我自己对远程调用事务的理解就是,远程调用第三方服务接口时,由于网络抖动等不稳定性,可能会导致调用异常,比如SocketTimeOut等,这时候要保证结果的正确性,就需要事务机制,要么调用都成功,要么调用都回滚。因为当前的业务中只是多次调用接口获取结果,也就是查,而不是增删改这种操作,因此这里的处理方式就简单了很多,只需要有异常时重试即可,如果某次调用重试后仍然异常,那么就记录此次定时任务失败信息到数据库,后续可以进行定时任务补偿。

因此,本文主要是记录如何自定义HttpClient重试策略。
这里先贴两个参考的文章:

  • https://www.codeleading.com/article/2829663803/
  • https://www.cnblogs.com/kingszelda/p/8886403.html

下面是我的代码:

@ConfigurationProperties(prefix = "http.client")
@Configuration
@Data
@ToString
public class HttpClientConfig {

    /**
     * 连接超时时间 http.client.connection-timeout
     */
    private Integer connectionTimeout = 6000;

    /**
     * 请求超时时间 http.client.socket-timeout
     */
    private Integer socketTimeout = 600000;

    private Integer connectionRequestTimeout = 60000;

    /**
     * 重试次数 http.client.retry-times
     */
    private Integer retryTimes = 5;

    /**
     * 重试间隔(s) http.client.retry-interval
     */
    private Integer retryInterval = 3000;

}
@Component
@ConditionalOnBean(HttpClientConfig.class)
public class HttpClientUtil {

    private static Logger logger = LoggerFactory.getLogger(HttpClient.class);

    private static PoolingHttpClientConnectionManager connectionManager;
    private static RequestConfig requestConfig;
    private static HttpClientConfig httpClientConfig;
    private static final CustomRetryHandler retryHandler = new CustomRetryHandler();

    @Autowired
    public HttpClientUtil(HttpClientConfig httpClientConfig) {
        HttpClientUtil.httpClientConfig = httpClientConfig;
        // 设置连接池
        connectionManager = new PoolingHttpClientConnectionManager();
        // 设置连接池大小
        connectionManager.setMaxTotal(100);
        connectionManager.setDefaultMaxPerRoute(connectionManager.getMaxTotal());

        RequestConfig.Builder configBuilder = RequestConfig.custom();
        // 设置连接超时
        configBuilder.setConnectTimeout(httpClientConfig.getConnectionTimeout());
        // 设置读取超时时间为10分钟
        configBuilder.setSocketTimeout(httpClientConfig.getSocketTimeout());
        // 设置从连接池获取连接实例的超时
        configBuilder.setConnectionRequestTimeout(httpClientConfig.getConnectionRequestTimeout());
        // 在提交请求之前 测试连接是否可用
        configBuilder.setStaleConnectionCheckEnabled(true);
        requestConfig = configBuilder.build();
    }

    /**
     * 发送 POST 请求(HTTP),JSON形式
     * @param apiUrl
     * @param json
     * @return
     */
    public static String doPost(String apiUrl, Object json) throws Exception {
        CloseableHttpClient httpClient = getClient(true);
        String httpStr = null;
        HttpPost httpPost = new HttpPost(apiUrl);
        CloseableHttpResponse response = null;
        try {
            logger.info("http客户端配置:{}", httpClientConfig.toString());
            httpPost.setConfig(requestConfig);
            StringEntity stringEntity = new StringEntity(json.toString(), "UTF-8");// 解决中文乱码问题
            stringEntity.setContentEncoding("UTF-8");
            stringEntity.setContentType("application/json");
            httpPost.setEntity(stringEntity);
            response = httpClient.execute(httpPost);
            if (response != null){
                if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                    httpStr = EntityUtils.toString(response.getEntity(),"UTF-8");
                } else {
                    logger.error("doPost Exception,status code = {},request uri = {}", response.getStatusLine().getStatusCode(), httpPost.getURI());
                }
            }
        } catch (IOException e) {
            logger.error(LoggerHelper.message("doPost Exception"), e);
            throw new Exception(e);
        } finally {
            if (response != null) {
                try {
                    EntityUtils.consume(response.getEntity());
                } catch (IOException e) {
                    logger.error(LoggerHelper.message("doPost Exception"), e);
                }
            }
        }
        return httpStr;
    }

    /**
     * 自定义重试策略
     */
    static class CustomRetryHandler implements HttpRequestRetryHandler {
        @Override
        public boolean retryRequest(IOException arg0, int retryTimes, HttpContext arg2) {
            boolean retry = false;
            if (retryTimes > httpClientConfig.getRetryTimes()) {//最多重试3次,如果超过3次,则不再重试,直接抛异常
                retry = false;
            }else if (arg0 instanceof UnknownHostException || arg0 instanceof ConnectTimeoutException
                    || !(arg0 instanceof SSLException) || arg0 instanceof NoHttpResponseException) {
                retry = true;
            }else if (HttpClientContext.adapt(arg2).getRequest() instanceof HttpEntityEnclosingRequest){//如果是幂等请求,则直接重试
                retry = true;
            }

            //设置重试时间间隔
            if (retry) {
                try {
                    logger.info("=== 第 {} 次重试,最多 {} 次,间隔 {} 秒 ===",retryTimes,httpClientConfig.getRetryTimes(),httpClientConfig.getRetryInterval()/1000);
                    Thread.sleep(httpClientConfig.getRetryInterval());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return retry;
        }
    }

    /**
     * @param isPooled 是否使用连接池
     * @return
     */
    public static CloseableHttpClient getClient(boolean isPooled) {
        if (isPooled) {
            return HttpClients.custom().setConnectionManager(connectionManager).setRetryHandler(retryHandler).build();
        }
        return HttpClients.createDefault();
    }
}

可以通过配置参数指定重试次数、重试间隔等。
doPost()方法在有异常时直接抛出,外层业务代码捕获后,根据业务判断后续处理逻辑,比如保存操作记录用于后续补偿。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值