From a969e99539cb81cb5f08ebd0a87c9e13e4a5987f Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Wed, 22 Jul 2026 06:39:16 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(repository):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=A4=9A=E4=BB=93=E5=BA=93=E6=94=AF=E6=8C=81=E5=92=8C=E5=BB=B6?= =?UTF-8?q?=E8=BF=9F=E5=88=9D=E5=A7=8B=E5=8C=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AbstractUserRepositoryImpl 抽象类实现用户仓库基础功能 - 配置 application.yml 和测试环境 yml 文件支持多仓库配置 - 在 AutoRepositoryConfiguration 中注册 RepositoryBeanPostProcessor 和 MultiRepositoryBeanPostProcessor - 添加 BeanDebugTest 测试类用于调试 Spring 容器中的 bean - 优化 ElasticsearchRepositoryDelegate 使用 volatile 和懒加载机制 - 改进 GenericTypeResolver 的泛型解析逻辑增加继承链查找 - 扩展 InfraProperties 添加 multiRepositoryEnabled 和 defaultRepositoryType 配置项 - 重构 JpaRepositoryDelegate 实现延迟初始化和异常处理 - 添加 MockMongoConfiguration 测试配置类提供 MongoDB 操作模拟 - 修改 ElasticsearchRepositoryDelegate 使用 getPoClass() 方法获取持久化对象类型 - 优化多仓库委托类的泛型类型解析机制增加容错处理 --- .trae/documents/multi_repository_plan.md | 221 +++++ ...ository_creation_flow_verification_plan.md | 793 ++++++++++++++++++ .../repository_type_inference_plan.md | 182 ++++ .../ElasticsearchRepositoryDelegate.java | 146 ++-- .../jpa/repository/JpaRepositoryDelegate.java | 128 ++- .../repository/MongoRepositoryDelegate.java | 258 +++--- .../MybatisPlusRepositoryDelegate.java | 140 +++- structure-infra-sample/pom.xml | 3 +- .../structure-infra-sample-multi/pom.xml | 58 ++ .../sample/multi/MultiSampleApplication.java | 12 + .../multi/domain/entity/UserEntity.java | 23 + .../domain/repository/UserRepository.java | 9 + .../sample/multi/infra/mapper/UserMapper.java | 9 + .../sample/multi/infra/po/MongoUserPO.java | 27 + .../sample/multi/infra/po/MybatisUserPO.java | 30 + .../AbstractUserRepositoryImpl.java | 14 + .../infra/repository/UserRepositoryImpl.java | 7 + .../delegate/UserRepositoryDelegate.java | 9 + .../repository/mongodb/UserMongoDelegate.java | 20 + .../mybatis/UserMybatisPlusDelegate.java | 26 + .../multi/service/MultiRepositoryService.java | 128 +++ .../src/main/resources/application.yml | 24 + .../src/main/resources/schema.sql | 9 + .../sample/multi/MultiRepositoryTest.java | 216 +++++ .../multi/config/MockMongoConfiguration.java | 558 ++++++++++++ .../sample/multi/config/MultiTestConfig.java | 11 + .../src/test/resources/application.yml | 24 + .../sample/repository/BeanDebugTest.java | 31 + .../annotations/RepositoryTypeAnnotation.java | 13 + .../AutoRepositoryConfiguration.java | 12 + .../infra/properties/InfraProperties.java | 20 + .../infra/repository/GenericTypeResolver.java | 40 +- .../MultiRepositoryBeanPostProcessor.java | 296 +++++++ .../repository/MultiRepositoryFacade.java | 130 +++ .../RepositoryBeanPostProcessor.java | 25 +- .../infra/repository/RepositoryFacade.java | 8 +- .../repository/RepositoryTypeContext.java | 38 + 37 files changed, 3447 insertions(+), 251 deletions(-) create mode 100644 .trae/documents/multi_repository_plan.md create mode 100644 .trae/documents/repository_creation_flow_verification_plan.md create mode 100644 .trae/documents/repository_type_inference_plan.md create mode 100644 structure-infra-sample/structure-infra-sample-multi/pom.xml create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/MultiSampleApplication.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/domain/entity/UserEntity.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/domain/repository/UserRepository.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/mapper/UserMapper.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/po/MongoUserPO.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/po/MybatisUserPO.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/AbstractUserRepositoryImpl.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/UserRepositoryImpl.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/delegate/UserRepositoryDelegate.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mongodb/UserMongoDelegate.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mybatis/UserMybatisPlusDelegate.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/service/MultiRepositoryService.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/resources/application.yml create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/main/resources/schema.sql create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/MultiRepositoryTest.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/config/MockMongoConfiguration.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/config/MultiTestConfig.java create mode 100644 structure-infra-sample/structure-infra-sample-multi/src/test/resources/application.yml create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/BeanDebugTest.java create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/annotations/RepositoryTypeAnnotation.java create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryTypeContext.java diff --git a/.trae/documents/multi_repository_plan.md b/.trae/documents/multi_repository_plan.md new file mode 100644 index 0000000..b3a74ba --- /dev/null +++ b/.trae/documents/multi_repository_plan.md @@ -0,0 +1,221 @@ +# 多类型仓库支持实现计划 + +## 一、需求分析 + +基于用户需求和现有代码库结构,需要实现以下功能: + +| 需求点 | 描述 | +|-------|------| +| 多仓库门面类 | 新建一个继承 `RepositoryFacade` 的门面类,支持多种仓储类型切换 | +| 仓库类型注解 | 新建注解用于标记仓库类型,业务侧可使用 | +| 自动装配机制 | 项目启动时将所有支持的仓库类型装配到多仓库门面中 | +| 混合实现支持 | 支持默认仓库和多仓库的混合使用 | + +## 二、架构设计 + +### 2.1 核心组件 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 业务层 (Service) │ +├─────────────────────────────────────────────────────────────────┤ +│ RepositoryFacade / CqrsRepositoryFacade / │ +│ MultiRepositoryFacade (多仓库门面) │ +│ ┌───────────────────────────────────────────────────────────┐ │ +│ │ @RepositoryType(MYBATIS_PLUS) → MybatisPlusDelegate │ │ +│ │ @RepositoryType(JPA) → JpaDelegate │ │ +│ │ @RepositoryType(MONGODB) → MongoDelegate │ │ +│ │ @RepositoryType(ELASTICSEARCH) → ElasticsearchDelegate │ │ +│ │ 默认路由 → defaultDelegate │ │ +│ └───────────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────────┤ +│ RepositoryTypeContext (线程上下文) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 类继承关系 + +``` +RepositoryFacade> + │ + ├── CqrsRepositoryFacade // CQRS 读写分离 + │ └── delegate: D (写代理) + │ └── readDelegate: RD (读代理) + │ + └── MultiRepositoryFacade // 多数据源切换 + └── delegates: Map + └── defaultDelegate: D +``` + +### 2.3 设计决策 + +| 决策点 | 方案 | 理由 | +|-------|------|------| +| 多仓库实现位置 | Facade 层(而非 Delegate 层) | 与 CQRS 设计保持一致,都是 Facade 的子类 | +| 路由机制 | ThreadLocal + 注解 | 支持编程式切换和声明式指定,灵活度高 | +| 注解作用域 | 类级别 + 方法级别 | 支持细粒度控制仓库类型 | +| 默认路由 | 配置指定默认类型 | 兼容现有单仓库模式 | +| 装配方式 | BeanPostProcessor | 复用现有装配机制,保持一致性 | + +## 三、实现步骤 + +### 3.1 新增文件 + +#### 3.1.1 `@RepositoryType` 注解 + +**文件路径**: `structure-infra-starter/src/main/java/cn/structure/infra/annotations/RepositoryType.java` + +**功能**: 标记仓库类型,可用于: +- Delegate 实现类:标记该 delegate 对应的仓库类型 +- Repository 接口:标记该仓库默认使用的仓库类型 +- Service 方法:临时切换仓库类型 + +```java +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepositoryType { + RepositoryTypeEnum value(); +} +``` + +#### 3.1.2 `RepositoryTypeEnum` 枚举 + +**文件路径**: `structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryTypeEnum.java` + +**功能**: 扩展现有 `RepositoryType`,用于路由匹配 + +#### 3.1.3 `MultiRepositoryFacade` 门面类 + +**文件路径**: `structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java` + +**核心功能**: +- 继承 `RepositoryFacade` +- 内部维护 `Map` +- 根据上下文或注解选择 delegate +- 重写所有 CRUD 方法的路由分发 + +#### 3.1.4 `RepositoryTypeContext` 上下文 + +**文件路径**: `structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryTypeContext.java` + +**功能**: +- ThreadLocal 存储当前仓库类型 +- 提供静态方法设置/获取/清除上下文 +- 支持 try-with-resources 模式 + +#### 3.1.5 `MultiRepositoryBeanPostProcessor` + +**文件路径**: `structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java` + +**功能**: +- 扫描所有 `RepositoryDelegate` 实现 +- 根据 `@RepositoryType` 注解分类 +- 注入到 `MultiRepositoryFacade` 中 + +### 3.2 修改文件 + +#### 3.2.1 `InfraProperties` + +**修改内容**: 添加多仓库相关配置 +```java +// 新增配置 +private boolean multiRepositoryEnabled = false; +private RepositoryTypeEnum defaultRepositoryType = RepositoryTypeEnum.AUTO; +``` + +#### 3.2.2 `RepositoryBeanPostProcessor` + +**修改内容**: 支持 `MultiRepositoryFacade` 的注入逻辑 + +#### 3.2.3 各 Delegate 实现类 + +**修改内容**: 添加 `@RepositoryType` 注解标记类型 +- `JpaRepositoryDelegate` → `@RepositoryType(JPA)` +- `MybatisPlusRepositoryDelegate` → `@RepositoryType(MYBATIS_PLUS)` +- `MongoRepositoryDelegate` → `@RepositoryType(MONGODB)` +- `ElasticsearchRepositoryDelegate` → `@RepositoryType(ELASTICSEARCH)` + +#### 3.2.4 删除 `MultiRepositoryDelegate` + +**删除文件**: `structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryDelegate.java` + +**原因**: 多仓库切换逻辑应在 Facade 层实现,而非 Delegate 层 + +## 四、配置示例 + +### 4.1 YAML 配置 + +```yaml +structure: + infra: + multi-repository-enabled: true + default-repository-type: MYBATIS_PLUS +``` + +### 4.2 业务侧使用 + +```java +// 方式1:使用 MultiRepositoryFacade +public interface UserRepository extends MultiRepositoryFacade { + UserEntity findByName(String name); +} + +// 方式2:编程式切换 +try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryTypeEnum.MONGODB)) { + userRepository.save(user); +} + +// 方式3:默认使用配置的仓库类型 +userRepository.save(user); +``` + +### 4.3 自定义方法实现 + +```java +public abstract class AbstractUserRepositoryImpl extends MultiRepositoryFacade implements UserRepository { + + @Override + public UserEntity findByName(String name) { + UserRepositoryDelegate currentDelegate = getCurrentDelegate(); + return currentDelegate.findByName(name); + } +} +``` + +## 五、风险与注意事项 + +| 风险点 | 应对措施 | +|-------|---------| +| 线程安全 | 使用 ThreadLocal,注意清理 | +| 性能开销 | 路由逻辑简单,可忽略 | +| 类型匹配 | 使用泛型类型检查确保安全性 | +| 事务管理 | 多仓库场景需注意事务一致性 | + +## 六、测试计划 + +| 测试场景 | 描述 | +|---------|------| +| 单仓库模式 | 验证默认仓库类型正常工作 | +| 多仓库模式 | 验证多类型仓库正确路由 | +| 编程式切换 | 验证 ThreadLocal 上下文切换 | +| 注解标记 | 验证类级别和方法级别注解生效 | +| 混合使用 | 验证默认和指定类型混合使用 | + +## 七、文件清单 + +| 操作 | 文件路径 | +|------|---------| +| 新增 | `structure-infra-starter/src/main/java/cn/structure/infra/annotations/RepositoryType.java` | +| 新增 | `structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryTypeEnum.java` | +| 新增 | `structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java` | +| 新增 | `structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryTypeContext.java` | +| 新增 | `structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java` | +| 删除 | `structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryDelegate.java` | +| 修改 | `structure-infra-starter/src/main/java/cn/structure/infra/properties/InfraProperties.java` | +| 修改 | `structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java` | +| 修改 | `structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java` | +| 修改 | `structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java` | +| 修改 | `structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java` | +| 修改 | `structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java` | +| 修改 | `structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/AbstractUserRepositoryImpl.java` | \ No newline at end of file diff --git a/.trae/documents/repository_creation_flow_verification_plan.md b/.trae/documents/repository_creation_flow_verification_plan.md new file mode 100644 index 0000000..1fb9c17 --- /dev/null +++ b/.trae/documents/repository_creation_flow_verification_plan.md @@ -0,0 +1,793 @@ +# 三种仓库场景创建流程验证计划 + +## 一、设计原则 + +### 核心原则 + +1. **普通单仓库场景:Spring 默认管理,不接管** + + * 完全由 Spring 容器负责依赖注入 + + * 支持构造函数注入、`@Autowired` 字段注入等原生方式 + + * `RepositoryBeanPostProcessor` 不处理普通 `RepositoryFacade` + +2. **CQRS 场景:RepositoryBeanPostProcessor 接管增强** + + * 有明确注解标记(`@WriteDelegate` / `@ReadDelegate`) + + * 接管但不破坏原生注入:base delegate 仍可通过构造函数注入 + + * BeanPostProcessor 负责注入 readDelegate(额外的依赖) + +3. **多仓库场景:MultiRepositoryBeanPostProcessor 接管增强** + + * 有明确类型标记(`@RepositoryTypeAnnotation` 或类型推断) + + * 接管但不破坏原生注入:default delegate 仍可通过构造函数注入 + + * BeanPostProcessor 负责注册多个 delegates 到 Map 中 + +### 判断标准:是否接管? + +| 场景 | 有注解标记? | 注入方式 | 谁负责 | +| ----- | ---------------------------------- | --------------------------------------- | ----------------------------------------- | +| 普通单仓库 | ❌ 无 | 构造函数 / @Autowired | Spring 原生 | +| CQRS | ✅ @WriteDelegate / @ReadDelegate | 构造函数(base) + BeanPostProcessor(read) | Spring + RepositoryBeanPostProcessor | +| 多仓库 | ✅ @RepositoryTypeAnnotation / 类型推断 | 构造函数(default) + BeanPostProcessor(注册多个) | Spring + MultiRepositoryBeanPostProcessor | + +*** + +## 二、现状分析 + +### 2.1 CQRS 场景 + +**基类层次:** + +``` +RepositoryFacade + ↑ +CqrsRepositoryFacade // 增加 readDelegate 字段 +``` + +**当前注入机制:** + +* 处理器:[RepositoryBeanPostProcessor](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java) + +* 时机:`postProcessAfterInitialization()` 中检测 `CqrsRepositoryFacade` 实例 + +* 方式:通过反射注入 `delegate`(写)和 `readDelegate`(读)字段 + +* 注解标记: + + * `@WriteDelegate`:标记写委托类 + + * `@ReadDelegate`:标记读委托类 + +* 匹配逻辑: + + * 写委托:优先 `@WriteDelegate` 标记 → 无 `@ReadDelegate` 标记 → 第一个匹配 + + * 读委托:优先 `@ReadDelegate` 标记 → 第一个匹配 + +**当前问题:** + +* delegate 完全通过反射注入,绕过了 Spring 原生注入 + +* 不支持构造函数注入方式 + +* 如果用户在子类中通过构造函数注入 delegate,会被 BeanPostProcessor 覆盖 + +**示例:** + +* Facade: [UserCqrsRepository](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repository/UserCqrsRepository.java) + + * `extends CqrsRepositoryFacade` + +* 写委托: [UserWriteDelegate](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/delegate/write/UserWriteDelegate.java) - `@WriteDelegate` + +* 读委托: [UserReadDelegate](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/delegate/read/UserReadDelegate.java) - `@ReadDelegate` + +*** + +### 2.2 多仓库场景 + +**基类层次:** + +``` +RepositoryFacade + ↑ +MultiRepositoryFacade // 维护 delegates Map,支持动态切换 +``` + +**当前注入机制:** + +* 处理器:[MultiRepositoryBeanPostProcessor](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java) + +* 时机: + + 1. `postProcessAfterInitialization()`:收集所有 `RepositoryDelegate`,注册到 `delegateRegistry` + 2. `afterSingletonsInstantiated()`:将注册的 delegate 注入到所有 `MultiRepositoryFacade` + +* 方式:通过 `facade.registerDelegate(type, delegate)` 注册到 delegates Map + +* 类型推断(RepositoryType): + + 1. `@RepositoryTypeAnnotation` 注解标记 + 2. 父类/接口上的 `@RepositoryTypeAnnotation` + 3. 类名匹配(JPA/MYBATIS/MONGO/ELASTICSEARCH) + 4. 父类名匹配(JpaRepositoryDelegate/MybatisPlusRepositoryDelegate 等) + +**当前问题:** + +* delegate 完全通过反射注入/注册,绕过了 Spring 原生注入 + +* 不支持构造函数注入方式 + +* defaultDelegate 的设置逻辑在 BeanPostProcessor 中硬编码 + +**示例:** + +* Facade: [AbstractUserRepositoryImpl](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/AbstractUserRepositoryImpl.java) + + * `extends MultiRepositoryFacade` + +* 委托实现: + + * [UserMybatisPlusDelegate](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mybatis/UserMybatisPlusDelegate.java) + + * [UserMongoDelegate](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mongodb/UserMongoDelegate.java) + +*** + +### 2.3 普通单仓库场景 + +**基类层次:** + +``` +RepositoryFacade // 单个 delegate 字段 +``` + +**当前注入机制:** + +* 处理器:[RepositoryBeanPostProcessor](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java) + +* 时机:`afterSingletonsInstantiated()` 中遍历所有 `RepositoryFacade` bean + +* 方式:通过反射注入 `delegate` 字段 + +* 匹配逻辑:按泛型类型 `D` 查找 bean,使用 `findWriteDelegate()` 逻辑 + +* **无专门注解标记 → 不应该接管!** + +**当前问题:** + +* 普通单仓库没有注解标记,但仍然被 `RepositoryBeanPostProcessor` 接管 + +* 绕过了 Spring 原生注入机制 + +* 用户无法使用构造函数注入等 Spring 标准方式 + +**示例:** + +* Facade: [UserJpaRepositoryImpl](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java) + + * `extends AbstractUserRepositoryImpl extends RepositoryFacade` + +* 委托: [UserJpaDelegate](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java) + +*** + +## 三、修复方案设计 + +### 3.1 总体架构 + +``` +RepositoryFacade (普通单仓库) + │ 完全由 Spring 管理:构造函数注入 / @Autowired + │ RepositoryBeanPostProcessor 不处理 + │ + ├─ CqrsRepositoryFacade (CQRS) + │ base delegate: Spring 构造函数注入 + │ readDelegate: RepositoryBeanPostProcessor 注入(增强) + │ + └─ MultiRepositoryFacade (多仓库) + default delegate: Spring 构造函数注入 + delegates Map: MultiRepositoryBeanPostProcessor 注册(增强) +``` + +### 3.2 RepositoryFacade 改造(基础层) + +**目标:** 支持 Spring 原生注入方式(构造函数注入优先) + +**修改内容:** + +```java +public class RepositoryFacade> implements ICrudRepository { + + protected final D delegate; // final,通过构造函数注入 + + protected Class entityClass; + + // 构造函数注入:支持 Spring 原生方式 + public RepositoryFacade(D delegate) { + this.delegate = delegate; + } + + // 保留无参构造函数,兼容 BeanPostProcessor 反射注入(过渡期) + protected RepositoryFacade() { + this.delegate = null; + } + + // ... 其他方法使用 this.delegate +} +``` + +**关键点:** + +1. `delegate` 字段改为 `final`,通过构造函数注入 +2. 提供带 `D delegate` 参数的构造函数 +3. 保留无参构造函数(protected),供 BeanPostProcessor 反射场景兼容 +4. 子类如果使用构造函数注入,Spring 自动按类型匹配 delegate + +### 3.3 CqrsRepositoryFacade 改造(CQRS 增强) + +**目标:** base delegate 走 Spring 注入,readDelegate 由 BeanPostProcessor 增强 + +**修改内容:** + +```java +public class CqrsRepositoryFacade, RD extends IQueryDelegate> + extends RepositoryFacade { + + protected RD readDelegate; // 非 final,由 BeanPostProcessor 注入 + + // 构造函数注入 base delegate + public CqrsRepositoryFacade(D delegate) { + super(delegate); + } + + // 保留无参构造函数(过渡期兼容) + protected CqrsRepositoryFacade() { + super(); + } + + // ... 读操作使用 readDelegate,失败回退 super.delegate +} +``` + +**RepositoryBeanPostProcessor 改造:** + +* 移除 `processRepositoryFacade()` 方法(普通单仓库不再处理) + +* 保留 `processCqrsFacade()` 方法,但只注入 `readDelegate` + +* `delegate` 由 Spring 构造函数注入,BeanPostProcessor 不覆盖 + +* `afterSingletonsInstantiated()` 中移除对普通 `RepositoryFacade` 的遍历 + +**注入流程:** + +1. Spring 实例化 CqrsRepositoryFacade 子类 → 构造函数注入 base delegate +2. `postProcessAfterInitialization()` → RepositoryBeanPostProcessor 注入 readDelegate +3. 结果:base delegate 由 Spring 管理,readDelegate 由 BeanPostProcessor 增强 + +### 3.4 MultiRepositoryFacade 改造(多仓库增强) + +**目标:** default delegate 走 Spring 注入(复用父类 delegate 字段),其他 delegates 由 BeanPostProcessor 注册 + +**修正要点:** + +1. **移除冗余的** **`defaultDelegate`** **字段** → 复用父类 `RepositoryFacade` 的 `delegate` 字段作为 default delegate +2. 构造函数注入 default delegate → 传给 `super(defaultDelegate)` +3. `getCurrentDelegate()` 回退时使用 `getDelegate()`(父类方法) +4. delegates Map 存储所有可用的 delegate,按类型索引 + +**修改内容:** + +```java +public class MultiRepositoryFacade> extends RepositoryFacade { + + // 存储所有可用的 delegate,按 RepositoryType 索引 + private final Map delegates = new HashMap<>(); + + // 默认使用的仓库类型(AUTO 表示自动选择) + @Getter + @Setter + private RepositoryType defaultType = RepositoryType.AUTO; + + // 构造函数注入 default delegate,传给父类 + // 父类的 delegate 字段即为 default delegate + public MultiRepositoryFacade(D defaultDelegate) { + super(defaultDelegate); + } + + // 保留无参构造函数(过渡期兼容,配合 BeanPostProcessor 反射注入) + protected MultiRepositoryFacade() { + super(); + } + + /** + * 获取当前使用的 delegate + * 优先级:上下文类型 > 默认类型 > 父类 delegate(default) > delegates Map 第一个 + */ + public D getCurrentDelegate() { + // 1. 优先使用线程上下文中指定的类型 + RepositoryType contextType = RepositoryTypeContext.get(); + if (contextType != null && delegates.containsKey(contextType)) { + log.debug("Using context repository type: {}", contextType); + return delegates.get(contextType); + } + + // 2. 其次使用配置的 defaultType + if (defaultType != RepositoryType.AUTO && delegates.containsKey(defaultType)) { + log.debug("Using default repository type: {}", defaultType); + return delegates.get(defaultType); + } + + // 3. 回退到父类的 delegate(构造函数注入的 default delegate) + if (getDelegate() != null) { + log.debug("Using default delegate from constructor injection"); + return getDelegate(); + } + + // 4. 最后尝试 delegates Map 中的第一个 + if (!delegates.isEmpty()) { + D firstDelegate = delegates.values().iterator().next(); + log.debug("Using first available delegate: {}", firstDelegate.getClass().getSimpleName()); + return firstDelegate; + } + + throw new IllegalStateException("No repository delegate available"); + } + + /** + * 注册指定类型的 delegate + * 由 MultiRepositoryBeanPostProcessor 调用 + */ + public void registerDelegate(RepositoryType type, D delegate) { + delegates.put(type, delegate); + log.info("Registered repository delegate: type={}, delegate={}", type, delegate.getClass().getSimpleName()); + } + + /** + * 检查是否有指定类型的 delegate + */ + public boolean hasDelegate(RepositoryType type) { + return delegates.containsKey(type); + } + + /** + * 获取所有已注册的类型 + */ + public Set getRegisteredTypes() { + return Collections.unmodifiableSet(delegates.keySet()); + } + + // ... CRUD 方法全部重写,使用 getCurrentDelegate() + // (当前代码已经重写了,保持不变) +} +``` + +**关键设计说明:** + +| 字段 | 来源 | 作用 | +| --------------- | ---------------------- | -------------------- | +| `delegate`(父类) | 构造函数注入(Spring) | default delegate,兜底用 | +| `delegates` Map | BeanPostProcessor 注册 | 所有可用的 delegate,按类型切换 | +| `defaultType` | 配置 / BeanPostProcessor | 默认使用的仓库类型 | + +**为什么不直接用 delegates Map 存 default?** + +* 构造函数注入的 delegate 类型未知(BeanPostProcessor 还没运行) + +* 父类 delegate 字段是 RepositoryFacade 的标准,保持一致性 + +* 双重保障:即使 delegates Map 为空,也有 default delegate 可用 + +**MultiRepositoryBeanPostProcessor 改造:** + +* 职责更清晰:只负责收集 delegate 并注册到 MultiRepositoryFacade + +* 不负责设置 defaultDelegate(default 由 Spring 构造函数注入,复用父类 delegate) + +* `injectDelegatesToMultiFacades()` 中移除 `facade.setDefaultDelegate()` 的调用 + +* 可选:如果 defaultType 配置了,且对应 delegate 存在,可以设置 defaultType + +**注入流程:** + +1. Spring 实例化 MultiRepositoryFacade 子类 → 构造函数注入 default delegate → 传给父类 +2. `postProcessAfterInitialization()` → 收集所有 RepositoryDelegate,注册到 delegateRegistry +3. `afterSingletonsInstantiated()` → 将所有 delegate 注册到 MultiRepositoryFacade 的 delegates Map +4. 结果:default delegate 由 Spring 管理(父类 delegate),其他 delegates 由 BeanPostProcessor 增强注册 + +### 3.5 RepositoryBeanPostProcessor 改造(职责收缩) + +**修改前:** + +* `postProcessAfterInitialization()`:处理 CqrsRepositoryFacade(注入 delegate + readDelegate) + +* `afterSingletonsInstantiated()`:处理所有 RepositoryFacade(注入 delegate) + +**修改后:** + +* `postProcessAfterInitialization()`:处理 CqrsRepositoryFacade(**只注入 readDelegate**) + +* `afterSingletonsInstantiated()`:**移除**,不再处理普通 RepositoryFacade + +* 普通 RepositoryFacade 完全由 Spring 管理 + +**代码变更示意:** + +```java +public class RepositoryBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { + + private DefaultListableBeanFactory beanFactory; + private InfraProperties infraProperties; + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (Boolean.TRUE.equals(infraProperties.getMultiRepositoryEnabled())) { + return bean; + } + // 只处理 CQRS 场景,且只注入 readDelegate + if (bean instanceof CqrsRepositoryFacade) { + processCqrsReadDelegate((CqrsRepositoryFacade) bean, beanName); + } + return bean; + } + + // 移除 afterSingletonsInstantiated() 方法 + // 移除 processRepositoryFacade() 方法 + + private void processCqrsReadDelegate(CqrsRepositoryFacade facade, String beanName) { + // 只注入 readDelegate,delegate 由 Spring 构造函数注入 + Type[] typeArgs = resolveTypeArguments(facade.getClass(), CqrsRepositoryFacade.class); + if (typeArgs == null || typeArgs.length < 4) { + log.warn("Cannot resolve generic types for CqrsRepositoryFacade: {}", beanName); + return; + } + + Class readDelegateType = getRawType(typeArgs[3]); + if (readDelegateType != null) { + Object readDelegate = findReadDelegate(readDelegateType); + if (readDelegate != null) { + setDelegate(facade, "readDelegate", readDelegate); + log.info("Injected read delegate [{}] to CqrsRepositoryFacade [{}]", + readDelegate.getClass().getSimpleName(), beanName); + } + } + } + + // ... 其他辅助方法保留 +} +``` + +### 3.6 子类实现方式(用户代码) + +**普通单仓库(Spring 原生方式):** + +```java +@Component("userRepository") +public class UserJpaRepositoryImpl extends AbstractUserRepositoryImpl { + + // 构造函数注入,Spring 自动按类型匹配 UserJpaDelegate + public UserJpaRepositoryImpl(UserJpaDelegate delegate) { + super(delegate); + } +} +``` + +**CQRS 场景:** + +```java +@Component("userCqrsRepository") +public class UserCqrsRepository extends CqrsRepositoryFacade + implements UserRepository { + + // 构造函数注入 base delegate(写代理) + public UserCqrsRepository(UserWriteDelegate writeDelegate) { + super(writeDelegate); + } + + // readDelegate 由 RepositoryBeanPostProcessor 自动注入 +} +``` + +**多仓库场景:** + +```java +@Component("userRepository") +public class UserRepositoryImpl extends MultiRepositoryFacade implements UserRepository{ + + // 构造函数注入 default delegate + public UserRepositoryImpl(UserMybatisPlusDelegate defaultDelegate) { + super(defaultDelegate); + } + + // 其他 delegate 由 MultiRepositoryBeanPostProcessor 自动注册到 delegates Map +} +``` + +*** + +## 四、验证步骤 + +### 步骤 1:运行现有测试,确认基准 + +**目标:** 确认当前测试状态,作为修改后的对比基准 + +**操作:** + +```bash +# 普通单仓库 - JPA +mvn test -pl structure-infra-sample/structure-infra-sample-jpa -Dtest=UserJpaRepositoryTest + +# 普通单仓库 - MyBatis +mvn test -pl structure-infra-sample/structure-infra-sample-mybatis -Dtest=UserRepositoryTest + +# CQRS +mvn test -pl structure-infra-sample/structure-infra-sample-cqrs -Dtest=UserCqrsRepositoryTest + +# 多仓库 +mvn test -pl structure-infra-sample/structure-infra-sample-multi -Dtest=MultiRepositoryTest +``` + +**记录:** 各测试的通过/失败状态 + +*** + +### 步骤 2:修改 RepositoryFacade 支持构造函数注入 + +**文件:** [RepositoryFacade.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java) + +**修改内容:** + +1. `delegate` 字段改为 `protected final D delegate` +2. 增加带 `D delegate` 参数的构造函数 +3. 保留无参构造函数(`protected`,过渡期兼容) + +**验证:** 编译通过 + +*** + +### 步骤 3:修改 CqrsRepositoryFacade + +**文件:** [CqrsRepositoryFacade.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java) + +**修改内容:** + +1. 增加带 `D delegate` 参数的构造函数,调用 `super(delegate)` +2. 保留无参构造函数(过渡期兼容) +3. `readDelegate` 字段保持非 final(由 BeanPostProcessor 注入) + +**验证:** 编译通过 + +*** + +### 步骤 4:修改 MultiRepositoryFacade + +**文件:** [MultiRepositoryFacade.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java) + +**修改内容:** + +1. **移除** **`defaultDelegate`** **字段**(冗余,复用父类 `delegate` 字段) +2. 增加带 `D defaultDelegate` 参数的构造函数,调用 `super(defaultDelegate)` +3. 保留无参构造函数(过渡期兼容) +4. `getCurrentDelegate()` 增加回退到 `getDelegate()`(父类)的逻辑 +5. 增加 `hasDelegate()` 和 `getRegisteredTypes()` 辅助方法 + +**验证:** 编译通过 + +*** + +### 步骤 5:修改 RepositoryBeanPostProcessor - 职责收缩 + +**文件:** [RepositoryBeanPostProcessor.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java) + +**修改内容:** + +1. 移除 `implements SmartInitializingSingleton` +2. 移除 `afterSingletonsInstantiated()` 方法 +3. 移除 `processRepositoryFacade()` 方法 +4. `processCqrsFacade()` 改名为 `processCqrsReadDelegate()`,只注入 readDelegate +5. 移除注入 `delegate` 字段的逻辑(由 Spring 构造函数负责) + +**验证:** 编译通过 + +*** + +### 步骤 6:修改 MultiRepositoryBeanPostProcessor - 简化 default 处理 + +**文件:** [MultiRepositoryBeanPostProcessor.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java) + +**修改内容:** + +1. `injectDelegatesToMultiFacades()` 中移除设置 defaultDelegate 的逻辑 +2. defaultDelegate 由 Spring 构造函数注入 + +**验证:** 编译通过 + +*** + +### 步骤 7:修改示例子类 - 构造函数注入 + +**修改各示例模块的 RepositoryImpl 类:** + +1. **JPA 单仓库:** [UserJpaRepositoryImpl.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java) + + * 增加带 delegate 参数的构造函数 + +2. **MyBatis 单仓库:** [UserRepositoryImpl.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/UserRepositoryImpl.java) + + * 增加带 delegate 参数的构造函数 + +3. **CQRS:** [UserCqrsRepository.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repository/UserCqrsRepository.java) + + * 增加带 writeDelegate 参数的构造函数 + +4. **多仓库:** [UserRepositoryImpl.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/UserRepositoryImpl.java) + + * 增加带 defaultDelegate 参数的构造函数 + +5. **抽象基类:** + + * [AbstractUserRepositoryImpl (core)](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java) + + * [AbstractUserRepositoryImpl (multi)](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/AbstractUserRepositoryImpl.java) + + * 增加带 delegate 参数的构造函数 + +**验证:** 编译通过 + +*** + +### 步骤 8:运行测试验证 + +**运行所有模块测试:** + +```bash +# 普通单仓库 - JPA +mvn test -pl structure-infra-sample/structure-infra-sample-jpa -Dtest=UserJpaRepositoryTest + +# 普通单仓库 - MyBatis +mvn test -pl structure-infra-sample/structure-infra-sample-mybatis -Dtest=UserRepositoryTest + +# CQRS +mvn test -pl structure-infra-sample/structure-infra-sample-cqrs -Dtest=UserCqrsRepositoryTest + +# 多仓库 +mvn test -pl structure-infra-sample/structure-infra-sample-multi -Dtest=MultiRepositoryTest +``` + +**预期结果:** + +* 所有测试通过 + +* 普通单仓库:通过 Spring 构造函数注入 delegate,RepositoryBeanPostProcessor 不干预 + +* CQRS:base delegate 构造函数注入,readDelegate 由 BeanPostProcessor 注入 + +* 多仓库:default delegate 构造函数注入,其他 delegates 由 BeanPostProcessor 注册 + +*** + +### 步骤 9:边界情况验证 + +**测试场景:** + +1. **普通单仓库 - 构造函数注入正常工作** + + * 验证:delegate 不为 null,操作正常 + + * 验证:RepositoryBeanPostProcessor 日志中没有注入普通 RepositoryFacade 的记录 + +2. **CQRS - base delegate 由 Spring 注入,readDelegate 由 BeanPostProcessor 注入** + + * 验证:delegate(写)不为 null + + * 验证:readDelegate(读)不为 null + + * 验证:读操作优先走 readDelegate + +3. **多仓库 - default delegate 由 Spring 注入,其他 delegate 由 BeanPostProcessor 注册** + + * 验证:getDelegate() 返回 default delegate + + * 验证:delegates Map 中有多个 delegate + + * 验证:切换 RepositoryType 后使用不同的 delegate + +4. **多仓库模式开关 - multiRepositoryEnabled=true** + + * 验证:RepositoryBeanPostProcessor 跳过 CQRS 处理 + + * 验证:MultiRepositoryBeanPostProcessor 正常工作 + +5. **无参构造函数兼容性(过渡期)** + + * 验证:如果子类不提供带参构造函数,仍然可以通过反射注入(兼容旧代码) + +*** + +## 五、文件清单 + +### 核心修改文件 + +| 文件 | 修改类型 | 修改说明 | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------- | +| [RepositoryFacade.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java) | 结构修改 | 增加带参构造函数,delegate 改 final | +| [CqrsRepositoryFacade.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java) | 结构修改 | 增加带参构造函数 | +| [MultiRepositoryFacade.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java) | 结构修改 | 移除冗余 defaultDelegate 字段,增加带参构造函数,优化 getCurrentDelegate() | +| [RepositoryBeanPostProcessor.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java) | 职责收缩 | 移除普通仓库处理,CQRS 只注入 readDelegate | +| [MultiRepositoryBeanPostProcessor.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java) | 简化 | 移除 defaultDelegate 设置逻辑 | + +### 示例代码修改文件 + +| 文件 | 修改类型 | 修改说明 | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | -------------------- | +| [AbstractUserRepositoryImpl (core)](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java) | 增加构造函数 | 带 delegate 参数 | +| [UserJpaRepositoryImpl.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java) | 增加构造函数 | 带 delegate 参数 | +| [UserRepositoryImpl (mybatis)](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/UserRepositoryImpl.java) | 增加构造函数 | 带 delegate 参数 | +| [UserCqrsRepository.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repository/UserCqrsRepository.java) | 增加构造函数 | 带 writeDelegate 参数 | +| [AbstractUserRepositoryImpl (multi)](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/AbstractUserRepositoryImpl.java) | 增加构造函数 | 带 delegate 参数 | +| [UserRepositoryImpl (multi)](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/UserRepositoryImpl.java) | 增加构造函数 | 带 defaultDelegate 参数 | + +*** + +## 六、风险与注意事项 + +### 6.1 潜在风险 + +1. **向后兼容性** + + * 风险:现有代码可能依赖无参构造函数 + 反射注入 + + * 应对:保留无参构造函数(protected),作为过渡期兼容方案 + + * 后续版本:逐步废弃无参构造函数方式 + +2. **泛型类型解析** + + * 风险:Spring 构造函数注入时,泛型类型 `D` 能否正确解析 + + * 应对:子类构造函数使用具体类型(如 `UserJpaDelegate`),Spring 能正确匹配 + + * 验证:通过测试确认 + +3. **多仓库场景下的 bean 冲突** + + * 风险:多仓库模式下,同一实体有多个 delegate bean,Spring 按类型注入会报错 + + * 应对:多仓库模式下,用户应使用 `@Qualifier` 或 `@Primary` 指定 default delegate + + * 或者:MultiRepositoryFacade 使用 `Map` 让 Spring 注入所有同类型 bean + +4. **MongoDB / Elasticsearch 等其他模块** + + * 风险:其他模块的 RepositoryImpl 也需要修改为构造函数注入 + + * 应对:检查所有 sample 模块,统一修改 + +### 6.2 过渡期策略 + +1. **双轨并行**:构造函数注入和反射注入都支持 +2. **优先构造函数**:如果子类提供了带参构造函数,Spring 自动使用 +3. **反射兜底**:如果 delegate 为 null(无参构造),由 BeanPostProcessor 反射注入(仅 CQRS 和多仓库) +4. **日志提示**:对使用反射注入的场景打 warn 日志,提醒用户迁移到构造函数注入 + +*** + +## 七、验证结论模板 + +验证完成后,应明确以下结论: + +1. ✅ / ❌ 普通单仓库场景:完全由 Spring 构造函数注入,RepositoryBeanPostProcessor 不接管 +2. ✅ / ❌ CQRS 场景:base delegate 由 Spring 注入,readDelegate 由 RepositoryBeanPostProcessor 增强注入 +3. ✅ / ❌ 多仓库场景:default delegate 由 Spring 注入,其他 delegates 由 MultiRepositoryBeanPostProcessor 注册 +4. ✅ / ❌ 三种场景边界清晰,互不干扰 +5. ✅ / ❌ 构造函数注入等原生注入方式正常工作 +6. 风险点:xxx +7. 建议:xxx + diff --git a/.trae/documents/repository_type_inference_plan.md b/.trae/documents/repository_type_inference_plan.md new file mode 100644 index 0000000..afe4dea --- /dev/null +++ b/.trae/documents/repository_type_inference_plan.md @@ -0,0 +1,182 @@ +# 仓库类型推断方案改进计划 + +## 一、当前实现分析 + +### 1.1 当前推断逻辑 + +在 [MultiRepositoryBeanPostProcessor.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java#L59-L77) 中的 `inferRepositoryType` 方法: + +```java +private RepositoryType inferRepositoryType(Class delegateClass) { + // 1. 优先检查 @RepositoryType 注解 + RepositoryType annotation = AnnotationUtils.findAnnotation(delegateClass, RepositoryType.class); + if (annotation != null) { + return annotation.value(); + } + + // 2. 通过类名推断 + String className = delegateClass.getSimpleName().toUpperCase(); + if (className.contains("JPA")) { + return RepositoryType.JPA; + } else if (className.contains("MYBATIS")) { + return RepositoryType.MYBATIS_PLUS; + } else if (className.contains("MONGO")) { + return RepositoryType.MONGODB; + } else if (className.contains("ELASTICSEARCH")) { + return RepositoryType.ELASTICSEARCH; + } + + return RepositoryType.AUTO; +} +``` + +### 1.2 当前推断策略的问题 + +| 问题点 | 描述 | 示例 | +|-------|------|------| +| 类名依赖 | 强依赖类名包含特定关键词 | `CustomUserDelegate` 无法识别 | +| 继承链未利用 | 未利用父类继承关系 | `UserJpaDelegate extends JpaRepositoryDelegate` 应识别为 JPA | +| 注解标记受限 | 在基础 delegate 类上标记注解会影响所有子类 | 无法为不同实体定制类型 | + +## 二、改进方案 + +### 2.1 新的推断策略(优先级从高到低) + +``` +1. 子类实现上的 @RepositoryType 注解 + ↓ +2. 父类链上的 @RepositoryType 注解(递归查找) + ↓ +3. 父类类型匹配(JpaRepositoryDelegate → JPA 等) + ↓ +4. 类名关键词推断(兜底策略) + ↓ +5. AUTO(无法识别) +``` + +### 2.2 各 delegate 的父类关系 + +| 基础 Delegate | 父类/接口 | 仓库类型 | +|--------------|----------|---------| +| JpaRepositoryDelegate | implements RepositoryDelegate | JPA | +| MybatisPlusRepositoryDelegate | implements RepositoryDelegate | MYBATIS_PLUS | +| MongoRepositoryDelegate | implements RepositoryDelegate | MONGODB | +| ElasticsearchRepositoryDelegate | implements RepositoryDelegate | ELASTICSEARCH | +| InMemoryRepositoryDelegate | implements RepositoryDelegate | AUTO | + +### 2.3 示例工程 delegate 继承关系 + +``` +UserJpaDelegate → JpaRepositoryDelegate → RepositoryDelegate +UserMybatisPlusDelegate → MybatisPlusRepositoryDelegate → RepositoryDelegate +UserMongoDelegate → MongoRepositoryDelegate → RepositoryDelegate +UserEsDelegate → ElasticsearchRepositoryDelegate → RepositoryDelegate +``` + +## 三、实现步骤 + +### 3.1 修改 `MultiRepositoryBeanPostProcessor` + +**文件**: `structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java` + +**修改内容**: +1. 增强 `inferRepositoryType` 方法,支持: + - 递归查找父类链上的 `@RepositoryType` 注解 + - 通过父类类型匹配仓库类型 +2. 添加 `isJpaDelegate`, `isMybatisPlusDelegate`, `isMongoDelegate`, `isElasticsearchDelegate` 辅助方法 + +### 3.2 在基础 Delegate 类上添加 `@RepositoryType` 注解 + +**注意**:由于各 starter 模块已依赖 `structure-infra-starter`,可以直接使用注解 + +| 文件 | 注解 | +|------|------| +| JpaRepositoryDelegate.java | `@RepositoryType(RepositoryType.JPA)` | +| MybatisPlusRepositoryDelegate.java | `@RepositoryType(RepositoryType.MYBATIS_PLUS)` | +| MongoRepositoryDelegate.java | `@RepositoryType(RepositoryType.MONGODB)` | +| ElasticsearchRepositoryDelegate.java | `@RepositoryType(RepositoryType.ELASTICSEARCH)` | + +### 3.3 在示例工程的子类上标记注解(可选) + +用户可以在自定义 delegate 子类上标记注解,覆盖父类的默认类型: + +```java +// 默认继承父类的 JPA 类型 +@Component +public class UserJpaDelegate extends JpaRepositoryDelegate + implements UserRepositoryDelegate { ... } + +// 显式标记类型(可选,增强可读性) +@Component +@RepositoryType(RepositoryType.JPA) +public class UserJpaDelegate extends JpaRepositoryDelegate + implements UserRepositoryDelegate { ... } +``` + +### 3.4 创建多仓库示例工程 + +**新增模块**: `structure-infra-sample-multi` + +**功能**: +- 同时引入 MyBatis Plus 和 MongoDB 依赖 +- 配置多仓库支持 +- 演示如何通过上下文切换仓库类型 + +## 四、文件清单 + +### 4.1 修改文件 + +| 文件 | 修改内容 | +|------|---------| +| `MultiRepositoryBeanPostProcessor.java` | 增强 `inferRepositoryType` 方法 | +| `JpaRepositoryDelegate.java` | 添加 `@RepositoryType(JPA)` 注解 | +| `MybatisPlusRepositoryDelegate.java` | 添加 `@RepositoryType(MYBATIS_PLUS)` 注解 | +| `MongoRepositoryDelegate.java` | 添加 `@RepositoryType(MONGODB)` 注解 | +| `ElasticsearchRepositoryDelegate.java` | 添加 `@RepositoryType(ELASTICSEARCH)` 注解 | + +### 4.2 新增文件(示例工程) + +| 文件 | 描述 | +|------|------| +| `structure-infra-sample-multi/pom.xml` | 多仓库示例工程依赖 | +| `MultiSampleApplication.java` | 启动类 | +| `application.yml` | 配置文件(启用多仓库) | +| `MultiRepositoryTest.java` | 测试用例 | + +## 五、配置示例 + +### 5.1 YAML 配置 + +```yaml +structure: + infra: + multi-repository-enabled: true + default-repository-type: MYBATIS_PLUS +``` + +### 5.2 业务侧使用 + +```java +// 方式1:通过上下文切换(编程式) +try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MONGODB)) { + userRepository.save(user); +} + +// 方式2:在自定义 delegate 上标记(声明式) +@Component +@RepositoryType(RepositoryType.ELASTICSEARCH) +public class SearchUserDelegate extends ElasticsearchRepositoryDelegate + implements UserRepositoryDelegate { ... } + +// 方式3:默认使用配置的仓库类型 +userRepository.save(user); +``` + +## 六、风险与注意事项 + +| 风险点 | 应对措施 | +|-------|---------| +| 注解冲突 | 子类注解优先级高于父类 | +| 类型匹配失败 | 提供类名推断作为兜底 | +| 循环依赖 | 注解定义在 starter 模块,各模块已依赖 starter | +| 性能开销 | 缓存推断结果,避免重复计算 | \ No newline at end of file diff --git a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java index 53d7d7a..a547a3c 100644 --- a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java +++ b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java @@ -57,69 +57,119 @@ public class ElasticsearchRepositoryDelegate implements RepositoryDele @Autowired protected ElasticsearchOperations elasticsearchOperations; - protected Class entityClass; - protected Class

