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

简介: 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: 明明如月 liuwangyangedu@163.com

* @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<>();

       } else {

           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;

           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<Predicate> 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<Class<? extends Exception>> 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: 明明如月 liuwangyangedu@163.com

* @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) {

                       return result;

                   }

               }

           } catch (Exception e) {

               /* ---------------- 不需要重试的异常 -------------- */

               List<Class<? extends Exception>> 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<Class<? extends Exception>> abortExceptions) {

       if (CollectionUtils.isEmpty(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) {

       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: 明明如月 liuwangyangedu@163.com

* @date: 2019-04-04 10:42

*/

@Slf4j

@RunWith(PowerMockRunner.class)

@TestInstance(TestInstance.Lifecycle.PER_CLASS)

public class SimpleRetryUtilTest {

   @Mock

   private Callable<Integer> callable;

   @Mock

   private Consumer<List<Integer>> 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<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>

       <dependency>

           <groupId>log4j</groupId>

           <artifactId>log4j</artifactId>

           <version>1.2.17</version>

       </dependency>

       <dependency>

           <groupId>org.slf4j</groupId>

           <artifactId>slf4j-log4j12</artifactId>

           <version>1.7.25</version>

       </dependency>

       <dependency>

           <groupId>org.projectlombok</groupId>

           <artifactId>lombok</artifactId>

           <version>1.18.2</version>

       </dependency>

       <dependency>

           <groupId>junit</groupId>

           <artifactId>junit</artifactId>

           <version>4.11</version>

           <scope>test</scope>

       </dependency>

       <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->

       <dependency>

           <groupId>org.apache.commons</groupId>

           <artifactId>commons-lang3</artifactId>

           <version>3.8.1</version>

       </dependency>

       <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->

       <dependency>

           <groupId>org.apache.commons</groupId>

           <artifactId>commons-collections4</artifactId>

           <version>4.3</version>

       </dependency>

       <dependency>

           <groupId>org.junit.jupiter</groupId>

           <artifactId>junit-jupiter-engine</artifactId>

           <version>${junit-jupiter.version}</version>

           <scope>test</scope>

       </dependency>

       <dependency>

           <groupId>org.powermock</groupId>

           <artifactId>powermock-module-junit4</artifactId>

           <version>2.0.0</version>

           <scope>test</scope>

       </dependency>

       <dependency>

           <groupId>org.powermock</groupId>

           <artifactId>powermock-api-mockito2</artifactId>

           <version>2.0.0</version>

           <scope>test</scope>

       </dependency>

   </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>liuwangyangedu@163.com</email>

           <organization>https://github.com/chujianyun</organization>

           <timezone>+8</timezone>

       </developer>

   </developers>

   <scm>

       <connection>scm:git:git@github.com:chujianyun/simple-retry4j.git</connection>

       <developerConnection>scm:git:git@github.com: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>

           <id>ossrh</id>

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

           <plugin>

               <groupId>org.apache.maven.plugins</groupId>

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

               <executions>

                   <execution>

                       <id>attach-javadocs</id>

                       <goals>

                           <goal>jar</goal>

                       </goals>

                   </execution>

               </executions>

           </plugin>

           <plugin>

               <groupId>org.apache.maven.plugins</groupId>

               <artifactId>maven-gpg-plugin</artifactId>

               <version>1.5</version>

               <executions>

                   <execution>

                       <id>sign-artifacts</id>

                       <phase>verify</phase>

                       <goals>

                           <goal>sign</goal>

                       </goals>

                   </execution>

               </executions>

           </plugin>

           <plugin>

               <groupId>org.sonatype.plugins</groupId>

               <artifactId>nexus-staging-maven-plugin</artifactId>

               <version>1.6.7</version>

               <extensions>true</extensions>

               <configuration>

                   <serverId>ossrh</serverId>

                   <nexusUrl>https://oss.sonatype.org/</nexusUrl>

                   <autoReleaseAfterClose>true</autoReleaseAfterClose>

               </configuration>

           </plugin>

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

相关文章
|
11天前
|
分布式计算 DataWorks Java
DataWorks操作报错合集之在使用MaxCompute的Java SDK创建函数时,出现找不到文件资源的情况,是BUG吗
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
25 0
|
2月前
|
JavaScript 前端开发 Java
Java Script中的函数原型是什么
Java Script中的函数原型是什么
11 0
|
6月前
|
JavaScript 前端开发 Java
javascript实现像java、c#之类的sleep暂停的函数功能
javascript实现像java、c#之类的sleep暂停的函数功能
42 0
|
6月前
|
Java
java实现Beta函数
java实现Beta函数
|
7月前
|
缓存 算法 Java
在阿里云上部署和运行Java函数时
在阿里云上部署和运行Java函数时
101 2
|
8月前
|
存储 Java
【Java】 剑指offer(30) 包含min函数的栈
【Java】 剑指offer(30) 包含min函数的栈
|
2月前
|
Java 数据库连接 API
Java 学习路线:基础知识、数据类型、条件语句、函数、循环、异常处理、数据结构、面向对象编程、包、文件和 API
Java 是一种广泛使用的、面向对象的编程语言,始于1995年,以其跨平台性、安全性和可靠性著称,应用于从移动设备到数据中心的各种场景。基础概念包括变量(如局部、实例和静态变量)、数据类型(原始和非原始)、条件语句(if、else、switch等)、函数、循环、异常处理、数据结构(如数组、链表)和面向对象编程(类、接口、继承等)。深入学习还包括包、内存管理、集合框架、序列化、网络套接字、泛型、流、JVM、垃圾回收和线程。构建工具如Gradle、Maven和Ant简化了开发流程,Web框架如Spring和Spring Boot支持Web应用开发。ORM工具如JPA、Hibernate处理对象与数
95 3
|
7月前
|
存储 Java BI
如何使用 Java 程序通过 SAP Java Connector 调用 ABAP 系统的函数试读版
如何使用 Java 程序通过 SAP Java Connector 调用 ABAP 系统的函数试读版
52 0
|
4月前
|
Java
java的OutputStream.writeUTF()函数
java的OutputStream.writeUTF()函数
|
5月前
|
算法 Java
给定一个字符串数组,如何找到其中最长的回文子串? 要求:编写一个Java函数,输入一个字符串数组,输出其中最长的回文子串。要求时间复杂度为O(n^2)。可以考虑使用动态规划或中心扩展的方法来优化算法。
给定一个字符串数组,如何找到其中最长的回文子串? 要求:编写一个Java函数,输入一个字符串数组,输出其中最长的回文子串。要求时间复杂度为O(n^2)。可以考虑使用动态规划或中心扩展的方法来优化算法。
50 1