SpringBoot项目接口调用失败重试

在调用第三方系统以及服务内调用的场景下,由于网络波动、响应超时等原因会导致接口调用失败,这时就需要进行失败重试,本文基于AOP实现实现一种较简单的接口调用失败重试方案。

自定义注解
package com.cube.share.apiretry.annotations;

import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

/**
 * @author cube.li
 * @date 2021/6/14 22:20
 * @description 接口失败重试注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retry {

    /**
     * 重试日志标题
     */
    @AliasFor("value")
    String title() default "";

    @AliasFor("title")
    String value() default "";

    /**
     * 重试次数
     */
    int retryTimes() default 1;

    /**
     * 重试时间间隔,单位毫秒
     */
    long executeInterval() default 0;

    /**
     * 为指定异常重试
     */
    Class<? extends Throwable>[] retryFor() default {Exception.class};

    /**
     * 排除的异常不进行重试,注意如果一个异常在retryFor和noRetryFor中同时指定,
     * 则不再进行重试
     */
    Class<? extends Throwable>[] noRetryFor() default {};

}

切面
package com.cube.share.apiretry.annotations.aspects;

import cn.hutool.core.thread.ThreadUtil;
import com.cube.share.apiretry.annotations.Retry;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
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.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @author cube.li
 * @date 2021/6/14 22:31
 * @description 重试切面
 */
@Component
@Slf4j
@Aspect
@ConditionalOnProperty(prefix = "retry", name = "enabled", havingValue = "true")
public class RetryAspect {

    @Pointcut("@annotation(com.cube.share.apiretry.annotations.Retry)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object doRetry(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            Retry retry = AnnotationUtils.findAnnotation(method, Retry.class);

            if (retry != null) {
                int retryTimes = retry.retryTimes();
                Class<? extends Throwable>[] retryForExceptions = retry.retryFor();
                Class<? extends Throwable>[] excludeExceptions = retry.noRetryFor();
                String title = retry.title();
                long executeInterval = retry.executeInterval();

                if (retryTimes < 1
                        || isInstance(excludeExceptions, throwable)
                        || !isInstance(retryForExceptions, throwable)) {
                    throw throwable;
                }

                int currentRetryTimes = 1;

                while (currentRetryTimes <= retryTimes) {
                    log.info("{} 接口失败重试中,第 {} 次重试...", title, currentRetryTimes++);
                    try {
                        if (executeInterval > 0) {
                            ThreadUtil.safeSleep(executeInterval);
                        }
                        return joinPoint.proceed();
                    } catch (Throwable e) {
                        if (currentRetryTimes > retryTimes) {
                            throw e;
                        }
                    }
                }
            }

            throw throwable;
        }
    }

    private boolean isInstance(Class<? extends Throwable>[] array, Throwable e) {
        if (ArrayUtils.isEmpty(array) || e == null) {
            return false;
        }

        for (Class<? extends Throwable> th : array) {
            if (th.isInstance(e)) {
                return true;
            }
        }
        return false;
    }
}

如上,定义了注解@Retry和对应的切面,这种方案的使用场景有一定的限制,需要重试方法的所在类必须交由spring托管(通俗的说,就是作为spring容器的Bean),在需要进行失败重试的接口上加上@Retry注解即可(如果是同一个类内的方法调用要采用代理对象调用,否则会失效)。

测试

模拟业务执行的service

package com.cube.share.apiretry.annotations.service;

import com.cube.share.apiretry.annotations.Retry;
import org.springframework.stereotype.Service;

/**
 * @author cube.li
 * @date 2021/6/15 22:51
 * @description
 */
@Service
public class MockService {

    @Retry
    public void retry() {
        System.out.println("执行业务逻辑...");
        throw new RuntimeException();
    }
}

测试类

package com.cube.share.apiretry.annotations.service;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

import static org.junit.jupiter.api.Assertions.*;

/**
 * @author cube.li
 * @date 2021/6/15 22:53
 * @description
 */
@SpringBootTest
class MockServiceTest {

    @Resource
    MockService mockService;

    @Test
    void retry() {
        mockService.retry();
    }
}

测试结果如下:
1.默认参数

执行业务逻辑...
2021-06-15 23:26:07.964  INFO 16608 --- [           main] c.c.s.a.annotations.aspects.RetryAspect  :  接口失败重试中,第 1 次重试...
执行业务逻辑...


java.lang.RuntimeException
    at com.cube.share.apiretry.annotations.service.MockService.retry(MockService.java:17)
    at com.cube.share.apiretry.annotations.service.MockService$$FastClassBySpringCGLIB$$e4e38523.invoke

2.指定参数

package com.cube.share.apiretry.annotations.service;

import com.cube.share.apiretry.annotations.Retry;
import org.springframework.stereotype.Service;

/**
 * @author cube.li
 * @date 2021/6/15 22:51
 * @description
 */
@Service
public class MockService {

    @Retry(title = "模拟业务", retryTimes = 2, executeInterval = 500)
    public void retry() {
        System.out.println("执行业务逻辑...");
        throw new RuntimeException();
    }
}

控制台打印如下:

执行业务逻辑...
2021-06-15 23:29:34.379  INFO 1576 --- [           main] c.c.s.a.annotations.aspects.RetryAspect  : 模拟业务 接口失败重试中,第 1 次重试...
执行业务逻辑...
2021-06-15 23:29:34.883  INFO 1576 --- [           main] c.c.s.a.annotations.aspects.RetryAspect  : 模拟业务 接口失败重试中,第 2 次重试...
执行业务逻辑...


java.lang.RuntimeException
    at com.cube.share.apiretry.annotations.service.MockService.retry(MockService.java:17)

可以看出一共进行了两次失败重试,并且两次的时间间隔为500毫秒
3.指定异常不进行重试

package com.cube.share.apiretry.annotations.service;

import com.cube.share.apiretry.annotations.Retry;
import org.springframework.stereotype.Service;

/**
 * @author cube.li
 * @date 2021/6/15 22:51
 * @description
 */
@Service
public class MockService {

    @Retry(title = "模拟业务", retryTimes = 2, executeInterval = 500, noRetryFor = RuntimeException.class)
    public void retry() {
        System.out.println("执行业务逻辑...");
        throw new RuntimeException();
    }
}

再次执行,从控制台输出可以看出并未进行失败重试。

执行业务逻辑...


java.lang.RuntimeException
    at com.cube.share.apiretry.annotations.service.MockService.retry(MockService.java:17)
    at com.cube.share.apiretry.annotations.service.MockService$$FastClassBySpringCGLIB$$e4e38523.invoke

实例代码 (https://gitee.com/li-cube/share/tree/master/api-retry
)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容