poClass; - protected Class idClass; - protected String idFieldName = "id"; + private volatile Class entityClass; + private volatile Class

poClass; + private volatile Class idClass; + private volatile String idFieldName; public ElasticsearchRepositoryDelegate() { - resolveGenericTypes(); - resolveIdFieldName(); - log.info("ElasticsearchRepositoryDelegate initialized: entity={}, po={}, id={}, idField={}", - entityClass != null ? entityClass.getSimpleName() : "null", - poClass != null ? poClass.getSimpleName() : "null", - idClass != null ? idClass.getSimpleName() : "null", - idFieldName); - } - - @SuppressWarnings("unchecked") - protected void resolveGenericTypes() { - this.entityClass = (Class) GenericTypeResolver.resolveEntityClass(getClass()); - this.poClass = (Class

) GenericTypeResolver.resolvePoClass(getClass()); - this.idClass = (Class) GenericTypeResolver.resolveIdClass(getClass()); - } - - protected void resolveIdFieldName() { - if (poClass != null) { - Field idField = findFieldWithAnnotation(poClass, Id.class); - if (idField != null) { - this.idFieldName = idField.getName(); - } - } - } - - private Field findFieldWithAnnotation(Class clazz, Class annotationClass) { - for (Field field : clazz.getDeclaredFields()) { - if (field.isAnnotationPresent((Class) annotationClass)) { - return field; - } - } - if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) { - return findFieldWithAnnotation(clazz.getSuperclass(), annotationClass); - } - return null; } @Override public Class getEntityClass() { + if (entityClass == null) { + synchronized (this) { + if (entityClass == null) { + entityClass = resolveEntityClass(); + log.debug("Resolved entityClass: {}", entityClass != null ? entityClass.getSimpleName() : "null"); + } + } + } return entityClass; } @Override public Class getPoClass() { + if (poClass == null) { + synchronized (this) { + if (poClass == null) { + poClass = resolvePoClass(); + log.debug("Resolved poClass: {}", poClass != null ? poClass.getSimpleName() : "null"); + } + } + } return poClass; } @Override public Class getIdClass() { + if (idClass == null) { + synchronized (this) { + if (idClass == null) { + idClass = resolveIdClass(); + log.debug("Resolved idClass: {}", idClass != null ? idClass.getSimpleName() : "null"); + } + } + } return idClass; } @Override public String getIdFieldName() { + if (idFieldName == null) { + synchronized (this) { + if (idFieldName == null) { + idFieldName = resolveIdFieldName(); + log.debug("Resolved idFieldName: {}", idFieldName); + } + } + } return idFieldName; } + @SuppressWarnings("unchecked") + private Class resolveEntityClass() { + try { + return (Class) GenericTypeResolver.resolveEntityClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve entityClass: {}", e.getMessage()); + return null; + } + } + + @SuppressWarnings("unchecked") + private Class

resolvePoClass() { + try { + return (Class

) GenericTypeResolver.resolvePoClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve poClass: {}", e.getMessage()); + return null; + } + } + + @SuppressWarnings("unchecked") + private Class resolveIdClass() { + try { + return (Class) GenericTypeResolver.resolveIdClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve idClass: {}", e.getMessage()); + return null; + } + } + + private String resolveIdFieldName() { + Class poType = getPoClass(); + if (poType != null) { + Field idField = findFieldWithAnnotation(poType, Id.class); + if (idField != null) { + return idField.getName(); + } + } + return "id"; + } + + private Field findFieldWithAnnotation(Class clazz, Class annotationClass) { + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent((Class) annotationClass)) { + return field; + } + } + if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) { + return findFieldWithAnnotation(clazz.getSuperclass(), annotationClass); + } + return null; + } + @Override public E save(E entity) { if (entity == null) { @@ -134,7 +184,7 @@ public E save(E entity) { @Override public void removeById(ID id) { if (id != null) { - elasticsearchOperations.delete(String.valueOf(id), poClass); + elasticsearchOperations.delete(String.valueOf(id), getPoClass()); log.debug("Removed entity: id={}", id); } } @@ -144,7 +194,7 @@ public E findById(ID id) { if (id == null) { return null; } - P po = elasticsearchOperations.get(String.valueOf(id), poClass); + P po = elasticsearchOperations.get(String.valueOf(id), getPoClass()); log.debug("Find by id: id={}, found={}", id, po != null); return toEntity(po); } @@ -166,7 +216,7 @@ public E queryOne(E condition) { } P poCondition = toPo(condition); Query query = buildQuery(poCondition); - SearchHits

searchHits = elasticsearchOperations.search(query, poClass); + SearchHits

searchHits = elasticsearchOperations.search(query, getPoClass()); P po = searchHits.hasSearchHits() ? searchHits.getSearchHit(0).getContent() : null; return toEntity(po); } @@ -180,14 +230,14 @@ public Optional queryOneOptional(E condition) { public List queryList(E condition) { if (condition == null) { Query query = new CriteriaQuery(Criteria.where("*").exists()); - SearchHits

searchHits = elasticsearchOperations.search(query, poClass); + SearchHits

searchHits = elasticsearchOperations.search(query, getPoClass()); return searchHits.getSearchHits().stream() .map(hit -> toEntity(hit.getContent())) .collect(Collectors.toList()); } P poCondition = toPo(condition); Query query = buildQuery(poCondition); - SearchHits

searchHits = elasticsearchOperations.search(query, poClass); + SearchHits

searchHits = elasticsearchOperations.search(query, getPoClass()); return searchHits.getSearchHits().stream() .map(hit -> toEntity(hit.getContent())) .collect(Collectors.toList()); @@ -202,7 +252,7 @@ public ResPage queryPage(ReqPage reqPage) { PageRequest pageRequest = PageRequest.of(pageNum, pageSize, Sort.unsorted()); query.setPageable(pageRequest); - SearchHits

searchHits = elasticsearchOperations.search(query, poClass); + SearchHits

searchHits = elasticsearchOperations.search(query, getPoClass()); ResPage resPage = new ResPage<>(); resPage.setCurrent((long) (pageNum + 1)); @@ -259,7 +309,7 @@ public List saveBatch(List entities) { @Override public void removeBatchByIds(List ids) { if (ids != null && !ids.isEmpty()) { - ids.forEach(id -> elasticsearchOperations.delete(String.valueOf(id), poClass)); + ids.forEach(id -> elasticsearchOperations.delete(String.valueOf(id), getPoClass())); } } @@ -278,11 +328,11 @@ public List listByIds(List ids) { public long count(E condition) { if (condition == null) { Query query = new CriteriaQuery(Criteria.where("*").exists()); - return elasticsearchOperations.count(query, poClass); + return elasticsearchOperations.count(query, getPoClass()); } P poCondition = toPo(condition); Query query = buildQuery(poCondition); - return elasticsearchOperations.count(query, poClass); + return elasticsearchOperations.count(query, getPoClass()); } @Override diff --git a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java index 168399b..c3497dd 100644 --- a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java +++ b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java @@ -55,69 +55,119 @@ public class JpaRepositoryDelegate implements RepositoryDelegate entityClass; - protected Class

poClass; - protected Class idClass; - protected String idFieldName = "id"; + private volatile Class entityClass; + private volatile Class

poClass; + private volatile Class idClass; + private volatile String idFieldName; public JpaRepositoryDelegate() { - resolveGenericTypes(); - resolveIdFieldName(); - log.info("JpaRepositoryDelegate initialized: entity={}, po={}, id={}, idField={}", - entityClass != null ? entityClass.getSimpleName() : "null", - poClass != null ? poClass.getSimpleName() : "null", - idClass != null ? idClass.getSimpleName() : "null", - idFieldName); - } - - @SuppressWarnings("unchecked") - protected void resolveGenericTypes() { - this.entityClass = (Class) GenericTypeResolver.resolveEntityClass(getClass()); - this.poClass = (Class

) GenericTypeResolver.resolvePoClass(getClass()); - this.idClass = (Class) GenericTypeResolver.resolveIdClass(getClass()); - } - - protected void resolveIdFieldName() { - if (poClass != null) { - Field idField = findFieldWithAnnotation(poClass, Id.class); - if (idField != null) { - this.idFieldName = idField.getName(); - } - } - } - - private Field findFieldWithAnnotation(Class clazz, Class annotationClass) { - for (Field field : clazz.getDeclaredFields()) { - if (field.isAnnotationPresent((Class) annotationClass)) { - return field; - } - } - if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) { - return findFieldWithAnnotation(clazz.getSuperclass(), annotationClass); - } - return null; } @Override public Class getEntityClass() { + if (entityClass == null) { + synchronized (this) { + if (entityClass == null) { + entityClass = resolveEntityClass(); + log.debug("Resolved entityClass: {}", entityClass != null ? entityClass.getSimpleName() : "null"); + } + } + } return entityClass; } @Override public Class getPoClass() { + if (poClass == null) { + synchronized (this) { + if (poClass == null) { + poClass = resolvePoClass(); + log.debug("Resolved poClass: {}", poClass != null ? poClass.getSimpleName() : "null"); + } + } + } return poClass; } @Override public Class getIdClass() { + if (idClass == null) { + synchronized (this) { + if (idClass == null) { + idClass = resolveIdClass(); + log.debug("Resolved idClass: {}", idClass != null ? idClass.getSimpleName() : "null"); + } + } + } return idClass; } @Override public String getIdFieldName() { + if (idFieldName == null) { + synchronized (this) { + if (idFieldName == null) { + idFieldName = resolveIdFieldName(); + log.debug("Resolved idFieldName: {}", idFieldName); + } + } + } return idFieldName; } + @SuppressWarnings("unchecked") + private Class resolveEntityClass() { + try { + return (Class) GenericTypeResolver.resolveEntityClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve entityClass: {}", e.getMessage()); + return null; + } + } + + @SuppressWarnings("unchecked") + private Class

resolvePoClass() { + try { + return (Class

) GenericTypeResolver.resolvePoClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve poClass: {}", e.getMessage()); + return null; + } + } + + @SuppressWarnings("unchecked") + private Class resolveIdClass() { + try { + return (Class) GenericTypeResolver.resolveIdClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve idClass: {}", e.getMessage()); + return null; + } + } + + private String resolveIdFieldName() { + Class poType = getPoClass(); + if (poType != null) { + Field idField = findFieldWithAnnotation(poType, Id.class); + if (idField != null) { + return idField.getName(); + } + } + return "id"; + } + + private Field findFieldWithAnnotation(Class clazz, Class annotationClass) { + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent((Class) annotationClass)) { + return field; + } + } + if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) { + return findFieldWithAnnotation(clazz.getSuperclass(), annotationClass); + } + return null; + } + @Override public E save(E entity) { if (entity == null || entityManager == null || poClass == null) { diff --git a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java index a447592..5e2d93d 100644 --- a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java +++ b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java @@ -12,6 +12,7 @@ import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; import jakarta.persistence.Id; import java.lang.reflect.Field; @@ -20,102 +21,130 @@ import java.util.Optional; import java.util.stream.Collectors; -/** - * 基于 MongoDB 的 RepositoryDelegate 适配实现 - *

- * 该类是 RepositoryDelegate SPI 在 MongoDB 存储类型下的标准实现,委托 - * {@link MongoTemplate} 完成文档的 CRUD 操作。它在仓储框架中扮演"具体存储适配层"的角色: - *

    - *
  • 上层由 {@code RepositoryFacade} 统一暴露给业务方,本类不直接面向业务
  • - *
  • 当用户未提供自定义 Delegate 时,由 {@link MongoDelegateFactory} 自动创建本类实例
  • - *
  • 当用户提供自定义 Delegate 子类时,由 {@link MongoDelegateBeanPostProcessor} - * 在 Bean 初始化后自动注入 MongoTemplate 与实体类型
  • - *
- *

- * 实现说明: - *

    - *
  • 查询条件通过反射读取实体非空字段,组装为 {@link Criteria}(等值匹配)并拼装到 {@link Query}
  • - *
  • ID 字段名通过 PO 类的 {@link Id} 注解自动识别,默认为 "id"
  • - *
  • save 委托给 {@link MongoTemplate#save(Object)},自动判断新增或更新(依据 _id 是否存在)
  • - *
  • 分页使用 {@link PageRequest} + count,由 MongoTemplate 生成原生分页查询
  • - *
  • Entity ↔ PO 转换在此层完成,Facade 层只操作领域实体
  • - *
- * - * @param 领域实体类型 - * @param

持久化对象类型(MongoDB Document) - * @param 主键类型 - * @author chuck - * @version 1.0.3 - * @since 2026/6/28 - */ @Slf4j public class MongoRepositoryDelegate implements RepositoryDelegate { @Autowired protected MongoTemplate mongoTemplate; - protected Class entityClass; - protected Class

poClass; - protected Class idClass; - protected String idFieldName = "id"; - public MongoRepositoryDelegate() { - resolveGenericTypes(); - resolveIdFieldName(); - log.info("MongoRepositoryDelegate initialized: entity={}, po={}, id={}, idField={}", - entityClass != null ? entityClass.getSimpleName() : "null", - poClass != null ? poClass.getSimpleName() : "null", - idClass != null ? idClass.getSimpleName() : "null", - idFieldName); - } + private volatile Class entityClass; + private volatile Class

poClass; + private volatile Class idClass; + private volatile String idFieldName; - @SuppressWarnings("unchecked") - protected void resolveGenericTypes() { - this.entityClass = (Class) GenericTypeResolver.resolveEntityClass(getClass()); - this.poClass = (Class

) GenericTypeResolver.resolvePoClass(getClass()); - this.idClass = (Class) GenericTypeResolver.resolveIdClass(getClass()); - } - - protected void resolveIdFieldName() { - if (poClass != null) { - Field idField = findFieldWithAnnotation(poClass, Id.class); - if (idField != null) { - this.idFieldName = idField.getName(); - } - } - } - - private Field findFieldWithAnnotation(Class clazz, Class annotationClass) { - for (Field field : clazz.getDeclaredFields()) { - if (field.isAnnotationPresent((Class) annotationClass)) { - return field; - } - } - if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) { - return findFieldWithAnnotation(clazz.getSuperclass(), annotationClass); - } - return null; + public MongoRepositoryDelegate() { } @Override public Class getEntityClass() { + if (entityClass == null) { + synchronized (this) { + if (entityClass == null) { + entityClass = resolveEntityClass(); + log.debug("Resolved entityClass: {}", entityClass != null ? entityClass.getSimpleName() : "null"); + } + } + } return entityClass; } @Override public Class getPoClass() { + if (poClass == null) { + synchronized (this) { + if (poClass == null) { + poClass = resolvePoClass(); + log.debug("Resolved poClass: {}", poClass != null ? poClass.getSimpleName() : "null"); + } + } + } return poClass; } + @SuppressWarnings("unchecked") + private Class

getPoClassInternal() { + return (Class

) getPoClass(); + } + @Override public Class getIdClass() { + if (idClass == null) { + synchronized (this) { + if (idClass == null) { + idClass = resolveIdClass(); + log.debug("Resolved idClass: {}", idClass != null ? idClass.getSimpleName() : "null"); + } + } + } return idClass; } @Override public String getIdFieldName() { + if (idFieldName == null) { + synchronized (this) { + if (idFieldName == null) { + idFieldName = resolveIdFieldName(); + log.debug("Resolved idFieldName: {}", idFieldName); + } + } + } return idFieldName; } + @SuppressWarnings("unchecked") + private Class resolveEntityClass() { + try { + return (Class) GenericTypeResolver.resolveEntityClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve entityClass: {}", e.getMessage()); + return null; + } + } + + @SuppressWarnings("unchecked") + private Class

resolvePoClass() { + try { + return (Class

) GenericTypeResolver.resolvePoClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve poClass: {}", e.getMessage()); + return null; + } + } + + @SuppressWarnings("unchecked") + private Class resolveIdClass() { + try { + return (Class) GenericTypeResolver.resolveIdClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve idClass: {}", e.getMessage()); + return null; + } + } + + private String resolveIdFieldName() { + Class poType = getPoClass(); + if (poType != null) { + Field idField = findFieldWithAnnotation(poType, Id.class); + if (idField != null) { + return idField.getName(); + } + } + return "id"; + } + + private Field findFieldWithAnnotation(Class clazz, Class annotationClass) { + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent((Class) annotationClass)) { + return field; + } + } + if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) { + return findFieldWithAnnotation(clazz.getSuperclass(), annotationClass); + } + return null; + } + @Override public E save(E entity) { if (entity == null) { @@ -129,20 +158,20 @@ public E save(E entity) { @Override public void removeById(ID id) { - if (id != null && poClass != null) { - Query query = new Query(Criteria.where(idFieldName).is(id)); - mongoTemplate.remove(query, poClass); + if (id != null) { + Query query = new Query(Criteria.where(getIdFieldName()).is(id)); + mongoTemplate.remove(query, getPoClassInternal()); log.debug("Removed entity: id={}", id); } } @Override public E findById(ID id) { - if (id == null || poClass == null) { + if (id == null) { return null; } - Query query = new Query(Criteria.where(idFieldName).is(id)); - P po = mongoTemplate.findOne(query, poClass); + Query query = new Query(Criteria.where(getIdFieldName()).is(id)); + P po = mongoTemplate.findOne(query, getPoClassInternal()); log.debug("Find by id: id={}, found={}", id, po != null); return toEntity(po); } @@ -162,8 +191,9 @@ public E queryOne(E condition) { if (condition == null) { return null; } - Query query = buildQuery(condition); - P po = mongoTemplate.findOne(query, poClass); + P poCondition = toPo(condition); + Query query = buildQuery(poCondition); + P po = mongoTemplate.findOne(query, getPoClassInternal()); return toEntity(po); } @@ -175,14 +205,13 @@ public Optional queryOneOptional(E condition) { @Override public List queryList(E condition) { if (condition == null) { - return mongoTemplate.findAll(poClass).stream() - .map(this::toEntity) - .collect(Collectors.toList()); + List

poList = mongoTemplate.findAll(getPoClassInternal()); + return toEntityList(poList); } - Query query = buildQuery(condition); - return mongoTemplate.find(query, poClass).stream() - .map(this::toEntity) - .collect(Collectors.toList()); + P poCondition = toPo(condition); + Query query = buildQuery(poCondition); + List

poList = mongoTemplate.find(query, getPoClassInternal()); + return toEntityList(poList); } @Override @@ -191,26 +220,31 @@ public ResPage queryPage(ReqPage reqPage) { int pageSize = reqPage.getSize() != null ? reqPage.getSize() : 10; Query query = new Query(); - long total = mongoTemplate.count(query, poClass); + long total = mongoTemplate.count(query, getPoClassInternal()); - Query pageQuery = query.with(PageRequest.of(pageNum, pageSize, Sort.unsorted())); - List records = mongoTemplate.find(pageQuery, poClass).stream() - .map(this::toEntity) - .collect(Collectors.toList()); + List pageContent; + if (total > 0) { + query.with(PageRequest.of(pageNum, pageSize, Sort.unsorted())); + pageContent = mongoTemplate.find(query, getPoClassInternal()).stream() + .map(this::toEntity) + .collect(Collectors.toList()); + } else { + pageContent = List.of(); + } ResPage resPage = new ResPage<>(); resPage.setCurrent((long) (pageNum + 1)); resPage.setPages(total > 0 ? (total + pageSize - 1) / pageSize : 0); resPage.setSize((long) pageSize); resPage.setTotal(total); - resPage.setRecords(records); + resPage.setRecords(pageContent); log.debug("Query page: page={}, size={}, total={}, records={}", - pageNum + 1, pageSize, total, records.size()); + pageNum + 1, pageSize, total, pageContent.size()); return resPage; } - private Query buildQuery(E condition) { + private Query buildQuery(P condition) { Query query = new Query(); try { Field[] fields = getAllFields(condition.getClass()); @@ -241,18 +275,18 @@ public List saveBatch(List entities) { if (entities == null || entities.isEmpty()) { return List.of(); } - return entities.stream() + List

poList = entities.stream() .map(this::toPo) - .map(mongoTemplate::save) - .map(this::toEntity) .collect(Collectors.toList()); + mongoTemplate.insert(poList, getPoClassInternal()); + return toEntityList(poList); } @Override public void removeBatchByIds(List ids) { if (ids != null && !ids.isEmpty()) { - Query query = new Query(Criteria.where(idFieldName).in(ids)); - mongoTemplate.remove(query, poClass); + Query query = new Query(Criteria.where(getIdFieldName()).in(ids)); + mongoTemplate.remove(query, getPoClassInternal()); } } @@ -261,19 +295,19 @@ public List listByIds(List ids) { if (ids == null || ids.isEmpty()) { return List.of(); } - Query query = new Query(Criteria.where(idFieldName).in(ids)); - return mongoTemplate.find(query, poClass).stream() - .map(this::toEntity) - .collect(Collectors.toList()); + Query query = new Query(Criteria.where(getIdFieldName()).in(ids)); + List

poList = mongoTemplate.find(query, getPoClassInternal()); + return toEntityList(poList); } @Override public long count(E condition) { if (condition == null) { - return mongoTemplate.count(new Query(), poClass); + return mongoTemplate.count(new Query(), getPoClassInternal()); } - Query query = buildQuery(condition); - return mongoTemplate.count(query, poClass); + P poCondition = toPo(condition); + Query query = buildQuery(poCondition); + return mongoTemplate.count(query, getPoClassInternal()); } @Override @@ -285,11 +319,8 @@ protected E toEntity(P po) { if (po == null) { return null; } - if (entityClass == null) { - return (E) po; - } try { - E entity = entityClass.getDeclaredConstructor().newInstance(); + E entity = getEntityClass().getDeclaredConstructor().newInstance(); BeanUtils.copyProperties(po, entity); return entity; } catch (Exception e) { @@ -297,15 +328,22 @@ protected E toEntity(P po) { } } + protected List toEntityList(List

poList) { + if (poList == null || poList.isEmpty()) { + return List.of(); + } + return poList.stream() + .map(this::toEntity) + .collect(Collectors.toList()); + } + + @SuppressWarnings("unchecked") protected P toPo(E entity) { if (entity == null) { return null; } - if (poClass == null) { - return (P) entity; - } try { - P po = poClass.getDeclaredConstructor().newInstance(); + P po = ((Class

) getPoClass()).getDeclaredConstructor().newInstance(); BeanUtils.copyProperties(entity, po); return po; } catch (Exception e) { diff --git a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java index fa79939..eca5d6d 100644 --- a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java +++ b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java @@ -10,9 +10,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.annotation.Autowired; import jakarta.persistence.Id; +import org.springframework.beans.factory.annotation.Autowired; + import java.io.Serializable; import java.lang.reflect.Field; import java.util.Arrays; @@ -53,71 +54,121 @@ @Slf4j public class MybatisPlusRepositoryDelegate implements RepositoryDelegate { - @Autowired + @Autowired(required = false) protected BaseMapper

baseMapper; - protected Class entityClass; - protected Class

poClass; - protected Class idClass; - protected String idFieldName = "id"; + private volatile Class entityClass; + private volatile Class

poClass; + private volatile Class idClass; + private volatile String idFieldName; public MybatisPlusRepositoryDelegate() { - resolveGenericTypes(); - resolveIdFieldName(); - log.info("MybatisPlusRepositoryDelegate initialized: entity={}, po={}, id={}, idField={}", - entityClass != null ? entityClass.getSimpleName() : "null", - poClass != null ? poClass.getSimpleName() : "null", - idClass != null ? idClass.getSimpleName() : "null", - idFieldName); - } - - @SuppressWarnings("unchecked") - protected void resolveGenericTypes() { - this.entityClass = (Class) GenericTypeResolver.resolveEntityClass(getClass()); - this.poClass = (Class

) GenericTypeResolver.resolvePoClass(getClass()); - this.idClass = (Class) GenericTypeResolver.resolveIdClass(getClass()); - } - - protected void resolveIdFieldName() { - if (poClass != null) { - Field idField = findFieldWithAnnotation(poClass, Id.class); - if (idField != null) { - this.idFieldName = idField.getName(); - } - } - } - - private Field findFieldWithAnnotation(Class clazz, Class annotationClass) { - for (Field field : clazz.getDeclaredFields()) { - if (field.isAnnotationPresent((Class) annotationClass)) { - return field; - } - } - if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) { - return findFieldWithAnnotation(clazz.getSuperclass(), annotationClass); - } - return null; } @Override public Class getEntityClass() { + if (entityClass == null) { + synchronized (this) { + if (entityClass == null) { + entityClass = resolveEntityClass(); + log.debug("Resolved entityClass: {}", entityClass != null ? entityClass.getSimpleName() : "null"); + } + } + } return entityClass; } @Override public Class getPoClass() { + if (poClass == null) { + synchronized (this) { + if (poClass == null) { + poClass = resolvePoClass(); + log.debug("Resolved poClass: {}", poClass != null ? poClass.getSimpleName() : "null"); + } + } + } return poClass; } @Override public Class getIdClass() { + if (idClass == null) { + synchronized (this) { + if (idClass == null) { + idClass = resolveIdClass(); + log.debug("Resolved idClass: {}", idClass != null ? idClass.getSimpleName() : "null"); + } + } + } return idClass; } @Override public String getIdFieldName() { + if (idFieldName == null) { + synchronized (this) { + if (idFieldName == null) { + idFieldName = resolveIdFieldName(); + log.debug("Resolved idFieldName: {}", idFieldName); + } + } + } return idFieldName; } + @SuppressWarnings("unchecked") + private Class resolveEntityClass() { + try { + return (Class) GenericTypeResolver.resolveEntityClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve entityClass: {}", e.getMessage()); + return null; + } + } + + @SuppressWarnings("unchecked") + private Class

resolvePoClass() { + try { + return (Class

) GenericTypeResolver.resolvePoClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve poClass: {}", e.getMessage()); + return null; + } + } + + @SuppressWarnings("unchecked") + private Class resolveIdClass() { + try { + return (Class) GenericTypeResolver.resolveIdClass(getClass()); + } catch (Exception e) { + log.warn("Cannot resolve idClass: {}", e.getMessage()); + return null; + } + } + + private String resolveIdFieldName() { + Class poType = getPoClass(); + if (poType != null) { + Field idField = findFieldWithAnnotation(poType, Id.class); + if (idField != null) { + return idField.getName(); + } + } + return "id"; + } + + private Field findFieldWithAnnotation(Class clazz, Class annotationClass) { + for (Field field : clazz.getDeclaredFields()) { + if (field.isAnnotationPresent((Class) annotationClass)) { + return field; + } + } + if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) { + return findFieldWithAnnotation(clazz.getSuperclass(), annotationClass); + } + return null; + } + @Override public E save(E entity) { if (entity == null) { @@ -298,7 +349,7 @@ private ID getIdValue(P po) { private Field findIdField(Class clazz) { try { - Field field = clazz.getDeclaredField(idFieldName); + Field field = clazz.getDeclaredField(getIdFieldName()); return field; } catch (NoSuchFieldException e) { if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) { @@ -331,7 +382,7 @@ protected E toEntity(P po) { return null; } try { - E entity = entityClass.getDeclaredConstructor().newInstance(); + E entity = getEntityClass().getDeclaredConstructor().newInstance(); BeanUtils.copyProperties(po, entity); return entity; } catch (Exception e) { @@ -348,12 +399,13 @@ protected List toEntityList(List

poList) { .collect(Collectors.toList()); } + @SuppressWarnings("unchecked") protected P toPo(E entity) { if (entity == null) { return null; } try { - P po = poClass.getDeclaredConstructor().newInstance(); + P po = ((Class

) getPoClass()).getDeclaredConstructor().newInstance(); BeanUtils.copyProperties(entity, po); return po; } catch (Exception e) { diff --git a/structure-infra-sample/pom.xml b/structure-infra-sample/pom.xml index d6a52a4..da30059 100644 --- a/structure-infra-sample/pom.xml +++ b/structure-infra-sample/pom.xml @@ -22,7 +22,7 @@ structure-infra-sample-core - structure-infra-sample-mybatis + structure-infra-sample-jpa structure-infra-sample-mongodb structure-infra-sample-elasticsearch @@ -30,6 +30,7 @@ structure-infra-sample-stream structure-infra-sample-xxljob structure-infra-sample-schedule + structure-infra-sample-multi \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/pom.xml b/structure-infra-sample/structure-infra-sample-multi/pom.xml new file mode 100644 index 0000000..34d578f --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + cn.structured + structure-infra-sample + ${revision} + ../pom.xml + + + structure-infra-sample-multi + structure-infra-sample-multi + 多仓库示例模块 - 演示如何同时使用多种仓库类型 + jar + + + + cn.structured + structure-infra-mybatis-plus-starter + + + cn.structured + structure-infra-mongodb-starter + ${revision} + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/MultiSampleApplication.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/MultiSampleApplication.java new file mode 100644 index 0000000..012f60d --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/MultiSampleApplication.java @@ -0,0 +1,12 @@ +package cn.structure.infra.sample.multi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MultiSampleApplication { + + public static void main(String[] args) { + SpringApplication.run(MultiSampleApplication.class, args); + } +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/domain/entity/UserEntity.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/domain/entity/UserEntity.java new file mode 100644 index 0000000..ac6a841 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/domain/entity/UserEntity.java @@ -0,0 +1,23 @@ +package cn.structure.infra.sample.multi.domain.entity; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class UserEntity { + + private Long id; + + private String username; + + private String password; + + private String email; + + private Integer age; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/domain/repository/UserRepository.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/domain/repository/UserRepository.java new file mode 100644 index 0000000..b811d7a --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/domain/repository/UserRepository.java @@ -0,0 +1,9 @@ +package cn.structure.infra.sample.multi.domain.repository; + +import cn.structure.common.repository.ICrudRepository; +import cn.structure.infra.sample.multi.domain.entity.UserEntity; + +public interface UserRepository extends ICrudRepository { + + UserEntity findByName(String name); +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/mapper/UserMapper.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/mapper/UserMapper.java new file mode 100644 index 0000000..9d0a3e2 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/mapper/UserMapper.java @@ -0,0 +1,9 @@ +package cn.structure.infra.sample.multi.infra.mapper; + +import cn.structure.infra.sample.multi.infra.po.MybatisUserPO; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserMapper extends BaseMapper { +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/po/MongoUserPO.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/po/MongoUserPO.java new file mode 100644 index 0000000..38e36b1 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/po/MongoUserPO.java @@ -0,0 +1,27 @@ +package cn.structure.infra.sample.multi.infra.po; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Data +@Document(collection = "t_user") +public class MongoUserPO { + + @Id + private Long id; + + private String username; + + private String password; + + private String email; + + private Integer age; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/po/MybatisUserPO.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/po/MybatisUserPO.java new file mode 100644 index 0000000..91ac7d8 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/po/MybatisUserPO.java @@ -0,0 +1,30 @@ +package cn.structure.infra.sample.multi.infra.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import jakarta.persistence.Id; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("t_user") +public class MybatisUserPO { + + @Id + @TableId(type = IdType.AUTO) + private Long id; + + private String username; + + private String password; + + private String email; + + private Integer age; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/AbstractUserRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/AbstractUserRepositoryImpl.java new file mode 100644 index 0000000..8c9401b --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/AbstractUserRepositoryImpl.java @@ -0,0 +1,14 @@ +package cn.structure.infra.sample.multi.infra.repository; + +import cn.structure.infra.repository.MultiRepositoryFacade; +import cn.structure.infra.sample.multi.domain.entity.UserEntity; +import cn.structure.infra.sample.multi.domain.repository.UserRepository; +import cn.structure.infra.sample.multi.infra.repository.delegate.UserRepositoryDelegate; + +public abstract class AbstractUserRepositoryImpl extends MultiRepositoryFacade implements UserRepository { + + @Override + public UserEntity findByName(String name) { + return getCurrentDelegate().findByName(name); + } +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/UserRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/UserRepositoryImpl.java new file mode 100644 index 0000000..034490f --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/UserRepositoryImpl.java @@ -0,0 +1,7 @@ +package cn.structure.infra.sample.multi.infra.repository; + +import org.springframework.stereotype.Component; + +@Component("userRepository") +public class UserRepositoryImpl extends AbstractUserRepositoryImpl { +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/delegate/UserRepositoryDelegate.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/delegate/UserRepositoryDelegate.java new file mode 100644 index 0000000..352fe21 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/delegate/UserRepositoryDelegate.java @@ -0,0 +1,9 @@ +package cn.structure.infra.sample.multi.infra.repository.delegate; + +import cn.structure.infra.repository.RepositoryDelegate; +import cn.structure.infra.sample.multi.domain.entity.UserEntity; + +public interface UserRepositoryDelegate extends RepositoryDelegate { + + UserEntity findByName(String name); +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mongodb/UserMongoDelegate.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mongodb/UserMongoDelegate.java new file mode 100644 index 0000000..04d0e46 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mongodb/UserMongoDelegate.java @@ -0,0 +1,20 @@ +package cn.structure.infra.sample.multi.infra.repository.mongodb; + +import cn.structure.infra.mongodb.repository.MongoRepositoryDelegate; +import cn.structure.infra.sample.multi.domain.entity.UserEntity; +import cn.structure.infra.sample.multi.infra.po.MongoUserPO; +import cn.structure.infra.sample.multi.infra.repository.delegate.UserRepositoryDelegate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class UserMongoDelegate extends MongoRepositoryDelegate implements UserRepositoryDelegate { + + @Override + public UserEntity findByName(String name) { + UserEntity condition = new UserEntity(); + condition.setUsername(name); + return queryOne(condition); + } +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mybatis/UserMybatisPlusDelegate.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mybatis/UserMybatisPlusDelegate.java new file mode 100644 index 0000000..d31b18a --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mybatis/UserMybatisPlusDelegate.java @@ -0,0 +1,26 @@ +package cn.structure.infra.sample.multi.infra.repository.mybatis; + +import cn.structure.infra.mybatis.plus.repository.MybatisPlusRepositoryDelegate; +import cn.structure.infra.sample.multi.domain.entity.UserEntity; +import cn.structure.infra.sample.multi.infra.mapper.UserMapper; +import cn.structure.infra.sample.multi.infra.po.MybatisUserPO; +import cn.structure.infra.sample.multi.infra.repository.delegate.UserRepositoryDelegate; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate { + + public UserMybatisPlusDelegate(UserMapper userMapper) { + this.baseMapper = userMapper; + } + + @Override + public UserEntity findByName(String name) { + List pos = baseMapper.selectList(Wrappers.lambdaQuery().eq(MybatisUserPO::getUsername, name)); + return pos != null && !pos.isEmpty() ? toEntity(pos.get(0)) : null; + } +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/service/MultiRepositoryService.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/service/MultiRepositoryService.java new file mode 100644 index 0000000..563d322 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/service/MultiRepositoryService.java @@ -0,0 +1,128 @@ +package cn.structure.infra.sample.multi.service; + +import cn.structure.infra.repository.RepositoryType; +import cn.structure.infra.repository.RepositoryTypeContext; +import cn.structure.infra.sample.multi.domain.entity.UserEntity; +import cn.structure.infra.sample.multi.domain.repository.UserRepository; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +@AllArgsConstructor +public class MultiRepositoryService { + + private final UserRepository userRepository; + + public UserEntity saveToMybatis(UserEntity user) { + log.info("Saving user to MyBatis Plus repository"); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MYBATIS_PLUS)) { + UserEntity saved = userRepository.save(user); + log.info("User saved to MyBatis Plus with id: {}", saved.getId()); + return saved; + } + } + + public UserEntity saveToMongo(UserEntity user) { + log.info("Saving user to MongoDB repository"); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MONGODB)) { + UserEntity saved = userRepository.save(user); + log.info("User saved to MongoDB with id: {}", saved.getId()); + return saved; + } + } + + public UserEntity saveToDefault(UserEntity user) { + log.info("Saving user to default repository (configured in application.yml)"); + UserEntity saved = userRepository.save(user); + log.info("User saved to default repository with id: {}", saved.getId()); + return saved; + } + + public UserEntity findFromMybatis(Long id) { + log.info("Finding user from MyBatis Plus repository, id: {}", id); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MYBATIS_PLUS)) { + return userRepository.queryById(id); + } + } + + public UserEntity findFromMongo(Long id) { + log.info("Finding user from MongoDB repository, id: {}", id); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MONGODB)) { + return userRepository.queryById(id); + } + } + + public List queryListFromMybatis(UserEntity condition) { + log.info("Querying user list from MyBatis Plus repository"); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MYBATIS_PLUS)) { + return userRepository.queryList(condition); + } + } + + public List queryListFromMongo(UserEntity condition) { + log.info("Querying user list from MongoDB repository"); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MONGODB)) { + return userRepository.queryList(condition); + } + } + + public void deleteFromMybatis(Long id) { + log.info("Deleting user from MyBatis Plus repository, id: {}", id); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MYBATIS_PLUS)) { + userRepository.removeById(id); + } + } + + public void deleteFromMongo(Long id) { + log.info("Deleting user from MongoDB repository, id: {}", id); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MONGODB)) { + userRepository.removeById(id); + } + } + + public UserEntity findByNameFromMybatis(String name) { + log.info("Finding user by name from MyBatis Plus repository, name: {}", name); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MYBATIS_PLUS)) { + return userRepository.findByName(name); + } + } + + public UserEntity findByNameFromMongo(String name) { + log.info("Finding user by name from MongoDB repository, name: {}", name); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MONGODB)) { + return userRepository.findByName(name); + } + } + + public long countInMybatis(UserEntity condition) { + log.info("Counting users in MyBatis Plus repository"); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MYBATIS_PLUS)) { + return userRepository.count(condition); + } + } + + public long countInMongo(UserEntity condition) { + log.info("Counting users in MongoDB repository"); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MONGODB)) { + return userRepository.count(condition); + } + } + + public boolean existsInMybatis(UserEntity condition) { + log.info("Checking user existence in MyBatis Plus repository"); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MYBATIS_PLUS)) { + return userRepository.exists(condition); + } + } + + public boolean existsInMongo(UserEntity condition) { + log.info("Checking user existence in MongoDB repository"); + try (RepositoryTypeContext context = RepositoryTypeContext.use(RepositoryType.MONGODB)) { + return userRepository.exists(condition); + } + } +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/resources/application.yml b/structure-infra-sample/structure-infra-sample-multi/src/main/resources/application.yml new file mode 100644 index 0000000..da251e3 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/resources/application.yml @@ -0,0 +1,24 @@ +server: + port: 8080 + +spring: + application: + name: structure-infra-sample-multi + datasource: + url: jdbc:h2:mem:multi_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + driver-class-name: org.h2.Driver + sql: + init: + mode: always + +structure: + infra: + multi-repository-enabled: true + default-repository-type: MYBATIS_PLUS + +logging: + level: + cn.structure.infra.repository: DEBUG + cn.structure.infra.sample.multi: DEBUG \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/resources/schema.sql b/structure-infra-sample/structure-infra-sample-multi/src/main/resources/schema.sql new file mode 100644 index 0000000..e52866f --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/resources/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS t_user ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) NOT NULL, + password VARCHAR(255), + email VARCHAR(255), + age INT, + create_time TIMESTAMP, + update_time TIMESTAMP +); \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/MultiRepositoryTest.java b/structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/MultiRepositoryTest.java new file mode 100644 index 0000000..fb8014f --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/MultiRepositoryTest.java @@ -0,0 +1,216 @@ +package cn.structure.infra.sample.multi; + +import cn.structure.infra.sample.multi.config.MockMongoConfiguration; +import cn.structure.infra.sample.multi.domain.entity.UserEntity; +import cn.structure.infra.sample.multi.service.MultiRepositoryService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +@SpringBootTest(classes = cn.structure.infra.sample.multi.config.MultiTestConfig.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class MultiRepositoryTest { + + @Autowired + private MultiRepositoryService multiRepositoryService; + + @Autowired + private MockMongoConfiguration mockMongoConfig; + + private UserEntity testUser; + + @BeforeEach + public void setUp() { + mockMongoConfig.reset(); + testUser = new UserEntity(); + testUser.setUsername("test_multi_user"); + testUser.setEmail("test_multi@example.com"); + testUser.setAge(25); + } + + @Test + public void testSaveToMybatisAndFind() { + log.info("=== Test: Save to MyBatis Plus and find ==="); + + UserEntity saved = multiRepositoryService.saveToMybatis(testUser); + assertNotNull(saved.getId()); + assertEquals("test_multi_user", saved.getUsername()); + + UserEntity found = multiRepositoryService.findFromMybatis(saved.getId()); + assertNotNull(found); + assertEquals("test_multi_user", found.getUsername()); + assertEquals("test_multi@example.com", found.getEmail()); + assertEquals(25, found.getAge()); + + log.info("✓ MyBatis Plus save and find test passed"); + } + + @Test + public void testSaveToMongoAndFind() { + log.info("=== Test: Save to MongoDB and find ==="); + + UserEntity saved = multiRepositoryService.saveToMongo(testUser); + assertNotNull(saved.getId()); + assertEquals("test_multi_user", saved.getUsername()); + + UserEntity found = multiRepositoryService.findFromMongo(saved.getId()); + assertNotNull(found); + assertEquals("test_multi_user", found.getUsername()); + + log.info("✓ MongoDB save and find test passed"); + } + + @Test + public void testDataIsolationBetweenRepositories() { + log.info("=== Test: Data isolation between repositories ==="); + + UserEntity mybatisUser = new UserEntity(); + mybatisUser.setUsername("mybatis_only_user"); + mybatisUser.setEmail("mybatis@example.com"); + UserEntity savedToMybatis = multiRepositoryService.saveToMybatis(mybatisUser); + assertNotNull(savedToMybatis.getId()); + log.info("Saved to MyBatis with id: {}, username: {}", savedToMybatis.getId(), savedToMybatis.getUsername()); + + UserEntity foundInMongoByName = multiRepositoryService.findByNameFromMongo("mybatis_only_user"); + assertNull(foundInMongoByName, "Data saved to MyBatis should NOT be visible in MongoDB"); + + UserEntity mongoUser = new UserEntity(); + mongoUser.setUsername("mongo_only_user"); + mongoUser.setEmail("mongo@example.com"); + UserEntity savedToMongo = multiRepositoryService.saveToMongo(mongoUser); + assertNotNull(savedToMongo.getId()); + log.info("Saved to MongoDB with id: {}, username: {}", savedToMongo.getId(), savedToMongo.getUsername()); + + UserEntity foundInMybatisByName = multiRepositoryService.findByNameFromMybatis("mongo_only_user"); + assertNull(foundInMybatisByName, "Data saved to MongoDB should NOT be visible in MyBatis"); + + log.info("✓ Data isolation test passed"); + } + + @Test + public void testSaveToDefaultRepository() { + log.info("=== Test: Save to default repository (configured as MYBATIS_PLUS) ==="); + + UserEntity saved = multiRepositoryService.saveToDefault(testUser); + assertNotNull(saved.getId()); + + UserEntity foundInMybatis = multiRepositoryService.findFromMybatis(saved.getId()); + assertNotNull(foundInMybatis); + assertEquals("test_multi_user", foundInMybatis.getUsername()); + + log.info("✓ Default repository test passed"); + } + + @Test + public void testCountAndExistsOperations() { + log.info("=== Test: Count and exists operations ==="); + + multiRepositoryService.saveToMybatis(testUser); + + long countInMybatis = multiRepositoryService.countInMybatis(new UserEntity()); + assertTrue(countInMybatis >= 1); + + UserEntity condition = new UserEntity(); + condition.setUsername("test_multi_user"); + boolean existsInMybatis = multiRepositoryService.existsInMybatis(condition); + assertTrue(existsInMybatis); + + long countInMongo = multiRepositoryService.countInMongo(new UserEntity()); + assertEquals(0, countInMongo); + + UserEntity mongoCondition = new UserEntity(); + mongoCondition.setUsername("test_multi_user"); + boolean existsInMongo = multiRepositoryService.existsInMongo(mongoCondition); + assertFalse(existsInMongo); + + log.info("✓ Count and exists operations test passed"); + } + + @Test + public void testDeleteOperations() { + log.info("=== Test: Delete operations ==="); + + UserEntity savedToMybatis = multiRepositoryService.saveToMybatis(testUser); + assertNotNull(savedToMybatis.getId()); + + multiRepositoryService.deleteFromMybatis(savedToMybatis.getId()); + + UserEntity found = multiRepositoryService.findFromMybatis(savedToMybatis.getId()); + assertNull(found); + + log.info("✓ Delete operations test passed"); + } + + @Test + public void testFindByNameOperation() { + log.info("=== Test: Find by name operation ==="); + + UserEntity savedToMybatis = multiRepositoryService.saveToMybatis(testUser); + log.info("Saved to MyBatis with id: {}", savedToMybatis.getId()); + + UserEntity foundByName = multiRepositoryService.findByNameFromMybatis("test_multi_user"); + assertNotNull(foundByName); + log.info("Found by name with id: {}", foundByName.getId()); + assertEquals(savedToMybatis.getId(), foundByName.getId()); + + log.info("✓ Find by name operation test passed"); + } + + @Test + public void testQueryListOperation() { + log.info("=== Test: Query list operation ==="); + + UserEntity user1 = new UserEntity(); + user1.setUsername("user1"); + user1.setEmail("user1@example.com"); + UserEntity user2 = new UserEntity(); + user2.setUsername("user2"); + user2.setEmail("user2@example.com"); + + multiRepositoryService.saveToMybatis(user1); + multiRepositoryService.saveToMybatis(user2); + + var users = multiRepositoryService.queryListFromMybatis(new UserEntity()); + assertNotNull(users); + assertTrue(users.size() >= 2); + + log.info("✓ Query list operation test passed"); + } + + @Test + public void testMultiRepositorySwitchingInSequence() { + log.info("=== Test: Multi repository switching in sequence ==="); + + UserEntity mybatisUser = new UserEntity(); + mybatisUser.setUsername("mybatis_sequence_user"); + mybatisUser.setEmail("mybatis_sequence@example.com"); + UserEntity mongoUser = new UserEntity(); + mongoUser.setUsername("mongo_sequence_user"); + mongoUser.setEmail("mongo_sequence@example.com"); + + UserEntity savedToMybatis = multiRepositoryService.saveToMybatis(mybatisUser); + UserEntity savedToMongo = multiRepositoryService.saveToMongo(mongoUser); + + assertNotNull(savedToMybatis.getId()); + assertNotNull(savedToMongo.getId()); + + UserEntity foundMybatis = multiRepositoryService.findFromMybatis(savedToMybatis.getId()); + UserEntity foundMongo = multiRepositoryService.findFromMongo(savedToMongo.getId()); + + assertNotNull(foundMybatis); + assertNotNull(foundMongo); + assertEquals("mybatis_sequence_user", foundMybatis.getUsername()); + assertEquals("mongo_sequence_user", foundMongo.getUsername()); + + assertNull(multiRepositoryService.findByNameFromMongo("mybatis_sequence_user")); + assertNull(multiRepositoryService.findByNameFromMybatis("mongo_sequence_user")); + + log.info("✓ Multi repository switching in sequence test passed"); + } +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/config/MockMongoConfiguration.java b/structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/config/MockMongoConfiguration.java new file mode 100644 index 0000000..0c84e9d --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/config/MockMongoConfiguration.java @@ -0,0 +1,558 @@ +package cn.structure.infra.sample.multi.config; + +import org.bson.Document; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.index.Index; +import org.springframework.data.mongodb.core.index.IndexOperations; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@TestConfiguration +public class MockMongoConfiguration { + + private final Map, Map> dataStore = new HashMap<>(); + private long idGenerator = 1; + + private final Map> docCollections = new ConcurrentHashMap<>(); + private final AtomicLong docIdGenerator = new AtomicLong(1); + private final List insertCalls = new ArrayList<>(); + + private final MongoMappingContext mappingContext = new MongoMappingContext(); + private final MongoConverter converter; + private final MongoTemplate template; + + public MockMongoConfiguration() { + this.converter = createMongoConverter(); + this.template = createMongoTemplate(); + } + + public void reset() { + dataStore.clear(); + idGenerator = 1; + docCollections.clear(); + docIdGenerator.set(1); + insertCalls.clear(); + } + + public List getInsertCalls() { + return insertCalls; + } + + private MongoConverter createMongoConverter() { + MappingMongoConverter converter = mock(MappingMongoConverter.class); + when(converter.getMappingContext()).thenReturn((MappingContext) mappingContext); + return converter; + } + + private MongoTemplate createMongoTemplate() { + MongoTemplate template = mock(MongoTemplate.class); + + when(template.save(any())).thenAnswer(invocation -> { + Object po = invocation.getArgument(0); + Class poClass = po.getClass(); + + Map classStore = dataStore.computeIfAbsent(poClass, k -> new HashMap<>()); + + try { + Field idField = findIdField(poClass); + if (idField != null) { + idField.setAccessible(true); + Object idValue = idField.get(po); + Long id = null; + + if (idValue == null || (idValue instanceof Number && ((Number) idValue).longValue() == 0)) { + id = idGenerator++; + setIdValue(po, id); + } else if (idValue instanceof Long) { + id = (Long) idValue; + } else if (idValue instanceof String) { + id = Long.valueOf((String) idValue); + } else if (idValue instanceof Number) { + id = ((Number) idValue).longValue(); + } + + if (id != null) { + classStore.put(id, po); + } + } + } catch (Exception ignored) { + } + + return po; + }); + + when(template.findById(any(Long.class), any(Class.class))).thenAnswer(invocation -> { + Long id = invocation.getArgument(0); + Class poClass = invocation.getArgument(1); + Map classStore = dataStore.get(poClass); + return classStore != null ? classStore.get(id) : null; + }); + + when(template.findById(anyString(), any(Class.class))).thenAnswer(invocation -> { + String id = invocation.getArgument(0); + Class poClass = invocation.getArgument(1); + Map classStore = dataStore.get(poClass); + return classStore != null ? classStore.get(Long.valueOf(id)) : null; + }); + + when(template.findOne(any(Query.class), any(Class.class))).thenAnswer(invocation -> { + Query query = invocation.getArgument(0); + Class poClass = invocation.getArgument(1); + List allData = getAllData(poClass); + List results = filterByQuery(query, allData); + return results.isEmpty() ? null : results.get(0); + }); + + when(template.findAll(any(Class.class))).thenAnswer(invocation -> { + Class poClass = invocation.getArgument(0); + return new ArrayList<>(getAllData(poClass)); + }); + + when(template.find(any(Query.class), any(Class.class))).thenAnswer(invocation -> { + Query query = invocation.getArgument(0); + Class poClass = invocation.getArgument(1); + List allData = getAllData(poClass); + return filterByQuery(query, allData); + }); + + when(template.remove(any(Query.class), any(Class.class))).thenAnswer(invocation -> { + Query query = invocation.getArgument(0); + Class poClass = invocation.getArgument(1); + List allData = getAllData(poClass); + List results = filterByQuery(query, allData); + + Map classStore = dataStore.get(poClass); + if (classStore != null) { + for (Object po : results) { + try { + Field idField = findIdField(poClass); + if (idField != null) { + idField.setAccessible(true); + Object idValue = idField.get(po); + Long id = null; + if (idValue instanceof Long) { + id = (Long) idValue; + } else if (idValue instanceof String) { + id = Long.valueOf((String) idValue); + } else if (idValue instanceof Number) { + id = ((Number) idValue).longValue(); + } + if (id != null) { + classStore.remove(id); + } + } + } catch (Exception ignored) { + } + } + } + return null; + }); + + when(template.count(any(Query.class), any(Class.class))).thenAnswer(invocation -> { + Query query = invocation.getArgument(0); + Class poClass = invocation.getArgument(1); + List allData = getAllData(poClass); + List results = filterByQuery(query, allData); + return (long) results.size(); + }); + + when(template.collectionExists(anyString())).thenAnswer(invocation -> { + String collectionName = invocation.getArgument(0); + return docCollections.containsKey(collectionName); + }); + + doAnswer(invocation -> { + String collectionName = invocation.getArgument(0); + docCollections.putIfAbsent(collectionName, new LinkedHashMap<>()); + return null; + }).when(template).createCollection(anyString()); + + doAnswer(invocation -> { + String collectionName = invocation.getArgument(0); + docCollections.remove(collectionName); + return null; + }).when(template).dropCollection(anyString()); + + IndexOperations indexOps = mock(IndexOperations.class); + when(indexOps.createIndex(any(Index.class))).thenReturn(""); + when(template.indexOps(anyString())).thenReturn(indexOps); + + when(template.findOne(any(Query.class), any(Class.class), anyString())).thenAnswer(invocation -> { + Query query = invocation.getArgument(0); + String collectionName = invocation.getArgument(2); + Map collection = docCollections.get(collectionName); + if (collection == null || collection.isEmpty()) { + return null; + } + List allDocs = new ArrayList<>(collection.values()); + List filtered = filterDocsByQuery(query, allDocs); + return filtered.isEmpty() ? null : filtered.get(0); + }); + + when(template.find(any(Query.class), any(Class.class), anyString())).thenAnswer(invocation -> { + Query query = invocation.getArgument(0); + String collectionName = invocation.getArgument(2); + Map collection = docCollections.get(collectionName); + if (collection == null || collection.isEmpty()) { + return new ArrayList(); + } + List allDocs = new ArrayList<>(collection.values()); + return filterDocsByQuery(query, allDocs); + }); + + when(template.insert(any(Document.class), anyString())).thenAnswer(invocation -> { + Document doc = invocation.getArgument(0); + String collectionName = invocation.getArgument(1); + insertCalls.add("insert(Document, String): " + collectionName); + Map collection = docCollections.computeIfAbsent(collectionName, k -> new LinkedHashMap<>()); + + String idField = "id"; + if (!doc.containsKey(idField) || doc.get(idField) == null) { + doc.put(idField, docIdGenerator.getAndIncrement()); + } + Object id = doc.get(idField); + collection.put(id, doc); + return doc; + }); + + when(template.save(any(Document.class), anyString())).thenAnswer(invocation -> { + Document doc = invocation.getArgument(0); + String collectionName = invocation.getArgument(1); + Map collection = docCollections.computeIfAbsent(collectionName, k -> new LinkedHashMap<>()); + + String idField = "id"; + if (!doc.containsKey(idField) || doc.get(idField) == null) { + doc.put(idField, docIdGenerator.getAndIncrement()); + } + Object id = doc.get(idField); + collection.put(id, doc); + return doc; + }); + + when(template.updateFirst(any(Query.class), any(Update.class), anyString())).thenAnswer(invocation -> { + String collectionName = invocation.getArgument(2); + Map collection = docCollections.get(collectionName); + if (collection == null || collection.isEmpty()) { + return null; + } + Document firstDoc = collection.values().iterator().next(); + Update update = invocation.getArgument(1); + Map updates = extractDocUpdateValues(update); + firstDoc.putAll(updates); + return null; + }); + + doAnswer(invocation -> { + Query query = invocation.getArgument(0); + String collectionName = invocation.getArgument(1); + Map collection = docCollections.get(collectionName); + if (collection == null || collection.isEmpty()) { + return null; + } + List allDocs = new ArrayList<>(collection.values()); + List filtered = filterDocsByQuery(query, allDocs); + for (Document doc : filtered) { + Object id = doc.get("id"); + if (id != null) { + collection.remove(id); + } + } + return null; + }).when(template).remove(any(Query.class), anyString()); + + when(template.count(any(Query.class), anyString())).thenAnswer(invocation -> { + Query query = invocation.getArgument(0); + String collectionName = invocation.getArgument(1); + Map collection = docCollections.get(collectionName); + if (collection == null || collection.isEmpty()) { + return 0L; + } + List allDocs = new ArrayList<>(collection.values()); + List filtered = filterDocsByQuery(query, allDocs); + return (long) filtered.size(); + }); + + when(template.getConverter()).thenReturn(converter); + + return template; + } + + private List getAllData(Class poClass) { + Map classStore = dataStore.get(poClass); + return classStore != null ? new ArrayList<>(classStore.values()) : new ArrayList<>(); + } + + private List filterByQuery(Query query, List data) { + try { + Field criteriaField = Query.class.getDeclaredField("criteria"); + criteriaField.setAccessible(true); + Object criteriaMap = criteriaField.get(query); + + if (criteriaMap instanceof Map) { + List> conditions = new ArrayList<>(); + for (Object entry : ((Map) criteriaMap).entrySet()) { + Map.Entry mapEntry = (Map.Entry) entry; + String key = mapEntry.getKey().toString(); + Object criteriaDef = mapEntry.getValue(); + Object value = extractIsValue(criteriaDef); + if (value != null) { + conditions.add(Map.entry(key, value)); + } + } + if (!conditions.isEmpty()) { + return data.stream() + .filter(po -> matchesConditions(po, conditions)) + .collect(Collectors.toList()); + } + } + } catch (Exception ignored) { + } + return data; + } + + private Object extractIsValue(Object criteriaDef) { + try { + Field isValueField = Criteria.class.getDeclaredField("isValue"); + isValueField.setAccessible(true); + Object value = isValueField.get(criteriaDef); + if (value != null && !"NOT_SET".equals(value.toString())) { + return value; + } + } catch (Exception ignored) { + } + return null; + } + + private List> extractConditions(Criteria criteria) { + List> conditions = new ArrayList<>(); + try { + Field criteriaChainField = Criteria.class.getDeclaredField("criteriaChain"); + criteriaChainField.setAccessible(true); + Object criteriaChain = criteriaChainField.get(criteria); + if (criteriaChain instanceof List) { + for (Object c : (List) criteriaChain) { + extractSingleCondition(c, conditions); + } + } else { + extractSingleCondition(criteria, conditions); + } + } catch (Exception ignored) { + extractSingleCondition(criteria, conditions); + } + return conditions; + } + + private void extractSingleCondition(Object criteria, List> conditions) { + try { + Field keyField = Criteria.class.getDeclaredField("key"); + Field valueField = Criteria.class.getDeclaredField("value"); + keyField.setAccessible(true); + valueField.setAccessible(true); + + Object key = keyField.get(criteria); + Object value = valueField.get(criteria); + + if (key != null && value != null) { + conditions.add(Map.entry(key.toString(), value)); + } + } catch (Exception ignored) { + } + } + + private boolean matchesConditions(Object po, List> conditions) { + for (Map.Entry condition : conditions) { + String fieldName = condition.getKey(); + Object expectedValue = condition.getValue(); + + try { + Field field = findField(po.getClass(), fieldName); + if (field != null) { + field.setAccessible(true); + Object actualValue = field.get(po); + + if (!expectedValue.equals(actualValue)) { + return false; + } + } + } catch (Exception ignored) { + } + } + return true; + } + + private Field findField(Class clazz, String fieldName) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) { + return findField(clazz.getSuperclass(), fieldName); + } + return null; + } + } + + private Field findIdField(Class clazz) { + return findField(clazz, "id"); + } + + private void setIdValue(Object po, Long id) throws Exception { + Field field = findIdField(po.getClass()); + if (field != null) { + field.setAccessible(true); + if (field.getType() == Long.class || field.getType() == long.class) { + field.set(po, id); + } else if (field.getType() == Integer.class || field.getType() == int.class) { + field.set(po, id.intValue()); + } else if (field.getType() == String.class) { + field.set(po, String.valueOf(id)); + } else { + field.set(po, id); + } + } + } + + private Map extractDocUpdateValues(Update update) { + Map result = new HashMap<>(); + try { + Field updatesField = Update.class.getDeclaredField("updates"); + updatesField.setAccessible(true); + Object updates = updatesField.get(update); + if (updates instanceof List) { + for (Object u : (List) updates) { + try { + Field keyField = u.getClass().getDeclaredField("key"); + Field valueField = u.getClass().getDeclaredField("value"); + keyField.setAccessible(true); + valueField.setAccessible(true); + String key = (String) keyField.get(u); + Object value = valueField.get(u); + result.put(key, value); + } catch (Exception ignored) { + } + } + } + } catch (Exception ignored) { + } + return result; + } + + private List filterDocsByQuery(Query query, List docs) { + try { + Field criteriaField = Query.class.getDeclaredField("criteria"); + criteriaField.setAccessible(true); + Object criteriaMap = criteriaField.get(query); + + if (criteriaMap instanceof Map) { + List> conditions = new ArrayList<>(); + for (Object entry : ((Map) criteriaMap).entrySet()) { + Map.Entry mapEntry = (Map.Entry) entry; + String key = mapEntry.getKey().toString(); + Object criteriaDef = mapEntry.getValue(); + Object value = extractIsValue(criteriaDef); + if (value != null) { + conditions.add(Map.entry(key, value)); + } + } + if (!conditions.isEmpty()) { + return docs.stream() + .filter(doc -> matchesDocConditions(doc, conditions)) + .collect(Collectors.toList()); + } + } + } catch (Exception ignored) { + } + return docs; + } + + private List> extractDocConditions(Criteria criteria) { + List> conditions = new ArrayList<>(); + try { + Field criteriaChainField = Criteria.class.getDeclaredField("criteriaChain"); + criteriaChainField.setAccessible(true); + Object criteriaChain = criteriaChainField.get(criteria); + if (criteriaChain instanceof List) { + for (Object c : (List) criteriaChain) { + extractSingleDocCondition(c, conditions); + } + } else { + extractSingleDocCondition(criteria, conditions); + } + } catch (Exception ignored) { + extractSingleDocCondition(criteria, conditions); + } + return conditions; + } + + private void extractSingleDocCondition(Object criteria, List> conditions) { + try { + Field keyField = Criteria.class.getDeclaredField("key"); + Field valueField = Criteria.class.getDeclaredField("value"); + keyField.setAccessible(true); + valueField.setAccessible(true); + + Object key = keyField.get(criteria); + Object value = valueField.get(criteria); + + if (key != null && value != null) { + conditions.add(Map.entry(key.toString(), value)); + } + } catch (Exception ignored) { + } + } + + private boolean matchesDocConditions(Document doc, List> conditions) { + for (Map.Entry condition : conditions) { + String fieldName = condition.getKey(); + Object expectedValue = condition.getValue(); + + Object actualValue = doc.get(fieldName); + if (actualValue == null || !actualValue.equals(expectedValue)) { + return false; + } + } + return true; + } + + @Bean + @Primary + public MongoDatabaseFactory mongoDatabaseFactory() { + return mock(MongoDatabaseFactory.class); + } + + @Bean + @Primary + public MongoMappingContext mongoMappingContext() { + return mappingContext; + } + + @Bean + @Primary + public MongoConverter mongoConverter() { + return converter; + } + + @Bean + @Primary + public MongoTemplate mongoTemplate() { + return template; + } +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/config/MultiTestConfig.java b/structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/config/MultiTestConfig.java new file mode 100644 index 0000000..40abd04 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/test/java/cn/structure/infra/sample/multi/config/MultiTestConfig.java @@ -0,0 +1,11 @@ +package cn.structure.infra.sample.multi.config; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@SpringBootApplication(scanBasePackages = "cn.structure.infra.sample.multi") +@Import(MockMongoConfiguration.class) +public class MultiTestConfig { +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-multi/src/test/resources/application.yml b/structure-infra-sample/structure-infra-sample-multi/src/test/resources/application.yml new file mode 100644 index 0000000..ef266e0 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-multi/src/test/resources/application.yml @@ -0,0 +1,24 @@ +server: + port: 8080 + +spring: + application: + name: structure-infra-sample-multi + datasource: + url: jdbc:h2:mem:multi_test_db;DB_CLOSE_DELAY=0;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + driver-class-name: org.h2.Driver + sql: + init: + mode: always + +structure: + infra: + multi-repository-enabled: true + default-repository-type: MYBATIS_PLUS + +logging: + level: + cn.structure.infra.repository: DEBUG + cn.structure.infra.sample.multi: DEBUG \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/BeanDebugTest.java b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/BeanDebugTest.java new file mode 100644 index 0000000..6a11a06 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/BeanDebugTest.java @@ -0,0 +1,31 @@ +package cn.structure.infra.sample.repository; + +import cn.structure.infra.sample.config.MybatisOnlyConfig; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(classes = MybatisOnlyConfig.class) +class BeanDebugTest { + + @Autowired + private ApplicationContext context; + + @Test + void listBeans() { + String[] beanNames = context.getBeanDefinitionNames(); + System.out.println("=== All sample beans ==="); + for (String name : beanNames) { + Object bean = context.getBean(name); + String className = bean.getClass().getName(); + if (className.contains("cn.structure.infra.sample")) { + System.out.println(" " + name + " -> " + className); + } + } + System.out.println("=== End ==="); + System.out.println("Total beans: " + beanNames.length); + } +} diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/annotations/RepositoryTypeAnnotation.java b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/RepositoryTypeAnnotation.java new file mode 100644 index 0000000..c7e2dfc --- /dev/null +++ b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/RepositoryTypeAnnotation.java @@ -0,0 +1,13 @@ +package cn.structure.infra.annotations; + +import cn.structure.infra.repository.RepositoryType; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RepositoryTypeAnnotation { + + RepositoryType value(); +} \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoRepositoryConfiguration.java b/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoRepositoryConfiguration.java index d978ce4..d4df1b8 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoRepositoryConfiguration.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoRepositoryConfiguration.java @@ -1,5 +1,8 @@ package cn.structure.infra.configuration; +import cn.structure.infra.repository.MultiRepositoryBeanPostProcessor; +import cn.structure.infra.repository.RepositoryBeanPostProcessor; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** @@ -21,4 +24,13 @@ @Configuration public class AutoRepositoryConfiguration { + @Bean + public RepositoryBeanPostProcessor repositoryBeanPostProcessor() { + return new RepositoryBeanPostProcessor(); + } + + @Bean + public MultiRepositoryBeanPostProcessor multiRepositoryBeanPostProcessor() { + return new MultiRepositoryBeanPostProcessor(); + } } \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/properties/InfraProperties.java b/structure-infra-starter/src/main/java/cn/structure/infra/properties/InfraProperties.java index ee31b8b..1bd51e8 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/properties/InfraProperties.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/properties/InfraProperties.java @@ -1,6 +1,7 @@ package cn.structure.infra.properties; import cn.structure.infra.event.EventChannel; +import cn.structure.infra.repository.RepositoryType; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -55,4 +56,23 @@ public class InfraProperties { * @return 调度线程池大小 */ private Integer schedulePoolSize = Runtime.getRuntime().availableProcessors(); + + /** + * 是否启用多仓库支持 + *

+ * 启用后,框架会自动收集所有 RepositoryDelegate 实现,并创建 MultiRepositoryDelegate + * 支持根据上下文或配置动态切换仓库类型 + * + * @return true 表示启用 + */ + private Boolean multiRepositoryEnabled = false; + + /** + * 默认仓库类型 + *

+ * 当多仓库模式启用且未通过上下文指定仓库类型时,使用此配置 + * + * @return 默认仓库类型 + */ + private RepositoryType defaultRepositoryType = RepositoryType.AUTO; } diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/GenericTypeResolver.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/GenericTypeResolver.java index 1c1e35a..d50059e 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/GenericTypeResolver.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/GenericTypeResolver.java @@ -47,17 +47,53 @@ public static Class resolveType(Class clazz, Class targetInterface, int } public static Class resolveEntityClass(Class clazz) { + Class result = resolveFromSuperclass(clazz, 0); + if (result != null) { + return result; + } return resolveType(clazz, RepositoryDelegate.class, 0); } public static Class resolvePoClass(Class clazz) { + Class result = resolveFromSuperclass(clazz, 1); + if (result != null) { + return result; + } return resolveType(clazz, RepositoryDelegate.class, 1); } public static Class resolveIdClass(Class clazz) { - return resolveType(clazz, RepositoryDelegate.class, 2); + Class result = resolveFromSuperclass(clazz, 2); + if (result != null) { + return result; + } + return resolveType(clazz, RepositoryDelegate.class, 1); + } + + private static Class resolveFromSuperclass(Class clazz, int index) { + Type genericSuperclass = clazz.getGenericSuperclass(); + while (genericSuperclass != null) { + if (genericSuperclass instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + Type[] typeArgs = parameterizedType.getActualTypeArguments(); + if (typeArgs.length > index) { + Type typeArg = typeArgs[index]; + if (typeArg instanceof Class) { + return (Class) typeArg; + } else if (typeArg instanceof ParameterizedType) { + return (Class) ((ParameterizedType) typeArg).getRawType(); + } + } + } + clazz = clazz.getSuperclass(); + if (clazz == null) { + break; + } + genericSuperclass = clazz.getGenericSuperclass(); + } + return null; } private GenericTypeResolver() { } -} +} \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java new file mode 100644 index 0000000..98eeeb0 --- /dev/null +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java @@ -0,0 +1,296 @@ +package cn.structure.infra.repository; + +import cn.structure.infra.properties.InfraProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +public class MultiRepositoryBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware, org.springframework.core.Ordered, org.springframework.beans.factory.SmartInitializingSingleton { + + private DefaultListableBeanFactory beanFactory; + + private final Map, Map>> delegateRegistry = new ConcurrentHashMap<>(); + + private final Map, RepositoryType> typeCache = new ConcurrentHashMap<>(); + + private final List> unresolvedDelegates = new ArrayList<>(); + + private InfraProperties infraProperties; + + public MultiRepositoryBeanPostProcessor() { + log.info("MultiRepositoryBeanPostProcessor created"); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + log.info("MultiRepositoryBeanPostProcessor.setApplicationContext() called"); + this.beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); + this.infraProperties = applicationContext.getBean(InfraProperties.class); + log.info("MultiRepositoryBeanPostProcessor.infraProperties: {}", infraProperties); + log.info("MultiRepositoryBeanPostProcessor.multiRepositoryEnabled: {}", infraProperties.getMultiRepositoryEnabled()); + } + + @Override + public int getOrder() { + return org.springframework.core.Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public void afterSingletonsInstantiated() { + log.info("MultiRepositoryBeanPostProcessor.afterSingletonsInstantiated() called"); + + for (RepositoryDelegate delegate : unresolvedDelegates) { + try { + processRepositoryDelegate(delegate, delegate.getClass().getSimpleName()); + } catch (Exception e) { + log.warn("Failed to process delegate after singletons instantiated: {}", e.getMessage()); + } + } + unresolvedDelegates.clear(); + + log.info("delegateRegistry size: {}", delegateRegistry.size()); + for (Map.Entry, Map>> entry : delegateRegistry.entrySet()) { + log.info(" Entity: {}, delegates: {}", entry.getKey().getSimpleName(), entry.getValue().keySet()); + } + if (Boolean.TRUE.equals(infraProperties.getMultiRepositoryEnabled())) { + log.info("Multi-repository enabled, injecting delegates to MultiRepositoryFacade..."); + injectDelegatesToMultiFacades(); + } + } + + @SuppressWarnings("unchecked") + private void injectDelegatesToMultiFacades() { + Map allBeans = beanFactory.getBeansWithAnnotation(Component.class); + log.info("Found {} Component beans", allBeans.size()); + + for (Map.Entry entry : allBeans.entrySet()) { + Object bean = entry.getValue(); + if (isMultiRepositoryFacade(bean.getClass())) { + MultiRepositoryFacade facade = (MultiRepositoryFacade) bean; + log.info("Found MultiRepositoryFacade subclass: {}", entry.getKey()); + try { + Type[] typeArgs = resolveTypeArguments(facade.getClass(), MultiRepositoryFacade.class); + if (typeArgs != null && typeArgs.length >= 2) { + Class entityClass = getRawType(typeArgs[0]); + log.info("MultiRepositoryFacade [{}] entityClass: {}", entry.getKey(), entityClass); + + if (entityClass == null) { + log.warn("Entity class is null for MultiRepositoryFacade [{}]", entry.getKey()); + continue; + } + + Map> typeDelegates = delegateRegistry.get(entityClass); + if (typeDelegates != null && !typeDelegates.isEmpty()) { + for (Map.Entry> delegateEntry : typeDelegates.entrySet()) { + facade.registerDelegate(delegateEntry.getKey(), (RepositoryDelegate) delegateEntry.getValue()); + } + + RepositoryType defaultType = infraProperties.getDefaultRepositoryType(); + if (defaultType != null && defaultType != RepositoryType.AUTO) { + facade.setDefaultType(defaultType); + } + + RepositoryDelegate firstDelegate = (RepositoryDelegate) typeDelegates.values().iterator().next(); + facade.setDefaultDelegate((RepositoryDelegate) firstDelegate); + + log.info("Injected {} delegates to MultiRepositoryFacade [{}]", typeDelegates.size(), entry.getKey()); + } else { + log.warn("No delegates found for entity class: {}", entityClass); + } + } else { + log.warn("Cannot resolve type arguments for {}", entry.getKey()); + } + } catch (Exception e) { + log.error("Failed to inject delegates to MultiRepositoryFacade [{}]: {}", entry.getKey(), e.getMessage(), e); + } + } + } + } + + private boolean isMultiRepositoryFacade(Class clazz) { + if (clazz == null || clazz == Object.class) { + return false; + } + if (MultiRepositoryFacade.class.isAssignableFrom(clazz)) { + return true; + } + return isMultiRepositoryFacade(clazz.getSuperclass()); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof RepositoryDelegate) { + log.info("Processing RepositoryDelegate bean: {}, type: {}", beanName, bean.getClass().getName()); + try { + processRepositoryDelegate((RepositoryDelegate) bean, beanName); + } catch (Exception e) { + log.warn("Cannot resolve entity class for delegate {} during initialization, will retry later: {}", beanName, e.getMessage()); + unresolvedDelegates.add((RepositoryDelegate) bean); + } + } + return bean; + } + + private Type[] resolveTypeArguments(Class clazz, Class targetClass) { + Type genericSuperclass = clazz.getGenericSuperclass(); + while (genericSuperclass != null) { + if (genericSuperclass instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + Type rawType = parameterizedType.getRawType(); + if (targetClass.equals(rawType)) { + return parameterizedType.getActualTypeArguments(); + } + } + clazz = clazz.getSuperclass(); + if (clazz == null) { + break; + } + genericSuperclass = clazz.getGenericSuperclass(); + } + return null; + } + + private Class getRawType(Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + return (Class) ((ParameterizedType) type).getRawType(); + } + return null; + } + + private Field findField(Class clazz, String fieldName) { + while (clazz != null) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + return null; + } + + @SuppressWarnings("unchecked") + private void processRepositoryDelegate(RepositoryDelegate delegate, String beanName) { + Class entityClass = delegate.getEntityClass(); + if (entityClass == null) { + log.warn("Cannot resolve entity class for delegate: {}", beanName); + return; + } + + RepositoryType type = inferRepositoryType(delegate.getClass()); + log.info("Processing repository delegate: entity={}, type={}, bean={}", + entityClass.getSimpleName(), type, beanName); + + delegateRegistry.computeIfAbsent(entityClass, k -> new ConcurrentHashMap<>()) + .put(type, delegate); + } + + private RepositoryType inferRepositoryType(Class delegateClass) { + if (typeCache.containsKey(delegateClass)) { + return typeCache.get(delegateClass); + } + + cn.structure.infra.annotations.RepositoryTypeAnnotation annotation = + AnnotationUtils.findAnnotation(delegateClass, cn.structure.infra.annotations.RepositoryTypeAnnotation.class); + if (annotation != null) { + RepositoryType type = (RepositoryType) annotation.value(); + typeCache.put(delegateClass, type); + return type; + } + + RepositoryType typeFromSuperclass = findTypeFromSuperclass(delegateClass); + if (typeFromSuperclass != null) { + typeCache.put(delegateClass, typeFromSuperclass); + return typeFromSuperclass; + } + + RepositoryType typeFromInterface = findTypeFromInterface(delegateClass); + if (typeFromInterface != null) { + typeCache.put(delegateClass, typeFromInterface); + return typeFromInterface; + } + + String className = delegateClass.getSimpleName().toUpperCase(); + if (className.contains("JPA")) { + typeCache.put(delegateClass, RepositoryType.JPA); + return RepositoryType.JPA; + } else if (className.contains("MYBATIS")) { + typeCache.put(delegateClass, RepositoryType.MYBATIS_PLUS); + return RepositoryType.MYBATIS_PLUS; + } else if (className.contains("MONGO")) { + typeCache.put(delegateClass, RepositoryType.MONGODB); + return RepositoryType.MONGODB; + } else if (className.contains("ELASTICSEARCH")) { + typeCache.put(delegateClass, RepositoryType.ELASTICSEARCH); + return RepositoryType.ELASTICSEARCH; + } + + typeCache.put(delegateClass, RepositoryType.AUTO); + return RepositoryType.AUTO; + } + + private RepositoryType findTypeFromSuperclass(Class clazz) { + Class superclass = clazz.getSuperclass(); + while (superclass != null && superclass != Object.class) { + cn.structure.infra.annotations.RepositoryTypeAnnotation annotation = + AnnotationUtils.findAnnotation(superclass, cn.structure.infra.annotations.RepositoryTypeAnnotation.class); + if (annotation != null) { + return annotation.value(); + } + + RepositoryType type = matchBySuperclassType(superclass); + if (type != null) { + return type; + } + + superclass = superclass.getSuperclass(); + } + return null; + } + + private RepositoryType findTypeFromInterface(Class clazz) { + Class[] interfaces = clazz.getInterfaces(); + for (Class iface : interfaces) { + cn.structure.infra.annotations.RepositoryTypeAnnotation annotation = + AnnotationUtils.findAnnotation(iface, cn.structure.infra.annotations.RepositoryTypeAnnotation.class); + if (annotation != null) { + return annotation.value(); + } + + RepositoryType type = findTypeFromInterface(iface); + if (type != null) { + return type; + } + } + return null; + } + + private RepositoryType matchBySuperclassType(Class superclass) { + String className = superclass.getName(); + if (className.contains("JpaRepositoryDelegate")) { + return RepositoryType.JPA; + } else if (className.contains("MybatisPlusRepositoryDelegate")) { + return RepositoryType.MYBATIS_PLUS; + } else if (className.contains("MongoRepositoryDelegate")) { + return RepositoryType.MONGODB; + } else if (className.contains("ElasticsearchRepositoryDelegate")) { + return RepositoryType.ELASTICSEARCH; + } + return null; + } +} \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java new file mode 100644 index 0000000..68b7dec --- /dev/null +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java @@ -0,0 +1,130 @@ +package cn.structure.infra.repository; + +import cn.structure.common.repository.ICrudRepository; +import cn.structure.common.vo.ReqPage; +import cn.structure.common.vo.ResPage; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Slf4j +public class MultiRepositoryFacade> extends RepositoryFacade { + + @Getter + @Setter + private Map delegates = new HashMap<>(); + + @Getter + @Setter + private RepositoryType defaultType = RepositoryType.AUTO; + + @Getter + @Setter + private D defaultDelegate; + + public void registerDelegate(RepositoryType type, D delegate) { + delegates.put(type, delegate); + log.info("Registered repository delegate: type={}, delegate={}", type, delegate.getClass().getSimpleName()); + } + + public D getCurrentDelegate() { + RepositoryType contextType = RepositoryTypeContext.get(); + if (contextType != null && delegates.containsKey(contextType)) { + log.debug("Using context repository type: {}", contextType); + return delegates.get(contextType); + } + + if (defaultType != RepositoryType.AUTO && delegates.containsKey(defaultType)) { + log.debug("Using default repository type: {}", defaultType); + return delegates.get(defaultType); + } + + if (defaultDelegate != null) { + log.debug("Using default delegate"); + return defaultDelegate; + } + + if (!delegates.isEmpty()) { + D firstDelegate = delegates.values().iterator().next(); + log.debug("Using first available delegate: {}", firstDelegate.getClass().getSimpleName()); + return firstDelegate; + } + + throw new IllegalStateException("No repository delegate available"); + } + + @Override + public T save(T entity) { + return getCurrentDelegate().save(entity); + } + + @Override + public void removeById(ID id) { + getCurrentDelegate().removeById(id); + } + + @Override + public T findById(ID id) { + return getCurrentDelegate().findById(id); + } + + @Override + public T queryById(ID id) { + return getCurrentDelegate().queryById(id); + } + + @Override + public Optional queryByIdOptional(ID id) { + return getCurrentDelegate().queryByIdOptional(id); + } + + @Override + public T queryOne(T entity) { + return getCurrentDelegate().queryOne(entity); + } + + @Override + public Optional queryOneOptional(T entity) { + return getCurrentDelegate().queryOneOptional(entity); + } + + @Override + public List queryList(T entity) { + return getCurrentDelegate().queryList(entity); + } + + @Override + public ResPage queryPage(ReqPage reqPage) { + return getCurrentDelegate().queryPage(reqPage); + } + + @Override + public List saveBatch(List entities) { + return getCurrentDelegate().saveBatch(entities); + } + + @Override + public void removeBatchByIds(List ids) { + getCurrentDelegate().removeBatchByIds(ids); + } + + @Override + public List listByIds(List ids) { + return getCurrentDelegate().listByIds(ids); + } + + @Override + public long count(T entity) { + return getCurrentDelegate().count(entity); + } + + @Override + public boolean exists(T entity) { + return getCurrentDelegate().exists(entity); + } +} \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java index 80f2562..8ae763a 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java @@ -2,6 +2,7 @@ import cn.structure.infra.annotations.ReadDelegate; import cn.structure.infra.annotations.WriteDelegate; +import cn.structure.infra.properties.InfraProperties; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Qualifier; @@ -22,25 +23,41 @@ @Slf4j @Component -public class RepositoryBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { +public class RepositoryBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware, org.springframework.beans.factory.SmartInitializingSingleton { private DefaultListableBeanFactory beanFactory; + private InfraProperties infraProperties; + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); + this.infraProperties = applicationContext.getBean(InfraProperties.class); } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (Boolean.TRUE.equals(infraProperties.getMultiRepositoryEnabled())) { + return bean; + } if (bean instanceof CqrsRepositoryFacade) { processCqrsFacade((CqrsRepositoryFacade) bean, beanName); - } else if (bean instanceof RepositoryFacade) { - processRepositoryFacade((RepositoryFacade) bean, beanName); } return bean; } + @Override + public void afterSingletonsInstantiated() { + if (Boolean.TRUE.equals(infraProperties.getMultiRepositoryEnabled())) { + log.info("Multi-repository enabled, skipping RepositoryBeanPostProcessor injection"); + return; + } + Map facades = beanFactory.getBeansOfType(RepositoryFacade.class); + for (Map.Entry entry : facades.entrySet()) { + processRepositoryFacade(entry.getValue(), entry.getKey()); + } + } + private void processCqrsFacade(CqrsRepositoryFacade facade, String beanName) { Type[] typeArgs = resolveTypeArguments(facade.getClass(), CqrsRepositoryFacade.class); if (typeArgs == null || typeArgs.length < 4) { @@ -95,7 +112,7 @@ private Object findWriteDelegate(Class delegateType) { } List candidates = new ArrayList<>(beans.values()); - + Object writeDelegate = findByAnnotation(candidates, WriteDelegate.class); if (writeDelegate != null) { return writeDelegate; diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java index b5784e3..762355f 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java @@ -6,15 +6,17 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import java.util.List; import java.util.Optional; @Getter -@Setter -@Slf4j -public class RepositoryFacade> implements ICrudRepository { + @Setter + @Slf4j + public class RepositoryFacade> implements ICrudRepository { + @Autowired protected D delegate; protected Class entityClass; diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryTypeContext.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryTypeContext.java new file mode 100644 index 0000000..a093d2f --- /dev/null +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryTypeContext.java @@ -0,0 +1,38 @@ +package cn.structure.infra.repository; + +import lombok.extern.slf4j.Slf4j; + +import java.io.Closeable; + +@Slf4j +public class RepositoryTypeContext implements Closeable { + + private static final ThreadLocal context = new ThreadLocal<>(); + + private RepositoryTypeContext(RepositoryType type) { + context.set(type); + } + + public static RepositoryType get() { + return context.get(); + } + + public static void set(RepositoryType type) { + context.set(type); + log.debug("Set repository type context: {}", type); + } + + public static void clear() { + context.remove(); + } + + public static RepositoryTypeContext use(RepositoryType type) { + return new RepositoryTypeContext(type); + } + + @Override + public void close() { + clear(); + log.debug("Cleared repository type context"); + } +} \ No newline at end of file From 7278473456931f3b0a70c790baa6d6ad3bd0e2c4 Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Wed, 22 Jul 2026 08:03:59 +0800 Subject: [PATCH 2/6] =?UTF-8?q?refactor(repository):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=A4=9A=E4=BB=93=E5=BA=93=E9=85=8D=E7=BD=AE=E5=92=8C=E5=A7=94?= =?UTF-8?q?=E6=89=98=E5=A4=84=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在JpaRepositoryDelegate中为所有公共方法添加getPoClass()调用确保实体类初始化 - 重构MultiRepositoryBeanPostProcessor设置默认委托到父类字段并改进日志记录 - 修改MultiRepositoryFacade移除多余字段并将默认委托改为继承父类字段 - 更新RepositoryBeanPostProcessor仅处理CQRS场景的读取委托注入 - 在测试配置中添加组件扫描过滤器排除特定仓库实现避免冲突 - 启用mybatis模块并为多仓库示例添加主委托注解 - 移除构造函数注解改用显式构造函数初始化mapper --- .../jpa/repository/JpaRepositoryDelegate.java | 10 ++++ structure-infra-sample/pom.xml | 2 +- .../delegate/write/UserWriteDelegate.java | 7 +++ .../sample/cqrs/config/CqrsTestConfig.java | 5 ++ .../mybatis/UserMybatisPlusDelegate.java | 2 + .../mybatis/UserMybatisPlusDelegate.java | 7 ++- .../sample/config/MybatisOnlyConfig.java | 2 + .../MultiRepositoryBeanPostProcessor.java | 25 ++++++++-- .../repository/MultiRepositoryFacade.java | 27 +++++------ .../RepositoryBeanPostProcessor.java | 48 ++----------------- 10 files changed, 71 insertions(+), 64 deletions(-) diff --git a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java index c3497dd..138562c 100644 --- a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java +++ b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java @@ -170,6 +170,7 @@ private Field findFieldWithAnnotation(Class clazz, Class annotationClass) @Override public E save(E entity) { + getPoClass(); if (entity == null || entityManager == null || poClass == null) { return null; } @@ -181,6 +182,7 @@ public E save(E entity) { @Override public void removeById(ID id) { + getPoClass(); if (id != null && entityManager != null && poClass != null) { P po = entityManager.find(poClass, id); if (po != null) { @@ -192,6 +194,7 @@ public void removeById(ID id) { @Override public E findById(ID id) { + getPoClass(); if (id == null || entityManager == null || poClass == null) { return null; } @@ -234,6 +237,7 @@ public List queryList(E condition) { @Override public ResPage queryPage(ReqPage reqPage) { + getPoClass(); if (entityManager == null || poClass == null) { ResPage emptyPage = new ResPage<>(); emptyPage.setCurrent(1L); @@ -286,6 +290,7 @@ private long executeCountQuery(CriteriaBuilder cb) { } private List findAll() { + getPoClass(); if (entityManager == null || poClass == null) { return List.of(); } @@ -298,6 +303,7 @@ private List findAll() { } private List queryByCondition(E condition) { + getPoClass(); if (entityManager == null || poClass == null) { return List.of(); } @@ -362,6 +368,8 @@ public void removeBatchByIds(List ids) { @Override public List listByIds(List ids) { + getPoClass(); + getIdFieldName(); if (ids == null || ids.isEmpty() || entityManager == null || poClass == null) { return List.of(); } @@ -379,6 +387,7 @@ public List listByIds(List ids) { @Override public long count(E condition) { + getPoClass(); if (entityManager == null || poClass == null) { return 0; } @@ -426,6 +435,7 @@ protected P toPo(E entity) { if (entity == null) { return null; } + getPoClass(); if (poClass == null) { return (P) entity; } diff --git a/structure-infra-sample/pom.xml b/structure-infra-sample/pom.xml index da30059..5d52b18 100644 --- a/structure-infra-sample/pom.xml +++ b/structure-infra-sample/pom.xml @@ -22,7 +22,7 @@ structure-infra-sample-core - + structure-infra-sample-mybatis structure-infra-sample-jpa structure-infra-sample-mongodb structure-infra-sample-elasticsearch diff --git a/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/delegate/write/UserWriteDelegate.java b/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/delegate/write/UserWriteDelegate.java index 0107b15..c0fe8c0 100644 --- a/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/delegate/write/UserWriteDelegate.java +++ b/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/delegate/write/UserWriteDelegate.java @@ -3,9 +3,11 @@ import cn.structure.infra.annotations.WriteDelegate; import cn.structure.infra.mybatis.plus.repository.MybatisPlusRepositoryDelegate; import cn.structure.infra.sample.domain.entity.UserEntity; +import cn.structure.infra.sample.infra.mapper.UserMapper; import cn.structure.infra.sample.infra.po.MybatisUserPO; import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; /** @@ -23,9 +25,14 @@ */ @Slf4j @Component +@Primary @WriteDelegate public class UserWriteDelegate extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate { + public UserWriteDelegate(UserMapper userMapper) { + this.baseMapper = userMapper; + } + @Override public UserEntity findByName(String name) { return null; diff --git a/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/config/CqrsTestConfig.java b/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/config/CqrsTestConfig.java index 8cea5f4..2fd510e 100644 --- a/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/config/CqrsTestConfig.java +++ b/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/config/CqrsTestConfig.java @@ -9,6 +9,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.Import; /** @@ -50,6 +51,10 @@ "cn.structure.infra.repository", "cn.structure.infra.mybatis.plus", "cn.structure.infra.elasticsearch" +}, excludeFilters = { + @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.mybatis.*"), + @ComponentScan.Filter(type = FilterType.REGEX, pattern = "cn.structure.infra.sample.infra.repository.jpa.*"), + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = cn.structure.infra.sample.InfraSampleApplication.class) }) @MapperScan("cn.structure.infra.sample.infra.mapper") @Import({ diff --git a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mybatis/UserMybatisPlusDelegate.java b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mybatis/UserMybatisPlusDelegate.java index d31b18a..e85f9f1 100644 --- a/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mybatis/UserMybatisPlusDelegate.java +++ b/structure-infra-sample/structure-infra-sample-multi/src/main/java/cn/structure/infra/sample/multi/infra/repository/mybatis/UserMybatisPlusDelegate.java @@ -8,10 +8,12 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import java.util.List; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; @Slf4j @Component +@Primary public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate { public UserMybatisPlusDelegate(UserMapper userMapper) { diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/mybatis/UserMybatisPlusDelegate.java b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/mybatis/UserMybatisPlusDelegate.java index 9b7a3db..c5a50a5 100644 --- a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/mybatis/UserMybatisPlusDelegate.java +++ b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/mybatis/UserMybatisPlusDelegate.java @@ -6,7 +6,6 @@ import cn.structure.infra.sample.infra.po.MybatisUserPO; import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate; import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -25,11 +24,15 @@ */ @Slf4j @Component -@AllArgsConstructor public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate { private final UserMapper userMapper; + public UserMybatisPlusDelegate(UserMapper userMapper) { + this.userMapper = userMapper; + this.baseMapper = userMapper; + } + /** * 根据用户名查询用户 *

diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/MybatisOnlyConfig.java b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/MybatisOnlyConfig.java index b0909f0..c47349a 100644 --- a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/MybatisOnlyConfig.java +++ b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/MybatisOnlyConfig.java @@ -4,12 +4,14 @@ import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; @Configuration +@EnableAutoConfiguration @ComponentScan(basePackages = { "cn.structure.infra.sample", "cn.structure.infra.repository" diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java index 98eeeb0..f7e5f11 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryBeanPostProcessor.java @@ -104,10 +104,15 @@ private void injectDelegatesToMultiFacades() { facade.setDefaultType(defaultType); } - RepositoryDelegate firstDelegate = (RepositoryDelegate) typeDelegates.values().iterator().next(); - facade.setDefaultDelegate((RepositoryDelegate) firstDelegate); + // 设置默认 delegate 到父类 RepositoryFacade 的 delegate 字段 + RepositoryDelegate defaultDelegate = typeDelegates.values().iterator().next(); + if (defaultType != null && defaultType != RepositoryType.AUTO && typeDelegates.containsKey(defaultType)) { + defaultDelegate = typeDelegates.get(defaultType); + } + setParentDelegate(facade, defaultDelegate); - log.info("Injected {} delegates to MultiRepositoryFacade [{}]", typeDelegates.size(), entry.getKey()); + log.info("Injected {} delegates to MultiRepositoryFacade [{}], default delegate: {}", + typeDelegates.size(), entry.getKey(), defaultDelegate.getClass().getSimpleName()); } else { log.warn("No delegates found for entity class: {}", entityClass); } @@ -184,6 +189,20 @@ private Field findField(Class clazz, String fieldName) { return null; } + @SuppressWarnings("unchecked") + private void setParentDelegate(MultiRepositoryFacade facade, RepositoryDelegate delegate) { + try { + Field field = findField(facade.getClass(), "delegate"); + if (field != null) { + field.setAccessible(true); + field.set(facade, delegate); + log.debug("Set parent delegate [{}] to MultiRepositoryFacade", delegate.getClass().getSimpleName()); + } + } catch (Exception e) { + log.warn("Failed to set parent delegate: {}", e.getMessage()); + } + } + @SuppressWarnings("unchecked") private void processRepositoryDelegate(RepositoryDelegate delegate, String beanName) { Class entityClass = delegate.getEntityClass(); diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java index 68b7dec..080a489 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/MultiRepositoryFacade.java @@ -7,26 +7,17 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; @Slf4j public class MultiRepositoryFacade> extends RepositoryFacade { - @Getter - @Setter - private Map delegates = new HashMap<>(); + private final Map delegates = new HashMap<>(); @Getter @Setter private RepositoryType defaultType = RepositoryType.AUTO; - @Getter - @Setter - private D defaultDelegate; - public void registerDelegate(RepositoryType type, D delegate) { delegates.put(type, delegate); log.info("Registered repository delegate: type={}, delegate={}", type, delegate.getClass().getSimpleName()); @@ -44,9 +35,9 @@ public D getCurrentDelegate() { return delegates.get(defaultType); } - if (defaultDelegate != null) { - log.debug("Using default delegate"); - return defaultDelegate; + if (getDelegate() != null) { + log.debug("Using default delegate from parent"); + return getDelegate(); } if (!delegates.isEmpty()) { @@ -58,6 +49,14 @@ public D getCurrentDelegate() { throw new IllegalStateException("No repository delegate available"); } + public boolean hasDelegate(RepositoryType type) { + return delegates.containsKey(type); + } + + public Set getRegisteredTypes() { + return Collections.unmodifiableSet(delegates.keySet()); + } + @Override public T save(T entity) { return getCurrentDelegate().save(entity); diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java index 8ae763a..76bc73e 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java @@ -23,7 +23,7 @@ @Slf4j @Component -public class RepositoryBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware, org.springframework.beans.factory.SmartInitializingSingleton { +public class RepositoryBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { private DefaultListableBeanFactory beanFactory; @@ -40,43 +40,21 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw if (Boolean.TRUE.equals(infraProperties.getMultiRepositoryEnabled())) { return bean; } + // 只处理 CQRS 场景,且只注入 readDelegate if (bean instanceof CqrsRepositoryFacade) { - processCqrsFacade((CqrsRepositoryFacade) bean, beanName); + processCqrsReadDelegate((CqrsRepositoryFacade) bean, beanName); } return bean; } - @Override - public void afterSingletonsInstantiated() { - if (Boolean.TRUE.equals(infraProperties.getMultiRepositoryEnabled())) { - log.info("Multi-repository enabled, skipping RepositoryBeanPostProcessor injection"); - return; - } - Map facades = beanFactory.getBeansOfType(RepositoryFacade.class); - for (Map.Entry entry : facades.entrySet()) { - processRepositoryFacade(entry.getValue(), entry.getKey()); - } - } - - private void processCqrsFacade(CqrsRepositoryFacade facade, String beanName) { + private void processCqrsReadDelegate(CqrsRepositoryFacade facade, String beanName) { Type[] typeArgs = resolveTypeArguments(facade.getClass(), CqrsRepositoryFacade.class); if (typeArgs == null || typeArgs.length < 4) { log.warn("Cannot resolve generic types for CqrsRepositoryFacade: {}", beanName); return; } - Class writeDelegateType = getRawType(typeArgs[2]); Class readDelegateType = getRawType(typeArgs[3]); - - if (writeDelegateType != null) { - Object writeDelegate = findWriteDelegate(writeDelegateType); - if (writeDelegate != null) { - setDelegate(facade, "delegate", writeDelegate); - log.info("Injected write delegate [{}] to CqrsRepositoryFacade [{}]", - writeDelegate.getClass().getSimpleName(), beanName); - } - } - if (readDelegateType != null) { Object readDelegate = findReadDelegate(readDelegateType); if (readDelegate != null) { @@ -87,24 +65,6 @@ private void processCqrsFacade(CqrsRepositoryFacade facade, String b } } - private void processRepositoryFacade(RepositoryFacade facade, String beanName) { - Type[] typeArgs = resolveTypeArguments(facade.getClass(), RepositoryFacade.class); - if (typeArgs == null || typeArgs.length < 3) { - log.warn("Cannot resolve generic types for RepositoryFacade: {}", beanName); - return; - } - - Class delegateType = getRawType(typeArgs[2]); - if (delegateType != null) { - Object delegate = findWriteDelegate(delegateType); - if (delegate != null) { - setDelegate(facade, "delegate", delegate); - log.info("Injected delegate [{}] to RepositoryFacade [{}]", - delegate.getClass().getSimpleName(), beanName); - } - } - } - private Object findWriteDelegate(Class delegateType) { Map beans = beanFactory.getBeansOfType(delegateType); if (beans.isEmpty()) { From d8e8b120226cee0c880653cad6911a96b7753163 Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Wed, 22 Jul 2026 08:05:03 +0800 Subject: [PATCH 3/6] =?UTF-8?q?chore(version):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E7=89=88=E6=9C=AC=E5=88=B01.3.0-SNAPSHOT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将pom.xml中的revision从1.2.0-SNAPSHOT更新到1.3.0-SNAPSHOT - 更新README.md中所有结构化基础设施启动器的版本号到1.3.0 - 包括MyBatis Plus、JPA、MongoDB、Elasticsearch等适配器 - 包含本地任务调度和XXL-Job分布式任务调度组件 - 更新流式事件路由和其他相关依赖版本 --- README.md | 16 ++++++++-------- pom.xml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6a30457..134124c 100644 --- a/README.md +++ b/README.md @@ -223,56 +223,56 @@ mvn spring-boot:run -pl structure-infra-sample/structure-infra-sample-schedule cn.structured structure-infra-starter - 1.2.0 + 1.3.0 cn.structured structure-infra-mybatis-plus-starter - 1.2.0 + 1.3.0 cn.structured structure-infra-jpa-starter - 1.2.0 + 1.3.0 cn.structured structure-infra-mongodb-starter - 1.2.0 + 1.3.0 cn.structured structure-infra-elasticsearch-starter - 1.2.0 + 1.3.0 cn.structured structure-infra-schedule-starter - 1.2.0 + 1.3.0 cn.structured structure-infra-xxljob-starter - 1.2.0 + 1.3.0 cn.structured structure-infra-stream-starter - 1.2.0 + 1.3.0 ``` diff --git a/pom.xml b/pom.xml index 0a03bca..03149c5 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ pro项目父工程 - 1.2.0-SNAPSHOT + 1.3.0-SNAPSHOT 4.0.6 5.0.0 3.5.16 From a4a04453eda2f11e92f06b4216eaab4c631e9a83 Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Wed, 22 Jul 2026 08:12:50 +0800 Subject: [PATCH 4/6] =?UTF-8?q?refactor(elasticsearch):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96ElasticsearchRepositoryDelegate=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E6=B3=9B=E5=9E=8B=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将getPoClass方法的返回类型从Class更改为Class

以提高类型安全性 - 通过使用泛型类型参数P来消除原始类型警告 - 改进代码的类型检查和编译时验证 - 提升代码可读性和维护性 - 遵循Java泛型最佳实践原则 --- .../repository/ElasticsearchRepositoryDelegate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java index a547a3c..2226687 100644 --- a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java +++ b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java @@ -79,7 +79,7 @@ public Class getEntityClass() { } @Override - public Class getPoClass() { + public Class

getPoClass() { if (poClass == null) { synchronized (this) { if (poClass == null) { From c99788a0cbb84fc04911a54c5a621f9a7e2315da Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Wed, 22 Jul 2026 08:41:18 +0800 Subject: [PATCH 5/6] =?UTF-8?q?refactor(repository):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=AE=9E=E4=BD=93=E7=B1=BB=E8=8E=B7=E5=8F=96=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=B9=B6=E6=94=B9=E8=BF=9BMongoDB=E6=B5=8B=E8=AF=95=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在ElasticsearchRepositoryDelegate中使用getEntityClass()方法替代entityClass字段 - 在JpaRepositoryDelegate中使用getEntityClass()和getPoClass()方法替代直接字段访问 - 重构MongoRepositoryDelegate中的导入顺序和无用导入清理 - 为MockMongoConfiguration添加批量插入操作的模拟实现 - 改进MongoDB测试中的ID生成和数据存储逻辑 --- .../ElasticsearchRepositoryDelegate.java | 10 +++--- .../jpa/repository/JpaRepositoryDelegate.java | 11 ++++--- .../repository/MongoRepositoryDelegate.java | 3 +- .../config/MockMongoConfiguration.java | 31 +++++++++++++++++++ 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java index 2226687..d63e85b 100644 --- a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java +++ b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java @@ -344,11 +344,12 @@ protected E toEntity(P po) { if (po == null) { return null; } - if (entityClass == null) { + Class entityType = getEntityClass(); + if (entityType == null) { return (E) po; } try { - E entity = entityClass.getDeclaredConstructor().newInstance(); + E entity = entityType.getDeclaredConstructor().newInstance(); BeanUtils.copyProperties(po, entity); return entity; } catch (Exception e) { @@ -360,11 +361,12 @@ protected P toPo(E entity) { if (entity == null) { return null; } - if (poClass == null) { + Class

poType = getPoClass(); + if (poType == null) { return (P) entity; } try { - P po = poClass.getDeclaredConstructor().newInstance(); + P po = poType.getDeclaredConstructor().newInstance(); BeanUtils.copyProperties(entity, po); return po; } catch (Exception e) { diff --git a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java index 138562c..5f09627 100644 --- a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java +++ b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java @@ -419,11 +419,12 @@ protected E toEntity(P po) { if (po == null) { return null; } - if (entityClass == null) { + Class entityType = getEntityClass(); + if (entityType == null) { return (E) po; } try { - E entity = entityClass.getDeclaredConstructor().newInstance(); + E entity = entityType.getDeclaredConstructor().newInstance(); BeanUtils.copyProperties(po, entity); return entity; } catch (Exception e) { @@ -435,12 +436,12 @@ protected P toPo(E entity) { if (entity == null) { return null; } - getPoClass(); - if (poClass == null) { + Class

poType = getPoClass(); + if (poType == null) { return (P) entity; } try { - P po = poClass.getDeclaredConstructor().newInstance(); + P po = poType.getDeclaredConstructor().newInstance(); BeanUtils.copyProperties(entity, po); return po; } catch (Exception e) { diff --git a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java index 5e2d93d..0eaef76 100644 --- a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java +++ b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java @@ -4,6 +4,7 @@ import cn.structure.common.vo.ResPage; import cn.structure.infra.repository.GenericTypeResolver; import cn.structure.infra.repository.RepositoryDelegate; +import jakarta.persistence.Id; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -12,9 +13,7 @@ import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.core.query.Update; -import jakarta.persistence.Id; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MockMongoConfiguration.java b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MockMongoConfiguration.java index 270f1f2..fd2f544 100644 --- a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MockMongoConfiguration.java +++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/config/MockMongoConfiguration.java @@ -66,6 +66,37 @@ private MongoConverter createMongoConverter() { private MongoTemplate createMongoTemplate() { MongoTemplate template = mock(MongoTemplate.class); + when(template.insert(anyList(), any(Class.class))).thenAnswer(invocation -> { + List poList = invocation.getArgument(0); + for (Object po : poList) { + Class poClass = po.getClass(); + Map classStore = dataStore.computeIfAbsent(poClass, k -> new HashMap<>()); + try { + Field idField = findIdField(poClass); + if (idField != null) { + idField.setAccessible(true); + Object idValue = idField.get(po); + Long id = null; + if (idValue == null || (idValue instanceof Number && ((Number) idValue).longValue() == 0)) { + id = idGenerator++; + setIdValue(po, id); + } else if (idValue instanceof Long) { + id = (Long) idValue; + } else if (idValue instanceof String) { + id = Long.valueOf((String) idValue); + } else if (idValue instanceof Number) { + id = ((Number) idValue).longValue(); + } + if (id != null) { + classStore.put(id, po); + } + } + } catch (Exception ignored) { + } + } + return poList; + }); + when(template.save(any())).thenAnswer(invocation -> { Object po = invocation.getArgument(0); Class poClass = po.getClass(); From 98701fc666298c8430b3a5157c766b148f7bad9b Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Wed, 22 Jul 2026 12:03:33 +0800 Subject: [PATCH 6/6] =?UTF-8?q?refactor(repository):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E5=A7=94=E6=89=98=E7=B1=BB=E7=9A=84=E6=B3=9B?= =?UTF-8?q?=E5=9E=8B=E7=B1=BB=E5=9E=8B=E5=AE=89=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 JpaRepositoryDelegate 中 getPoClass 方法的返回类型从 Class 改为 Class

- 在 JpaRepositoryDelegate 中添加 SuppressWarnings 注解以消除类型转换警告 - 将 MongoRepositoryDelegate 中 getPoClass 方法的返回类型从 Class 改为 Class

- 将 MybatisPlusRepositoryDelegate 中 getPoClass 方法的返回类型从 Class 改为 Class

- 提高了代码的类型安全性并减少了潜在的运行时类型转换异常 --- .../infra/jpa/repository/JpaRepositoryDelegate.java | 5 +++-- .../infra/mongodb/repository/MongoRepositoryDelegate.java | 2 +- .../plus/repository/MybatisPlusRepositoryDelegate.java | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java index 5f09627..efbf3b5 100644 --- a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java +++ b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java @@ -77,7 +77,7 @@ public Class getEntityClass() { } @Override - public Class getPoClass() { + public Class

getPoClass() { if (poClass == null) { synchronized (this) { if (poClass == null) { @@ -436,7 +436,8 @@ protected P toPo(E entity) { if (entity == null) { return null; } - Class

poType = getPoClass(); + @SuppressWarnings("unchecked") + Class

poType = getPoClass(); if (poType == null) { return (P) entity; } diff --git a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java index 0eaef76..1082e7f 100644 --- a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java +++ b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java @@ -48,7 +48,7 @@ public Class getEntityClass() { } @Override - public Class getPoClass() { + public Class

getPoClass() { if (poClass == null) { synchronized (this) { if (poClass == null) { diff --git a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java index eca5d6d..21e29c4 100644 --- a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java +++ b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java @@ -78,7 +78,7 @@ public Class getEntityClass() { } @Override - public Class getPoClass() { + public Class

getPoClass() { if (poClass == null) { synchronized (this) { if (poClass == null) {