1. 概述

在这个教程中,我们将探讨如何在使用Apache HttpClient时重试HTTP请求。我们还会研究库的默认重试行为以及如何配置它。

2. 默认重试策略

在深入讨论之前,我们先创建一个测试类,包含HttpClient实例和请求计数器:

public class ApacheHttpClientRetryUnitTest {

    private Integer requestCounter;
    private CloseableHttpClient httpClient;

    @BeforeEach
    void setUp() {
        requestCounter = 0;
    }

    @AfterEach
    void tearDown() throws IOException {
        if (httpClient != null) {
            httpClient.close();
        }
    }
}

首先来看默认行为:Apache HttpClient最多会尝试3次所有幂等请求,这些请求完成后会抛出IOException,因此总共会有4次请求。这里我们将创建一个HttpClient,每次请求都会抛出IOException,仅用于演示:

private void createFailingHttpClient() {
   this.httpClient = HttpClientBuilder
     .create()
     .addInterceptorFirst((HttpRequestInterceptor) (request, context) -> requestCounter++)
     .addInterceptorLast((HttpResponseInterceptor) (response, context) -> { throw new IOException(); })
     .build()
}

@Test
public void givenDefaultConfiguration_whenReceviedIOException_thenRetriesPerformed() {
    createFailingHttpClient();
    assertThrows(IOException.class, () -> httpClient.execute(new HttpGet("https://httpstat.us/200")));
    assertThat(requestCounter).isEqualTo(4);
}

有一些IOException的子类,HttpClient认为它们是不可重试的。具体来说,它们包括:

  • InterruptedIOException
  • ConnectException
  • UnknownHostException
  • SSLException
  • NoRouteToHostException

例如,如果我们无法解析目标主机的DNS名称,请求将不会被重试:

public void createDefaultApacheHttpClient() {
    this.httpClient = HttpClientBuilder
      .create()
      .addInterceptorFirst((HttpRequestInterceptor) (httpRequest, httpContext) -> {
          requestCounter++;
      }).build();
}

@Test
public void givenDefaultConfiguration_whenDomainNameNotResolved_thenNoRetryApplied() {
    createDefaultApacheHttpClient();
    HttpGet request = new HttpGet(URI.create("http://domain.that.does.not.exist:80/api/v1"));

    assertThrows(UnknownHostException.class, () -> httpClient.execute(request));
    assertThat(requestCounter).isEqualTo(1);
}

我们可以注意到,这些异常通常表示网络或TLS问题。这意味着它们不涉及HTTP请求处理的失败。这意味着如果服务器对我们的请求响应了5xx或4xx状态码,那么将不会应用重试逻辑:

@Test
public void givenDefaultConfiguration_whenGotInternalServerError_thenNoRetryLogicApplied() throws IOException {
    createDefaultApacheHttpClient();
    HttpGet request = new HttpGet(URI.create("https://httpstat.us/500"));

    CloseableHttpResponse response = assertDoesNotThrow(() -> httpClient.execute(request));
    assertThat(response.getStatusLine().getStatusCode()).isEqualTo(500);
    assertThat(requestCounter).isEqualTo(1);
    response.close();
}

但在大多数情况下,我们通常希望至少在5xx状态码上进行重试。因此,我们需要覆盖默认行为。我们将在下一节中进行操作。

3. 幂等性

在自定义重试之前,我们需要详细说明请求的幂等性。这是重要的,因为Apache HTTP客户端认为所有实现HttpEntityEnclosingRequest接口的请求都是非幂等的。这个接口的常见实现包括HttpPostHttpPutHttpPatch类。因此,我们的PATCH和PUT请求默认不会被重试:

@Test
public void givenDefaultConfiguration_whenHttpPatchRequest_thenRetryIsNotApplied() {
    createFailingHttpClient();
    HttpPatch request = new HttpPatch(URI.create("https://httpstat.us/500"));

    assertThrows(IOException.class, () -> httpClient.execute(request));
    assertThat(requestCounter).isEqualTo(1);
}

@Test
public void givenDefaultConfiguration_whenHttpPutRequest_thenRetryIsNotApplied() {
    createFailingHttpClient();
    HttpPut request = new HttpPut(URI.create("https://httpstat.us/500"));

    assertThrows(IOException.class, () -> httpClient.execute(request));
    assertThat(requestCounter).isEqualTo(1);
}

