Guava Retry 源码解析

2,234 阅读7分钟

简述

java的重试其实挺多的包括spring也提供了@Retryable注解方式方便快捷,guava retry 也是非常优秀的,有兴趣简单看看。

使用

很简单,先给懒人提供下地址

maven: https://mvnrepository.com/artifact/com.github.rholder/guava-retrying/2.0.0
github https://github.com/rholder/guava-retrying

引入依赖

<dependency>    
    <groupId>com.github.rholder</groupId>    
    <artifactId>guava-retrying</artifactId>    
    <version>2.0.0</version>
</dependency>

这里举一个需要重试例子,比如请求用户信息的接口如果请求异常了重试3次每次间隔5秒

Retryer<UserData> retryer = RetryerBuilder.<UserData>newBuilder()    
    .retryIfRuntimeException() 
    .withStopStrategy(StopStrategies.stopAfterAttempt(3))    
    .withWaitStrategy(WaitStrategies.fixedWait(5, TimeUnit.SECONDS))
    .build();

retryer.call(() -> {
    String url = "https://xxxx.com/user/{id}";
    Long userId = 123;
    //这里是重试关注的点,网络不稳定或目的服务器不稳定会报异常
    return HttpUtil.get(url, userId, UserData.class);
});

是不是很简单,看到这里有人会说了,这也太麻烦了,使用spring注解一个注解不就搞定了,比如这样

@Retryable(value = { RuntimeException.class }, maxAttempts = 3, backoff = @Backoff(delay = 5*1000l))
public UserData getUserData(Long userId) {
   String url = "https://xxxx.com/user/{id}";
   //这里是重试关注的点,网络不稳定或目的服务器不稳定会报异常
   return HttpUtil.get(url, userId, UserData.class);
}

确实,如果你只用到了重试的这些功能,spring的@Retryable就可以了,好! 结束,大家洗洗睡吧,打扰了。

好了不闹了,说说为什么要使用guava retry

  • 支持通过返回结果重试,例如用户数据获取为null时或随你自己定
Retryer<UserData> retryer = RetryerBuilder.<UserData>newBuilder()
    .retryIfResult(userData -> userData == null)
    ....
  • 支持重试的时候通知(回调)你,例如统计获取用户接口失败次数或你想要的做的
Retryer<UserData> retryer = RetryerBuilder.<UserData>newBuilder()
    .retryIfResult(userData -> userData == null)
    .withRetryListener(new RetryListener() {
    @Override
    public <UserData> void onRetry(Attempt<UserData> attempt) {
    //这里记录你想要做的事
    }
})
    ...

这里不让使用lambda,原因:如果函数接口中的方法具有类型参数,则不能对函数接口使用lambda表达式

  • 支持多重试条件,例如获取用户数据接口异常,返回为null,或返回指定异常都重试
Retryer<UserData> retryer = RetryerBuilder.<UserData>newBuilder()
    .retryIfRuntimeException() 
    .retryIfResult(userData -> userData == null)
    .retryIfExceptionOfType(XxxException.class)    
    ...
  • 丰富的各种策略,下面会讲到

源码

RetryerBuilder

快速创建Retryer的构建类,上面代码中都是它通过构建者模式创建的,有了这个类大大降低了使用门槛。

    public Retryer<V> build() {
        AttemptTimeLimiter<V> theAttemptTimeLimiter = attemptTimeLimiter == null ? AttemptTimeLimiters.<V>noTimeLimit() : attemptTimeLimiter;
        StopStrategy theStopStrategy = stopStrategy == null ? StopStrategies.neverStop() : stopStrategy;
        WaitStrategy theWaitStrategy = waitStrategy == null ? WaitStrategies.noWait() : waitStrategy;
        BlockStrategy theBlockStrategy = blockStrategy == null ? BlockStrategies.threadSleepStrategy() : blockStrategy;

        return new Retryer<V>(theAttemptTimeLimiter, theStopStrategy, theWaitStrategy, theBlockStrategy, rejectionPredicate, listeners);
    }

build()方法将停止重试策略,等待策略,等待类型策略指定默认值,创建Retryer

StopStrategy

终止重试策略接口,retryer内部使用类似while(true)的方式重试,StopStrategy指定了什么时候停止循环,一般用于判断重试次数,终止循环的条件之一(Predicate也是终止循环条件,后面会讲到),返回true就结束循环,接口中就一个方法

boolean shouldStop(Attempt failedAttempt);
StopStrategies

要是不想创建StopStrategy实现类,那就使用这个类里提供的默认StopStrategy,这个类也是快速得到一个StopStrategy的

提供了三个StopStrategy,一般都使用StopAfterAttemptStrategy重试指定次数后停止重试,这个一般就够用了

WaitStrategy

重试后等待策略接口,也是一个方法,返回等待毫秒数

long computeSleepTime(Attempt failedAttempt);
WaitStrategies

这个类提供了实现好的WaitStrategy,懒人福利

简单说几个

FixedWaitStrategy 等待固定时间
RandomWaitStrategy 在指定最小最大值随机时间
IncrementingWaitStrategy 随重试次数递增等待时间递增 initialSleepTime + (increment * (failedAttempt.getAttemptNumber() - 1))
ExceptionWaitStrategy 遇到异常时等待指定时间
Predicate

断言,暗示,这个比较重要,根据返回结果重试,指定异常类型重试等开关都是通过apply()方法来判断的,接口重要方法apply返回true为需要重试,是否需要重试重要条件之一

@CanIgnoreReturnValue 
boolean apply(@Nullable T input);
Predicates

这个类我想都知道是干什么了,也是提供了许多默认的Predicate

