初始化项目
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
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>
|
||||
<parent>
|
||||
<groupId>org.xujun</groupId>
|
||||
<artifactId>xtools-boot</artifactId>
|
||||
<version>5.1.0</version>
|
||||
</parent>
|
||||
<artifactId>xtools-boot-idempotent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<!-- 子模块 -->
|
||||
<modules>
|
||||
<module>xtools-boot-idempotent-base</module>
|
||||
<module>xtools-boot-idempotent-web</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
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>
|
||||
<parent>
|
||||
<groupId>org.xujun</groupId>
|
||||
<artifactId>xtools-boot-idempotent</artifactId>
|
||||
<version>5.1.0</version>
|
||||
</parent>
|
||||
<artifactId>xtools-boot-idempotent-base</artifactId>
|
||||
|
||||
<!-- 依赖 -->
|
||||
<dependencies>
|
||||
<!-- xtools-boot begin -->
|
||||
<!-- xtools-boot-core -->
|
||||
<dependency>
|
||||
<groupId>org.xujun</groupId>
|
||||
<artifactId>xtools-boot-core</artifactId>
|
||||
</dependency>
|
||||
<!-- xtools-boot-cache-redis(Redis 引擎) -->
|
||||
<dependency>
|
||||
<groupId>org.xujun</groupId>
|
||||
<artifactId>xtools-boot-cache-redis</artifactId>
|
||||
</dependency>
|
||||
<!-- xtools-boot-api(BizWarning) -->
|
||||
<dependency>
|
||||
<groupId>org.xujun</groupId>
|
||||
<artifactId>xtools-boot-api</artifactId>
|
||||
</dependency>
|
||||
<!-- xtools-boot end -->
|
||||
|
||||
<!-- SpringBoot begin -->
|
||||
<!-- SpringBoot-AspectJ(AOP) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aspectj</artifactId>
|
||||
</dependency>
|
||||
<!-- SpringBoot end -->
|
||||
|
||||
<!-- Fastjson2(参数摘要序列化) -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package xtools.boot.idempotent.base;
|
||||
|
||||
import org.springframework.context.annotation.Import;
|
||||
import xtools.boot.core.utils.ModuleLoadUtils;
|
||||
import xtools.boot.idempotent.base.selector.BootIdempotentBaseImportSelector;
|
||||
|
||||
/**
|
||||
* <p>Title : BootIdempotentBaseConfiguration</p>
|
||||
* <p>Description : BootIdempotentBaseConfiguration</p>
|
||||
* <p>DevelopTools : Idea_x64_v2026.1</p>
|
||||
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
|
||||
* <p>Company : org.xujun</p>
|
||||
*
|
||||
* @author : XuJun
|
||||
* @version : 5.0.0
|
||||
* @date : 2026/07/29 10:00
|
||||
*/
|
||||
@Import(BootIdempotentBaseImportSelector.class)
|
||||
public class BootIdempotentBaseConfiguration {
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
*/
|
||||
public BootIdempotentBaseConfiguration() {
|
||||
ModuleLoadUtils.loadSuccess(BootIdempotentBaseConfiguration.class);
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package xtools.boot.idempotent.base.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* <p>Title : Idempotent</p>
|
||||
* <p>Description : Idempotent</p>
|
||||
* <p>DevelopTools : Idea_x64_v2026.1</p>
|
||||
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
|
||||
* <p>Company : org.xujun</p>
|
||||
*
|
||||
* @author : XuJun
|
||||
* @version : 5.0.0
|
||||
* @date : 2026/07/28 09:30
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Idempotent {
|
||||
|
||||
/**
|
||||
* Redis Key 前缀
|
||||
*/
|
||||
String prefix() default "idempotent";
|
||||
|
||||
/**
|
||||
* 幂等 Key(SpEL 表达式),为空时使用默认策略: 类名+方法名+参数摘要
|
||||
* 如: #orderId, #p0, #req.id
|
||||
*
|
||||
* @return SpEL 表达式
|
||||
*/
|
||||
String key() default "";
|
||||
|
||||
/**
|
||||
* 幂等有效时间(秒)
|
||||
*
|
||||
* @return 有效时间
|
||||
*/
|
||||
long expire() default 5;
|
||||
|
||||
/**
|
||||
* 重复提交提示信息
|
||||
*
|
||||
* @return 提示信息
|
||||
*/
|
||||
String message() default "请勿重复提交";
|
||||
|
||||
/**
|
||||
* 是否在方法执行成功后立即删除 Key(放行下一次提交).
|
||||
* false: Key 保留至 expire 过期,实现时间窗口内的幂等(默认)
|
||||
* true: 方法成功后立即释放,仅防止并发重复提交
|
||||
*
|
||||
* @return 是否立即删除 Key
|
||||
*/
|
||||
boolean delKey() default false;
|
||||
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package xtools.boot.idempotent.base.aspect;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
import xtools.boot.api.exection.BizWarning;
|
||||
import xtools.boot.idempotent.base.annotation.Idempotent;
|
||||
import xtools.boot.idempotent.base.key.IdempotentKeyResolver;
|
||||
import xtools.boot.idempotent.base.service.IdempotentService;
|
||||
|
||||
/**
|
||||
* <p>Title : IdempotentAspect</p>
|
||||
* <p>Description : IdempotentAspect</p>
|
||||
* <p>DevelopTools : Idea_x64_v2026.1</p>
|
||||
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
|
||||
* <p>Company : org.xujun</p>
|
||||
*
|
||||
* @author : XuJun
|
||||
* @version : 5.0.0
|
||||
* @date : 2026/07/28 09:30
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class IdempotentAspect {
|
||||
|
||||
/**
|
||||
* 幂等服务
|
||||
*/
|
||||
private final IdempotentService idempotentService;
|
||||
|
||||
/**
|
||||
* 幂等 Key 解析器
|
||||
*/
|
||||
private final IdempotentKeyResolver idempotentKeyResolver;
|
||||
|
||||
/**
|
||||
* 幂等拦截
|
||||
*
|
||||
* @param joinPoint 连接点
|
||||
* @param idempotent 幂等注解
|
||||
* @return 方法执行结果
|
||||
* @throws Throwable 方法执行异常
|
||||
*/
|
||||
@Around("@annotation(idempotent)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
|
||||
// 解析 Key
|
||||
String key = idempotentKeyResolver.resolve(joinPoint, idempotent);
|
||||
// 尝试获取幂等锁,失败则视为重复提交
|
||||
boolean acquired = idempotentService.tryAcquire(key, idempotent.expire());
|
||||
if (!acquired) {
|
||||
throw new BizWarning(idempotent.message());
|
||||
}
|
||||
// 执行业务
|
||||
Object result = joinPoint.proceed();
|
||||
if (idempotent.delKey()) {
|
||||
// 成功后立即释放
|
||||
idempotentService.release(key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package xtools.boot.idempotent.base.key;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
import xtools.boot.idempotent.base.annotation.Idempotent;
|
||||
import xtools.core.StringUtils;
|
||||
import xtools.core.encrypt.Md5Utils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static xtools.base.config.Constants.CP_COLON;
|
||||
import static xtools.base.config.Constants.CP_EMPTY;
|
||||
import static xtools.base.config.Constants.CP_NUM0;
|
||||
|
||||
/**
|
||||
* <p>Title : IdempotentKeyResolver</p>
|
||||
* <p>Description : IdempotentKeyResolver</p>
|
||||
* <p>DevelopTools : Idea_x64_v2026.1</p>
|
||||
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
|
||||
* <p>Company : org.xujun</p>
|
||||
*
|
||||
* @author : XuJun
|
||||
* @version : 5.0.0
|
||||
* @date : 2026/07/28 09:30
|
||||
*/
|
||||
@Component
|
||||
public class IdempotentKeyResolver {
|
||||
|
||||
/**
|
||||
* SpEL 解析器
|
||||
*/
|
||||
private static final ExpressionParser PARSER = new SpelExpressionParser();
|
||||
|
||||
/**
|
||||
* 参数名发现器
|
||||
*/
|
||||
private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
|
||||
|
||||
/**
|
||||
* 解析幂等 Key
|
||||
*
|
||||
* @param joinPoint 连接点
|
||||
* @param idempotent 幂等注解
|
||||
* @return Redis Key
|
||||
*/
|
||||
public String resolve(JoinPoint joinPoint, Idempotent idempotent) {
|
||||
// 获取目标方法与参数
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
Object[] args = joinPoint.getArgs();
|
||||
Object target = joinPoint.getTarget();
|
||||
|
||||
// Key 片段:配置了 SpEL 则按 SpEL 求值,否则使用默认策略
|
||||
String keyPart;
|
||||
if (StringUtils.isBlank(idempotent.key())) {
|
||||
keyPart = defaultKey(target.getClass(), method, args);
|
||||
} else {
|
||||
keyPart = spelKey(method, args, target, idempotent.key());
|
||||
}
|
||||
return idempotent.prefix() + CP_COLON + keyPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认 Key:类名 + 方法名 + 参数摘要
|
||||
*
|
||||
* @param targetClass 目标类
|
||||
* @param method 方法
|
||||
* @param args 参数
|
||||
* @return Key 片段
|
||||
*/
|
||||
private String defaultKey(Class<?> targetClass, Method method, Object[] args) {
|
||||
String argsDigest = Md5Utils.encryptToString(JSON.toJSONString(Arrays.asList(args)));
|
||||
return targetClass.getSimpleName() + CP_COLON + method.getName() + CP_COLON + argsDigest;
|
||||
}
|
||||
|
||||
/**
|
||||
* SpEL Key:支持 #参数名,#p0,#a0 及 root 对象属性
|
||||
*
|
||||
* @param method 方法
|
||||
* @param args 参数
|
||||
* @param target 目标对象
|
||||
* @param spel SpEL 表达式
|
||||
* @return Key 片段
|
||||
*/
|
||||
private String spelKey(Method method, Object[] args, Object target, String spel) {
|
||||
// 构建求值上下文,root 对象为目标对象
|
||||
StandardEvaluationContext context = new StandardEvaluationContext(target);
|
||||
context.setRootObject(target);
|
||||
// 绑定参数名与索引别名(#p0,#a0)
|
||||
String[] paramNames = NAME_DISCOVERER.getParameterNames(method);
|
||||
if (paramNames != null) {
|
||||
for (int i = CP_NUM0; i < paramNames.length && i < args.length; i++) {
|
||||
context.setVariable(paramNames[i], args[i]);
|
||||
}
|
||||
}
|
||||
for (int i = CP_NUM0; i < args.length; i++) {
|
||||
context.setVariable("p" + i, args[i]);
|
||||
context.setVariable("a" + i, args[i]);
|
||||
}
|
||||
// 解析表达式
|
||||
Expression expression = PARSER.parseExpression(spel);
|
||||
Object value = expression.getValue(context);
|
||||
return value == null ? CP_EMPTY : value.toString();
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package xtools.boot.idempotent.base.selector;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
|
||||
/**
|
||||
* <p>Title : BootIdempotentBaseImportSelector</p>
|
||||
* <p>Description : BootIdempotentBaseImportSelector</p>
|
||||
* <p>DevelopTools : Idea_x64_v2026.1</p>
|
||||
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
|
||||
* <p>Company : org.xujun</p>
|
||||
*
|
||||
* @author : XuJun
|
||||
* @version : 5.0.0
|
||||
* @date : 2026/07/29 10:00
|
||||
*/
|
||||
public class BootIdempotentBaseImportSelector implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
/**
|
||||
* 根据给定的注释元数据,根据需要注册bean
|
||||
*
|
||||
* @param importingClassMetadata AnnotationMetadata
|
||||
* @param registry BeanDefinitionRegistry
|
||||
*/
|
||||
@Override
|
||||
public void registerBeanDefinitions(@NonNull AnnotationMetadata importingClassMetadata, @NonNull BeanDefinitionRegistry registry) {
|
||||
// 构建扫描对象
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, true);
|
||||
// 扫描核心层包
|
||||
scanner.scan("xtools.boot.idempotent.base");
|
||||
}
|
||||
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package xtools.boot.idempotent.base.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import xtools.boot.cache.redis.base.RedisService;
|
||||
import xtools.core.StringUtils;
|
||||
import xtools.core.UuidUtils;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* <p>Title : IdempotentService</p>
|
||||
* <p>Description : IdempotentService</p>
|
||||
* <p>DevelopTools : Idea_x64_v2026.1</p>
|
||||
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
|
||||
* <p>Company : org.xujun</p>
|
||||
*
|
||||
* @author : XuJun
|
||||
* @version : 5.0.0
|
||||
* @date : 2026/07/28 09:30
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class IdempotentService {
|
||||
|
||||
private final RedisService redisService;
|
||||
|
||||
/**
|
||||
* 尝试获取幂等锁
|
||||
*
|
||||
* @param key 锁 Key
|
||||
* @param expire 有效时间(时间窗口)
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean tryAcquire(String key, long expire) {
|
||||
// 参数校验
|
||||
if (StringUtils.isBlank(key) || expire <= 0) {
|
||||
return true;
|
||||
}
|
||||
Boolean result = redisService.setNx(key, UuidUtils.get(), expire, TimeUnit.SECONDS);
|
||||
return Boolean.TRUE.equals(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放幂等锁
|
||||
*
|
||||
* @param key 锁 Key
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean release(String key) {
|
||||
if (StringUtils.isBlank(key)) {
|
||||
return false;
|
||||
}
|
||||
Boolean result = redisService.del(key);
|
||||
return Boolean.TRUE.equals(result);
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
xtools.boot.idempotent.base.BootIdempotentBaseConfiguration
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
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>
|
||||
<parent>
|
||||
<groupId>org.xujun</groupId>
|
||||
<artifactId>xtools-boot-idempotent</artifactId>
|
||||
<version>5.1.0</version>
|
||||
</parent>
|
||||
<artifactId>xtools-boot-idempotent-web</artifactId>
|
||||
|
||||
<!-- 依赖 -->
|
||||
<dependencies>
|
||||
<!-- xtools-boot begin -->
|
||||
<!-- xtools-boot-idempotent-base(核心层) -->
|
||||
<dependency>
|
||||
<groupId>org.xujun</groupId>
|
||||
<artifactId>xtools-boot-idempotent-base</artifactId>
|
||||
</dependency>
|
||||
<!-- xtools-boot end -->
|
||||
|
||||
<!-- xtools begin -->
|
||||
<!-- xtools-web(HeaderUtils 获取 Web 信息) -->
|
||||
<dependency>
|
||||
<groupId>org.xujun</groupId>
|
||||
<artifactId>xtools-web</artifactId>
|
||||
</dependency>
|
||||
<!-- xtools end -->
|
||||
|
||||
<!-- Spring-Web(RequestContextHolder) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Jakarta Servlet -->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package xtools.boot.idempotent.web;
|
||||
|
||||
import org.springframework.context.annotation.Import;
|
||||
import xtools.boot.core.utils.ModuleLoadUtils;
|
||||
import xtools.boot.idempotent.web.selector.BootIdempotentWebImportSelector;
|
||||
|
||||
/**
|
||||
* <p>Title : BootIdempotentWebConfiguration</p>
|
||||
* <p>Description : BootIdempotentWebConfiguration</p>
|
||||
* <p>DevelopTools : Idea_x64_v2026.1</p>
|
||||
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
|
||||
* <p>Company : org.xujun</p>
|
||||
*
|
||||
* @author : XuJun
|
||||
* @version : 5.0.0
|
||||
* @date : 2026/07/29 10:00
|
||||
*/
|
||||
@Import(BootIdempotentWebImportSelector.class)
|
||||
public class BootIdempotentWebConfiguration {
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
*/
|
||||
public BootIdempotentWebConfiguration() {
|
||||
ModuleLoadUtils.loadSuccess(BootIdempotentWebConfiguration.class);
|
||||
}
|
||||
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package xtools.boot.idempotent.web.annotation;
|
||||
|
||||
import xtools.boot.idempotent.web.enums.RepeatSubmitKeyType;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* <p>Title : RepeatSubmit</p>
|
||||
* <p>Description : RepeatSubmit</p>
|
||||
* <p>DevelopTools : Idea_x64_v2026.1</p>
|
||||
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
|
||||
* <p>Company : org.xujun</p>
|
||||
*
|
||||
* @author : XuJun
|
||||
* @version : 5.0.0
|
||||
* @date : 2026/07/28 09:30
|
||||
*/
|
||||
@Documented
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface RepeatSubmit {
|
||||
|
||||
/**
|
||||
* Redis Key 前缀
|
||||
*
|
||||
* @return 前缀
|
||||
*/
|
||||
String prefix() default "repeat-submit";
|
||||
|
||||
/**
|
||||
* 防重复提交时间间隔(秒)
|
||||
*
|
||||
* @return 时间间隔
|
||||
*/
|
||||
long interval() default 5;
|
||||
|
||||
/**
|
||||
* 重复提交提示信息
|
||||
*
|
||||
* @return 提示信息
|
||||
*/
|
||||
String message() default "请勿重复提交";
|
||||
|
||||
/**
|
||||
* Key 生成策略
|
||||
*
|
||||
* @return Key 生成策略
|
||||
*/
|
||||
RepeatSubmitKeyType keyType() default RepeatSubmitKeyType.DEFAULT;
|
||||
|
||||
/**
|
||||
* Token 请求头名称
|
||||
*
|
||||
* @return 请求头名称
|
||||
*/
|
||||
String tokenHeader() default "Bearer Authorization";
|
||||
|
||||
/**
|
||||
* 是否将请求参数(查询参数/表单)参与 Key 计算
|
||||
*
|
||||
* @return 是否参与
|
||||
*/
|
||||
boolean includeParams() default true;
|
||||
|
||||
/**
|
||||
* 是否在方法执行成功后立即删除 Key(放行下一次提交).
|
||||
* false: Key 保留至 expire 过期,实现时间窗口内的幂等(默认)
|
||||
* true: 方法成功后立即释放,仅防止并发重复提交
|
||||
*
|
||||
* @return 是否立即删除 Key
|
||||
*/
|
||||
boolean delKey() default false;
|
||||
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package xtools.boot.idempotent.web.aspect;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import xtools.boot.api.exection.BizWarning;
|
||||
import xtools.boot.idempotent.base.service.IdempotentService;
|
||||
import xtools.boot.idempotent.web.annotation.RepeatSubmit;
|
||||
import xtools.core.StringUtils;
|
||||
import xtools.core.encrypt.Md5Utils;
|
||||
import xtools.web.HeaderUtils;
|
||||
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import static xtools.base.config.Constants.CP_COLON;
|
||||
import static xtools.base.config.Constants.CP_EMPTY;
|
||||
|
||||
/**
|
||||
* <p>Title : RepeatSubmitAspect</p>
|
||||
* <p>Description : RepeatSubmitAspect</p>
|
||||
* <p>DevelopTools : Idea_x64_v2026.1</p>
|
||||
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
|
||||
* <p>Company : org.xujun</p>
|
||||
*
|
||||
* @author : XuJun
|
||||
* @version : 5.0.0
|
||||
* @date : 2026/07/28 09:30
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RepeatSubmitAspect {
|
||||
|
||||
/**
|
||||
* 幂等服务
|
||||
*/
|
||||
private final IdempotentService idempotentService;
|
||||
|
||||
/**
|
||||
* 防重复提交拦截
|
||||
*
|
||||
* @param joinPoint 连接点
|
||||
* @param repeatSubmit 防重复提交注解
|
||||
* @return 方法执行结果
|
||||
* @throws Throwable 方法执行异常
|
||||
*/
|
||||
@Around("@annotation(repeatSubmit)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
// 生成幂等 Key
|
||||
String key = buildKey(attributes.getRequest(), repeatSubmit);
|
||||
boolean acquired = idempotentService.tryAcquire(key, repeatSubmit.interval());
|
||||
if (!acquired) {
|
||||
throw new BizWarning(repeatSubmit.message());
|
||||
}
|
||||
// 执行业务
|
||||
Object result = joinPoint.proceed();
|
||||
if (repeatSubmit.delKey()) {
|
||||
// 成功后立即释放
|
||||
idempotentService.release(key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建幂等 Key
|
||||
*
|
||||
* @param request 请求
|
||||
* @param repeatSubmit 注解
|
||||
* @return Key
|
||||
*/
|
||||
private String buildKey(HttpServletRequest request, RepeatSubmit repeatSubmit) {
|
||||
StringJoiner joiner = new StringJoiner(CP_COLON);
|
||||
joiner.add(repeatSubmit.prefix());
|
||||
switch (repeatSubmit.keyType()) {
|
||||
case TOKEN -> joiner.add(token(request, repeatSubmit.tokenHeader()));
|
||||
case IP -> joiner.add(clientIp(request));
|
||||
case URI -> {
|
||||
}
|
||||
default -> joiner.add(identity(request, repeatSubmit.tokenHeader()));
|
||||
}
|
||||
joiner.add(HeaderUtils.getUri(request));
|
||||
if (repeatSubmit.includeParams()) {
|
||||
String paramsDigest = paramsDigest(request);
|
||||
if (StringUtils.isNotBlank(paramsDigest)) {
|
||||
joiner.add(paramsDigest);
|
||||
}
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端标识:Token 优先,无 Token 则使用 IP
|
||||
*
|
||||
* @param request 请求
|
||||
* @param headerName Token 头名称
|
||||
* @return 客户端标识
|
||||
*/
|
||||
private String identity(HttpServletRequest request, String headerName) {
|
||||
String token = token(request, headerName);
|
||||
return StringUtils.isNotBlank(token) ? token : clientIp(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Token(去除 Bearer 前缀)
|
||||
*
|
||||
* @param request 请求
|
||||
* @param headerName 头名称
|
||||
* @return Token
|
||||
*/
|
||||
private String token(HttpServletRequest request, String headerName) {
|
||||
String header = HeaderUtils.getHeader(request, headerName);
|
||||
return StringUtils.isBlank(header) ? CP_EMPTY : header.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端 IP
|
||||
*
|
||||
* @param request 请求
|
||||
* @return IP
|
||||
*/
|
||||
private String clientIp(HttpServletRequest request) {
|
||||
String ip = HeaderUtils.getIp(request);
|
||||
return ip == null ? CP_EMPTY : ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求参数摘要(MD5)
|
||||
*
|
||||
* @param request 请求
|
||||
* @return 摘要
|
||||
*/
|
||||
private String paramsDigest(HttpServletRequest request) {
|
||||
String params = HeaderUtils.getParamsToString(request);
|
||||
return StringUtils.isBlank(params) ? null : Md5Utils.encryptToString(params);
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package xtools.boot.idempotent.web.enums;
|
||||
|
||||
/**
|
||||
* <p>Title : RepeatSubmitKeyType</p>
|
||||
* <p>Description : RepeatSubmitKeyType</p>
|
||||
* <p>DevelopTools : Idea_x64_v2026.1</p>
|
||||
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
|
||||
* <p>Company : org.xujun</p>
|
||||
*
|
||||
* @author : XuJun
|
||||
* @version : 5.0.0
|
||||
* @date : 2026/07/28 09:30
|
||||
*/
|
||||
public enum RepeatSubmitKeyType {
|
||||
|
||||
/**
|
||||
* Token + URI
|
||||
*/
|
||||
TOKEN,
|
||||
|
||||
/**
|
||||
* 客户端 IP + URI
|
||||
*/
|
||||
IP,
|
||||
|
||||
/**
|
||||
* URI
|
||||
*/
|
||||
URI,
|
||||
|
||||
/**
|
||||
* 默认:Token(无则 IP) + URI
|
||||
*/
|
||||
DEFAULT,
|
||||
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package xtools.boot.idempotent.web.selector;
|
||||
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
|
||||
/**
|
||||
* <p>Title : BootIdempotentWebImportSelector</p>
|
||||
* <p>Description : BootIdempotentWebImportSelector</p>
|
||||
* <p>DevelopTools : Idea_x64_v2026.1</p>
|
||||
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
|
||||
* <p>Company : org.xujun</p>
|
||||
*
|
||||
* @author : XuJun
|
||||
* @version : 5.0.0
|
||||
* @date : 2026/07/29 10:00
|
||||
*/
|
||||
public class BootIdempotentWebImportSelector implements ImportBeanDefinitionRegistrar {
|
||||
|
||||
/**
|
||||
* 根据给定的注释元数据,根据需要注册bean
|
||||
*
|
||||
* @param importingClassMetadata AnnotationMetadata
|
||||
* @param registry BeanDefinitionRegistry
|
||||
*/
|
||||
@Override
|
||||
public void registerBeanDefinitions(@NonNull AnnotationMetadata importingClassMetadata, @NonNull BeanDefinitionRegistry registry) {
|
||||
// 构建扫描对象
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, true);
|
||||
// 扫描 Controller 包装层包
|
||||
scanner.scan("xtools.boot.idempotent.web");
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
xtools.boot.idempotent.web.BootIdempotentWebConfiguration
|
||||
Reference in New Issue
Block a user