使用 Apache HttpClient 重試請求
一、簡介
在本教程中,我們將了解如何在使用 Apache HtttpClient
時重試 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);
}
有一些HttpClient
認為不可重試的IOException
子類。更具體地說,它們是:
-
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
,我們可以設置我們想要的重試次數以及HttpClient
何時應該重試冪等請求:
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 次重試。請參閱第一個構造函數參數。此外,我們啟用了冪等請求的重試。請參閱第二個構造函數布爾參數。因此, HttpCleint
執行了 7 次 POST 請求——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.禁用重試邏輯
最後,有些情況下我們想要禁用 reties。我們可以提供一個總是返回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
中的所有重試。這意味著沒有請求會被淘汰。
六,結論
在本教程中,我們討論了 Apache HttpClient
中的默認重試行為。考慮到異常發生,開箱即用的RetryHandler
將重試冪等請求 3 次。但是,我們可以配置重試次數和非冪等請求重試策略。此外,我們可以提供自己的RetryHandler
實現以進行進一步的定制。最後,我們可以通過在HttpClient
構造期間調用HttpClientBuilder
上的方法來禁用 retires。
與往常一樣,本文中使用的源代碼可在 GitHub 上獲得。