HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

时间:2024-04-11 16:43:33

首先介绍一下我的版本是httpclient 4.3.4,采用的是  PoolingHttpClientConnectionManager 连接池的方式构造 CloseableHttpClient,代码如下:

private static final Integer REQ_TIMEOUT = 60000;     //请求超时时间ms
private static final Integer CONN_TIMEOUT = 60000;     //连接超时时间ms
private static final Integer SOCK_TIMEOUT = 60000;    //读取超时时间ms 
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
      cm.setMaxTotal(50000);
      cm.setDefaultMaxPerRoute(500);
RequestConfig requestConfig = RequestConfig.custom()
      .setConnectionRequestTimeout(REQ_TIMEOUT)
      .setConnectTimeout(CONN_TIMEOUT).setSocketTimeout(SOCK_TIMEOUT)
      .build();
httpClient = HttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(requestConfig).build();

接下来,执行如下图所示的get 请求:

HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

查看execute 方法 ,一直到下图所示的

 HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

doExecute 方法,可以看到多种实现方式,查看构造httpclient的build 构造方法到最后一行 发现 new InternalHttpClient ,然后进入其 doExecute 方法 ,直到下图的这一行

HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

发现会有一个 ClientExecChain execChain 执行链 在执行,继续查看 httpclient的build 方法 ,可以看到如下的3段代码

HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)   HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

有多个执行链在进行调用,这里采用的是一个责任链的设计模式,每一个 execChain 链都会调用其上一个的

execute 方法,所以会依次进入 MainClientExec 、ProtocolExec 、RetryExec的 execute方法,首先查看MainClientExec 的execute方法 查看到下面这行代码

持有一个 requestExecutor 会执行,然后 会执行 HttpRequestExecutor execute方法 

HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

这段代码会执行http 请求获取响应值。现在我们假设请求出现超时,会抛出  IOException 到 ProtocolExec 的 execute 方法中,如下图:

HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

所以异常继续上抛到  RetryExec 的 execute 方法中,如下图:

HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

 

会有一个循环,捕获到  IOException 后 由于 execAware.isAborted() 默认返回false, 会进入到  retryHandler.retryRequest 这段方法中. 接下来继续 查看 上图中  httpclient的build 方法 会发现 若重试类  为空,则会默认构造  DefaultHttpRequestRetryHandler ,查看 其构造方法,如下图:

HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

默认重试次数为 3,不会重试已经发送的请求。查看 其重试请求方法 

HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

可以看到只有当请求幂等 和 请求未被完全发送时才会重试,若抛出 InterruptedIOException 、UnknownHostException、ConnectException 、SSLException 这四种异常,http请求不会被重试的。

HttpClient 重试机制的源码解析 (httpclient 超时不重试问题解析)

上图我们发现 超时异常均继承于 InterruptedIOException ,所以无论是连接超时还是读取超时都不会触发重试机制。

我的解决方法是重新实现一个 重试类 HttpRequestRetryHandler ,代码如下:

HttpRequestRetryHandler requestRetryHandler=new HttpRequestRetryHandler() {
   public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
      if (executionCount > 3) //超过重试次数,就放弃
         return false;
      if (exception instanceof NoHttpResponseException) {//没有响应,重试
         return true;
      }else if (exception instanceof ConnectTimeoutException) {//连接超时,重试
         return true;
      } else if (exception instanceof SocketTimeoutException) {//连接或读取超时,重试
         return true;
      }else if (exception instanceof SSLHandshakeException) {//本地证书异常
         return false;
      } else if (exception instanceof InterruptedIOException) {//被中断
         return false;
      } else if (exception instanceof UnknownHostException) {//找不到服务器
         return false;
      }  else if (exception instanceof SSLException) {//SSL异常
         return false;
      } else {
         LOGGER.error("未记录的请求异常:" + exception.getClass());
      }
      HttpClientContext clientContext = HttpClientContext.adapt(context);
      HttpRequest request = clientContext.getRequest();
      // 如果请求是幂等的,则重试
      if (!(request instanceof HttpEntityEnclosingRequest)) return true;
      return false;
   }
};
httpClient = HttpClients.custom().setConnectionManager(cm).setDefaultRequestConfig(requestConfig).setRetryHandler(requestRetryHandler).build();

然后在构造 httpClient 时 重新设置进去即可