在Java中进行重试 guava-retrying

Guava-retrying

简介

在实际的过程中,我们开发一个函数功能往往会需要用到其他的服务,但是我们无法保证其他服务是稳定可用的,于是重试逻辑就是一个非常常见的行为,同时也是一个比较普遍适用的功能。同时考虑到对代码的侵入性,我们希望能够有一个统一的外部逻辑来进行函数的重试。guava-retrying就能够提供这样的功能,它是google Guava库的一个小扩展,允许为任意函数调用创建可配置的重试策略,为我们提供了一个简单易用的重试框架。该项目的官方地址为rholder/guava-retrying

Guava:Google Guava是一组由Google开源的Java通用库,其中提供了许多在Java中实用的功能。

Quick Start

首先我们需要引入相关依赖:

1
2
3
4
5
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>

通常来说,我们需要重试的情况有函数出现异常,函数返回值与预期不一致等,因此我们首先可以通过随机数来模拟可能的情况,下面两个函数在运行的时候会随机返回,一个随机返回null,另一个随机抛出错误。这里MyException是继承了RuntimeException的自定义异常,主要用于异常统一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Boolean randomFailed() {
boolean res = random.nextBoolean();
if (res) {
System.out.println("return true");
return true;
} else {
System.out.println("return null");
return null;
}
}

private void randomThrowException() {
boolean res = random.nextBoolean();
if (res) {
System.out.println("throw my exception.");
throw new MyException("res is true");
} else {
System.out.println("dont throw my exception");
}
}

上面的两个方法在运行的时候是带有随机性的,无法保证每次运行都能返回正确的结果,因此我们需要引入重试功能。guava-retrying采用Callable的方式对需要进行重试的方法进行包装,然后对运行结果进行监听,从而控制重试逻辑。

对于第一个随机失败的方法来说,我们可以设置当结果返回null的时候进行重试,那么可以用下面的代码完成。这里需要通过RetryerBuilder来构造出一个Retryer对象,通过该对象来调用Callable对象。在构造的时候,我们可以指定对应的重试逻辑,像这里我们就可以指定当结果返回null的时候进行重试,同时重试策略为重试三次之后不再重试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Boolean randomFailedWithRetry() {
Callable<Boolean> callable = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return randomFailed();
}
};
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Objects::isNull)
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();

try {
return retryer.call(callable);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (RetryException e) {
throw new RuntimeException(e);
}
}

对于第二个随机抛出错误的方法来说,我们也可以设置当出现异常的时候进行重试,相关代码如下。注意这里设置返回类型为Void,还是需要一个返回结果的,这里选择返回为null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void randomThrowExceptionWithRetry() {
Callable<Void> callable = new Callable<Void>() {
@Override
public Void call() throws Exception {
randomThrowException();
return null;
}
};
Retryer<Void> retryer = RetryerBuilder.<Void>newBuilder()
.retryIfExceptionOfType(MyException.class)
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();

try {
retryer.call(callable);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (RetryException e) {
throw new RuntimeException(e);
}
}

我们可以看到上面,构造出Retryer对象的过程是非常类似的,所以对于类似的重试逻辑,我们完全可以再进行一层封装,方便后续的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static <T> T retry(Callable<T> callable, int retryTimes,
Class<? extends Throwable> retryException) throws RuntimeException {
Retryer<T> retryer = RetryerBuilder.<T>newBuilder()
.retryIfExceptionOfType(retryException)
.withStopStrategy(StopStrategies.stopAfterAttempt(retryTimes))
.build();

try {
return retryer.call(callable);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

当然除了代码中使用到的这些,guava-retrying还提供了很多其他更加灵活的重试策略,我们可以在使用的时候进行参考使用。

RetryListener

如果在发生重试之后,我们需要做一些额外的处理动作,例如告警,那么可以使用RetryListener。每次重试之后,guava-retrying会自动回调我们注册的监听。当然也可以注册多个 RetryListener,会按照注册顺序依次调用。

首先,我们可以实现自定义的监听类,需要实现RetryListener接口以及其中的方法。其中的Attempt对象会给我们提供许多重试的相关信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyRetryListener implements RetryListener {
@Override
public <V> void onRetry(Attempt<V> attempt) {
// 重试次数,第一次重试指的是第一次调用
System.out.println("retry times = " + attempt.getAttemptNumber());
// 该次重试距离第一次调用的延迟
System.out.println("delay = " + attempt.getDelaySinceFirstAttempt());
// 重试中是否出现异常
System.out.println("hasException = " + attempt.hasException());
System.out.println("hasResult = " + attempt.hasResult());
// 如果有异常可以获取异常,否则直接拿到返回结果
if(attempt.hasException()){
System.out.println("causeBy = " + attempt.getExceptionCause());
}else{
System.out.println("result = " + attempt.getResult());
}
}
}

完成之后,只需要在构造Retry对象的时候增加相关配置即可:

1
.withRetryListener(new MyRetryListener())

参考文章

  1. Java开发利器之重试框架guava-retrying
  2. 重试工具: Guaa Retryer
  3. 线程任务接口Callable使用void作为返回类型

在Java中进行重试 guava-retrying
http://example.com/2023/04/29/在Java中进行重试-guava-retrying/
作者
EverNorif
发布于
2023年4月29日
许可协议