如图所示,没有进行任何重试,即使我们收到了IOException

4. 定制RetryHandler

我们提到的默认行为可以被覆盖。首先,我们可以设置RetryHandler。为此,可以选择使用DefaultHttpRequestRetryHandler。这是一个方便的现成的RetryHandler实现,事实上,库默认使用的就是这个。这个默认实现也实现了我们之前讨论的默认行为。

通过使用DefaultHttpRequestRetryHandler,我们可以设置想要的重试次数,以及何时重试幂等请求:

private void createHttpClientWithRetryHandler() {
    this.httpClient = HttpClientBuilder
      .create()
      .addInterceptorFirst((HttpRequestInterceptor) (httpRequest, httpContext) -> requestCounter++)
      .addInterceptorLast((HttpResponseInterceptor) (httpRequest, httpContext) -> { throw new IOException(); })
      .setRetryHandler(new DefaultHttpRequestRetryHandler(6, true))
      .build();
}

@Test
public void givenConfiguredRetryHandler_whenHttpPostRequest_thenRetriesPerformed() {
    createHttpClientWithRetryHandler();

    HttpPost request = new HttpPost(URI.create("https://httpstat.us/500"));

    assertThrows(IOException.class, () -> httpClient.execute(request));
    assertThat(requestCounter).isEqualTo(7);
}

如图所示,我们配置了DefaultHttpRequestRetryHandler进行6次重试。看第一个构造函数参数。此外,我们启用了幂等请求的重试。看第二个构造函数的布尔参数。因此,HttpClient执行我们的POST请求7次 - 原始请求1次和重试6次。

如果这种程度的定制不够,我们还可以创建自己的RetryHandler

private void createHttpClientWithCustomRetryHandler() {
    this.httpClient = HttpClientBuilder
      .create()
      .addInterceptorFirst((HttpRequestInterceptor) (httpRequest, httpContext) -> requestCounter++)
      .addInterceptorLast((HttpResponseInterceptor) (httpRequest, httpContext) -> { throw new IOException(); })
      .setRetryHandler((exception, executionCount, context) -> {
          if (executionCount <= 4 && Objects.equals("GET", ((HttpClientContext) context).getRequest().getRequestLine().getMethod())) {
              return true;
          } else {
              return false;
          }
    }).build();
}

@Test
public void givenCustomRetryHandler_whenUnknownHostException_thenRetryAnyway() {
    createHttpClientWithCustomRetryHandler();

    HttpGet request = new HttpGet(URI.create("https://domain.that.does.not.exist/200"));

    assertThrows(IOException.class, () -> httpClient.execute(request));
    assertThat(requestCounter).isEqualTo(5);
}

在这里,我们基本上说-无论发生何种异常,都将对所有GET请求重试4次。所以,在上面的例子中,我们重试了UnknownHostException

5. 关闭重试逻辑

最后,有些情况下我们可能希望禁用重试。我们可以提供一个始终返回falseRetryHandler,或者使用disableAutomaticRetries()

private void createHttpClientWithRetriesDisabled() {
    this.httpClient = HttpClientBuilder
      .create()
      .addInterceptorFirst((HttpRequestInterceptor) (httpRequest, httpContext) -> requestCounter++)
      .addInterceptorLast((HttpResponseInterceptor) (httpRequest, httpContext) -> { throw new IOException(); })
      .disableAutomaticRetries()
      .build();
}

@Test
public void givenDisabledRetries_whenExecutedHttpRequestEndUpWithIOException_thenRetryIsNotApplied() {
    createHttpClientWithRetriesDisabled();
    HttpGet request = new HttpGet(URI.create("https://httpstat.us/200"));

    assertThrows(IOException.class, () -> httpClient.execute(request));
    assertThat(requestCounter).isEqualTo(1);
}

在HttpClientBuilder上调用disableAutomaticRetries(),则会禁用HttpClient中的所有重试。这意味着没有任何请求会被重试。

6. 总结

在这篇教程中,我们讨论了Apache HttpClient的默认重试行为。默认的RetryHandler会在遇到异常的情况下,对幂等请求重试3次。然而,我们可以配置重试次数和非幂等请求的重试策略。此外,我们还可以提供自定义的RetryHandler实现以进行更深入的定制。最后,我们可以通过在HttpClient构建过程中调用方法来关闭重试功能。如往常一样,文章中使用的源代码可在GitHub上获取。