Java 接口重试的几种实现
问题引入
现有一个接口,调用4次后才可已返回正常结果
public class RetryService {
private AtomicLong times = new AtomicLong();
public String hello () {
long l = times.incrementAndGet();
if (l % 4 == 0) {
throw new RuntimeException();
}
return "hello world";
}
}
解决方案
方式一: 硬核捕获
public void method1 () {
for(int i = 0;i< 4;i++) {
try{
retryService.hello();
} catch (Exception) {
log.error("接口请求失败!");
}
}
throw new RuntimeException("处理失败");
}
方式二: 动态代理
调用
@Test
public void test2 () {
// jdk 反向代理使用
IRetryService retryService = new RetryService();
IRetryService proxy = (IRetryService) RetryInvocationHandler.getProxy(retryService);
String hello = proxy.hello();
System.out.println(hello);
// cglib 反向代理使用
CglibRetryProxy cglibRetryProxy = new CglibRetryProxy();
Object cglibProxy = cglibRetryProxy.getCglibProxy(retryService);
}
JDK 动态代理
package com.example.chaoming.exercise.jdk;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class RetryInvocationHandler implements InvocationHandler {
private final Object subject;
public RetryInvocationHandler(Object object) {
this.subject = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
int times = 0;
while (times <4) {
try {
return method.invoke(subject,args);
} catch (Exception e) {
times++;
System.out.println("失败第"+times+"次");
if (times >= 4) {
throw new RuntimeException();
}
}
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
/**
* 获取动态代理
* @param realSubject
* @return
*/
public static Object getProxy(Object realSubject) {
InvocationHandler handler = new RetryInvocationHandler(realSubject);
return Proxy.newProxyInstance(handler.getClass().getClassLoader(),realSubject.getClass().getInterfaces(),handler);
}
}
spring 注入代理 工具类
package com.example.chaoming.exercise.jdk;
import com.sun.javafx.fxml.PropertyNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
@Component
public class RetryServiceProxyHandler {
@Autowired
private ConfigurableApplicationContext applicationContext;
public Object getProxy (Class clazz) {
// 1. 从bean 中获取对象
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
Map<String,Object> beans = beanFactory.getBeansOfType(clazz);
Set<Map.Entry<String,Object>> entries = beans.entrySet();
if (entries.size()<= 0) {
throw new PropertyNotFoundException();
}
Object bean = null;
// 2. 判断该对象的代理对象是否存在
Object source = beans.entrySet().iterator().next().getValue();
String proxyBeanName = clazz.getSimpleName();
boolean exist = beanFactory.containsBean(proxyBeanName);
if (exist) {
bean = beanFactory.getBean(proxyBeanName);
return bean;
}
// 3. 不存在则生成代理对象
bean = RetryInvocationHandler.getProxy(source);
// 4. 将bean 注入到spring 容器中
beanFactory.registerSingleton(proxyBeanName,bean);
return bean;
}
}
CGlib 动态代理
package com.example.chaoming.exercise.jdk;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibRetryProxy implements MethodInterceptor {
private Object target;
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
int times = 0;
while (times <4) {
try {
return method.invoke(target,objects);
} catch (Exception e) {
times++;
System.out.println("失败第"+times+"次");
if (times >= 4) {
throw new RuntimeException();
}
}
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
// 获取动态代理对象
public Object getCglibProxy (Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
Object o = enhancer.create();
return o;
}
}
方式三:Spring Aop实现
- 定义注解
package com.example.chaoming.exercise.jdk.aop;
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {
// 重试次数
int retryTimes() default 3;
// 重试时间间隔
int retryInterval() default 1;
}
- 处理切面逻辑
package com.example.chaoming.exercise.jdk.aop;
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.cglib.proxy.MethodInterceptor;
import java.lang.reflect.Method;
@Aspect
public class RetryAspect {
@Pointcut("@annotation(com.example.chaoming.exercise.jdk.aop.Retryable)")
private void retryMethod (){}
@Around("retryMethod()")
public void retry(ProceedingJoinPoint point) throws InterruptedException {
Retryable retryable = ((MethodSignature) point.getSignature()).getMethod().getAnnotation(Retryable.class);
int interval = retryable.retryInterval();
int times = retryable.retryTimes();
Throwable exception = new RuntimeException();
for (int retryTime = 1; retryTime < times; retryTime++) {
try {
point.proceed();
} catch (Throwable throwable) {
exception = throwable;
System.out.println("发生调用异常");
}
Thread.sleep(interval * 1000);
}
throw new RuntimeException("重试机会耗尽");
}
}
- 使用我们自定义的注解
@Test
@Retryable(retryTimes = 3,retryInterval = 1)
public void method1 () {
for(int i = 0;i< 6;i++) {
try{
String hello = hello();
System.out.println(hello);
} catch (Exception e) {
System.out.println("接口请求失败");
}
}
throw new RuntimeException("处理失败");
}
方式四:Spring 自带重试工具
使用步骤
- 引入pom
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<scope>test</scope>
</dependency>
- 在启动类或者配置类 打上注解 @EnableRetry
- 使用
@Service
public class RetryServiceImpl implements RetryService {
private static final Logger LOGGER = LoggerFactory.getLogger(RetryServiceImpl.class);
private AtomicInteger count = new AtomicInteger(1);
@Override
@Retryable(value = { RemoteAccessException.class }, maxAttemptsExpression = "${retry.maxAttempts:10}",
backoff = @Backoff(delayExpression = "${retry.backoff:1000}"))
public void retry() {
LOGGER.info("start to retry : " + count.getAndIncrement());
throw new RemoteAccessException("here " + count.get());
}
@Recover
public void recover(RemoteAccessException t) {
LOGGER.info("SampleRetryService.recover:{}", t.getClass().getName());
}
}
方式五:Gavua 重试用法
特点:相比Spring Retry,Guava Retry具有更强的灵活性,可以根据返回值校验来判断是否需要进行重试。
步骤
- 引入pom
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
- 创建一个Retryer实例
使用这个实例对需要重试的方法进行调用,可以通过很多方法来设置重试机制,比如使用retryIfException来对所有异常进行重试,使用retryIfExceptionOfType方法来设置对指定异常进行重试,使用retryIfResult来对不符合预期的返回结果进行重试,使用retryIfRuntimeException方法来对所有RuntimeException进行重试。
还有五个以with开头的方法,用来对重试策略/等待策略/阻塞策略/单次任务执行时间限制/自定义监听器进行设置,以实现更加强大的异常处理。
通过跟Spring AOP的结合,可以实现比Spring Retry更加强大的重试功能。
仔细对比之下,Guava Retry可以提供的特性有:
可以设置任务单次执行的时间限制,如果超时则抛出异常。
可以设置重试监听器,用来执行额外的处理工作。
可以设置任务阻塞策略,即可以设置当前重试完成,下次重试开始前的这段时间做什么事情。
可以通过停止重试策略和等待策略结合使用来设置更加灵活的策略,比如指数等待时长并最多10次调用,随机等待时长并永不停止等等。
public static void main(String[] args) throws Exception {
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.equalTo(false))
.retryIfException()
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(5))
.withRetryListener(new MyRetryListener<>())
.build();
try {
retryer.call(()->{
//call something
return true;
});
} catch (Exception e) {
e.printStackTrace();
}
}
总结
本文由浅入深的对多种重试的姿势进行了360度无死角教学,从最简单的手动重试,到使用静态代理,再到JDK动态代理和CGLib动态代理,再到Spring AOP,都是手工造轮子的过程,最后介绍了两种目前比较好用的轮子,一个是Spring Retry,使用起来简单粗暴,与Spring框架天生搭配,一个注解搞定所有事情,另一个便是Guava Retry,不依赖于Spring框架,自成体系,使用起来更加灵活强大。
个人认为,大部分场景下,Spring Retry提供的重试机制已经足够强大,如果不需要Guava Retry提供的额外灵活性,使用Spring Retry就很棒了。当然,具体情况具体分析,但没有必要的情况下,不鼓励重复造轮子,研究别人怎么造轮子 比较重要。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步