在调用第三方系统以及服务内调用的场景下,由于网络波动、响应超时等原因会导致接口调用失败,这时就需要进行失败重试,本文基于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
)