天天看点

Java函数调用重试的正确姿势

1、引言

业务开发中很可能与回到重试的场景。

重试主要在调用失败时重试,尤其是发生dubbo相关异常,网络相关异常的时候。

下面对该功能简单作封装,然后给出一些相对用的多一些的开源代码地址。

核心功能

提供重试工具类,

支持传入操作、重试次数和延时时间。

支持定义不再重试的异常和条件。

主要应用场景

只要适用于对任务丢失要求不高的场景。

此工具类只适合单机版,因此任务的丢失要求高的场景建议用中间件,如缓存中间件redis或者消息中间件。

主要场景如下:

- 乐观锁重试

- 上游业务保证重试的场景且没有其他好的重试机制

- 需要轮询直到得到想要的结果的场景

- 其他需要控制重试时间间隔的场景

2、简单封装

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<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