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> 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);
        }

    }

}

 

封装重试工具类

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: 明明如月 [email protected]
 * @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;
    }

}

 

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

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

测试类

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: 明明如月 [email protected]
 * @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);
    }


}

日志配置

# 设置
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文件



    4.0.0

    com.github.chujianyun
    simple-retry4j
    1.1.2
    jar

    simple-retry4j
    A Java method retry and batch execute open source lib.
    https://github.com/chujianyun/simple-retry4j/tree/master

    
        UTF-8
        5.3.1

        1.8
        1.8
    

    
        
        
            org.slf4j
            slf4j-api
            1.7.26
        

        
            log4j
            log4j
            1.2.17
        
        
            org.slf4j
            slf4j-log4j12
            1.7.25
        

        
            org.projectlombok
            lombok
            1.18.2
        

        
            junit
            junit
            4.11
            test
        

        
        
            org.apache.commons
            commons-lang3
            3.8.1
        

        
        
            org.apache.commons
            commons-collections4
            4.3
        

        
            org.junit.jupiter
            junit-jupiter-engine
            ${junit-jupiter.version}
            test
        

        
            org.powermock
            powermock-module-junit4
            2.0.0
            test
        
        
            org.powermock
            powermock-api-mockito2
            2.0.0
            test
        

    


    
        
            The Apache Software License, Version 2.0
            http://www.apache.org/licenses/LICENSE-2.0.txt
            repo
        
    
    
        
            liuwangyang
            [email protected]
            https://github.com/chujianyun
            +8
        
    
    
        scm:git:[email protected]:chujianyun/simple-retry4j.git
        scm:git:[email protected]:chujianyun/simple-retry4j.git
        https://github.com/chujianyun/simple-retry4j/tree/master
    

    
        
            ossrh
            https://oss.sonatype.org/content/repositories/snapshots
        
        
            ossrh
            https://oss.sonatype.org/service/local/staging/deploy/maven2/
        
    

    
        
            
                org.apache.maven.plugins
                maven-source-plugin
                2.2.1
                
                    
                        attach-sources
                        
                            jar-no-fork
                        
                    
                
            
            
                org.apache.maven.plugins
                maven-javadoc-plugin
                2.9.1
                
                    private
                    true
                    UTF-8
                    UTF-8
                    UTF-8
                    -Xdoclint:none  
                
                
                    
                        attach-javadocs
                        
                            jar
                        
                    
                
            
            
                org.apache.maven.plugins
                maven-gpg-plugin
                1.5
                
                    
                        sign-artifacts
                        verify
                        
                            sign
                        
                    
                
            
            
                org.sonatype.plugins
                nexus-staging-maven-plugin
                1.6.7
                true
                
                    ossrh
                    https://oss.sonatype.org/
                    true
                
            
        
    


3、其他方案

 

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

https://github.com/elennick/retry4j

 

你可能感兴趣的:(Java基础,通用方案)