没画线的都是它的实现类,Predicates中实现的Predicate太多不太方便画线了

可以看出很丰富,简单说几个

ObjectPredicate 枚举,实现了几个常用的常,例如ALWAYS_TRUE始终返回true,ALWAYS_FALSE始终返回false,IS_NULL是否等于NotPredicateNotPredicate 取反
AndPredicate 内部有List来收集Predicate,都为true时返回true
OrPredicate 内部也有一个List来收集Predicate,任意个为true事返回true
InstanceOfPredicate 内部有Class属性当于apply入参类型一致时返回true
InPredicate 内部维护了一个Collection检测到apply入参包含其中时返回true
RetryListener

重试监听接口

<V> void onRetry(Attempt<V> attempt);

在重试时会回调并传入重试参数Attempt

Attempt

这个接口用于描述重试属性,列几个方法(被监控方法就是写在Retryer.call中的自己写的需要重试的方法)

public V get() throws ExecutionException;
//用于获取结果,如果没有异常为被监控方法法的返回结果
public long getAttemptNumber(); //当前重试次数

其中有两个实现类比较重要

ResultAttempt 收集被监控方法结果
ExceptionAttempt 被监控方法异常时
Retryer

重试主类,上面的所有类都是为他服务的,主要说下call方法,通过注释简单的解读下

    public V call(Callable<V> callable) throws ExecutionException, RetryException {
        long startTime = System.nanoTime();
        // 这里相当为while(true)
        for (int attemptNumber = 1; ; attemptNumber++) {
            Attempt<V> attempt;
            try {
                // 执行被监控的方法
                V result = attemptTimeLimiter.call(callable);
                //被监控方法没有抛出任何异常收集下结果,需要注意这里并没有任何需要重试和终止检测
                attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            } catch (Throwable t) {
                //异常或错误了收集下异常,这里也不是重试标识
                attempt = new ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            }
            // 回调通知所有监听者,也就是所有RetryListener实现类,在RetryerBuilder构建Retryer时手动传入的
            for (RetryListener listener : listeners) {
                listener.onRetry(attempt);
            }
            //这里就是判断是否需要重试的条件,上面使用实例中根据返回结果重试,根据指定异常类型重试都是在这里判断的,Predicates中已实现了丰富的Predicate
            if (!rejectionPredicate.apply(attempt)) {
                return attempt.get();
            }
            //终止条件,注意执行终止策略会固定异常,也就是说重试m次后会被他通过跑异常的形式结束重试的
            if (stopStrategy.shouldStop(attempt)) {
                throw new RetryException(attemptNumber, attempt);
            } else {
                //重试后等待策略
                long sleepTime = waitStrategy.computeSleepTime(attempt);
                try {
                    //阻塞策略,这里面暂时就是Thread.sleep
                    blockStrategy.block(sleepTime);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RetryException(attemptNumber, attempt);
                }
            }
        }
    }

注解

注解的方式使用起来确实方便,可以将guava retry通过注解的方式使用吗,答案是确定的,这里通过aop形式简单实现下

  1. 定义一个注解,就这几个简单功能(复杂的功能自己扩展吧)
package com.lidenger.guavaretry.annotations;

import java.lang.annotation.*;

/**
 * guava annotation
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retry {
    /**
     * 重试次数,默认3次
     */
    int attemptNum() default 3;

    /**
     * 重试异常类型
     */
    Class<? extends Exception> exceptionType();

    /**
     * 重试等待毫秒数
     */
    long waitMillisecond() default 0;
}
  1. 写个Aspect
package com.lidenger.guavaretry.aspect;

import com.github.rholder.retry.*;
import com.lidenger.guavaretry.annotations.Retry;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
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.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * @author lidenger
 */
@Aspect
@Component
public class RetryAspect {

    //拦截所有这个注解呗
    @Pointcut("@annotation(com.lidenger.guavaretry.annotations.Retry)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("RetryAspect...");

        // 从ProceedingJoinPoint获取到Method,注解在方法上
        Signature sig = pjp.getSignature();
        MethodSignature msig;
        if (!(sig instanceof MethodSignature)) {
            throw new IllegalArgumentException("该注解只能用于方法");
        }
        msig = (MethodSignature) sig;
        Object target = pjp.getTarget();
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        // 获取方法上的Retry注解
        final Retry retry = currentMethod.getAnnotation(Retry.class);
        
        System.out.println(retry);
        //熟悉的代码来了,不解释了
        Retryer<Object> retryer = RetryerBuilder.newBuilder()
                .retryIfExceptionOfType(retry.exceptionType())
                .withStopStrategy(StopStrategies.stopAfterAttempt(retry.attemptNum()))
                .withWaitStrategy(WaitStrategies.fixedWait(retry.waitMillisecond(), TimeUnit.MILLISECONDS))
                .build();

        return retryer.call(() -> {
            try {
                return pjp.proceed();
            } catch (Throwable e) {
                throw retry.exceptionType().newInstance();
            }
        });

    }

}
  1. 用一下
package com.lidenger.guavaretry.service;

import com.lidenger.guavaretry.annotations.Retry;
import com.lidenger.guavaretry.exceptions.RetryTestException;
import org.springframework.stereotype.Component;

/**
 * @author lidenger
 */
@Component
public class Run {

    public static int num = 1;

    @Retry(attemptNum = 2, exceptionType = RetryTestException.class, waitMillisecond = 5 * 1000)
    public void test() {
        System.out.println("Run.test()>>" + num++);
        if (1 == 1) {
            throw new RetryTestException();
        }
    }
}

是不是很简单,就是穿了个马甲

总结

guava retry 提供了丰富的接口易扩展,同时也提供了丰富的实现类,灵活强大