提交 729ea163 作者: 方治民

feat: 新增 RetryExecutor 重试执行器,可用于处理可能出现异常但被允许重试的业务场景,封装简化 AOP 的切面重试实现,借助 Spring Retry 实现

上级 b5f73560
......@@ -14,6 +14,7 @@ import com.yiring.common.annotation.DownloadResponse;
import com.yiring.common.annotation.RateLimiter;
import com.yiring.common.core.I18n;
import com.yiring.common.core.Result;
import com.yiring.common.core.RetryExecutor;
import com.yiring.common.core.Status;
import com.yiring.common.param.IdParam;
import com.yiring.common.param.PageParam;
......@@ -60,6 +61,7 @@ public class ExampleController {
final Auths auths;
final UserExtensionRepository userExtensionRepository;
final FileManageService fileManageService;
final RetryExecutor retryExecutor;
@RateLimiter(count = 1)
@Operation(summary = "Hello World")
......@@ -75,11 +77,19 @@ public class ExampleController {
@GetMapping("fail")
public void fail() {
// 1. 直接抛出异常
Status.BAD_REQUEST.expose("Code.1");
// Status.BAD_REQUEST.expose("Code.1");
// 2. 手动抛出异常
// throw Status.BAD_REQUEST.exception("Code.1");
// throw BusinessException.i18n("Code.1");
// throw new FailStatusException(Status.BAD_REQUEST, "Code.1");
// 3. 重试抛出异常
throw retryExecutor.run(
ctx -> {
throw new RuntimeException("retry test fail");
},
ctx -> Status.BAD_REQUEST.exception("Code.1"),
template -> RetryExecutor.setSimplePolicy(template, 3, 1000L)
);
}
@SaCheckLogin
......
/* (C) 2024 YiRing, Inc. */
package com.yiring;
import cn.hutool.core.lang.Console;
import com.yiring.common.core.RetryExecutor;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.retry.annotation.EnableRetry;
@SpringBootConfiguration
@SpringBootTest
@EnableRetry
public class RetryTests {
RetryExecutor retry = new RetryExecutor();
/**
* 简易重试,超过默认次数会抛出异常
*/
@Test
void def() {
Integer value = retry.run(ctx -> {
Console.log("retry test, {}", ctx.getRetryCount());
String text = "0.0";
if (ctx.getRetryCount() > 1) {
text = "1";
}
return Integer.parseInt(text);
});
Console.log("[default] retry test result: {}", value);
}
/**
* 默认的退回策略
*/
@Test
void recover() {
Integer value = retry.run(
ctx -> {
Console.log("retry test, {}", ctx.getRetryCount());
throw new RuntimeException("test");
},
ctx -> {
Console.log("retry recover, {}", ctx.getRetryCount());
return 666;
},
template -> RetryExecutor.setSimplePolicy(template, 2, 1000L)
);
Console.log("[recover] retry test result: {}", value);
}
/**
* 重试 2 次,每次间隔 5s,失败默认退回 null
*/
@Test
void custom() {
Integer value = retry.run(
ctx -> {
Console.log("retry test, {}", ctx.getRetryCount());
throw new RuntimeException("test");
},
ctx -> {
Console.log("retry recover, {}", ctx.getRetryCount());
return null;
},
template -> RetryExecutor.setSimplePolicy(template, 2, 5000L)
);
Console.log("[recover] retry test result: {}", value);
}
}
......@@ -3,6 +3,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Spring Retry
api "org.springframework.retry:spring-retry"
// hutool
implementation "cn.hutool:hutool-core:${hutoolVersion}"
implementation "cn.hutool:hutool-http:${hutoolVersion}"
......
/* (C) 2024 YiRing, Inc. */
package com.yiring.common.core;
import jakarta.validation.constraints.NotNull;
import java.util.Objects;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.RetryContext;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component;
/**
* 重试执行器
* 用途: 主要用于解决切面重试机制的问题,减少自定义实现 AOP 重试机制的代码量
*
* @author Jim
*/
@SuppressWarnings("unused")
@Slf4j
@Component
@EnableRetry
public class RetryExecutor {
/**
* 包装重试方法实现
* 默认 RetryTemplate 配置为 1s 重试间隔 重试 3 次,可用过自定义参数配置
*
* @param accomplish 重试业务实现
* @param <S> 重试业务的返回值类型
* @return 重试业务的返回值,例如:一个连接对象、网络请求结果
*/
public <S> S run(@NotNull RetrySupplier<S> accomplish) {
return run(accomplish, null, null);
}
/**
* 包装重试方法实现
* 默认 RetryTemplate 配置为 1s 重试间隔 重试 3 次,可用过自定义参数配置
*
* @param accomplish 重试业务实现
* @param consumer 重试模板
* @param <S> 重试业务的返回值类型
* @return 重试业务的返回值,例如:一个连接对象、网络请求结果
*/
public <S> S run(@NotNull RetrySupplier<S> accomplish, Consumer<RetryTemplate> consumer) {
return run(accomplish, null, consumer);
}
/**
* 包装重试方法实现
*
* @param accomplish 重试业务实现
* @param recover 重试失败的回退实现
* @param consumer 重试模板
* @param <S> 重试业务的返回值类型
* @return 重试业务的返回值,例如:一个连接对象、网络请求结果
*/
public <S> S run(@NotNull RetrySupplier<S> accomplish, RetrySupplier<S> recover, Consumer<RetryTemplate> consumer) {
// 创建默认的重试模板
RetryTemplate template = RetryTemplate.builder().retryOn(Exception.class).build();
// 重试模板的自定义实现,例如一些重试机制的配置
if (Objects.nonNull(consumer)) {
consumer.accept(template);
}
// 通过模板执行重试实现机制和默认的回退实现
return template.execute(
accomplish::get,
ctx -> {
if (Objects.nonNull(recover)) {
return recover.get(ctx);
}
// 没有添加保底恢复实现的情况下,抛出原始异常
Throwable throwable = ctx.getLastThrowable();
log.error(
"[RetryExecutor] execute fail, retry count: {}, e: {}",
ctx.getRetryCount(),
throwable.getMessage()
);
throw new RuntimeException(throwable);
}
);
}
/**
* 设置简单的重试策略
*
* @param template 重试模板
* @param maxAttempts 最大重试次数
* @param backOffPeriod 重试间隔时间
*/
public static void setSimplePolicy(RetryTemplate template, int maxAttempts, long backOffPeriod) {
// 设置简单的重试次数
SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
simpleRetryPolicy.setMaxAttempts(maxAttempts);
template.setRetryPolicy(simpleRetryPolicy);
// 设置固定的重试时间间隔,单位: ms 毫秒
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(backOffPeriod);
template.setBackOffPolicy(backOffPolicy);
}
@FunctionalInterface
public interface RetrySupplier<R> {
R get(RetryContext ctx);
}
}
......@@ -129,7 +129,7 @@ subprojects {
gradle.taskGraph.whenReady {
tasks.each { task ->
if (task.name.contains("test")) {
task.enabled = false
task.enabled = true
}
}
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论