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实现

  1. 定义注解
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;

}

  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("重试机会耗尽");

    }
}

  1. 使用我们自定义的注解
    @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 自带重试工具

使用步骤

  1. 引入pom
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

<dependency>
       <groupId>org.springframework.retry</groupId>
       <artifactId>spring-retry</artifactId>
       <scope>test</scope>
</dependency>
  1. 在启动类或者配置类 打上注解 @EnableRetry
  2. 使用
@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具有更强的灵活性,可以根据返回值校验来判断是否需要进行重试。

步骤

  1. 引入pom
<dependency>
     <groupId>com.github.rholder</groupId>
     <artifactId>guava-retrying</artifactId>
     <version>2.0.0</version>
</dependency>
  1. 创建一个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就很棒了。当然,具体情况具体分析,但没有必要的情况下,不鼓励重复造轮子,研究别人怎么造轮子 比较重要。

posted @ 2020-11-11 09:53  朝明  阅读(8930)  评论(0编辑  收藏  举报