自定义注解实现方法重试
其实重试机制就是一个方法或者接口 调一次没有达到预想的期望,接着对此进行重新执行,直到重试次数用完为止
JAVA实现重试机制可以用模版方式、切面方式、消息总线方式
设计:
目标是实现一个优雅的重试机制。首先应该是
无侵入:不改动当前的业务逻辑,对于需要重试的方法或接口可以简单的实现
可配置的:重试次数、重试间隔时间、是否使用异步方式)
通用性:最好是无改动可支持绝大部分场景
一般希望做到无侵入,都采用的切面或者消息总线模式,可配置和通用性则比较清晰,基本上开始做就表示这两点是基础要求,唯一的要求就是不要硬编码,不能写死,基本上就能达到这一基础要求。所以要做的并不少
切面方式
这个思路比较清晰,在需要添加重试的方法上添加一个用于重试的自定义注解,然后在切面中实现重试的逻辑,主要的配置参数则根据注解的选项来初始化。也可以不传参,在对重试次数和间隔时间进行定义时做成可远程配置化
优点:真正的无侵入
缺点:某些方法无法被切面拦截的场景无法覆盖(如spring-aop无法切私有方法,final方法)
直接使用aspectj则有些小复杂;如果用spring-aop,则只能切被spring容器管理的bean
我们这里用的是比较常用的一个 AOP 切面方式实现
1. 先定义一个重试接口
package com.example.demo.util.retry;
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryDot {
/**
* 重试次数
* @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;
}
};
//这里进行次数和间隔时间的注入,注解不传参也可以自行做远程配置 统一控制
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;
}
/**
* 重试的业务执行代码
* 失败时请抛出一个异常
*
* todo 去定返回的封装类,根据返回结果的状态来判定是否需要重试 这里定义的是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("异步出大问题咯");
});
}
}
消息总线方式
这个也是比较容易理解,在需要重试的方法中,发送一个消息,并将业务逻辑作为回调方法传入;由一个订阅了重试消息的consumer来执行重试的业务逻辑
优点:重试机制不受任何限制,即在任何地方你都可以使用
利用EventBus 框架,都可以非常容易把框架搭起来
缺点:业务侵入,需要在重试的业务处,主动发起一条重试消息
调试理解复杂(消息总线方式的最大优点和缺点,就是过于灵活了,你可能都不知道什么地方处理这个消息,特别是新人维护这段代码 很头疼)
如果要获取返回结果,不太好处理,上下文参数不好处理
有兴趣的可以了解一下如何实现。