初始化项目
This commit is contained in:
@@ -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