自定義注解實(shí)現(xiàn)方法重試
其實(shí)重試機(jī)制就是一個(gè)方法或者接口 調(diào)一次沒有達(dá)到預(yù)想的期望扑媚,接著對(duì)此進(jìn)行重新執(zhí)行源织,直到重試次數(shù)用完為止
JAVA實(shí)現(xiàn)重試機(jī)制可以用模版方式售淡、切面方式、消息總線方式
設(shè)計(jì):
目標(biāo)是實(shí)現(xiàn)一個(gè)優(yōu)雅的重試機(jī)制朵栖。首先應(yīng)該是
無侵入:不改動(dòng)當(dāng)前的業(yè)務(wù)邏輯,對(duì)于需要重試的方法或接口可以簡單的實(shí)現(xiàn)
可配置的:重試次數(shù)羹与、重試間隔時(shí)間娩嚼、是否使用異步方式)
通用性:最好是無改動(dòng)可支持絕大部分場景
一般希望做到無侵入,都采用的切面或者消息總線模式腐泻,可配置和通用性則比較清晰决乎,基本上開始做就表示這兩點(diǎn)是基礎(chǔ)要求,唯一的要求就是不要硬編碼派桩,不能寫死构诚,基本上就能達(dá)到這一基礎(chǔ)要求。所以要做的并不少
切面方式
這個(gè)思路比較清晰铆惑,在需要添加重試的方法上添加一個(gè)用于重試的自定義注解范嘱,然后在切面中實(shí)現(xiàn)重試的邏輯,主要的配置參數(shù)則根據(jù)注解的選項(xiàng)來初始化员魏。也可以不傳參丑蛤,在對(duì)重試次數(shù)和間隔時(shí)間進(jìn)行定義時(shí)做成可遠(yuǎn)程配置化
優(yōu)點(diǎn):真正的無侵入
缺點(diǎn):某些方法無法被切面攔截的場景無法覆蓋(如spring-aop無法切私有方法,final方法)
直接使用aspectj則有些小復(fù)雜撕阎;如果用spring-aop受裹,則只能切被spring容器管理的bean
我們這里用的是比較常用的一個(gè) AOP 切面方式實(shí)現(xiàn)
1. 先定義一個(gè)重試接口
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;
/**
* 重試的間隔時(shí)間
* @return
*/
int sleep() default 0;
/**
* 是否支持異步重試方式
* @return
*/
boolean asyn() default false;
}
2. 定義AOP切面對(duì)加注解的方法
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ù)和間隔時(shí)間的注入,注解不傳參也可以自行做遠(yuǎn)程配置 統(tǒng)一控制
retryTemplate.setRetryTime(retryDot.count())
.setSleepTime(retryDot.sleep());
if(retryDot.asyn()) {
return retryTemplate.submit(executorService);
}else {
return retryTemplate.execute();
}
}
}
3. 模版方法 主要對(duì)重試邏輯判斷
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;
//重試的睡眠時(shí)間
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í)行代碼
* 失敗時(shí)請(qǐng)拋出一個(gè)異常
*
* 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("異步出大問題咯");
});
}
}
消息總線方式
這個(gè)也是比較容易理解棉饶,在需要重試的方法中,發(fā)送一個(gè)消息镇匀,并將業(yè)務(wù)邏輯作為回調(diào)方法傳入照藻;由一個(gè)訂閱了重試消息的consumer來執(zhí)行重試的業(yè)務(wù)邏輯
優(yōu)點(diǎn):重試機(jī)制不受任何限制,即在任何地方你都可以使用
利用EventBus 框架汗侵,都可以非常容易把框架搭起來
缺點(diǎn):業(yè)務(wù)侵入岩梳,需要在重試的業(yè)務(wù)處囊骤,主動(dòng)發(fā)起一條重試消息
調(diào)試?yán)斫鈴?fù)雜(消息總線方式的最大優(yōu)點(diǎn)和缺點(diǎn),就是過于靈活了冀值,你可能都不知道什么地方處理這個(gè)消息也物,特別是新人維護(hù)這段代碼 很頭疼)
如果要獲取返回結(jié)果,不太好處理列疗,上下文參數(shù)不好處理
有興趣的可以了解一下如何實(shí)現(xiàn)滑蚯。