业务开发中很可能与回到重试的场景。
重试主要在调用失败时重试,尤其是发生dubbo相关异常,网络相关异常的时候。
下面对该功能简单作封装,然后给出一些相对用的多一些的开源代码地址。
核心功能
提供重试工具类,
支持传入操作、重试次数和延时时间。
支持定义不再重试的异常和条件。
主要应用场景
只要适用于对任务丢失要求不高的场景。
此工具类只适合单机版,因此任务的丢失要求高的场景建议用中间件,如缓存中间件redis或者消息中间件。
主要场景如下:
- 乐观锁重试
- 上游业务保证重试的场景且没有其他好的重试机制
- 需要轮询直到得到想要的结果的场景
- 其他需要控制重试时间间隔的场景
github地址 https://github.com/chujianyun/simple-retry4j
maven依赖
https://search.maven.org/search?q=a:simple-retry4j
可下载运行,可fork改进,欢迎提出宝贵意见,欢迎贡献代码。
封装重试策略
package com.github.chujianyun.simpleretry4j;
import lombok.Data;
import org.apache.commons.collections4.CollectionUtils;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
/**
* 重试策略
*
* @author: 明明如月 [email protected]
* @date: 2019-04-05 10:06
*/
@Data
public class RetryPolicy {
/**
* 最大重试次数(如果不设置则默认不满足重试的异常或策略则无限重试)
*/
private Integer maxRetries;
/**
* 延时时间
*/
private Duration delayDuration;
/**
* 不需要重试的异常列表
*/
private List> abortExceptions;
/**
* 不需要重试的条件列表(满足其中一个则不重试,如果要传入泛型条件是返回值或者其父类类型)
*/
private List abortConditions;
public RetryPolicy(Builder builder) {
this.maxRetries = builder.maxRetries;
this.delayDuration = builder.delayDuration;
List> abortExceptions = builder.abortExceptions;
if (CollectionUtils.isEmpty(abortExceptions)) {
this.abortExceptions = new ArrayList<>();
} else {
this.abortExceptions = abortExceptions;
}
List abortConditions = builder.abortConditions;
if (CollectionUtils.isEmpty(abortConditions)) {
this.abortConditions = new ArrayList<>();
} else {
this.abortConditions = abortConditions;
}
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Integer maxRetries;
private Duration delayDuration;
private List> abortExceptions = new ArrayList<>();
private List abortConditions = new ArrayList<>();
/**
* 设置最大重试次数(如果不设置则默认不满足重试的异常或策略则无限重试)
*/
public Builder maxRetries(Integer maxRetries) {
if (maxRetries == null || maxRetries < 0) {
throw new IllegalArgumentException("maxRetries must not be null or negative");
}
this.maxRetries = maxRetries;
return this;
}
/**
* 重试的时间间隔
*/
public Builder delayDuration(Duration delayDuration) {
if (delayDuration == null || delayDuration.isNegative()) {
throw new IllegalArgumentException("delayDuration must not be null or negative");
}
this.delayDuration = delayDuration;
return this;
}
/**
* 重试的时间间隔
*/
public Builder delayDuration(Integer time, TimeUnit timeUnit) {
if (time == null || time < 0) {
throw new IllegalArgumentException("time must not be null or negative");
}
if (timeUnit == null) {
throw new IllegalArgumentException("timeUnit must not be null or negative");
}
this.delayDuration = Duration.ofMillis(timeUnit.toMillis(time));
return this;
}
/**
* 设置不重试的策略列表
*/
public Builder abortConditions(List predicates) {
if (CollectionUtils.isNotEmpty(predicates)) {
predicates.forEach(this::abortCondition);
}
return this;
}
/**
* 新增不重试的策略
*/
public Builder abortCondition(Predicate predicate) {
if (predicate != null) {
this.abortConditions.add(predicate);
}
return this;
}
/**
* 设置不重试的异常列表
*/
public Builder abortExceptions(List> abortExceptions) {
if (CollectionUtils.isNotEmpty(abortExceptions)) {
abortExceptions.forEach(this::abortException);
}
return this;
}
/**
* 新增不重试的异常
*/
public Builder abortException(Class extends Exception> exception) {
if (exception != null) {
this.abortExceptions.add(exception);
}
return this;
}
public RetryPolicy build() {
return new RetryPolicy(this);
}
}
}
封装重试工具类
package com.github.chujianyun.simpleretry4j;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* 方法重试工具类
*
* @author: 明明如月 [email protected]
* @date: 2019-04-05 02:09
*/
@Slf4j
public class SimpleRetryUtil {
/**
* 无返回值的重试方法
*/
public static void executeWithRetry(Consumer consumer, T data, RetryPolicy retryPolicy) throws Exception {
executeWithRetry(null, consumer, data, retryPolicy);
}
/**
* 带返回值的重试方法
*/
public static T executeWithRetry(Callable callable, RetryPolicy retryPolicy) throws Exception {
return executeWithRetry(callable, null, null, retryPolicy);
}
/**
* 带重试和延时的操作执行
*
* @param callable 执行的操作
* @param retryPolicy 重试策略
* @return 返回值
* @throws Exception 业务异常或者超过最大重试次数后的最后一次尝试抛出的异常
*/
private static T executeWithRetry(Callable callable, Consumer consumer, T data, RetryPolicy retryPolicy) throws Exception {
// 最大重试次数
Integer maxRetries = retryPolicy.getMaxRetries();
if (maxRetries != null && maxRetries < 0) {
throw new IllegalArgumentException("最大重试次数不能为负数");
}
int retryCount = 0;
Duration delayDuration = retryPolicy.getDelayDuration();
while (true) {
try {
// 不带返回值的
if (consumer != null) {
consumer.accept(data);
return null;
}
// 带返回值的
if (callable != null) {
T result = callable.call();
// 不设置终止条件或者设置了且满足则返回,否则还会重试
List abortConditions = retryPolicy.getAbortConditions();
/* ---------------- 不需要重试的返回值 -------------- */
if (isInCondition(result, abortConditions)) {
return result;
}
/* ---------------- 需要重试的返回值 -------------- */
boolean hasNextRetry = hasNextRetryAfterOperation(++retryCount, maxRetries, delayDuration);
if (!hasNextRetry) {
return result;
}
}
} catch (Exception e) {
/* ---------------- 不需要重试的异常 -------------- */
List> abortExceptions = retryPolicy.getAbortExceptions();
if (isInExceptions(e, abortExceptions)) {
throw e;
}
/* ---------------- 需要重试的异常 -------------- */
boolean hasNextRetry = hasNextRetryAfterOperation(++retryCount, maxRetries, delayDuration);
if (!hasNextRetry) {
throw e;
}
}
}
}
/**
* 判断运行之后是否还有下一次重试
*/
private static boolean hasNextRetryAfterOperation(int retryCount, Integer maxRetries, Duration delayDuration) throws InterruptedException {
// 有限次重试
if (maxRetries != null) {
if (retryCount > maxRetries) {
return false;
}
}
// 延时
if (delayDuration != null && !delayDuration.isNegative()) {
log.debug("延时{}毫秒", delayDuration.toMillis());
Thread.sleep(delayDuration.toMillis());
}
log.debug("第{}次重试", retryCount);
return true;
}
/**
* 是否在异常列表中
*/
private static boolean isInExceptions(Exception e, List> abortExceptions) {
if (CollectionUtils.isEmpty(abortExceptions)) {
return false;
}
for (Class extends Exception> clazz : abortExceptions) {
if (clazz.isAssignableFrom(e.getClass())) {
return true;
}
}
return false;
}
/**
* 是否符合不需要终止的条件
*/
private static boolean isInCondition(T result, List abortConditions) {
if (CollectionUtils.isEmpty(abortConditions)) {
return true;
}
for (Predicate predicate : abortConditions) {
if (predicate.test(result)) {
return true;
}
}
return false;
}
}
遇到业务异常就没必要重试了,直接扔出去。
当遇到非业务异常是,未超出最大重试次数时,不断重试,如果设置了延时则延时后重试。
测试类
package com.github.chujianyun.simpleretry4j;
import com.github.chujianyun.simpleretry4j.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.powermock.modules.junit4.PowerMockRunner;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static org.mockito.ArgumentMatchers.any;
/**
* 重试测试
*
* @author: 明明如月 [email protected]
* @date: 2019-04-04 10:42
*/
@Slf4j
@RunWith(PowerMockRunner.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class SimpleRetryUtilTest {
@Mock
private Callable callable;
@Mock
private Consumer> consumer;
/**
* 提供两种设置延时时间的方法
*/
@Test
public void delayDuration() {
RetryPolicy retryPolicy1 = RetryPolicy.builder()
.maxRetries(3)
.delayDuration(Duration.ofMillis(5))
.build();
RetryPolicy retryPolicy2 = RetryPolicy.builder()
.maxRetries(3)
.delayDuration(5, TimeUnit.MILLISECONDS)
.build();
Assert.assertEquals(retryPolicy1.getDelayDuration(), retryPolicy2.getDelayDuration());
}
/**
* 模拟异常重试
*/
@Test(expected = Exception.class)
public void executeWithRetry_Exception() throws Exception {
RetryPolicy retryPolicy = RetryPolicy.builder()
.maxRetries(3)
.build();
Mockito.doThrow(new Exception("test")).when(callable).call();
SimpleRetryUtil.executeWithRetry(callable, retryPolicy);
}
/**
* 模拟异常重试
*/
@Test(expected = BusinessException.class)
public void executeWithRetry_BusinessException() throws Exception {
RetryPolicy retryPolicy = RetryPolicy.builder()
.maxRetries(3)
.delayDuration(Duration.ofMillis(100))
.build();
Mockito.doThrow(new BusinessException()).when(callable).call();
SimpleRetryUtil.executeWithRetry(callable, retryPolicy);
}
/**
* 模拟终止异常不重试
*/
@Test(expected = IllegalArgumentException.class)
public void executeWithAbortException() throws Exception {
RetryPolicy retryPolicy = RetryPolicy.builder()
.maxRetries(3)
.delayDuration(Duration.ofMillis(100))
.abortException(IllegalArgumentException.class)
.abortException(BusinessException.class)
.build();
Mockito.doThrow(new IllegalArgumentException()).doReturn(1).when(callable).call();
Integer result = SimpleRetryUtil.executeWithRetry(callable, retryPolicy);
log.debug("最终返回值{}", result);
}
/**
* 模拟不在终止异常触发重试
*/
@Test
public void executeWithAbortException2() throws Exception {
RetryPolicy retryPolicy = RetryPolicy.builder()
.maxRetries(3)
.delayDuration(Duration.ofMillis(100))
.abortException(BusinessException.class)
.build();
Mockito.doThrow(new NullPointerException()).doReturn(1).when(callable).call();
Integer result = SimpleRetryUtil.executeWithRetry(callable, retryPolicy);
log.debug("最终返回值{}", result);
}
/**
* 满足条件的返回值不重试的设置
*/
@Test
public void executeWithAbortCondition() throws Exception {
RetryPolicy retryPolicy = RetryPolicy.builder()
.maxRetries(3)
.delayDuration(Duration.ofMillis(100))
.abortCondition(Objects::nonNull)
.build();
//前两次返回null 需要重试
Mockito.doReturn(null).doReturn(null).doReturn(1).when(callable).call();
Integer result = SimpleRetryUtil.executeWithRetry(callable, retryPolicy);
log.debug("最终返回值{}", result);
}
/**
* 测试无返回值的情况
*/
@Test
public void consumerTest() throws Exception {
RetryPolicy retryPolicy = RetryPolicy.builder()
.maxRetries(3)
.delayDuration(Duration.ofMillis(100))
.build();
List data = new ArrayList<>(4);
data.add(1);
data.add(2);
data.add(3);
data.add(4);
Mockito.doThrow(new RuntimeException("测试")).doThrow(new RuntimeException("测试2")).doAnswer(invocationOnMock -> {
Object param = invocationOnMock.getArgument(0);
System.out.println("消费成功,列表个数" + ((List) param).size());
return param;
}).when(consumer).accept(any());
SimpleRetryUtil.executeWithRetry(consumer, data, retryPolicy);
}
}
日志配置
# 设置
log4j.rootLogger = debug,stdout
# 输出信息到控制抬
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
pom文件
4.0.0
com.github.chujianyun
simple-retry4j
1.1.2
jar
simple-retry4j
A Java method retry and batch execute open source lib.
https://github.com/chujianyun/simple-retry4j/tree/master
UTF-8
5.3.1
1.8
1.8
org.slf4j
slf4j-api
1.7.26
log4j
log4j
1.2.17
org.slf4j
slf4j-log4j12
1.7.25
org.projectlombok
lombok
1.18.2
junit
junit
4.11
test
org.apache.commons
commons-lang3
3.8.1
org.apache.commons
commons-collections4
4.3
org.junit.jupiter
junit-jupiter-engine
${junit-jupiter.version}
test
org.powermock
powermock-module-junit4
2.0.0
test
org.powermock
powermock-api-mockito2
2.0.0
test
The Apache Software License, Version 2.0
http://www.apache.org/licenses/LICENSE-2.0.txt
repo
liuwangyang
[email protected]
https://github.com/chujianyun
+8
scm:git:[email protected]:chujianyun/simple-retry4j.git
scm:git:[email protected]:chujianyun/simple-retry4j.git
https://github.com/chujianyun/simple-retry4j/tree/master
ossrh
https://oss.sonatype.org/content/repositories/snapshots
ossrh
https://oss.sonatype.org/service/local/staging/deploy/maven2/
org.apache.maven.plugins
maven-source-plugin
2.2.1
attach-sources
jar-no-fork
org.apache.maven.plugins
maven-javadoc-plugin
2.9.1
private
true
UTF-8
UTF-8
UTF-8
-Xdoclint:none
attach-javadocs
jar
org.apache.maven.plugins
maven-gpg-plugin
1.5
sign-artifacts
verify
sign
org.sonatype.plugins
nexus-staging-maven-plugin
1.6.7
true
ossrh
https://oss.sonatype.org/
true
https://github.com/rholder/guava-retrying
https://github.com/elennick/retry4j