简述
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形式简单实现下
- 定义一个注解,就这几个简单功能(复杂的功能自己扩展吧)
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;
}
- 写个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();
}
});
}
}
- 用一下
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 提供了丰富的接口易扩展,同时也提供了丰富的实现类,灵活强大