前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java函数调用重试的正确姿势

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

作者头像
明明如月学长
发布2021-08-27 17:06:06
2.4K0
发布2021-08-27 17:06:06
举报
文章被收录于专栏:明明如月的技术专栏

1、引言

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

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

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

核心功能 提供重试工具类, 支持传入操作、重试次数和延时时间。 支持定义不再重试的异常和条件。

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

主要场景如下: - 乐观锁重试 - 上游业务保证重试的场景且没有其他好的重试机制 - 需要轮询直到得到想要的结果的场景 - 其他需要控制重试时间间隔的场景

2、简单封装

github地址 https://github.com/chujianyun/simple-retry4j

maven依赖

https://search.maven.org/search?q=a:simple-retry4j

可下载运行,可fork改进,欢迎提出宝贵意见,欢迎贡献代码。

封装重试策略

代码语言:javascript
复制
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> abortExceptions;

    /**
     * 不需要重试的条件列表(满足其中一个则不重试,如果要传入泛型条件是返回值或者其父类类型)
     */
    private List abortConditions;


    public RetryPolicy(Builder builder) {

        this.maxRetries = builder.maxRetries;
        this.delayDuration = builder.delayDuration;

        List> abortExceptions = builder.abortExceptions;
        if (CollectionUtils.isEmpty(abortExceptions)) {
            this.abortExceptions = new ArrayList<>();
        } else {
            this.abortExceptions = abortExceptions;
        }

        List 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> abortExceptions = new ArrayList<>();

        private List 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 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> abortExceptions) {
            if (CollectionUtils.isNotEmpty(abortExceptions)) {
                abortExceptions.forEach(this::abortException);
            }
            return this;
        }

        /**
         * 新增不重试的异常
         */
        public Builder abortException(Class exception) {
            if (exception != null) {
                this.abortExceptions.add(exception);
            }
            return this;
        }

        public RetryPolicy build() {
            return new RetryPolicy(this);
        }

    }

}

封装重试工具类

代码语言:javascript
复制
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  void executeWithRetry(Consumer consumer, T data, RetryPolicy retryPolicy) throws Exception {
        executeWithRetry(null, consumer, data, retryPolicy);
    }

    /**
     * 带返回值的重试方法
     */
    public static  T executeWithRetry(Callable callable, RetryPolicy retryPolicy) throws Exception {

        return executeWithRetry(callable, null, null, retryPolicy);
    }


    /**
     * 带重试和延时的操作执行
     *
     * @param callable    执行的操作
     * @param retryPolicy 重试策略
     * @return 返回值
     * @throws Exception 业务异常或者超过最大重试次数后的最后一次尝试抛出的异常
     */
    private static  T executeWithRetry(Callable callable, Consumer 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 abortConditions = retryPolicy.getAbortConditions();
                    /* ---------------- 不需要重试的返回值 -------------- */
                    if (isInCondition(result, abortConditions)) {
                        return result;
                    }

                    /* ---------------- 需要重试的返回值 -------------- */
                    boolean hasNextRetry = hasNextRetryAfterOperation(++retryCount, maxRetries, delayDuration);
                    if (!hasNextRetry) {
                        return result;
                    }
                }
            } catch (Exception e) {
                /* ---------------- 不需要重试的异常 -------------- */
                List> 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> abortExceptions) {
        if (CollectionUtils.isEmpty(abortExceptions)) {
            return false;
        }
        for (Class clazz : abortExceptions) {
            if (clazz.isAssignableFrom(e.getClass())) {
                return true;
            }
        }
        return false;
    }

    /**
     * 是否符合不需要终止的条件
     */
    private static  boolean isInCondition(T result, List abortConditions) {
        if (CollectionUtils.isEmpty(abortConditions)) {
            return true;
        }

        for (Predicate predicate : abortConditions) {
            if (predicate.test(result)) {
                return true;
            }
        }
        return false;
    }

}

遇到业务异常就没必要重试了,直接扔出去。

当遇到非业务异常是,未超出最大重试次数时,不断重试,如果设置了延时则延时后重试。

测试类

代码语言:javascript
复制
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);
    }
 
 
}

日志配置

代码语言:javascript
复制
# 设置
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文件

代码语言:javascript
复制
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 callable;

    @Mock
    private Consumer> 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 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);
    }


}

3、其他方案

https://github.com/rholder/guava-retrying

https://github.com/elennick/retry4j

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019/04/05 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、引言
  • 2、简单封装
  • 3、其他方案
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档