自定義注解實現(xiàn)方法重試
其實重試機制就是一個方法或者接口 調(diào)一次沒有達(dá)到預(yù)想的期望,接著對此進(jìn)行重新執(zhí)行,直到重試次數(shù)用完為止
JAVA實現(xiàn)重試機制可以用模版方式、切面方式、消息總線方式
設(shè)計:
目標(biāo)是實現(xiàn)一個優(yōu)雅的重試機制。首先應(yīng)該是
無侵入:不改動當(dāng)前的業(yè)務(wù)邏輯,對于需要重試的方法或接口可以簡單的實現(xiàn)
可配置的:重試次數(shù)、重試間隔時間、是否使用異步方式)
通用性:最好是無改動可支持絕大部分場景
一般希望做到無侵入,都采用的切面或者消息總線模式,可配置和通用性則比較清晰,基本上開始做就表示這兩點是基礎(chǔ)要求,唯一的要求就是不要硬編碼,不能寫死,基本上就能達(dá)到這一基礎(chǔ)要求。所以要做的并不少
切面方式
這個思路比較清晰,在需要添加重試的方法上添加一個用于重試的自定義注解,然后在切面中實現(xiàn)重試的邏輯,主要的配置參數(shù)則根據(jù)注解的選項來初始化。也可以不傳參,在對重試次數(shù)和間隔時間進(jìn)行定義時做成可遠(yuǎn)程配置化
優(yōu)點:真正的無侵入
缺點:某些方法無法被切面攔截的場景無法覆蓋(如spring-aop無法切私有方法,final方法)
直接使用aspectj則有些小復(fù)雜;如果用spring-aop,則只能切被spring容器管理的bean
我們這里用的是比較常用的一個 AOP 切面方式實現(xiàn)
1. 先定義一個重試接口
package com.example.demo.util.retry;
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryDot {
/**
* 重試次數(shù)
* @return
*/
int count() default 0;
/**
* 重試的間隔時間
* @return
*/
int sleep() default 0;
/**
* 是否支持異步重試方式
* @return
*/
boolean asyn() default false;
}
2. 定義AOP切面對加注解的方法
package com.example.demo.util.retry;
import com.example.demo.dto.vo.ResultData;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @ClassName RetryAspect
* @Description TODO
* @Date 2021/8/13 2:27 下午
* @Created by liyanyan
*/
@Aspect
@Component
@Slf4j
public class RetryAspect {
ExecutorService executorService = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<Runnable>());
@Around(value = "@annotation(retryDot)")
public Object execute(ProceedingJoinPoint joinPoint, RetryDot retryDot) throws Throwable {
RetryTemplate retryTemplate = new RetryTemplate() {
@Override
protected Object doBiz() throws Throwable {
System.out.println("重試......");
ResultData result = (ResultData) joinPoint.proceed();
return result;
}
};
//這里進(jìn)行次數(shù)和間隔時間的注入,注解不傳參也可以自行做遠(yuǎn)程配置 統(tǒng)一控制
retryTemplate.setRetryTime(retryDot.count())
.setSleepTime(retryDot.sleep());
if(retryDot.asyn()) {
return retryTemplate.submit(executorService);
}else {
return retryTemplate.execute();
}
}
}
3. 模版方法 主要對重試邏輯判斷
package com.example.demo.util.retry;
import com.example.demo.dto.vo.ResultData;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
/**
* @ClassName RetryTemplate
* @Description TODO
* @Date 2021/8/13 11:39 上午
* @Created by liyanyan
*/
@Slf4j
public abstract class RetryTemplate {
private static final int DEFAULT_RETRY_TIME = 1;
private int retryTime = DEFAULT_RETRY_TIME;
//重試的睡眠時間
private int sleepTime = 0;
public int getSleepTime() {
return sleepTime;
}
public RetryTemplate setSleepTime(int sleepTime) {
if(sleepTime < 0) {
throw new IllegalArgumentException("sleepTime should equal or bigger than 0");
}
this.sleepTime = sleepTime;
return this;
}
public int getRetryTime() {
return retryTime;
}
public RetryTemplate setRetryTime(int retryTime) {
if(retryTime <= 0) {
throw new IllegalArgumentException("retryTime should bigger than 0");
}
this.retryTime = retryTime;
return this;
}
/**
* 重試的業(yè)務(wù)執(zhí)行代碼
* 失敗時請拋出一個異常
*
* todo 去定返回的封裝類,根據(jù)返回結(jié)果的狀態(tài)來判定是否需要重試 這里定義的是ResultData
* @return
* @throws Exception
*/
protected abstract Object doBiz() throws Throwable;
public Object execute() throws Throwable {
ResultData result;
for(int i=0; i<retryTime; i++) {
result = (ResultData) doBiz();
if(result.getCode()==200) {
return doBiz();
}
Thread.sleep(sleepTime);
}
result = (ResultData) doBiz();
return result;
}
public Object submit(ExecutorService executorService) {
if(executorService == null) {
throw new IllegalArgumentException("please choose executorService!");
}
return executorService.submit((Callable<? extends Object>) () -> {
try {
return execute();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
throw new RuntimeException("異步出大問題咯");
});
}
}
消息總線方式
這個也是比較容易理解,在需要重試的方法中,發(fā)送一個消息,并將業(yè)務(wù)邏輯作為回調(diào)方法傳入;由一個訂閱了重試消息的consumer來執(zhí)行重試的業(yè)務(wù)邏輯
優(yōu)點:重試機制不受任何限制,即在任何地方你都可以使用
利用EventBus 框架,都可以非常容易把框架搭起來
缺點:業(yè)務(wù)侵入,需要在重試的業(yè)務(wù)處,主動發(fā)起一條重試消息
調(diào)試?yán)斫鈴?fù)雜(消息總線方式的最大優(yōu)點和缺點,就是過于靈活了,你可能都不知道什么地方處理這個消息,特別是新人維護(hù)這段代碼 很頭疼)
如果要獲取返回結(jié)果,不太好處理,上下文參數(shù)不好處理
有興趣的可以了解一下如何實現(xiàn)。