diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/thread/RetryableTask.java b/hutool-core/src/main/java/org/dromara/hutool/core/thread/RetryableTask.java new file mode 100644 index 000000000..d2d816561 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/thread/RetryableTask.java @@ -0,0 +1,205 @@ +package org.dromara.hutool.core.thread; + +import org.dromara.hutool.core.lang.Assert; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiPredicate; +import java.util.function.Supplier; + +import static java.util.Objects.nonNull; + +/** + * 重试任务类 + * + * @author kongweiguang + * @since 6.0.0 + */ +public class RetryableTask { + /** + * 执行结果 + */ + private T result; + /** + * 执行法方法 + */ + private final Supplier sup; + /** + * 重试策略 + */ + private final BiPredicate predicate; + /** + * 重试次数,默认3次 + */ + private long maxAttempts = 3; + /** + * 重试间隔,默认1秒 + */ + private Duration delay = Duration.ofSeconds(1); + + /** + * 构造方法,内部使用,调用请使用请用ofXXX + * + * @param sup 执行的方法 + * @param predicate 策略 {@link BiPredicate} + */ + private RetryableTask(Supplier sup, BiPredicate predicate) { + Assert.notNull(sup, "task parameter cannot be null"); + Assert.notNull(predicate, "predicate parameter cannot be null"); + + this.predicate = predicate; + this.sup = sup; + } + + + /** + * 重试根据指定的异常,没有返回值 + * + * @param 返回值类型 + * @param run 执行的方法 {@link Runnable} + * @param ths 指定异常 {@link Throwable} + * @return 当前对象 {@link RetryableTask} + */ + @SafeVarargs + public static RetryableTask retryForExceptions(Runnable run, Class... ths) { + return retryForExceptions(() -> { + run.run(); + return null; + }, ths); + } + + /** + * 重试根据指定的策略,没有返回值 + * + * @param 返回值类型 + * @param run 执行的方法 {@link Runnable} + * @param predicate 策略 {@link BiPredicate} + * @return 当前对象 {@link RetryableTask} + */ + public static RetryableTask retryForPredicate(Runnable run, BiPredicate predicate) { + return retryForPredicate(() -> { + run.run(); + return null; + }, predicate); + } + + /** + * 重试根据指定的异常,有返回值 + * + * @param 返回值类型 + * @param sup 执行的方法 {@link Supplier} + * @param ths 指定异常 {@link Throwable} + * @return 当前对象 {@link RetryableTask} + */ + @SafeVarargs + public static RetryableTask retryForExceptions(Supplier sup, Class... ths) { + Assert.isTrue(ths.length != 0, "exs cannot be empty"); + + BiPredicate strategy = (t, e) -> { + if (nonNull(e)) { + return Arrays.stream(ths).anyMatch(ex -> ex.isAssignableFrom(e.getClass())); + } + return false; + }; + + return new RetryableTask<>(sup, strategy); + } + + + /** + * 重试根据指定的策略,没有返回值 + * + * @param 返回值类型 + * @param sup 执行的方法 {@link Supplier} + * @param predicate 策略 {@link BiPredicate} + * @return 当前对象 {@link RetryableTask} + */ + public static RetryableTask retryForPredicate(Supplier sup, BiPredicate predicate) { + return new RetryableTask<>(sup, predicate); + } + + + /** + * 最大重试次数 + * + * @param maxAttempts 次数 + * @return 当前对象 {@link RetryableTask} + */ + public RetryableTask maxAttempts(long maxAttempts) { + Assert.isTrue(this.maxAttempts > 0, "maxAttempts must be greater than 0"); + + this.maxAttempts = maxAttempts; + return this; + } + + /** + * 重试间隔时间 + * + * @param delay 间隔时间 + * @return 当前对象 {@link RetryableTask} + */ + public RetryableTask delay(Duration delay) { + Assert.notNull(this.delay, "delay parameter cannot be null"); + + this.delay = delay; + return this; + } + + /** + * 获取结果 + * + * @return 返回包装了结果的 {@link Optional}对象 + */ + public Optional get() { + return Optional.ofNullable(this.result); + } + + /** + * 异步执行重试方法 + * + * @return 返回一个异步对象 {@link CompletableFuture} + */ + public CompletableFuture> asyncExecute() { + return CompletableFuture.supplyAsync(this::doExecute, GlobalThreadPool.getExecutor()); + } + + /** + * 同步执行重试方法 + * + * @return 当前对象 {@link RetryableTask} + */ + public RetryableTask execute() { + return doExecute(); + } + + /** + * 开始重试 + * + * @return 当前对象 {@link RetryableTask} + **/ + private RetryableTask doExecute() { + Throwable th = null; + + while (--this.maxAttempts >= 0) { + + try { + this.result = this.sup.get(); + } catch (Throwable t) { + th = t; + } finally { + //判断重试 + if (this.predicate.test(this.result, th)) { + ThreadUtil.sleep(delay.toMillis()); + } else { + break; + } + } + } + + return this; + } + +} + diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/util/RetryUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/util/RetryUtil.java new file mode 100644 index 000000000..37e482e2a --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/util/RetryUtil.java @@ -0,0 +1,148 @@ +package org.dromara.hutool.core.util; + +import org.dromara.hutool.core.array.ArrayUtil; +import org.dromara.hutool.core.thread.GlobalThreadPool; +import org.dromara.hutool.core.thread.RetryableTask; +import org.dromara.hutool.core.thread.ThreadUtil; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiPredicate; +import java.util.function.Supplier; + +/** + * 重试工具类 + * 自定义功能请使用{@link RetryableTask}类 + * + * @author kongweiguang + * @see RetryableTask + * @since 6.0.0 + */ +public class RetryUtil { + + /** + * 根据异常信息进行重试 + * 没有返回值,重试执行方法 + * + * @param run 执行方法 + * @param maxAttempts 最大的重试次数 + * @param delay 重试间隔 + * @param recover 达到最大重试次数后执行的备用方法,入参是重试过程中的异常 + * @param exs 指定的异常类型需要重试 + */ + @SafeVarargs + public static void ofException(Runnable run, long maxAttempts, Duration delay, Runnable recover, Class... exs) { + if (ArrayUtil.isEmpty(exs)) { + exs = ArrayUtil.append(exs, RuntimeException.class); + } + RetryableTask.retryForExceptions(run, exs) + .maxAttempts(maxAttempts) + .delay(delay) + .execute() + .get() + .orElseGet(() -> { + recover.run(); + return null; + }); + } + + /** + * 根据异常信息进行重试 + * 有返回值,重试执行方法 + * + * @param sup 执行方法 + * @param maxAttempts 最大的重试次数 + * @param delay 重试间隔 + * @param recover 达到最大重试次数后执行的备用方法,入参是重试过程中的异常 + * @param exs 指定的异常类型需要重试 + * @param 结果类型 + * @return 执行结果 + */ + @SafeVarargs + public static T ofException(Supplier sup, long maxAttempts, Duration delay, Supplier recover, Class... exs) { + if (ArrayUtil.isEmpty(exs)) { + exs = ArrayUtil.append(exs, RuntimeException.class); + } + return RetryableTask.retryForExceptions(sup, exs) + .maxAttempts(maxAttempts) + .delay(delay) + .execute() + .get() + .orElseGet(recover); + } + + /** + * 根据自定义结果进行重试 + * 没有返回值,重试执行方法 + * + * @param run 执行方法 + * @param maxAttempts 最大的重试次数 + * @param delay 重试间隔 + * @param recover 达到最大重试次数后执行的备用方法,入参是重试过程中的异常 + * @param predicate 自定义重试条件 + */ + public static void ofPredicate(Runnable run, long maxAttempts, Duration delay, Supplier recover, BiPredicate predicate) { + RetryableTask.retryForPredicate(run, predicate) + .delay(delay) + .maxAttempts(maxAttempts) + .execute() + .get() + .orElseGet(recover); + } + + + /** + * 根据异常信息进行重试 + * 有返回值,重试执行方法 + * + * @param sup 执行方法 + * @param maxAttempts 最大的重试次数 + * @param delay 重试间隔 + * @param recover 达到最大重试次数后执行的备用方法,入参是重试过程中的异常 + * @param predicate 自定义重试条件 + * @param 结果类型 + * @return 执行结果 + */ + public static T ofPredicate(Supplier sup, long maxAttempts, Duration delay, Supplier recover, BiPredicate predicate) { + return RetryableTask.retryForPredicate(sup, predicate) + .delay(delay) + .maxAttempts(maxAttempts) + .execute() + .get() + .orElseGet(recover); + } + + + /** + * 从不停止的执行方法 + * + * @param run 执行方法 + * @param delay 间隔时间 + * @param isEx true:出现异常继续执行;false:则出现异常跳出执行。 + */ + public static void ofNeverStop(Runnable run, Duration delay, boolean isEx) { + while (true) { + try { + run.run(); + } catch (Throwable e) { + if (!isEx) { + break; + } + } finally { + ThreadUtil.sleep(delay.toMillis()); + } + } + } + + /** + * 从不停止的执行方法,异步执行 + * + * @param run 执行方法 + * @param delay 间隔时间 + * @param isEx true:出现异常继续执行;false:则出现异常跳出执行。 + */ + public static void ofNeverStopAsync(Runnable run, Duration delay, boolean isEx) { + CompletableFuture.runAsync(() -> ofNeverStop(run, delay, isEx), GlobalThreadPool.getExecutor()); + } + +} diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/util/RetryUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/util/RetryUtilTest.java new file mode 100644 index 000000000..7b2629ed6 --- /dev/null +++ b/hutool-core/src/test/java/org/dromara/hutool/core/util/RetryUtilTest.java @@ -0,0 +1,91 @@ +package org.dromara.hutool.core.util; + +import org.dromara.hutool.core.thread.RetryableTask; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +public class RetryUtilTest { + + @Test + void test() { + //自定义根据异常重试 + CompletableFuture> task = RetryableTask.retryForExceptions(() -> { + System.out.println("1231231"); + int a = 1 / 0; + return "qqqq"; + }, ArithmeticException.class) + .delay(Duration.ofSeconds(1)) + .maxAttempts(3) + .asyncExecute(); + + Assertions.assertFalse(task.isDone()); + Assertions.assertEquals("兜底", task.join().get().orElseGet(() -> { + return "兜底"; + })); + Assertions.assertTrue(task.isDone()); + + } + + @Test + public void noReturnTest() { + //根据异常重试,没有返回值 + RetryUtil.ofException( + () -> { + System.out.println(123); + int a = 1 / 0; + }, + 3, + Duration.ofSeconds(1), + () -> { + System.out.println("兜底"); + }, + ArithmeticException.class + ); + } + + + @Test + public void hasReturnTest() { + //根据自定义策略重试 + String result = RetryUtil.ofPredicate( + () -> { + System.out.println(123); +// int a = 1 / 0; + return "ok"; + }, + 5, + Duration.ofSeconds(1), + () -> { + System.out.println("兜底"); + return "do"; + }, + (r, e) -> { + System.out.println("r = " + r); + System.out.println("e = " + e); + return r.equals("ok"); + } + ); + + Assertions.assertEquals("ok", result); + } + + + @Test + public void neverStop() { + //异步一直执行 + RetryUtil.ofNeverStopAsync(() -> { + System.out.println("async -->"); + }, Duration.ofSeconds(1), true); + + System.out.println(" ================ "); + //同步一直执行 + RetryUtil.ofNeverStop(() -> { + System.out.println(123); + }, Duration.ofSeconds(3), true); + + + } +}