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

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明明如月学长

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值