使用guava-retry優雅的實現介面重試
介紹
API 介面呼叫異常, 網路異常在我們日常開發中經常會遇到,這種情況下我們需要先重試幾次呼叫才能將其標識為錯誤並在確認錯誤之後傳送異常提醒。guava-retry可以靈活優雅的實現這一功能。
Guava retryer在支援重試次數和重試頻度控制基礎上,能夠相容支援多個異常或者自定義實體物件的重試源定義,讓重試功能有更多的靈活性。Guava Retryer也是執行緒安全的,入口呼叫邏輯採用的是Java.util.concurrent.Callable的call方法。
詳細說明
RetryerBuilder是一個factory建立者,可以定製設定重試源且可以支援多個重試源,可以配置重試次數或重試超時時間,以及可以配置等待時間間隔,建立重試者Retryer例項。
RetryerBuilder的重試源支援Exception異常物件 和自定義斷言物件,通過retryIfException 和retryIfResult設定,同時支援多個且能相容。
retryIfException,丟擲runtime異常、checked異常時都會重試,但是丟擲error不會重試。
retryIfRuntimeException只會在拋runtime異常的時候才重試,checked異常和error都不重試。
retryIfExceptionOfType允許我們只在發生特定異常的時候才重試,比如NullPointerException和IllegalStateException都屬於runtime異常,也包括自定義的error
如:
.retryIfExceptionOfType(Error.class)// 只在丟擲error重試
當然我們還可以在只有出現指定的異常的時候才重試,如:
.retryIfExceptionOfType(IllegalStateException.class) .retryIfExceptionOfType(NullPointerException.class)
或者通過Predicate實現
.retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class),Predicates.instanceOf(IllegalStateException.
retryIfResult可以指定你的Callable方法在返回值的時候進行重試,如
// 返回false重試 .retryIfResult(Predicates.equalTo(false)) //以_error結尾才重試 .retryIfResult(Predicates.containsPattern("_error$"))
當發生重試之後,假如我們需要做一些額外的處理動作,比如log一下異常,那麼可以使用RetryListener。每次重試之後,guava-retrying會自動回撥我們註冊的監聽。可以註冊多個RetryListener,會按照註冊順序依次呼叫。
.withRetryListener(new RetryListener {
@Override
public <T> void onRetry(Attempt<T> attempt) {
logger.error("第【{}】次呼叫失敗" , attempt.getAttemptNumber());
}
}
)
主要介面介紹
Attempt:一次執行任務;
AttemptTimeLimiter:單次任務執行時間限制(如果單次任務執行超時,則終止執行當前任務);
BlockStrategies:任務阻塞策略(通俗的講就是當前任務執行完,下次任務還沒開始這段時間做什麼……),預設策略為:BlockStrategies.THREAD_SLEEP_STRATEGY 也就是呼叫 Thread.sleep(sleepTime);
RetryException:重試異常;
RetryListener:自定義重試監聽器,可以用於非同步記錄錯誤日誌;
StopStrategy:停止重試策略,提供三種:
- StopAfterDelayStrategy :設定一個最長允許的執行時間;比如設定最長執行10s,無論任務執行次數,只要重試的時候超出了最長時間,則任務終止,並返回重試異常RetryException;
- NeverStopStrategy :不停止,用於需要一直輪訓知道返回期望結果的情況;
- StopAfterAttemptStrategy :設定最大重試次數,如果超出最大重試次數則停止重試,並返回重試異常;
WaitStrategy:等待時長策略(控制時間間隔),返回結果為下次執行時長:
- FixedWaitStrategy:固定等待時長策略;
- RandomWaitStrategy:隨機等待時長策略(可以提供一個最小和最大時長,等待時長為其區間隨機值)
- IncrementingWaitStrategy:遞增等待時長策略(提供一個初始值和步長,等待時間隨重試次數增加而增加)
- ExponentialWaitStrategy:指數等待時長策略;
- FibonacciWaitStrategy :Fibonacci 等待時長策略;
- ExceptionWaitStrategy :異常時長等待策略;
- CompositeWaitStrategy :複合時長等待策略;
依賴引入
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
工具類原始碼:
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.AttemptTimeLimiters;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.RetryListener;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* Created by pandechuan on 2018/2/24.
* 基於guava-retry的重試工具類
*/
public class RetryUtil {
private static final Logger logger = LoggerFactory.getLogger(RetryUtil.class);
/**
* @param task 要重試執行得任務
* @param fixedWaitTime 本次重試與上次重試之間的固定間隔時長
* @param maxEachExecuTime 一次重試的最大執行的時間
* @param timeUnit 時間單位
* @param attemptNumber 重試次數
*/
public static <T> T retry(Callable<T> task, long fixedWaitTime, long maxEachExecuTime, TimeUnit timeUnit, int attemptNumber) {
Retryer<T> retryer = RetryerBuilder
.<T>newBuilder()
//丟擲runtime異常、checked異常時都會重試,但是丟擲error不會重試。
.retryIfException()
//重試策略
.withWaitStrategy(WaitStrategies.fixedWait(fixedWaitTime, timeUnit))
//嘗試次數
.withStopStrategy(StopStrategies.stopAfterAttempt(attemptNumber))
//每次重試執行的最大時間限制
.withAttemptTimeLimiter(AttemptTimeLimiters.<T>fixedTimeLimit(maxEachExecuTime, timeUnit))
//重試監聽器
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
if (attempt.hasException()) {
logger.error("第【{}】次重試失敗", attempt.getAttemptNumber(), attempt.getExceptionCause());
}
}
}
).build();
T t = null;
try {
t = retryer.call(task);
} catch (ExecutionException e) {
logger.error("", e);
} catch (RetryException e) {
logger.error("", e);
}
return t;
}
/**
* @param task 要重試執行得任務
* @param predicate 符合預期結果需要重試
* @param fixedWaitTime 本次重試與上次重試之間的固定間隔時長
* @param maxEachExecuTime 一次重試的最大執行的時間
* @param attemptNumber 重試次數
*/
public static <T> T retry(Callable<T> task, Predicate<T> predicate, long fixedWaitTime, long maxEachExecuTime, TimeUnit timeUnit, int attemptNumber) {
Retryer<T> retryer = RetryerBuilder
.<T>newBuilder()
//丟擲runtime異常、checked異常時都會重試,但是丟擲error不會重試。
.retryIfException()
//對執行結果的預期。符合預期就重試
.retryIfResult(predicate)
//每次重試固定等待fixedWaitTime時間
.withWaitStrategy(WaitStrategies.fixedWait(fixedWaitTime, timeUnit))
//嘗試次數
.withStopStrategy(StopStrategies.stopAfterAttempt(attemptNumber))
//每次重試執行的最大時間限制(在規定的時間內沒有返回結果會TimeoutException)
.withAttemptTimeLimiter(AttemptTimeLimiters.<T>fixedTimeLimit(maxEachExecuTime, timeUnit))
//重試監聽器
.withRetryListener(new RetryListener() {
@Override
public <V> void onRetry(Attempt<V> attempt) {
if (attempt.hasException()) {
logger.error("第【{}】次重試失敗", attempt.getAttemptNumber(), attempt.getExceptionCause());
}
}
}
).build();
T t = null;
try {
t = retryer.call(task);
} catch (ExecutionException e) {
logger.error("", e);
} catch (RetryException e) {
logger.error("", e);
}
return t;
}
public static void main(String[] args) {
// here shows thress kinds of test case
Callable<Integer> task = () -> {
int a = 1 / 0;
return 2;
};
Callable<Integer> task2 = () -> {
Thread.sleep(2000L);
return 2;
};
Callable<Boolean> task3 = () -> {
return false;
};
//異常重試
Integer result = OptionalUtil.get(() -> retry(task, 30L, 1000L, TimeUnit.MILLISECONDS, 3)).orElseGet((() -> 0));
logger.info("result: {}", result);
//超時重試
Integer result2 = OptionalUtil.get(() -> retry(task2, 30L, 1000L, TimeUnit.MILLISECONDS, 3)).orElseGet(() -> 0);
logger.info("result: {}", result2);
//預期值重試
boolean result3 = OptionalUtil.get(() -> retry(task3, Predicates.equalTo(false), 30L, 1000L, TimeUnit.MILLISECONDS, 3)).orElseGet(() -> true);
logger.info("result: {}", result3);
}
}