Runnable和Callable都是多線程編程中常用的接口,通常是通過實現該接口編寫業務邏輯後,再由new Thread去發起線程調用。
主要區別在於Runnable沒有返回值,而Callable有返回值。下面就來看一個重試框架Retryer,針對Callable做的各種重試策略方法。
API 接口調用異常, 網絡異常在我們日常開發中經常會遇到,這種情況下我們需要先重試幾次調用才能將其標識爲錯誤並在確認錯誤之後發送異常提醒。guava-retry可以靈活的實現這一功能。Guava retryer在支持重試次數和重試頻度控制基礎上,能夠兼容支持多個異常或者自定義實體對象的重試源定義,讓重試功能有更多的靈活性。Guava Retryer也是線程安全的,入口調用邏輯採用的是Java.util.concurrent.Callable的call方法。
使用Guava retryer 很簡單,我們只要做以下幾步:
Step1、引入Guava-retry
<guava-retry.version>2.0.0</guava-retry.version>
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>${guava-retry.version}</version>
</dependency>
Step2、定義實現Callable接口的方法,以便Guava retryer能夠調用
/**
* @desc 更新可代理報銷人接口
* @author jianzhang11
* @date 2017/3/31 15:17
*/
private static Callable<Boolean> updateReimAgentsCall = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
String url = ConfigureUtil.get(OaConstants.OA_REIM_AGENT);
String result = HttpMethod.post(url, new ArrayList<BasicNameValuePair>());
if(StringUtils.isEmpty(result)){
throw new RemoteException("獲取OA可報銷代理人接口異常");
}
List<OAReimAgents> oaReimAgents = JSON.parseArray(result, OAReimAgents.class);
if(CollectionUtils.isNotEmpty(oaReimAgents)){
CacheUtil.put(Constants.REIM_AGENT_KEY,oaReimAgents);
return true;
}
return false;
}
};
Step3、定義Retry對象並設置相關策略
Retryer<Boolean> retryer = RetryerBuilder
.<Boolean>newBuilder()
//拋出runtime異常、checked異常時都會重試,但是拋出error不會重試。
.retryIfException()
//返回false也需要重試
.retryIfResult(Predicates.equalTo(false))
//重調策略
.withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS))
//嘗試次數
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(updateReimAgentsCall);
} catch (ExecutionException e) {
// e.printStackTrace();
} catch (RetryException e) {
logger.error("更新可代理報銷人異常,需要發送提醒郵件");
}
簡單三步就能使用Guava Retryer優雅的實現重調方法。
接下來對其進行詳細說明:
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.class)))
retryIfResult可以指定你的Callable方法在返回值的時候進行重試,如
// 返回false重試
.retryIfResult(Predicates.equalTo(false))
//以_error結尾才重試
.retryIfResult(Predicates.containsPattern("_error$"))
當發生重試之後,假如我們需要做一些額外的處理動作,比如發個告警郵件啥的,那麼可以使用RetryListener。每次重試之後,guava-retrying會自動回調我們註冊的監聽。可以註冊多個RetryListener,會按照註冊順序依次調用。
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryListener;
import java.util.concurrent.ExecutionException;
public class MyRetryListener<Boolean> implements RetryListener {
@Override
public <Boolean> void onRetry(Attempt<Boolean> attempt) {
// 第幾次重試,(注意:第一次重試其實是第一次調用)
System.out.print("[retry]time=" + attempt.getAttemptNumber());
// 距離第一次重試的延遲
System.out.print(",delay=" + attempt.getDelaySinceFirstAttempt());
// 重試結果: 是異常終止, 還是正常返回
System.out.print(",hasException=" + attempt.hasException());
System.out.print(",hasResult=" + attempt.hasResult());
// 是什麼原因導致異常
if (attempt.hasException()) {
System.out.print(",causeBy=" + attempt.getExceptionCause().toString());
} else {
// 正常返回時的結果
System.out.print(",result=" + attempt.getResult());
}
// bad practice: 增加了額外的異常處理代碼
try {
Boolean result = attempt.get();
System.out.print(",rude get=" + result);
} catch (ExecutionException e) {
System.err.println("this attempt produce exception." + e.getCause().toString());
}
System.out.println();
}
}
接下來在Retry對象中指定監聽:
.withRetryListener(new MyRetryListener<>())
效果如下: