这两天在做项目过程中,需要请求外部服务的api需要考虑重试,为了代码优雅,决定使用注解的方式进行重试的配置,手写了个注解,
支持配置:重试的次数
支持自定义策略:1、延迟重试、即每失败一次下次重试延迟
2、根据配置的错误码code开启重试,不传默认所有失败都重试
3、redis的策略配置,方法失败,删除特定的redis
非常好用,有问题可以提问,看到会回复,删除了公司业务代码,下面业务逻辑使用了伪代码,方便理解
废话不多说,上代码:
首先是三个需要配置的注解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author hanfeng
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryableProcess {
/**
* 失败重试次数
* @return
*/
int maxAttempts() default 3;
/**
* 重试策略、操作
* @return
*/
BackoffProcess backoff() default @BackoffProcess;
}
策略配置注解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author hanfeng
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface BackoffProcess {
/**
* 初始重试间隔毫秒数
* @return
*/
long value() default 0L;
/**
* 重试最大延迟
* @return
*/
long maxDelay() default 0L;
/**
* 重试乘数
* @return
*/
double multiplier() default 0.0D;
/**
* 根据失败错误码code开启重试,不传默认失败执行
* @return
*/
String[] retryExceptionCode() default "";
/**
* redis策略
* @return
*/
RetryableRedisProcess redisOperation() default @RetryableRedisProcess;
}
redis策略注解:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author hanfeng
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryableRedisProcess {
/**
* 重试时删除的redis
* @return
*/
String[] retryRedisRemove() default {};
}
切面文件:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
/**
* @author hanfeng
*/
@Component
@Aspect
public class RetryProcessAspect {
protected org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(this.getClass());
// 配置RetryableProcess.class路径
@Pointcut("@annotation(com.test.aop.RetryableProcess)")
public void pointCutR() {
}
@Autowired
private RedisTemplate redisTemplate;
/**
* 埋点拦截器具体实现
*/
@Around("pointCutR()")
public Object methodRHandler(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
RetryableProcess retryableProcess = targetMethod.getAnnotation(RetryableProcess.class);
BackoffProcess backoff = retryableProcess.backoff();
RetryableRedisProcess redisOperation = backoff.redisOperation();
int maxAttempts = retryableProcess.maxAttempts();
long sleepSecond = backoff.value();
double multiplier = backoff.multiplier();
if (multiplier <= 0) {
multiplier = 1;
}
Exception ex = null;
int retryCount = 0;
do {
try {
Object proceed = joinPoint.proceed();
return proceed;
} catch (BtException e) {
logger.info("等待{}毫秒", sleepSecond);
Thread.sleep(sleepSecond);
retryCount++;
sleepSecond = (long) (multiplier) * sleepSecond;
if (sleepSecond > backoff.maxDelay()) {
sleepSecond = backoff.maxDelay();
logger.info("等待时间太长,更新为{}毫秒", sleepSecond);
}
List<String> strings = Arrays.asList(backoff.retryExceptionCode());
if (backoff.retryExceptionCode().length > 0 && !strings.contains(e.getErrorEnum().getCode())) {
throw e;
}
ex = e;
redisOperation(redisOperation);
}
} while (retryCount <= maxAttempts);
throw ex;
}
/**
* redis操作
* @param redisOperation
*/
public void redisOperation(RetryableRedisProcess redisOperation){
String[] redisRemoves = redisOperation.retryRedisRemove();
for (String redisName : redisRemoves) {
redisTemplate.delete(redisName);
}
}
}
因为上面用到了错误码,所以把封装的异常类也贴下:
/**
*
* @author hanfeng
*
*/
public class BtException extends RuntimeException {
private String code;
private IErrorEnum errorEnum;
@SuppressWarnings("unused")
private BtException() {
}
public void setCode(String code) {
this.code = code;
}
public void setErrorEnum(IErrorEnum errorEnum) {
this.errorEnum = errorEnum;
}
protected boolean canEqual(Object other) {
return other instanceof BtException;
}
@Override
public String toString() {
return "BusinessException(code=" + getCode() + ", errorEnum=" + getErrorEnum() + ")";
}
public String getCode() {
return this.code;
}
public IErrorEnum getErrorEnum() {
return this.errorEnum;
}
public BtException(IErrorEnum errorEnum) {
super(errorEnum.getDescription());
this.errorEnum = errorEnum;
this.code = errorEnum.getCode();
}
public BtException(String code, String message) {
super(message);
this.code = code;
}
}
异常枚举类基类,可以创建枚举实现IErrorEnum 类,来配置错误码,也可以改代码用自己的错误码来实现,对错误码的控制
/**
* 异常枚举基类
* @author hanfeng
*
*/
public interface IErrorEnum {
String getCode();
String getDescription();
default String codeMsg() {
return getCode()+" "+ getDescription();
}
}
注解的使用:
// 第三方token失效请求错误code
static final String HTTP_TOKEN_FAIL = "000015";
// 第三方超时请求错误code
static final String REQUEST_TIME_OUT = "000016";
//删除redis key
static final String remove_redis_key = "ssss:redis:key";
@RetryableProcess(maxAttempts = 2, backoff = @BackoffProcess(retryExceptionCode = {HTTP_TOKEN_FAIL, REQUEST_TIME_OUT}, redisOperation = @RetryableRedisProcess(retryRedisRemove = {remove_redis_key})))
public void test1(){
// ...请求第三方代码忽略
if(第三方失败code为token失效){
throw new BtException(ErrorEnum.HTTP_TOKEN_FAIL);
}else if(网络请求超时){
throw new BtException(ErrorEnum.REQUEST_TIME_OUT);
}
}