天天看點

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