1. 引言
通過 REST 端點調用外部服務是常見活動,而諸如 Feign 這樣的庫極大地簡化了這一過程。然而,在這些調用過程中,可能會發生許多問題。許多問題是隨機的或暫時的。
在本教程中,我們將學習如何重試失敗的調用以及如何構建更健壯的 REST 客户端。
2. Feign 客户端設置
首先,讓我們創建一個簡單的 Feign 客户端構建器,稍後我們會用重試功能增強它。 我們將使用 OkHttpClient 作為 HTTP 客户端。 此外,我們還將使用 GsonEncoder 和 GsonDecoder 用於編碼和解碼請求和響應。 最後,我們需要指定目標 URI 和響應類型:
public class ResilientFeignClientBuilder {
public static <T> T createClient(Class<T> type, String uri) {
return Feign.builder()
.client(new OkHttpClient())
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(type, uri);
}
}
或者,如果使用 Spring,我們可以讓它自動注入 Feign 客户端,並使用可用的 Bean。
3. Feign Retryer
Fortunately, retrying abilities are baked in Feign, and they just need to be configured. We can do that by providing an implementation of the Retryer interface to the client builder.
Its most important method, continueOrPropagate, accepts RetryableException as an argument and returns nothing. Upon execution, it either throws an exception or exits successfully (usually after sleeping). If it doesn’t throw an exception, Feign will continue to retry the call. If the exception is thrown, it’ll be propagated and will effectively finish the call with an error.
3.1. Naive Implementation
Let’s write a very simple implementation of Retryer that will always retry calls after waiting one second:
public class NaiveRetryer implements feign.Retryer {
@Override
public void continueOrPropagate(RetryableException e) {
try {
Thread.sleep(1000L);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw e;
}
}
}
Because Retryer implements the Cloneable interface, we also needed to override the clone method.
@Override
public Retryer clone() {
return new NaiveRetryer();
}
Finally, we need to add our implementation to client builder:
public static <T> T createClient(Class<T> type, String uri) {
return Feign.builder()
// ...
.retryer(new NaiveRetryer())
// ...
}
Alternatively, if we are using Spring, we could annotate NaiveRetryer with @Component annotation or define a bean in the configuration class and let Spring do the rest of the work:
@Bean
public Retryer retryer() {
return new NaiveRetryer();
}
3.2. Default Implementation
Feign provides a sensible default implementation of the Retryer interface. It’ll retry only a given number of times, will start with some time interval, and then increase it with each retry up to provided maximum. Let’s define it with starting interval of 100 milliseconds, the maximum interval of 3 seconds, and the maximum number of attempts of 5:
public static <T> T createClient(Class<T> type, String uri) {
return Feign.builder()
// ...
.retryer(new Retryer.Default(100L, TimeUnit.SECONDS.toMillis(3L), 5))
// ...
}
3.3. No Retrying
If we don’t want Feign to ever retry any calls, we can provide Retryer.NEVER_RETRY implementation to the client builder. It’ll simply propagate the exception every time.
4. 創建可重試異常
在上一節中,我們學習瞭如何控制調用重試的頻率。現在,讓我們看看如何控制我們何時想要重試調用以及何時想要簡單地拋出異常。
4.1. ErrorDecoder 和 RetryableException
當收到錯誤響應時,Feign會將它傳遞給 ErrorDecoder 接口的一個實例,該實例決定如何處理它。最重要的是,解碼器可以將異常映射到 RetryableException 實例,從而使 Retryer 能夠重試調用。默認 ErrorDecoder 實現僅在響應包含“Retry-After”標頭時才創建 RetryableExeception 實例。 常見的場景是在 503 Service Unavailable 響應中發現它。
這很好用作默認行為,但有時我們需要更靈活。例如,我們可能與一個外部服務進行通信,該服務偶爾會隨機響應 500 Internal Server Error,我們無法修復它。我們可以重試調用,因為我們知道它下次可能會正常工作。要實現這一點,我們需要編寫自定義 ErrorDecoder 實現。
4.2. 創建自定義錯誤解碼器
我們自定義解碼器中只需要實現一個方法:decode。它接受兩個參數,一個 String 方法鍵,和一個 Response 對象。它返回一個異常,如果它是 RetryableException 實例或任何其他依賴於實現本身的異常,則返回該異常。
我們的 decode 方法將簡單地檢查響應的狀態碼是否大於或等於 500。如果是,它將創建一個 RetryableException。否則,它將返回來自 FeignException 類中 errorStatus 工廠函數創建的基本 FeignException:
public class Custom5xxErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
FeignException exception = feign.FeignException.errorStatus(methodKey, response);
int status = response.status();
if (status >= 500) {
return new RetryableException(
response.status(),
exception.getMessage(),
response.request().httpMethod(),
exception,
50L, // The retry interval
response.request());
}
return exception;
}
}
請注意,在這種情況,我們創建並返回異常,而不是拋出它。
最後,我們需要將解碼器插入客户端構建器中:
public static <T> T createClient(Class<T> type, String uri) {
return Feign.builder()
// ...
.errorDecoder(new Custom5xxErrorDecoder())
// ...
}
5. 總結
在本文中,我們學習瞭如何控制 Feign 庫的重試邏輯。我們研究了 Retryer 接口及其在控制重試時間和重試次數中的應用。然後,我們創建了 ErrorDecoder 以控制哪些響應應重試。