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
接口的请求都是非幂等的。这个接口的常见实现包括HttpPost
、HttpPut
和HttpPatch
类。因此,我们的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. 关闭重试逻辑
最后,有些情况下我们可能希望禁用重试。我们可以提供一个始终返回false
的RetryHandler
,或者使用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上获取。