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就很棒了。當然,具體情況具體分析,但沒有必要的情況下,不鼓勵重復造輪子,研究別人怎么造輪子 比較重要。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM