java自定义注解,实现方法重试,支持自定义重试策略

本文介绍了一个使用注解配置的API重试机制,支持自定义重试次数、延迟策略、错误码判断及Redis操作。通过实例展示了如何在代码中优雅地处理服务调用的失败并配置重试策略。

        这两天在做项目过程中,需要请求外部服务的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);
        }

    }

### 配置 @Retryable 注解自定义重试策略 maxAttemptsExpression 示例 在 Spring重试机制中,`@Retryable` 注解支持通过 `maxAttemptsExpression` 属性使用 SpEL(Spring Expression Language)来动态设置最大重试次数。这种配置方式提供了更高的灵活性,允许根据运行时条件调整重试行为。 以下是一个完整的示例,展示了如何通过 `maxAttemptsExpression` 和其他属性配置自定义重试策略: ```java import org.springframework.beans.factory.annotation.Value; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; @Service public class CustomRetryService { @Value("${retry.maxAttempts:3}") // 从配置文件读取最大重试次数,默认值为3 private int maxAttempts; @Value("${retry.delay:1000}") // 从配置文件读取延迟时间,默认值为1000毫秒 private long delay; @Retryable( value = {RuntimeException.class}, // 指定需要捕获并触发重试的异常类型 maxAttemptsExpression = "#{${retry.maxAttempts:3}}", // 使用SpEL表达式动态设置最大重试次数 backoff = @Backoff(delayExpression = "#{${retry.delay:1000}}") // 动态设置退避策略 ) public void performCustomRetryTask() { System.out.println("尝试执行任务..."); if (Math.random() > 0.5) { // 模拟任务失败的概率 throw new RuntimeException("任务执行失败"); } System.out.println("任务成功完成"); } } ``` #### 配置文件示例 如果使用的是 `application.properties` 文件: ```properties retry.maxAttempts=5 retry.delay=2000 ``` 如果使用的是 `application.yml` 文件: ```yaml retry: maxAttempts: 5 delay: 2000 ``` 上述配置表示最大重试次数为 5 次,每次重试之间的延迟时间为 2 秒[^2]。 #### 自定义重试策略实现 除了通过注解配置外,还可以通过自定义 `RetryTemplate` 来实现更复杂的重试逻辑。例如,可以定义一个全局的 `RetryTemplate` 并将其注入到服务中[^5]。 ```java import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.backoff.FixedBackOffPolicy; import org.springframework.retry.support.RetryTemplate; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RetryConfig { @Bean public RetryTemplate retryTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); // 设置重试策略 SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); retryPolicy.setMaxAttempts(5); // 设置最大重试次数 retryTemplate.setRetryPolicy(retryPolicy); // 设置退避策略 FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy(); backOffPolicy.setBackOffPeriod(5000L); // 设置每次重试之间的延迟时间 retryTemplate.setBackOffPolicy(backOffPolicy); return retryTemplate; } } ``` #### 注意事项 - 如果同时设置了 `maxAttempts` 和 `maxAttemptsExpression`,则 `maxAttemptsExpression` 的优先级更高[^2]。 - 确保 SpEL 表达式中的占位符能够正确解析,否则会导致应用程序启动失败或运行时错误。 - 在分布式系统中,重试机制可能会引发重复操作问题,因此需要结合幂等性设计来避免副作用[^3]。 ```java // 示例:结合 @Recover 注解处理最终失败的情况 @Recover public void recover(RuntimeException ex) { System.out.println("所有重试均失败:" + ex.getMessage()); } ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值