初始化项目

This commit is contained in:
2026-08-06 15:19:45 +08:00
commit 63c8d8b757
229 changed files with 12653 additions and 0 deletions
+20
View File
@@ -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>
@@ -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);
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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");
}
}
@@ -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);
}
}
@@ -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>
@@ -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);
}
}
@@ -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;
}
@@ -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);
}
}
@@ -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,
}
@@ -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");
}
}