1、引言
業務開發中很可能與回到重試的場景。
重試主要在調用失敗時重試,尤其是發生dubbo相關異常,網絡相關異常的時候。
下面對該功能簡單作封裝,然後給出一些相對用的多一些的開源代碼位址。
核心功能
提供重試工具類,
支援傳入操作、重試次數和延時時間。
支援定義不再重試的異常和條件。
主要應用場景
隻要适用于對任務丢失要求不高的場景。
此工具類隻适合單機版,是以任務的丢失要求高的場景建議用中間件,如緩存中間件redis或者消息中間件。
主要場景如下:
- 樂觀鎖重試
- 上遊業務保證重試的場景且沒有其他好的重試機制
- 需要輪詢直到得到想要的結果的場景
- 其他需要控制重試時間間隔的場景
2、簡單封裝
github位址
https://github.com/chujianyun/simple-retry4jmaven依賴
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<Class<? extends Exception>> abortExceptions;
* 不需要重試的條件清單(滿足其中一個則不重試,如果要傳入泛型條件是傳回值或者其父類類型)
private List<Predicate> abortConditions;
public RetryPolicy(Builder builder) {
this.maxRetries = builder.maxRetries;
this.delayDuration = builder.delayDuration;
List<Class<? extends Exception>> abortExceptions = builder.abortExceptions;
if (CollectionUtils.isEmpty(abortExceptions)) {
this.abortExceptions = new ArrayList<>();
} else {
this.abortExceptions = abortExceptions;
}
List<Predicate> abortConditions = builder.abortConditions;
if (CollectionUtils.isEmpty(abortConditions)) {
this.abortConditions = new ArrayList<>();
this.abortConditions = abortConditions;
}
public static Builder builder() {
return new Builder();
public static class Builder {
private Integer maxRetries;
private Duration delayDuration;
private List<Class<? extends Exception>> abortExceptions = new ArrayList<>();
private List<Predicate> 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;
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));
* 設定不重試的政策清單
public Builder abortConditions(List<Predicate> predicates) {
if (CollectionUtils.isNotEmpty(predicates)) {
predicates.forEach(this::abortCondition);
* 新增不重試的政策
public Builder abortCondition(Predicate predicate) {
if (predicate != null) {
this.abortConditions.add(predicate);
* 設定不重試的異常清單
public Builder abortExceptions(List<Class<? extends Exception>> abortExceptions) {
if (CollectionUtils.isNotEmpty(abortExceptions)) {
abortExceptions.forEach(this::abortException);
* 新增不重試的異常
public Builder abortException(Class<? extends Exception> exception) {
if (exception != null) {
this.abortExceptions.add(exception);
public RetryPolicy build() {
return new RetryPolicy(this);
}
封裝重試工具類
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
* 方法重試工具類
* @date: 2019-04-05 02:09
@Slf4j
public class SimpleRetryUtil {
* 無傳回值的重試方法
public static <T> void executeWithRetry(Consumer<T> consumer, T data, RetryPolicy retryPolicy) throws Exception {
executeWithRetry(null, consumer, data, retryPolicy);
* 帶傳回值的重試方法
public static <T> T executeWithRetry(Callable<T> callable, RetryPolicy retryPolicy) throws Exception {
return executeWithRetry(callable, null, null, retryPolicy);
* 帶重試和延時的操作執行
*
* @param callable 執行的操作
* @param retryPolicy 重試政策
* @return 傳回值
* @throws Exception 業務異常或者超過最大重試次數後的最後一次嘗試抛出的異常
private static <T> T executeWithRetry(Callable<T> callable, Consumer<T> 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<Predicate> abortConditions = retryPolicy.getAbortConditions();
/* ---------------- 不需要重試的傳回值 -------------- */
if (isInCondition(result, abortConditions)) {
return result;
}
/* ---------------- 需要重試的傳回值 -------------- */
boolean hasNextRetry = hasNextRetryAfterOperation(++retryCount, maxRetries, delayDuration);
if (!hasNextRetry) {
} catch (Exception e) {
/* ---------------- 不需要重試的異常 -------------- */
List<Class<? extends Exception>> abortExceptions = retryPolicy.getAbortExceptions();
if (isInExceptions(e, abortExceptions)) {
throw e;
/* ---------------- 需要重試的異常 -------------- */
boolean hasNextRetry = hasNextRetryAfterOperation(++retryCount, maxRetries, delayDuration);
if (!hasNextRetry) {
* 判斷運作之後是否還有下一次重試
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<Class<? extends Exception>> abortExceptions) {
return false;
for (Class<? extends Exception> clazz : abortExceptions) {
if (clazz.isAssignableFrom(e.getClass())) {
return true;
return false;
* 是否符合不需要終止的條件
private static <T> boolean isInCondition(T result, List<Predicate> abortConditions) {
return true;
for (Predicate predicate : abortConditions) {
if (predicate.test(result)) {
遇到業務異常就沒必要重試了,直接扔出去。
當遇到非業務異常是,未超出最大重試次數時,不斷重試,如果設定了延時則延時後重試。
測試類
import com.github.chujianyun.simpleretry4j.exception.BusinessException;
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.util.Objects;
import static org.mockito.ArgumentMatchers.any;
* 重試測試
* @date: 2019-04-04 10:42
@RunWith(PowerMockRunner.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class SimpleRetryUtilTest {
@Mock
private Callable<Integer> callable;
private Consumer<List<Integer>> consumer;
* 提供兩種設定延時時間的方法
@Test
public void delayDuration() {
RetryPolicy retryPolicy1 = RetryPolicy.builder()
.maxRetries(3)
.delayDuration(Duration.ofMillis(5))
.build();
RetryPolicy retryPolicy2 = RetryPolicy.builder()
.delayDuration(5, TimeUnit.MILLISECONDS)
Assert.assertEquals(retryPolicy1.getDelayDuration(), retryPolicy2.getDelayDuration());
* 模拟異常重試
@Test(expected = Exception.class)
public void executeWithRetry_Exception() throws Exception {
RetryPolicy retryPolicy = RetryPolicy.builder()
Mockito.doThrow(new Exception("test")).when(callable).call();
SimpleRetryUtil.executeWithRetry(callable, retryPolicy);
@Test(expected = BusinessException.class)
public void executeWithRetry_BusinessException() throws Exception {
.delayDuration(Duration.ofMillis(100))
Mockito.doThrow(new BusinessException()).when(callable).call();
* 模拟終止異常不重試
@Test(expected = IllegalArgumentException.class)
public void executeWithAbortException() throws Exception {
.abortException(IllegalArgumentException.class)
.abortException(BusinessException.class)
Mockito.doThrow(new IllegalArgumentException()).doReturn(1).when(callable).call();
Integer result = SimpleRetryUtil.executeWithRetry(callable, retryPolicy);
log.debug("最終傳回值{}", result);
* 模拟不在終止異常觸發重試
public void executeWithAbortException2() throws Exception {
Mockito.doThrow(new NullPointerException()).doReturn(1).when(callable).call();
Integer result = SimpleRetryUtil.executeWithRetry(callable, retryPolicy);
log.debug("最終傳回值{}", result);
* 滿足條件的傳回值不重試的設定
public void executeWithAbortCondition() throws Exception {
.abortCondition(Objects::nonNull)
//前兩次傳回null 需要重試
Mockito.doReturn(null).doReturn(null).doReturn(1).when(callable).call();
* 測試無傳回值的情況
public void consumerTest() throws Exception {
List<Integer> 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檔案
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion>
<groupId>com.github.chujianyun</groupId>
<artifactId>simple-retry4j</artifactId>
<version>1.1.2</version>
<packaging>jar</packaging>
<name>simple-retry4j</name>
<description>A Java method retry and batch execute open source lib.</description>
<url>https://github.com/chujianyun/simple-retry4j/tree/master</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit-jupiter.version>5.3.1</junit-jupiter.version>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<!--
https://mvnrepository.com/artifact/org.slf4j/slf4j-api-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.26</version>
</dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
https://mvnrepository.com/artifact/org.apache.commons/commons-lang3<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
https://mvnrepository.com/artifact/org.apache.commons/commons-collections4<artifactId>commons-collections4</artifactId>
<version>4.3</version>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-jupiter.version}</version>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.0</version>
<artifactId>powermock-api-mockito2</artifactId>
</dependencies>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<name>liuwangyang</name>
<email>[email protected]</email>
<organization>https://github.com/chujianyun</organization>
<timezone>+8</timezone>
</developer>
</developers>
<scm>
<connection>scm:git:[email protected]:chujianyun/simple-retry4j.git</connection>
<developerConnection>scm:git:[email protected]:chujianyun/simple-retry4j.git</developerConnection>
<url>https://github.com/chujianyun/simple-retry4j/tree/master</url>
</scm>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<configuration>
<show>private</show>
<nohelp>true</nohelp>
<charset>UTF-8</charset>
<encoding>UTF-8</encoding>
<docencoding>UTF-8</docencoding>
<additionalparam>-Xdoclint:none</additionalparam> <!-- TODO 臨時解決不規範的javadoc生成報錯,後面要規範化後把這行去掉 -->
</configuration>
<id>attach-javadocs</id>
<goal>jar</goal>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<id>sign-artifacts</id>
<phase>verify</phase>
<goal>sign</goal>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.7</version>
<extensions>true</extensions>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</plugins>
</build>
</project>
3、其他方案
https://github.com/rholder/guava-retrying https://github.com/elennick/retry4j————————————————
版權聲明:本文為CSDN部落客「明明如月學長」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。
原文連結:
https://blog.csdn.net/w605283073/article/details/89038394