自定義注解實現(xiàn)方法重試

自定義注解實現(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)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容