From 5b6d037961b90dfc45fa5f831457fee98f9f16a7 Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Fri, 17 Jul 2026 15:01:49 +0800 Subject: [PATCH 1/9] =?UTF-8?q?refactor(infra):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E7=9B=B8=E5=85=B3=E9=85=8D=E7=BD=AE=E5=92=8C?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从 InfraProperties 中移除 cacheTime 和 cacheTimeUnit 属性 - 从 Repository 注解中移除 cache、cacheTime 和 cacheTimeUnit 参数 - 从 RepositoryDefinition 中移除缓存相关字段和设置逻辑 - 从 pom.xml 中移除 structure-datascope-cache 依赖 - 从 README.md 中移除缓存相关配置说明 - 从 RepositoryFacade 中移除 DataScopeCacheManager 字段 - 更新 BeanUtils.copyProperties 文档注释添加完整参数说明 --- structure-infra-starter/README.md | 5 ---- structure-infra-starter/pom.xml | 4 --- .../infra/annotations/Repository.java | 25 ++----------------- .../infra/properties/InfraProperties.java | 20 ++------------- .../repository/RepositoryDefinition.java | 20 +-------------- .../infra/repository/RepositoryFacade.java | 11 +++----- 6 files changed, 9 insertions(+), 76 deletions(-) diff --git a/structure-infra-starter/README.md b/structure-infra-starter/README.md index dd2f2fe..64c0e92 100644 --- a/structure-infra-starter/README.md +++ b/structure-infra-starter/README.md @@ -122,9 +122,6 @@ RepositoryDelegate createDelegate(Class poClass, Class idClass); | `po` | `Class` | `Object.class` | 持久化对象类 | | `id` | `Class` | `Long.class` | 主键类型 | | `description` | String | `""` | 描述 | -| `cache` | boolean | `false` | 是否启用缓存 | -| `cacheTime` | long | `60L` | 缓存 TTL | -| `cacheTimeUnit` | `TimeUnit` | `SECONDS` | 缓存单位 | | `cqrs` | boolean | `false` | 是否启用 CQRS | | `readDelegateClass` | `Class` | `Object.class` | 读代理类(CQRS 时必填) | @@ -385,8 +382,6 @@ public class UserEventListener { |-----|------|-------|------| | `defaultEventChannel` | `EventChannel` | `SPRING_EVENT` | `DEFAULT` 事件的默认通道 | | `cqrs` | `Boolean` | `false` | 全局 CQRS 开关(仅作建议,以 `@Repository` 注解为准) | -| `cacheTime` | `Long` | `60L` | 默认缓存 TTL | -| `cacheTimeUnit` | `TimeUnit` | `SECONDS` | 默认缓存单位 | | `schedulePoolSize` | `Integer` | `Runtime.availableProcessors()` | 调度线程池大小 | | `type` | `RepositoryType` | - | 默认持久化类型(用于触发各 starter 的条件装配) | diff --git a/structure-infra-starter/pom.xml b/structure-infra-starter/pom.xml index 5a0e82c..dd85241 100644 --- a/structure-infra-starter/pom.xml +++ b/structure-infra-starter/pom.xml @@ -32,10 +32,6 @@ org.springframework.boot spring-boot-data-commons - - cn.structured - structure-datascope-cache - cn.structured structure-infra-schedule-starter diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java index 077d350..4273fe6 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java @@ -3,13 +3,13 @@ import cn.structure.infra.repository.RepositoryType; import java.lang.annotation.*; -import java.util.concurrent.TimeUnit; + /** * 仓储标记注解 *

* 标注在 {@link cn.structure.infra.repository.RepositoryFacade} 的子类上, - * 用于声明一个领域仓储及其元数据(实体类型、PO 类型、主键类型、缓存策略、CQRS 配置等)。 + * 用于声明一个领域仓储及其元数据(实体类型、PO 类型、主键类型、CQRS 配置等)。 *

* 框架在启动时通过 {@link cn.structure.infra.repository.RepositoryBeanPostProcessor} * 扫描此注解,并根据配置自动注入对应的 BASE/READ Delegate。 @@ -90,27 +90,6 @@ String description() default ""; - /** - * 是否启用缓存 - * - * @return true 表示启用缓存 - */ - boolean cache() default false; - - /** - * 缓存时间 - * - * @return 缓存过期时间数值 - */ - long cacheTime() default 60L; - - /** - * 缓存时间单位 - * - * @return 缓存时间单位 - */ - TimeUnit cacheTimeUnit() default TimeUnit.SECONDS; - /** * 是否启用 CQRS 读写分离 *

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 ad06abd..ee31b8b 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 @@ -5,12 +5,12 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; -import java.util.concurrent.TimeUnit; + /** * 基础设施框架配置属性 *

- * 对应 YAML 配置前缀:{@code structure.infra},集中管理事件、CQRS、缓存、调度等 + * 对应 YAML 配置前缀:{@code structure.infra},集中管理事件、CQRS、调度等 * 框架级参数。被 {@link cn.structure.infra.configuration.AutoEventConfiguration}、 * {@link cn.structure.infra.configuration.AutoScheduleConfiguration} 等自动装配类引用。 *

@@ -20,8 +20,6 @@ * infra: * default-event-channel: SPRING_EVENT * cqrs: false - * cache-time: 60 - * cache-time-unit: SECONDS * schedule-pool-size: 8 * * @@ -51,20 +49,6 @@ public class InfraProperties { */ private Boolean cqrs = false; - /** - * 默认缓存时间 - * - * @return 缓存过期时间数值 - */ - private Long cacheTime = 60L; - - /** - * 默认缓存时间单位 - * - * @return 缓存时间单位 - */ - private TimeUnit cacheTimeUnit = TimeUnit.SECONDS; - /** * 调度线程池大小,默认 CPU 核心数 * diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java index c1d2b80..e2fce1f 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java @@ -4,7 +4,7 @@ import cn.structure.infra.repository.RepositoryType; import lombok.Data; -import java.util.concurrent.TimeUnit; + /** * 仓储定义元数据 @@ -48,21 +48,6 @@ public class RepositoryDefinition { */ private String description; - /** - * 是否启用缓存 - */ - private boolean cache; - - /** - * 缓存时间 - */ - private long cacheTime; - - /** - * 缓存时间单位 - */ - private TimeUnit cacheTimeUnit; - /** * 是否启用 CQRS 读写分离 *

@@ -101,9 +86,6 @@ public static RepositoryDefinition fromAnnotation(Repository annotation, String definition.setPoClass(annotation.po()); definition.setIdClass(annotation.id()); definition.setDescription(annotation.description()); - definition.setCache(annotation.cache()); - definition.setCacheTime(annotation.cacheTime()); - definition.setCacheTimeUnit(annotation.cacheTimeUnit()); definition.setCqrs(annotation.cqrs()); definition.setReadDelegateClass(annotation.readDelegateClass()); definition.setAnnotation(annotation); 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 308c076..fdf2f99 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 @@ -3,12 +3,12 @@ import cn.structure.common.repository.ICrudRepository; import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; -import cn.structured.datascope.cache.manager.DataScopeCacheManager; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; + import java.util.List; import java.util.Optional; @@ -39,10 +39,7 @@ @Slf4j public class RepositoryFacade> implements ICrudRepository { - /** - * 数据权限缓存管理器(保留扩展点,子类可启用缓存) - */ - protected DataScopeCacheManager cacheManager; + /** * 基础代理 @@ -354,7 +351,7 @@ protected interface ReadOperation { /** * PO → Entity 转换 *

- * 通过反射调用无参构造函数创建 Entity 实例,并使用 {@link BeanUtils#copyProperties} + * 通过反射调用无参构造函数创建 Entity 实例,并使用 {@link BeanUtils#copyProperties(Object, Object)} * 复制同名属性。子类可重写以实现自定义映射逻辑。 * * @param po 持久化对象,为 null 时返回 null @@ -377,7 +374,7 @@ protected T toEntity(P po) { /** * Entity → PO 转换 *

- * 通过反射调用无参构造函数创建 PO 实例,并使用 {@link BeanUtils#copyProperties} + * 通过反射调用无参构造函数创建 PO 实例,并使用 {@link BeanUtils#copyProperties(Object, Object)} * 复制同名属性。子类可重写以实现自定义映射逻辑。 * * @param entity 领域实体,为 null 时返回 null From 7b8f9b3458b1b56f8d5314c4cd014f5907081163 Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Fri, 17 Jul 2026 15:20:34 +0800 Subject: [PATCH 2/9] =?UTF-8?q?feat(stream):=20=E6=B7=BB=E5=8A=A0=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E6=B3=A8=E5=86=8CSpring=20Cloud=20Stream=20binding?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在DefaultStreamEventManagerImpl中注入ConfigurableEnvironment用于动态配置注册 - 新增registerCloudStreamBinding方法将binding配置注册到Spring环境确保StreamBridge能正确找到目标 - 为输入输出绑定自动生成对应的destination、content-type、group和并发配置 - 使用LinkedHashMap维护配置顺序并通过MapPropertySource动态添加到环境最前面 - 更新StreamAutoConfiguration构造方法以传入环境对象 - 修改测试代码适配新的构造函数参数 - 将版本号从1.1.0-SNAPSHOT升级到1.1-SNAPSHOT --- pom.xml | 2 +- .../consumer/StreamMessageConsumer.java | 2 +- .../sample/stream/StreamEventManagerTest.java | 4 +- .../StreamAutoConfiguration.java | 7 +- .../DefaultStreamEventManagerImpl.java | 81 ++++++++++++++++++- 5 files changed, 90 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index fdca93a..4a5d2ac 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ pro项目父工程 - 1.1.0-SNAPSHOT + 1.1.1-SNAPSHOT 4.0.6 5.0.0 3.5.16 diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/consumer/StreamMessageConsumer.java b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/consumer/StreamMessageConsumer.java index 7a09f88..d753654 100644 --- a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/consumer/StreamMessageConsumer.java +++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/consumer/StreamMessageConsumer.java @@ -12,7 +12,7 @@ import java.util.function.Consumer; -@Configuration +@Configuration(proxyBeanMethods = false) public class StreamMessageConsumer { private static final Logger log = LoggerFactory.getLogger(StreamMessageConsumer.class); diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/StreamEventManagerTest.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/StreamEventManagerTest.java index 0a83fc3..660b26b 100644 --- a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/StreamEventManagerTest.java +++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/StreamEventManagerTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.core.env.ConfigurableEnvironment; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; @@ -36,7 +37,8 @@ public void setUp() { streamProperties.getBindings().put("paymentEvent", paymentBinding); StreamBridge mockStreamBridge = mock(StreamBridge.class); - streamEventManager = new DefaultStreamEventManagerImpl(mockStreamBridge, streamProperties); + ConfigurableEnvironment mockEnvironment = mock(ConfigurableEnvironment.class); + streamEventManager = new DefaultStreamEventManagerImpl(mockStreamBridge, streamProperties, mockEnvironment); } @Test diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/configuration/StreamAutoConfiguration.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/configuration/StreamAutoConfiguration.java index f112d03..0557458 100644 --- a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/configuration/StreamAutoConfiguration.java +++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/configuration/StreamAutoConfiguration.java @@ -15,6 +15,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.stream.function.StreamBridge; import org.springframework.context.annotation.Bean; +import org.springframework.core.env.ConfigurableEnvironment; /** * stream 模块的自动配置类,注册所有核心 Bean。 @@ -54,13 +55,15 @@ public class StreamAutoConfiguration { * * @param streamBridge Spring Cloud Stream 桥接器 * @param streamProperties stream 主配置 + * @param environment Spring 配置环境,用于动态注册 Cloud Stream binding * @return 事件管理器实例 */ @Bean @ConditionalOnMissingBean public StreamEventManager streamEventManager(StreamBridge streamBridge, - StreamProperties streamProperties) { - return new DefaultStreamEventManagerImpl(streamBridge, streamProperties); + StreamProperties streamProperties, + ConfigurableEnvironment environment) { + return new DefaultStreamEventManagerImpl(streamBridge, streamProperties, environment); } /** diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/DefaultStreamEventManagerImpl.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/DefaultStreamEventManagerImpl.java index ce8742a..1a84c7b 100644 --- a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/DefaultStreamEventManagerImpl.java +++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/DefaultStreamEventManagerImpl.java @@ -5,14 +5,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.stream.function.StreamBridge; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.StringUtils; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; @@ -51,6 +55,10 @@ public class DefaultStreamEventManagerImpl implements StreamEventManager { * stream 主配置,提供 binding 元数据与全局默认值兜底。 */ private final StreamProperties streamProperties; + /** + * Spring 配置环境,用于动态注册 Spring Cloud Stream binding 配置。 + */ + private final ConfigurableEnvironment environment; /** * 监听器注册表:bindingName → 该 binding 下的所有监听器注册信息列表。 */ @@ -59,16 +67,27 @@ public class DefaultStreamEventManagerImpl implements StreamEventManager { * SpEL 表达式解析器,用于对监听器的 condition 进行求值。 */ private final SpelExpressionParser expressionParser = new SpelExpressionParser(); + /** + * Spring Cloud Stream binding 配置前缀。 + */ + private static final String SPRING_BINDINGS_PREFIX = "spring.cloud.stream.bindings"; + /** + * 动态 binding 属性源名称。 + */ + private static final String DYNAMIC_BINDING_PROPERTY_SOURCE = "stream-dynamic-binding"; /** * 构造方法,由 {@code StreamAutoConfiguration} 注入依赖。 * * @param streamBridge Spring Cloud Stream 桥接器 * @param streamProperties stream 主配置 + * @param environment Spring 配置环境 */ - public DefaultStreamEventManagerImpl(StreamBridge streamBridge, StreamProperties streamProperties) { + public DefaultStreamEventManagerImpl(StreamBridge streamBridge, StreamProperties streamProperties, + ConfigurableEnvironment environment) { this.streamBridge = streamBridge; this.streamProperties = streamProperties; + this.environment = environment; } /** @@ -293,6 +312,7 @@ public void registerBinding(String bindingName, String destination, String group * *

实现说明:使用 synchronized 保证并发注册的幂等性,已存在时仅打印告警并返回。 * contentType/concurrency 为 null 时分别回退到全局默认值。 + * 同时注册 Spring Cloud Stream binding 配置到 Environment,确保 StreamBridge 能找到正确的 destination。 */ @Override public void registerBinding(String bindingName, String destination, String group, String contentType, Integer concurrency) { @@ -311,11 +331,70 @@ public void registerBinding(String bindingName, String destination, String group streamProperties.getBindings().put(bindingName, binding); + // 注册 Spring Cloud Stream binding 配置到 Environment + registerCloudStreamBinding(bindingName, destination, group, binding.getContentType(), binding.getConcurrency()); + log.info("Dynamically registered binding: {}, destination: {}, group: {}, contentType: {}", bindingName, destination, group, binding.getContentType()); } } + /** + * 将 binding 配置注册到 Spring Cloud Stream 的 Environment,确保 StreamBridge 发送消息时能找到正确的 destination。 + * + * @param bindingName 绑定名称 + * @param destination 目标 destination(exchange/topic) + * @param group 消费者组 + * @param contentType 内容类型 + * @param concurrency 消费并发数 + */ + private void registerCloudStreamBinding(String bindingName, String destination, String group, + String contentType, Integer concurrency) { + String inputBinding = bindingName + "-in-0"; + String outputBinding = bindingName + "-out-0"; + + String inputDestKey = SPRING_BINDINGS_PREFIX + "." + inputBinding + ".destination"; + + // 检查是否已存在配置(避免覆盖已有的显式配置) + if (environment.containsProperty(inputDestKey)) { + log.debug("Cloud Stream binding already configured: {}", inputDestKey); + return; + } + + Map props = new LinkedHashMap<>(); + + // 注册 input/output binding 的 destination + props.put(inputDestKey, destination); + props.put(SPRING_BINDINGS_PREFIX + "." + outputBinding + ".destination", destination); + + // 注册 content-type + props.put(SPRING_BINDINGS_PREFIX + "." + inputBinding + ".content-type", contentType); + props.put(SPRING_BINDINGS_PREFIX + "." + outputBinding + ".content-type", contentType); + + // 注册 group(仅 input binding 需要) + if (StringUtils.hasText(group)) { + props.put(SPRING_BINDINGS_PREFIX + "." + inputBinding + ".group", group); + } + + // 注册 concurrency(仅 input binding 需要) + if (concurrency != null) { + props.put(SPRING_BINDINGS_PREFIX + "." + inputBinding + ".consumer.concurrency", concurrency); + } + + // 获取或创建动态 binding 属性源 + MapPropertySource propertySource = (MapPropertySource) environment.getPropertySources().get(DYNAMIC_BINDING_PROPERTY_SOURCE); + if (propertySource == null) { + propertySource = new MapPropertySource(DYNAMIC_BINDING_PROPERTY_SOURCE, new LinkedHashMap<>()); + environment.getPropertySources().addFirst(propertySource); + } + + // 将配置写入属性源 + propertySource.getSource().putAll(props); + + log.info("Registered Cloud Stream binding: {}, destination: {}, group: {}", + bindingName, destination, group); + } + /** * {@inheritDoc} * From fcaaf2f1726a408029b71ee71dd507b3fa64c1e8 Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Fri, 17 Jul 2026 19:12:28 +0800 Subject: [PATCH 3/9] =?UTF-8?q?refactor(stream):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E5=8F=91=E5=B8=83=E5=92=8C=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除动态binding名称构造,直接使用destination参数发送消息 - 更新日志记录中的参数顺序以匹配实际调用 - 修复binding配置检查逻辑,改为检查输出binding的目标配置 - 添加缺失的方法后缀换行以保持代码格式整洁 --- .../manager/DefaultStreamEventManagerImpl.java | 14 ++++++-------- .../StreamBindingBeanFactoryPostProcessor.java | 2 ++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/DefaultStreamEventManagerImpl.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/DefaultStreamEventManagerImpl.java index 1a84c7b..280fe95 100644 --- a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/DefaultStreamEventManagerImpl.java +++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/DefaultStreamEventManagerImpl.java @@ -123,19 +123,16 @@ public void publish(String bindingName, String destination, T event) { *

实现说明: *

    *
  1. 调用 {@link #ensureBindingRegistered} 保证 binding 已注册(动态 binding 注册)
  2. - *
  3. 构建 {@link Message},通过 {@link StreamBridge#send} 投递到 {bindingName}-out-0 输出通道
  4. + *
  5. 构建 {@link Message},通过 {@link StreamBridge#send} 直接投递到指定的 destination
  6. *
*/ @Override public void publish(String bindingName, String destination, String group, T event) { - // 动态 binding 注册:若 binding 缺失则补注册,确保 publish 链路可用 ensureBindingRegistered(bindingName, destination, group); - // Spring Cloud Stream 约定:输出 binding 名为 {bindingName}-out-0 - String outputBindingName = bindingName + "-out-0"; Message message = MessageBuilder.withPayload(event).build(); - streamBridge.send(outputBindingName, message); - log.debug("Published event to binding: {}, destination: {}, group: {}", outputBindingName, destination, group); + streamBridge.send(destination, message); + log.debug("Published event to destination: {}, bindingName: {}, group: {}", destination, bindingName, group); } /** @@ -354,10 +351,11 @@ private void registerCloudStreamBinding(String bindingName, String destination, String outputBinding = bindingName + "-out-0"; String inputDestKey = SPRING_BINDINGS_PREFIX + "." + inputBinding + ".destination"; + String outputDestKey = SPRING_BINDINGS_PREFIX + "." + outputBinding + ".destination"; // 检查是否已存在配置(避免覆盖已有的显式配置) - if (environment.containsProperty(inputDestKey)) { - log.debug("Cloud Stream binding already configured: {}", inputDestKey); + if (environment.containsProperty(outputDestKey)) { + log.debug("Cloud Stream binding already configured: {}", outputDestKey); return; } diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/processor/StreamBindingBeanFactoryPostProcessor.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/processor/StreamBindingBeanFactoryPostProcessor.java index 56cf41b..17fb09e 100644 --- a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/processor/StreamBindingBeanFactoryPostProcessor.java +++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/processor/StreamBindingBeanFactoryPostProcessor.java @@ -118,6 +118,8 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) if (routeAnnotation != null) { processStreamRouteHandler(routeAnnotation, defaultGroup, defaultContentType, defaultBinder, defaultConcurrency, properties, functionDefinitions); } + + } // 处理类级别的 @StreamEventListener 注解 From 890f1e4782ee0168ab63c886483bfe7dd6b206ac Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Tue, 21 Jul 2026 14:06:54 +0800 Subject: [PATCH 4/9] =?UTF-8?q?refactor(repository):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BB=93=E5=82=A8=E6=A1=86=E6=9E=B6=E5=AE=9E=E7=8E=B0=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 RepositoryBeanPostProcessor,改为通过 @Autowired 自动注入 Delegate - 删除 DelegateFor 注解,简化仓储配置方式 - 新增 CqrsRepositoryFacade 支持读写分离模式 - 移除调度子系统相关配置类 AutoScheduleConfiguration - 更新 ElasticsearchRepositoryDelegate 支持泛型 Entity/PO 转换 - 调整 AbstractUserRepositoryImpl 不再直接继承带 PO 泛型的基类 - 修改仓储框架自动配置移除复杂的后处理器机制 --- .trae/documents/facade_refactor_plan.md | 90 +++ .trae/documents/project_review_plan.md | 533 +++++++++++++++++ .trae/documents/repository-refactor-plan.md | 209 +++++++ structure-infra-elasticsearch-starter/pom.xml | 4 + .../ElasticsearchAutoConfiguration.java | 76 --- ...lasticsearchDelegateBeanPostProcessor.java | 80 --- .../ElasticsearchDelegateFactory.java | 65 --- .../ElasticsearchRepositoryDelegate.java | 343 +++++------ .../configuration/JpaAutoConfiguration.java | 72 --- .../JpaDelegateBeanPostProcessor.java | 117 ---- .../jpa/repository/JpaDelegateFactory.java | 66 --- .../jpa/repository/JpaRepositoryDelegate.java | 435 +++++++------- structure-infra-mongodb-starter/pom.xml | 4 + .../configuration/MongoAutoConfiguration.java | 76 --- .../MongoDelegateBeanPostProcessor.java | 79 --- .../repository/MongoDelegateFactory.java | 65 --- .../repository/MongoRepositoryDelegate.java | 339 +++++------ structure-infra-mybatis-plus-starter/pom.xml | 4 + .../MybatisPlusAutoConfiguration.java | 69 --- .../MybatisPlusDelegateBeanPostProcessor.java | 121 ---- .../MybatisPlusDelegateFactory.java | 102 ---- .../MybatisPlusRepositoryDelegate.java | 423 ++++++-------- .../infra/sample/infra/po/UserPO.java | 23 +- .../AbstractUserRepositoryImpl.java | 12 +- .../delegate/UserRepositoryDelegate.java | 13 +- .../infra/delegate/read/UserReadDelegate.java | 34 +- .../delegate/write/UserWriteDelegate.java | 20 +- .../infra/repositoory/UserCqrsRepository.java | 36 -- .../infra/repository/UserCqrsRepository.java | 32 ++ .../sample/cqrs/UserCqrsRepositoryTest.java | 16 +- .../sample/cqrs/config/CqrsTestConfig.java | 15 +- .../MockElasticsearchConfiguration.java | 1 - .../UserElasticsearchRepositoryImpl.java | 12 +- .../elasticsearch/UserEsDelegate.java | 43 +- ...UserElasticsearchDetailRepositoryTest.java | 4 +- .../UserElasticsearchRepositoryTest.java | 3 +- .../ElasticsearchLowCodeTestConfig.java | 2 +- .../MockElasticsearchConfiguration.java | 1 - .../ElasticsearchLowCodeRepositoryTest.java | 16 +- .../ElasticsearchLowCodeStorageTest.java | 3 +- .../infra/sample/infra/po/JpaUserPO.java | 41 ++ .../repository/UserJpaRepositoryImpl.java | 12 +- .../infra/repository/jpa/UserJpaDelegate.java | 43 +- .../sample/jpa/UserJpaRepositoryTest.java | 4 +- .../sample/jpa/config/JpaTestConfig.java | 11 +- .../repository/UserMongoRepositoryImpl.java | 12 +- .../repository/mongodb/UserMongoDelegate.java | 40 +- .../UserMongoRepositoryInjectionTest.java | 2 +- .../mongodb/UserMongoRepositoryTest.java | 6 +- .../config/MockMongoConfiguration.java | 6 +- .../lowcode/MongoLowCodeRepositoryTest.java | 13 +- .../infra/sample/infra/mapper/UserMapper.java | 11 +- .../infra/sample/infra/po/MybatisUserPO.java | 39 ++ .../infra/repository/UserRepositoryImpl.java | 19 +- .../mybatis/UserMybatisPlusDelegate.java | 44 +- .../infra/sample/config/TestConfig.java | 2 +- .../repository/LowCodeRepositoryTest.java | 1 - .../sample/repository/UserRepositoryTest.java | 4 +- .../schedule/handler/DemoTaskHandlers.java | 1 - .../controller/JobManagerController.java | 9 +- structure-infra-starter/pom.xml | 6 +- .../infra/annotations/DelegateFor.java | 83 --- .../infra/annotations/ReadDelegate.java | 12 + .../infra/annotations/Repository.java | 115 ---- .../AutoRepositoryConfiguration.java | 42 +- .../AutoScheduleConfiguration.java | 85 --- .../repository/CqrsRepositoryFacade.java | 223 ++++++++ .../infra/repository/GenericTypeResolver.java | 63 +++ .../infra/repository/IQueryDelegate.java | 8 +- .../InMemoryRepositoryDelegate.java | 52 +- .../RepositoryBeanPostProcessor.java | 535 ------------------ .../repository/RepositoryDefinition.java | 46 +- .../infra/repository/RepositoryDelegate.java | 43 +- .../repository/RepositoryDelegateFactory.java | 35 -- .../infra/repository/RepositoryFacade.java | 281 ++------- .../RepositoryFacadeFactoryBean.java | 105 ---- 76 files changed, 2302 insertions(+), 3410 deletions(-) create mode 100644 .trae/documents/facade_refactor_plan.md create mode 100644 .trae/documents/project_review_plan.md create mode 100644 .trae/documents/repository-refactor-plan.md delete mode 100644 structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchDelegateBeanPostProcessor.java delete mode 100644 structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchDelegateFactory.java delete mode 100644 structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaDelegateBeanPostProcessor.java delete mode 100644 structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaDelegateFactory.java delete mode 100644 structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoDelegateBeanPostProcessor.java delete mode 100644 structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoDelegateFactory.java delete mode 100644 structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusDelegateBeanPostProcessor.java delete mode 100644 structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusDelegateFactory.java delete mode 100644 structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repositoory/UserCqrsRepository.java create mode 100644 structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repository/UserCqrsRepository.java create mode 100644 structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/po/JpaUserPO.java create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/po/MybatisUserPO.java delete mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/annotations/DelegateFor.java create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/annotations/ReadDelegate.java delete mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java delete mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoScheduleConfiguration.java create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/GenericTypeResolver.java delete mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java delete mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegateFactory.java delete mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacadeFactoryBean.java diff --git a/.trae/documents/facade_refactor_plan.md b/.trae/documents/facade_refactor_plan.md new file mode 100644 index 0000000..395fddb --- /dev/null +++ b/.trae/documents/facade_refactor_plan.md @@ -0,0 +1,90 @@ +# 仓储门面重构计划 + +## 问题分析 + +### 当前架构问题 +1. **编译错误**:`RepositoryBeanPostProcessor.java` 引用了不存在的 `ICommandDelegate` 接口 +2. **层次结构混乱**:当前 `RepositoryFacade extends CqrsRepositoryFacade`,与用户期望相反 +3. **多余门面类**:`BaseRepositoryFacade` 和 `SimpleRepositoryFacade` 不需要,应移除 +4. **CQRS 泛型不足**:`CqrsRepositoryFacade` 只定义了一个代理泛型,需要两个 + +### 用户期望的架构 +``` +RepositoryFacade // 基础门面(单代理模式) + │ + └── CqrsRepositoryFacade // CQRS门面(读写分离,两个代理) +``` + +## 修改方案 + +### 1. 修复 RepositoryBeanPostProcessor 编译错误 +- **文件**:`structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java` +- **修改内容**:移除 `ICommandDelegate` 引用,只保留 `RepositoryDelegate` 和 `IQueryDelegate` 检查 + +### 2. 重构 RepositoryFacade(单代理模式) +- **文件**:`structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java` +- **修改内容**: + - 移除泛型参数 `D` + - 添加 `protected RepositoryDelegate delegate` + - 实现完整的 CRUD 方法(从 BaseRepositoryFacade 迁移) + +### 3. 重构 CqrsRepositoryFacade(双代理模式) +- **文件**:`structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java` +- **修改内容**: + - 继承 `RepositoryFacade` + - 添加两个泛型参数:`D extends RepositoryDelegate`(基础代理)和 `RD extends IQueryDelegate`(读代理) + - 添加 `protected D baseDelegate` 和 `protected RD readDelegate` + - 重写读方法,实现回退机制 + +### 4. 删除多余门面类 +- **删除文件**: + - `structure-infra-starter/src/main/java/cn/structure/infra/repository/BaseRepositoryFacade.java` + - `structure-infra-starter/src/main/java/cn/structure/infra/repository/SimpleRepositoryFacade.java` + +### 5. 更新示例代码 + +#### 5.1 更新 AbstractUserRepositoryImpl(单代理) +- **文件**:`structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java` +- **修改内容**:改为继承 `RepositoryFacade` + +#### 5.2 更新 UserCqrsRepository(双代理) +- **文件**:`structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repositoory/UserCqrsRepository.java` +- **修改内容**:改为继承 `CqrsRepositoryFacade` + +### 6. 更新 RepositoryBeanPostProcessor +- **文件**:`structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java` +- **修改内容**: + - 支持单代理模式的 `RepositoryFacade`(注入 `delegate`) + - 支持双代理模式的 `CqrsRepositoryFacade`(注入 `baseDelegate` 和 `readDelegate`) + +### 7. 修复测试代码 +- **文件**:`structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/UserCqrsRepositoryTest.java` +- **修改内容**:调整泛型类型和方法调用 + +## 依赖关系 + +``` +RepositoryFacade (单代理) + │ + ├── AbstractUserRepositoryImpl + │ ├── UserRepositoryImpl (MyBatis) + │ ├── UserJpaRepositoryImpl + │ ├── UserMongoRepositoryImpl + │ └── UserElasticsearchRepositoryImpl + │ + └── CqrsRepositoryFacade (双代理) + └── UserCqrsRepository +``` + +## 风险提示 + +1. **泛型兼容性**:修改泛型参数可能导致编译错误,需要全面检查所有继承 RepositoryFacade 的类 +2. **测试影响**:测试代码中使用的 `getBaseDelegate()` 和 `getReadDelegate()` 方法名可能需要调整 +3. **Bean 注入**:RepositoryBeanPostProcessor 需要正确识别单代理和双代理模式的门面 + +## 验证步骤 + +1. 编译整个项目,检查是否有编译错误 +2. 运行 MyBatis、JPA、MongoDB 模块测试(单代理模式) +3. 运行 CQRS 模块测试(双代理模式) +4. 验证读操作回退机制是否正常工作 diff --git a/.trae/documents/project_review_plan.md b/.trae/documents/project_review_plan.md new file mode 100644 index 0000000..3440629 --- /dev/null +++ b/.trae/documents/project_review_plan.md @@ -0,0 +1,533 @@ +# 项目设计与实现检查方案 + +## 一、架构设计评估 + +### 1.1 当前架构问题分析 + +当前项目采用 **Facade + Delegate + CQRS** 的分层架构,但存在以下架构性问题: + +| 问题 | 影响 | +| ------------------------------------- | ---------------------------------------------------------- | +| 依赖手动装配流程(RepositoryBeanPostProcessor) | Spring Bean 初始化顺序问题,依赖 Facade 的 Bean 在初始化时 delegate 为 null | +| 依赖 @Repository 和 @DelegateFor 注解 | 增加了学习成本和配置复杂度 | +| 依赖 \*DelegateBeanPostProcessor 进行依赖注入 | 违背 Spring 自动装配原则 | +| Delegate 创建时需要传入类型参数 | 重复代码,类型信息应该从泛型自解析 | + +### 1.2 目标架构:简化为纯 Spring DI 模式 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Domain Layer │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ UserRepository (接口) ← ICrudRepository │ │ +│ └─────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ Facade Layer (ACL) │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ RepositoryFacade │ │ +│ │ └── @Autowired RepositoryDelegate delegate│ │ +│ └─────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ CqrsRepositoryFacade │ │ +│ │ ├── @Autowired D baseDelegate │ │ +│ │ └── @Autowired @ReadDelegate RD readDelegate │ │ +│ └─────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ Delegate Layer │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ RepositoryDelegate │ │ +│ │ ├── @PostConstruct resolveGenericTypes() │ │ +│ │ └── @Autowired EntityManager / BaseMapper │ │ +│ └─────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ Infrastructure Layer │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ @ConditionalOnMissingBean 提供默认 Delegate │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 1.3 简化原则 + +| 原则 | 说明 | +| ------------------------------------ | ---------------------------------------------- | +| **移除所有 BeanPostProcessor** | 使用 Spring 原生 DI 替代手动装配 | +| **移除 @Repository 和 @DelegateFor 注解** | 通过泛型类型匹配自动装配 | +| **Delegate 自解析泛型类型** | 在 @PostConstruct 中通过反射获取 Entity/PO/ID 类型 | +| **依赖 Spring @Autowired** | Facade 自动注入 Delegate,Delegate 自动注入基础设施依赖 | +| **@ConditionalOnMissingBean 回退** | 当没有用户自定义 Delegate 时,由 AutoConfiguration 提供默认实现 | + +*** + +## 二、问题分类与修正方案 + +### 2.1 架构重构(核心变更) + +#### 问题1:移除手动装配流程,使用 Spring 原生 DI + +**问题描述**: +当前通过 `RepositoryBeanPostProcessor.onApplicationEvent(ContextRefreshedEvent)` 进行延迟绑定,导致 Bean 初始化顺序问题。 + +**受影响文件**: + +* [RepositoryBeanPostProcessor.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java) + +**修正方案**: + +1. **删除 RepositoryBeanPostProcessor**:完全移除这个类 +2. **RepositoryFacade 使用 @Autowired**:在 `RepositoryDelegate` 字段上添加 `@Autowired` 注解 +3. **Spring 泛型类型匹配**:Spring 会自动根据泛型参数 `RepositoryDelegate` 匹配正确的 Delegate Bean + +*** + +#### 问题2:移除 @Repository 和 @DelegateFor 注解 + +**问题描述**: +`@Repository` 和 `@DelegateFor` 注解增加了配置复杂度,类型信息应该通过泛型自动获取。 + +**受影响文件**: + +* [Repository.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java) + +* [DelegateFor.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/annotations/DelegateFor.java) + +**修正方案**: + +1. **删除 @Repository 注解**:Facade 类只需要 `@Component` 注解 +2. **删除 @DelegateFor 注解**:Delegate 类只需要 `@Component` 注解 +3. **类型信息从泛型提取**:Delegate 在 `@PostConstruct` 中自解析泛型类型 + +*** + +#### 问题3:移除所有 \*DelegateBeanPostProcessor + +**问题描述**: +`JpaDelegateBeanPostProcessor`、`MongoDelegateBeanPostProcessor`、`ElasticsearchDelegateBeanPostProcessor` 等类负责注入 EntityManager、BaseMapper 等依赖,违背了 Spring 自动装配原则。 + +**受影响文件**: + +* [JpaDelegateBeanPostProcessor.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaDelegateBeanPostProcessor.java) + +* [MongoDelegateBeanPostProcessor.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoDelegateBeanPostProcessor.java) + +* [ElasticsearchDelegateBeanPostProcessor.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchDelegateBeanPostProcessor.java) + +* [MybatisPlusDelegateBeanPostProcessor.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusDelegateBeanPostProcessor.java) + +**修正方案**: + +1. \**删除所有 DelegateBeanPostProcessor 类* +2. **Delegate 使用 @Autowired 注入依赖**:在 EntityManager、BaseMapper 等字段上添加 `@Autowired` 注解 +3. **Delegate 使用 @PostConstruct 自解析类型**:在初始化时通过反射解析泛型参数 + +*** + +#### 问题4:Delegate 自解析泛型类型(不再通过构造函数传入) + +**问题描述**: +当前 Delegate 创建时需要通过构造函数或 setter 传入 entityClass、poClass、idClass,应该改为内部自解析。 + +**受影响文件**: + +* [JpaRepositoryDelegate.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java#L52-L81) + +* [MybatisPlusRepositoryDelegate.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusRepositoryDelegate.java#L53-L90) + +* [MongoRepositoryDelegate.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoRepositoryDelegate.java) + +* [ElasticsearchRepositoryDelegate.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchRepositoryDelegate.java) + +**修正方案**: + +1. **添加 @PostConstruct resolveGenericTypes() 方法**:在每个 Delegate 基类中添加泛型类型自解析方法 +2. **移除带参数的构造函数**:保留无参构造函数,依赖 `@Autowired` 和 `@PostConstruct` +3. **提取通用工具方法**:创建 `GenericTypeResolver` 工具类,提供统一的泛型类型提取逻辑 + +*** + +#### 问题5:CQRS 模式下双代理注入 + +**问题描述**: +`CqrsRepositoryFacade` 需要注入两个不同类型的 Delegate(baseDelegate 和 readDelegate),Spring 默认无法区分。 + +**受影响文件**: + +* [CqrsRepositoryFacade.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java#L52-L72) + +**修正方案**: + +1. **创建 @ReadDelegate 限定注解**:用于标记读代理 Bean +2. **CqrsRepositoryFacade 使用 @Qualifier**: + + * `@Autowired D baseDelegate`:匹配普通的 RepositoryDelegate + + * `@Autowired @ReadDelegate RD readDelegate`:匹配带有 @ReadDelegate 注解的 Bean +3. **用户自定义 Delegate 时添加 @ReadDelegate**:用于标记读代理 + +*** + +#### 问题6:默认 Delegate 自动创建 + +**问题描述**: +当没有用户自定义 Delegate 时,需要自动创建默认实现。 + +**受影响文件**: + +* [JpaDelegateFactory.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaDelegateFactory.java) + +* [JpaAutoConfiguration.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/configuration/JpaAutoConfiguration.java) + +**修正方案**: + +1. **删除 RepositoryDelegateFactory 接口**:不再需要这个 SPI +2. **AutoConfiguration 中使用 @ConditionalOnMissingBean**:为每种存储类型提供默认 Delegate Bean +3. **泛型类型通过 @PostConstruct 自解析**:默认 Delegate 创建后自动解析类型 + +*** + +### 2.2 性能问题(建议优化) + +#### 问题7:JPA 实现存在严重性能问题 + +**问题描述**: +`JpaRepositoryDelegate` 存在多处性能问题: + +* `queryPage()`:先 `findAll()` 获取全量数据,再进行内存分页 + +* `count()`:调用 `queryList().size()`,执行完整查询后统计数量 + +* `listByIds()`:对每个 ID 单独查询,产生 N+1 问题 + +**受影响文件**: + +* [JpaRepositoryDelegate.java](file:///Users/chuck/projects/structure-projects/structure-pro-infra/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaRepositoryDelegate.java#L148-L262) + +**修正方案**: + +1. `queryPage()`:使用 `CriteriaQuery` + `setFirstResult`/`setMaxResults` 实现数据库分页 +2. `count()`:使用 `CriteriaBuilder.count()` 实现 COUNT 查询 +3. `listByIds()`:使用 `CriteriaBuilder.in()` 实现批量查询 + +*** + +### 2.3 代码规范问题 + +#### 问题8:目录拼写错误 + +**问题描述**: +CQRS 示例工程中存在目录拼写错误:`repositoory` 应为 `repository`。 + +**受影响文件**: + +* 目录路径:`structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repositoory/` + +**修正方案**: + +1. 重命名目录为 `repository` +2. 更新所有相关文件的包声明 +3. 更新 `pom.xml` 和其他引用该路径的配置文件 + +*** + +## 三、修正优先级 + +| 优先级 | 问题 | 原因 | +| ------ | ------------------------------------ | ----------------- | +| **P0** | 问题1 - 移除手动装配流程 | 架构性问题,影响所有组件 | +| **P0** | 问题2 - 移除 @Repository 和 @DelegateFor | 架构性问题,简化 API | +| **P0** | 问题3 - 移除 \*DelegateBeanPostProcessor | 架构性问题,简化依赖注入 | +| **P0** | 问题4 - Delegate 自解析泛型类型 | 架构性问题,类型获取方式变更 | +| **P0** | 问题5 - CQRS 双代理注入 | 架构性问题,CQRS 模式核心功能 | +| **P0** | 问题6 - 默认 Delegate 自动创建 | 架构性问题,自动装配回退机制 | +| **P2** | 问题7 - JPA 性能 | 影响大数据量场景性能 | +| **P3** | 问题8 - 目录拼写 | 代码规范问题 | + +*** + +## 四、修正步骤 + +### 阶段一:核心架构重构(P0) + +#### 1.1 创建通用工具类 + +1. 创建 `GenericTypeResolver` 工具类,提供泛型类型提取方法: + + * `resolveTypes(Class clazz, Class targetInterface)`:提取指定接口的泛型参数 + +#### 1.2 修改 Delegate 基类 + +1. **JpaRepositoryDelegate**: + + * 添加 `@Autowired` 注解到 `entityManager` 字段 + + * 添加 `@PostConstruct` 方法,调用 `resolveGenericTypes()` 自解析 Entity/PO/ID 类型 + + * 移除带参数的构造函数,保留无参构造函数 + +2. **MybatisPlusRepositoryDelegate**: + + * 添加 `@Autowired` 注解到 `baseMapper` 字段 + + * 添加 `@PostConstruct` 方法,调用 `resolveGenericTypes()` 自解析 Entity/PO/ID 类型 + + * 移除带参数的构造函数,保留无参构造函数 + +3. **MongoRepositoryDelegate**: + + * 添加 `@Autowired` 注解到 `MongoTemplate` 字段 + + * 添加 `@PostConstruct` 方法,调用 `resolveGenericTypes()` 自解析类型 + +4. **ElasticsearchRepositoryDelegate**: + + * 添加 `@Autowired` 注解到 `RestHighLevelClient` 字段 + + * 添加 `@PostConstruct` 方法,调用 `resolveGenericTypes()` 自解析类型 + +#### 1.3 修改 Facade 类 + +1. **RepositoryFacade**: + + * 在 `delegate` 字段上添加 `@Autowired` 注解 + + * 修改构造函数,移除 delegate 参数 + +2. **CqrsRepositoryFacade**: + + * 在 `baseDelegate` 字段上添加 `@Autowired` 注解 + + * 在 `readDelegate` 字段上添加 `@Autowired @ReadDelegate` 注解 + + * 修改构造函数,移除 delegate 参数 + +#### 1.4 创建 @ReadDelegate 限定注解 + +1. 创建 `@ReadDelegate` 注解,用于标记读代理 Bean + +#### 1.5 修改 AutoConfiguration + +1. **JpaAutoConfiguration**: + + * 删除 `JpaDelegateFactory` Bean + + * 删除 `JpaDelegateBeanPostProcessor` Bean + + * 添加默认 `JpaRepositoryDelegate` Bean,使用 `@ConditionalOnMissingBean` + +2. **MongoAutoConfiguration**: + + * 删除 `MongoDelegateFactory` Bean + + * 删除 `MongoDelegateBeanPostProcessor` Bean + + * 添加默认 `MongoRepositoryDelegate` Bean,使用 `@ConditionalOnMissingBean` + +3. **ElasticsearchAutoConfiguration**: + + * 删除 `ElasticsearchDelegateFactory` Bean + + * 删除 `ElasticsearchDelegateBeanPostProcessor` Bean + + * 添加默认 `ElasticsearchRepositoryDelegate` Bean,使用 `@ConditionalOnMissingBean` + +4. **MybatisPlusAutoConfiguration**: + + * 删除 `MybatisPlusDelegateFactory` Bean + + * 删除 `MybatisPlusDelegateBeanPostProcessor` Bean + + * 添加默认 `MybatisPlusRepositoryDelegate` Bean,使用 `@ConditionalOnMissingBean` + +#### 1.6 删除废弃的类 + +1. 删除 `RepositoryBeanPostProcessor` +2. 删除 `RepositoryDelegateFactory` 接口 +3. 删除 `@Repository` 注解 +4. 删除 `@DelegateFor` 注解 +5. 删除所有 `*DelegateBeanPostProcessor` 类 + +#### 1.7 更新示例代码 + +1. **UserRepositoryImpl**: + + * 移除 `@Repository` 注解(如果存在) + + * 确保继承 `RepositoryFacade` 并使用泛型参数 + +2. **UserCqrsRepository**: + + * 移除 `@Repository` 注解 + + * 确保读代理类添加 `@ReadDelegate` 注解 + +3. **UserMybatisPlusDelegate**: + + * 移除 `@DelegateFor` 注解 + + * 添加 `@Component` 注解 + +4. **UserWriteDelegate / UserReadDelegate**: + + * 移除 `@DelegateFor` 注解 + + * 添加 `@Component` 注解 + + * `UserReadDelegate` 添加 `@ReadDelegate` 注解 + +### 阶段二:性能优化(P2) + +1. 优化 `JpaRepositoryDelegate` 的分页、count 和 listByIds 方法 + +### 阶段三:代码规范(P3) + +1. 修复目录拼写错误 + +*** + +## 五、验证方案 + +### 5.1 编译验证 + +```bash +mvn clean compile -DskipTests +``` + +### 5.2 单元测试验证 + +```bash +# MyBatis 模块测试 +mvn test -pl structure-infra-sample/structure-infra-sample-mybatis + +# JPA 模块测试 +mvn test -pl structure-infra-sample/structure-infra-sample-jpa + +# MongoDB 模块测试 +mvn test -pl structure-infra-sample/structure-infra-sample-mongodb + +# CQRS 模块测试 +mvn test -pl structure-infra-sample/structure-infra-sample-cqrs +``` + +### 5.3 集成测试验证 + +```bash +# 运行所有测试 +mvn test +``` + +### 5.4 手动验证要点 + +1. 验证 Facade 正确返回领域实体类型 +2. 验证 Entity ↔ PO 转换正常工作 +3. 验证 CQRS 模式下读写操作正确路由 +4. 验证找不到 Delegate 时自动创建默认实现 +5. 验证 Bean 初始化顺序问题已修复(依赖 Facade 的 Bean 能正常初始化) +6. 验证默认 Delegate 使用 @ConditionalOnMissingBean 正确工作 + +*** + +## 六、风险与依赖考虑 + +### 6.1 风险评估 + +| 风险 | 级别 | 缓解措施 | +| ----------------------------------- | -- | ---------------------------- | +| Spring 泛型类型匹配可能不支持复杂泛型 | 中 | 测试各种泛型场景,必要时使用 @Qualifier 补充 | +| 现有代码大量使用 @Repository 和 @DelegateFor | 高 | 提供迁移指南,逐步替换 | +| CQRS 模式下双代理注入可能冲突 | 中 | 使用 @ReadDelegate 限定注解明确区分 | +| 默认 Delegate 自解析泛型失败 | 低 | 添加详细日志和异常处理 | + +### 6.2 依赖变更 + +* 移除 `RepositoryBeanPostProcessor` 相关依赖 + +* 移除 `*DelegateBeanPostProcessor` 相关依赖 + +* 移除 `@Repository` 和 `@DelegateFor` 注解依赖 + +* 添加 `@ReadDelegate` 限定注解 + +### 6.3 向后兼容性 + +* **不兼容变更**:移除了 `@Repository`、`@DelegateFor` 注解和所有 BeanPostProcessor + +* **兼容变更**:Facade 和 Delegate 的使用方式保持不变,只是配置方式简化 + +*** + +## 七、简化后的代码示例 + +### 7.1 RepositoryFacade + +```java +@Component +public class UserRepositoryImpl extends RepositoryFacade implements UserRepository { + // @Autowired 自动注入 RepositoryDelegate + + @Override + public UserEntity findByName(String name) { + return ((UserRepositoryDelegate) getDelegate()).findByName(name); + } +} +``` + +### 7.2 CqrsRepositoryFacade + +```java +@Component +public class UserCqrsRepository extends CqrsRepositoryFacade implements UserRepository { + // @Autowired 自动注入 UserWriteDelegate + // @Autowired @ReadDelegate 自动注入 UserReadDelegate +} +``` + +### 7.3 Delegate + +```java +@Component +public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate { + + @Autowired + private UserMapper userMapper; + + @Override + public UserEntity findByName(String name) { + MybatisUserPO po = userMapper.selectOne(Wrappers.lambdaQuery().eq(MybatisUserPO::getUsername, name)); + return toEntity(po); + } +} +``` + +### 7.4 读代理(CQRS) + +```java +@Component +@ReadDelegate +public class UserReadDelegate extends ElasticsearchRepositoryDelegate implements UserRepositoryDelegate { + // 只读操作实现 +} +``` + +### 7.5 AutoConfiguration 示例 + +```java +@AutoConfiguration +@ConditionalOnClass(name = "org.springframework.data.jpa.repository.JpaRepository") +public class JpaAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public JpaRepositoryDelegate jpaRepositoryDelegate(EntityManager entityManager) { + return new JpaRepositoryDelegate<>(); + // 泛型类型在 @PostConstruct 中自解析 + } +} +``` + +MongoRepositoryDelegate 这个不需要单独创建bean,因为他们是基础类和mabtis 使用方法类似。他应该是与子类创建时初始化。 + +MybatisPlusRepositoryDelegate.java 63-70 这个实现方式我并不满意应该在他的默认构造方法中初始化不应该使用spring 的注解,这个不对。 + +ongoRepositoryDelegate.java 55-73 甚至你可以理解没有这些属性但是可以有get方法来获取这个类型。 + +你不应该移除代码注释 \ No newline at end of file diff --git a/.trae/documents/repository-refactor-plan.md b/.trae/documents/repository-refactor-plan.md new file mode 100644 index 0000000..238db8e --- /dev/null +++ b/.trae/documents/repository-refactor-plan.md @@ -0,0 +1,209 @@ +# 仓储架构重构修正方案 + +## 一、现状分析 + +### 1.1 当前架构问题 + +| 问题点 | 描述 | 影响范围 | +|--------|------|----------| +| 注解冗余 | `@Repository` 和 `@DelegateFor` 注解与泛型定义重复 | 所有示例模块 | +| PO类型冗余 | PO类型既在注解中定义,又在泛型中定义 | Delegate实现类 | +| 注释缺失 | 之前修复过程中部分类/方法注释被移除 | 多处文件 | +| Facade设计 | RepositoryFacade承担了过多职责,缺乏分层 | 框架核心 | + +### 1.2 依赖关系分析 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 领域层 (Domain) │ +│ UserRepository (接口) ──> UserEntity (领域实体) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 仓储门面层 (Facade) │ +│ AbstractUserRepositoryImpl ──> RepositoryFacade │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 委托实现层 (Delegate) │ +│ UserMybatisPlusDelegate ──> MybatisPlusRepositoryDelegate │ +│ UserJpaDelegate ──> JpaRepositoryDelegate │ +│ UserMongoDelegate ──> MongoRepositoryDelegate │ +│ UserEsDelegate ──> ElasticsearchRepositoryDelegate │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 现有注解使用情况 + +| 注解 | 用途 | 是否可移除 | +|------|------|-----------| +| `@Repository` | 标记仓储实现、指定类型和实体 | 可移除,由泛型推断 | +| `@DelegateFor` | 标记委托实现、指定名称和PO类型 | 可移除,由命名约定和泛型推断 | +| `@Component` | Spring组件注册 | **保留** | + +--- + +## 二、修正方案 + +### 2.1 方案一:移除冗余注解(推荐) + +**核心思想**:利用泛型和命名约定替代注解配置,让Spring自动管理依赖注入。 + +#### 2.1.1 命名约定 + +| 组件类型 | 命名模式 | 示例 | +|----------|----------|------| +| Repository接口 | `{Entity}Repository` | `UserRepository` | +| Repository实现 | `{Entity}{Tech}RepositoryImpl` | `UserJpaRepositoryImpl` | +| Delegate接口 | `{Entity}RepositoryDelegate` | `UserRepositoryDelegate` | +| Delegate实现 | `{Entity}{Tech}Delegate` | `UserMybatisPlusDelegate` | + +#### 2.1.2 泛型解析策略 + +BeanPostProcessor通过反射解析泛型参数获取: +- Entity类型:从 `RepositoryDelegate` 获取 +- PO类型:从 `MybatisPlusRepositoryDelegate` 获取 +- ID类型:从泛型参数获取 + +### 2.2 方案二:Facade分层设计 + +**核心思想**:将RepositoryFacade拆分为基础门面和具体门面,支持独立分离的门面实现。 + +#### 2.2.1 分层结构 + +``` +BaseRepositoryFacade // 基础CRUD能力 + │ + ├── SimpleRepositoryFacade // 简化门面(无CQRS) + │ + └── CqrsRepositoryFacade // CQRS门面(读写分离) + │ + └── RepositoryFacade // 当前实现 +``` + +#### 2.2.2 职责划分 + +| 类 | 职责 | +|----|------| +| `BaseRepositoryFacade` | 基础CRUD接口实现、实体管理 | +| `SimpleRepositoryFacade` | 单代理模式(无CQRS) | +| `CqrsRepositoryFacade` | CQRS模式(读写分离)、回退机制 | + +--- + +## 三、实施步骤 + +### 3.1 步骤一:增强BeanPostProcessor泛型解析能力 + +**目标**:让BeanPostProcessor能够从泛型中解析Entity和PO类型,不再依赖注解。 + +**修改文件**: +- `structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java` + - 增强泛型解析逻辑,支持从继承链中提取Entity和PO类型 + - 根据命名约定自动匹配Delegate和Facade + +### 3.2 步骤二:移除示例工程中的冗余注解 + +**目标**:移除 `@Repository` 和 `@DelegateFor` 注解,验证泛型解析是否正常工作。 + +**修改文件**: +- `structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java` + - 移除 `@Repository` 注解 + - 保留 `@Component` 和类注释 + +- `structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java` + - 移除 `@DelegateFor` 注解 + - 保留 `@Component` 和类注释 + +- `structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/UserMongoRepositoryImpl.java` + - 移除 `@Repository` 注解 + +- `structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java` + - 移除 `@DelegateFor` 注解 + +- `structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/UserElasticsearchRepositoryImpl.java` + - 移除 `@Repository` 注解 + +- `structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java` + - 移除 `@DelegateFor` 注解 + +- `structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/mybatis/UserMybatisPlusDelegate.java` + - 移除 `@DelegateFor` 注解 + +### 3.3 步骤三:补充缺失的注释 + +**目标**:恢复之前修复过程中被移除的类注释、方法注释和行注释。 + +**修改文件**: +- `structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusDelegateBeanPostProcessor.java` + - 恢复 `findMapperByPoClass` 方法的注释 + +- `structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java` + - 恢复类注释和方法注释 + +- `structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java` + - 恢复类注释和方法注释 + +- `structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java` + - 恢复类注释和方法注释 + +### 3.4 步骤四:Facade分层设计(可选) + +**目标**:创建分层的Facade结构,支持独立分离的门面实现。 + +**新增文件**: +- `structure-infra-starter/src/main/java/cn/structure/infra/repository/BaseRepositoryFacade.java` + - 基础CRUD能力,不包含CQRS逻辑 + +- `structure-infra-starter/src/main/java/cn/structure/infra/repository/SimpleRepositoryFacade.java` + - 简化门面,单代理模式 + +**修改文件**: +- `structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java` + - 继承 `CqrsRepositoryFacade` + - 专注于CQRS模式 + +--- + +## 四、风险评估 + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| 泛型解析失败 | 无法正确识别Entity/PO类型 | 保留注解作为备用方案 | +| 命名约定冲突 | 多个Delegate匹配同一个Facade | 增加优先级配置 | +| BeanPostProcessor顺序问题 | 依赖未就绪导致注入失败 | 使用 `@DependsOn` 或 `@Order` | +| 兼容性问题 | 现有代码无法编译 | 逐步迁移,保留兼容层 | + +--- + +## 五、验证计划 + +### 5.1 编译验证 +```bash +mvn clean compile +``` + +### 5.2 单元测试验证 +```bash +mvn test -pl structure-infra-sample/structure-infra-sample-mybatis +mvn test -pl structure-infra-sample/structure-infra-sample-jpa +mvn test -pl structure-infra-sample/structure-infra-sample-mongodb +mvn test -pl structure-infra-sample/structure-infra-sample-elasticsearch +``` + +### 5.3 集成测试验证 +```bash +mvn test -pl structure-infra-sample/structure-infra-sample-cqrs +``` + +--- + +## 六、预期效果 + +1. **代码简化**:移除冗余注解,减少样板代码 +2. **类型安全**:泛型推断确保编译时类型检查 +3. **Spring原生**:利用Spring依赖注入机制,减少自定义配置 +4. **可扩展性**:分层Facade设计支持多种使用模式 +5. **注释完整**:恢复所有必要的文档注释 diff --git a/structure-infra-elasticsearch-starter/pom.xml b/structure-infra-elasticsearch-starter/pom.xml index 31271af..b8d54d6 100644 --- a/structure-infra-elasticsearch-starter/pom.xml +++ b/structure-infra-elasticsearch-starter/pom.xml @@ -25,6 +25,10 @@ org.springframework.boot spring-boot-starter-data-elasticsearch
+ + jakarta.persistence + jakarta.persistence-api + diff --git a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/configuration/ElasticsearchAutoConfiguration.java b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/configuration/ElasticsearchAutoConfiguration.java index d8e638d..59b4a3a 100644 --- a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/configuration/ElasticsearchAutoConfiguration.java +++ b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/configuration/ElasticsearchAutoConfiguration.java @@ -1,89 +1,13 @@ package cn.structure.infra.elasticsearch.configuration; -import cn.structure.infra.elasticsearch.repository.ElasticsearchDelegateBeanPostProcessor; -import cn.structure.infra.elasticsearch.repository.ElasticsearchDelegateFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; -/** - * Elasticsearch 自动配置类 - *

- * 当检测到 Elasticsearch 相关依赖({@link org.springframework.data.elasticsearch.core.ElasticsearchOperations})时自动配置, - * 注册 Elasticsearch 文档操作所需的核心组件,使其与仓储框架无缝集成。 - *

- * 注册的 Bean: - *

- *

- * 工作机制: - *

    - *
  1. 当 {@link cn.structure.infra.repository.RepositoryFacade} 需要获取 RepositoryDelegate 时, - * 会通过 {@link cn.structure.infra.repository.RepositoryBeanPostProcessor} 查找匹配的 Delegate
  2. - *
  3. 若未找到用户自定义的 Delegate,会通过 ElasticsearchDelegateFactory 自动创建
  4. - *
  5. DelegateBeanPostProcessor 确保用户自定义的 Delegate 实现能正确注入 ElasticsearchOperations
  6. - *
- *

- * 配置方式: - *

- *

- * 额外配置: - *

- * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ @AutoConfiguration @ConditionalOnClass(name = "org.springframework.data.elasticsearch.core.ElasticsearchOperations") @ConditionalOnProperty(prefix = "structure.infra", name = "type", havingValue = "ELASTICSEARCH", matchIfMissing = true) @EnableElasticsearchRepositories public class ElasticsearchAutoConfiguration { - - /** - * 创建 Elasticsearch 仓储委托工厂 - *

- * 负责根据 PO 类创建 ElasticsearchRepositoryDelegate 实例,通过 ElasticsearchOperations 进行文档操作。 - * 当 RepositoryFacade 需要获取 Elasticsearch 类型的 RepositoryDelegate 时,会通过此工厂进行创建。 - * - * @param elasticsearchOperations Elasticsearch 操作模板,用于执行索引、查询等操作 - * @return ElasticsearchDelegateFactory 实例 - */ - @Bean - @ConditionalOnBean(ElasticsearchOperations.class) - public ElasticsearchDelegateFactory elasticsearchDelegateFactory(ElasticsearchOperations elasticsearchOperations) { - return new ElasticsearchDelegateFactory(elasticsearchOperations); - } - - /** - * 创建 Elasticsearch 委托 Bean 后处理器 - *

- * 在 Bean 初始化完成后,自动为 ElasticsearchRepositoryDelegate 实现类注入 ElasticsearchOperations 和实体类。 - *

- * 处理逻辑: - * 1. 扫描所有 Bean,筛选出 ElasticsearchRepositoryDelegate 的实例 - * 2. 从 Spring 上下文获取 ElasticsearchOperations 并注入到 Delegate 实例中 - * 3. 检查是否存在 {@link cn.structure.infra.annotations.DelegateFor} 注解 - * 4. 将注解中指定的 PO 类设置到 Delegate 实例中 - * - * @return ElasticsearchDelegateBeanPostProcessor 实例 - */ - @Bean - @ConditionalOnBean(ElasticsearchOperations.class) - public ElasticsearchDelegateBeanPostProcessor elasticsearchDelegateBeanPostProcessor() { - return new ElasticsearchDelegateBeanPostProcessor(); - } } diff --git a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchDelegateBeanPostProcessor.java b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchDelegateBeanPostProcessor.java deleted file mode 100644 index 23c27b0..0000000 --- a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchDelegateBeanPostProcessor.java +++ /dev/null @@ -1,80 +0,0 @@ -package cn.structure.infra.elasticsearch.repository; - -import cn.structure.infra.annotations.DelegateFor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; - -/** - * Elasticsearch RepositoryDelegate 的 BeanPostProcessor,负责为用户自定义 Delegate 子类自动注入 ElasticsearchOperations 与实体类型。 - *

- * 在仓储框架中,业务方可继承 {@link ElasticsearchRepositoryDelegate} 实现自定义 Delegate,并通过 - * {@link DelegateFor} 注解声明其服务的 PO 类型。本后处理器在 Bean 初始化完成后: - *

    - *
  1. 识别所有 {@link ElasticsearchRepositoryDelegate} 类型的 Bean
  2. - *
  3. 按类型从容器获取 {@link ElasticsearchOperations} 并注入
  4. - *
  5. 读取 {@link DelegateFor#po()} 指定的 PO 类型并通过 setter 注入
  6. - *
- *

- * 与 {@link ElasticsearchDelegateFactory} 的分工:工厂负责"无自定义 Delegate 时自动创建", - * 本处理器负责"已有自定义 Delegate 时补齐依赖",二者协同保证 RepositoryFacade 总能拿到可用的 Delegate。 - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -@Slf4j -public class ElasticsearchDelegateBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { - - /** Spring 上下文,用于按类型获取 ElasticsearchOperations */ - private ApplicationContext applicationContext; - - /** - * 注入 Spring 应用上下文,供后续按类型查询 Bean。 - * - * @param applicationContext Spring 应用上下文 - * @throws BeansException 上下文注入异常 - */ - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - /** - * 在 Bean 初始化完成后,对自定义 ElasticsearchRepositoryDelegate 实现类注入 ElasticsearchOperations 与实体类型。 - *

- * 仅当 Bean 是 {@link ElasticsearchRepositoryDelegate} 实例时执行注入; - * ElasticsearchOperations 解析失败仅告警不抛异常。 - * - * @param bean 待处理的 Bean 实例 - * @param beanName Bean 名称 - * @return 原始 Bean(已注入依赖),未匹配类型时原样返回 - * @throws BeansException 处理过程中的异常 - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof ElasticsearchRepositoryDelegate) { - ElasticsearchRepositoryDelegate delegate = (ElasticsearchRepositoryDelegate) bean; - try { - // 按类型从容器获取 ElasticsearchOperations 并注入 - ElasticsearchOperations elasticsearchOperations = applicationContext.getBean(ElasticsearchOperations.class); - delegate.setElasticsearchOperations(elasticsearchOperations); - - // 读取 @DelegateFor 注解,识别该 Delegate 服务的 PO 类型并注入 - DelegateFor annotation = bean.getClass().getAnnotation(DelegateFor.class); - if (annotation != null && annotation.po() != void.class) { - delegate.setEntityClass(annotation.po()); - } - - log.info("Injected ElasticsearchOperations into ElasticsearchRepositoryDelegate: {}", beanName); - } catch (Exception e) { - log.warn("Failed to inject ElasticsearchOperations into ElasticsearchRepositoryDelegate {}: {}", beanName, e.getMessage()); - } - } - return bean; - } -} \ No newline at end of file diff --git a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchDelegateFactory.java b/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchDelegateFactory.java deleted file mode 100644 index d855024..0000000 --- a/structure-infra-elasticsearch-starter/src/main/java/cn/structure/infra/elasticsearch/repository/ElasticsearchDelegateFactory.java +++ /dev/null @@ -1,65 +0,0 @@ -package cn.structure.infra.elasticsearch.repository; - -import cn.structure.infra.repository.RepositoryDelegate; -import cn.structure.infra.repository.RepositoryDelegateFactory; -import cn.structure.infra.repository.RepositoryType; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; - -/** - * Elasticsearch 仓储委托工厂 - *

- * 实现 {@link RepositoryDelegateFactory} SPI,自动创建 {@link ElasticsearchRepositoryDelegate} 实例。 - * 在仓储框架中,当 {@code RepositoryFacade} 找不到用户自定义的 Delegate 时,会通过本工厂按 PO 类型 - * 创建默认 Delegate 实例(依赖容器中的 {@link ElasticsearchOperations})。 - *

- * 与 {@link ElasticsearchDelegateBeanPostProcessor} 的分工:本工厂负责"无自定义 Delegate 时创建默认实现", - * BeanPostProcessor 负责"已有自定义子类时补齐依赖"。 - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -public class ElasticsearchDelegateFactory implements RepositoryDelegateFactory { - - /** Elasticsearch 操作模板,由容器注入并共享给所有 Delegate 实例 */ - private final ElasticsearchOperations elasticsearchOperations; - - /** - * 构造工厂,注入 ElasticsearchOperations。 - * - * @param elasticsearchOperations Elasticsearch 操作模板 - */ - public ElasticsearchDelegateFactory(ElasticsearchOperations elasticsearchOperations) { - this.elasticsearchOperations = elasticsearchOperations; - } - - /** - * 返回该工厂支持的仓储类型,用于 SPI 路由匹配。 - * - * @return 固定返回 {@link RepositoryType#ELASTICSEARCH} - */ - @Override - public RepositoryType getType() { - return RepositoryType.ELASTICSEARCH; - } - - /** - * 为指定 PO 类型创建 {@link ElasticsearchRepositoryDelegate} 实例。 - *

- * Elasticsearch 实现不依赖 Mapper 查找,直接以入参 PO 类型构造 Delegate,因此失败概率较低; - * 出现异常时返回 null,由上层 RepositoryFacade 继续尝试其他工厂或抛出异常。 - * - * @param poClass PO 实体类型 - * @param idClass 主键类型(当前实现未使用,保留以匹配 SPI 签名) - * @return 已注入 ElasticsearchOperations 的 Delegate 实例;构造异常时返回 null - */ - @Override - @SuppressWarnings({"unchecked", "rawtypes"}) - public RepositoryDelegate createDelegate(Class poClass, Class idClass) { - try { - return new ElasticsearchRepositoryDelegate(elasticsearchOperations, poClass); - } catch (Exception e) { - return null; - } - } -} 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 2bfe80c..53d7d7a 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 @@ -2,8 +2,11 @@ import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; +import cn.structure.infra.repository.GenericTypeResolver; import cn.structure.infra.repository.RepositoryDelegate; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; @@ -12,6 +15,7 @@ import org.springframework.data.elasticsearch.core.query.CriteriaQuery; import org.springframework.data.elasticsearch.core.query.Query; +import jakarta.persistence.Id; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; @@ -33,229 +37,164 @@ * 实现说明: *

* - * @param 持久化对象类型(PO) + * @param 领域实体类型 + * @param

持久化对象类型(ES Document) * @param 主键类型 * @author chuck - * @version 1.0.1 + * @version 1.0.3 * @since 2026/6/28 */ @Slf4j -public class ElasticsearchRepositoryDelegate implements RepositoryDelegate { +public class ElasticsearchRepositoryDelegate implements RepositoryDelegate { - /** Elasticsearch 操作模板,承担实际文档操作 */ + @Autowired protected ElasticsearchOperations elasticsearchOperations; - /** PO 实体类型,用于反射读取字段与 search/get */ - protected Class entityClass; - /** 主键字段名,默认 "id",可被子类覆盖 */ - protected String idFieldName; - - /** - * 默认构造器,用于用户自定义子类场景。 - *

- * 创建后由 {@link ElasticsearchDelegateBeanPostProcessor} 通过 setter 注入依赖。 - */ + protected Class entityClass; + protected Class

poClass; + protected Class idClass; + protected String idFieldName = "id"; + 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(); + } + } } - /** - * 以默认主键字段名 "id" 构造 Delegate。 - * - * @param elasticsearchOperations Elasticsearch 操作模板 - * @param entityClass PO 实体类型 - */ - public ElasticsearchRepositoryDelegate(ElasticsearchOperations elasticsearchOperations, Class entityClass) { - this(elasticsearchOperations, entityClass, "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; } - /** - * 全参构造器,工厂自动创建场景使用。 - * - * @param elasticsearchOperations Elasticsearch 操作模板 - * @param entityClass PO 实体类型 - * @param idFieldName 主键字段名 - */ - public ElasticsearchRepositoryDelegate(ElasticsearchOperations elasticsearchOperations, Class entityClass, String idFieldName) { - this.elasticsearchOperations = elasticsearchOperations; - this.entityClass = entityClass; - this.idFieldName = idFieldName; - log.info("ElasticsearchRepositoryDelegate initialized for entity: {}", entityClass.getSimpleName()); + @Override + public Class getEntityClass() { + return entityClass; } - /** - * 注入 ElasticsearchOperations,供 BeanPostProcessor 在自定义子类上调用。 - * - * @param elasticsearchOperations Elasticsearch 操作模板 - */ - public void setElasticsearchOperations(ElasticsearchOperations elasticsearchOperations) { - this.elasticsearchOperations = elasticsearchOperations; + @Override + public Class getPoClass() { + return poClass; } - /** - * 注入 PO 实体类型,供 BeanPostProcessor 在自定义子类上调用。 - * - * @param entityClass PO 实体类型 - */ - public void setEntityClass(Class entityClass) { - this.entityClass = entityClass; + @Override + public Class getIdClass() { + return idClass; } - /** - * 设置主键字段名,供自定义子类覆盖默认 "id"。 - * - * @param idFieldName 主键字段名 - */ - public void setIdFieldName(String idFieldName) { - this.idFieldName = idFieldName; + @Override + public String getIdFieldName() { + return idFieldName; } - /** - * 保存或更新文档。 - *

- * 委托给 {@link ElasticsearchOperations#save(Object)},由 ES 依据 _id 自动判断新增或覆盖索引。 - * - * @param entity 实体对象,为 null 时返回 null - * @return 保存后的实体(与入参同一引用) - */ @Override - public T save(T entity) { + public E save(E entity) { if (entity == null) { return null; } - T saved = elasticsearchOperations.save(entity); - log.debug("Saved entity: {}", saved); - return saved; + P po = toPo(entity); + P savedPo = elasticsearchOperations.save(po); + log.debug("Saved entity: {}", savedPo); + return toEntity(savedPo); } - /** - * 根据主键删除文档。 - *

- * ES 文档 ID 必须为字符串,主键值通过 {@code String.valueOf(id)} 转换后再删除。 - * - * @param id 主键值,为 null 时不执行任何操作 - */ @Override public void removeById(ID id) { if (id != null) { - // ID 统一转换为 String 作为 ES 文档 ID - elasticsearchOperations.delete(String.valueOf(id), entityClass); + elasticsearchOperations.delete(String.valueOf(id), poClass); log.debug("Removed entity: id={}", id); } } - /** - * 根据主键查询文档。 - *

- * ES 文档 ID 必须为字符串,主键值通过 {@code String.valueOf(id)} 转换后再查询。 - * - * @param id 主键值,为 null 时返回 null - * @return 实体对象,未找到时返回 null - */ @Override - public T findById(ID id) { + public E findById(ID id) { if (id == null) { return null; } - // ID 统一转换为 String 作为 ES 文档 ID - T entity = elasticsearchOperations.get(String.valueOf(id), entityClass); - log.debug("Find by id: id={}, found={}", id, entity != null); - return entity; + P po = elasticsearchOperations.get(String.valueOf(id), poClass); + log.debug("Find by id: id={}, found={}", id, po != null); + return toEntity(po); } - /** - * 根据主键查询(与 findById 等价,语义上用于"读模型",常作为 CQRS 读侧)。 - * - * @param id 主键值 - * @return 实体对象,未找到时返回 null - */ @Override - public T queryById(ID id) { + public E queryById(ID id) { return findById(id); } - /** - * 根据主键查询并以 {@link Optional} 包装返回。 - * - * @param id 主键值 - * @return 包含实体的 Optional,未找到时为 {@link Optional#empty()} - */ @Override - public Optional queryByIdOptional(ID id) { + public Optional queryByIdOptional(ID id) { return Optional.ofNullable(queryById(id)); } - /** - * 根据非空字段等值匹配查询单条记录。 - *

- * 通过反射构建 {@link CriteriaQuery},取首条 SearchHit 的 content;无匹配时返回 null。 - * - * @param condition 查询条件对象,为 null 时返回 null - * @return 首条匹配记录,无匹配时返回 null - */ @Override - public T queryOne(T condition) { + public E queryOne(E condition) { if (condition == null) { return null; } - Query query = buildQuery(condition); - SearchHits searchHits = elasticsearchOperations.search(query, entityClass); - return searchHits.hasSearchHits() ? searchHits.getSearchHit(0).getContent() : null; + P poCondition = toPo(condition); + Query query = buildQuery(poCondition); + SearchHits

searchHits = elasticsearchOperations.search(query, poClass); + P po = searchHits.hasSearchHits() ? searchHits.getSearchHit(0).getContent() : null; + return toEntity(po); } - /** - * 根据条件查询单条记录,并以 {@link Optional} 包装返回。 - * - * @param condition 查询条件对象 - * @return 包含首条匹配记录的 Optional - */ @Override - public Optional queryOneOptional(T condition) { + public Optional queryOneOptional(E condition) { return Optional.ofNullable(queryOne(condition)); } - /** - * 根据条件查询列表。 - *

- * 条件为 null 时使用 {@code Criteria.where("*").exists()} 匹配全部文档; - * 否则按非空字段构建等值 Criteria。 - * - * @param condition 查询条件对象,可为 null - * @return 匹配的实体列表,无匹配时返回空列表 - */ @Override - public List queryList(T condition) { + public List queryList(E condition) { if (condition == null) { - // 匹配全部文档 Query query = new CriteriaQuery(Criteria.where("*").exists()); - SearchHits searchHits = elasticsearchOperations.search(query, entityClass); + SearchHits

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

searchHits = elasticsearchOperations.search(query, poClass); return searchHits.getSearchHits().stream() - .map(hit -> hit.getContent()) + .map(hit -> toEntity(hit.getContent())) .collect(Collectors.toList()); } - /** - * 分页查询。 - *

- * 通过 {@code Criteria.where("*").exists()} 匹配全部文档,叠加 {@link PageRequest} 分页参数, - * 由 ES 原生分页(from/size)执行。 - * - * @param reqPage 分页请求(页码从 1 开始、每页大小,为 null 时取默认 1/10) - * @return 分页结果,含当前页、总页数、总条数、当前页记录 - */ @Override - public ResPage queryPage(ReqPage reqPage) { - // ES 页码从 0 开始,业务页码从 1 开始,需减 1 + public ResPage queryPage(ReqPage reqPage) { int pageNum = reqPage.getPage() != null ? reqPage.getPage() - 1 : 0; int pageSize = reqPage.getSize() != null ? reqPage.getSize() : 10; @@ -263,17 +202,15 @@ public ResPage queryPage(ReqPage reqPage) { PageRequest pageRequest = PageRequest.of(pageNum, pageSize, Sort.unsorted()); query.setPageable(pageRequest); - SearchHits searchHits = elasticsearchOperations.search(query, entityClass); + SearchHits

searchHits = elasticsearchOperations.search(query, poClass); - ResPage resPage = new ResPage<>(); - // 返回业务侧时页码再加回 1 + ResPage resPage = new ResPage<>(); resPage.setCurrent((long) (pageNum + 1)); - // 总页数向上取整 resPage.setPages((long) (searchHits.getTotalHits() > 0 ? (searchHits.getTotalHits() + pageSize - 1) / pageSize : 0)); resPage.setSize((long) pageSize); resPage.setTotal(searchHits.getTotalHits()); resPage.setRecords(searchHits.getSearchHits().stream() - .map(hit -> hit.getContent()) + .map(hit -> toEntity(hit.getContent())) .collect(Collectors.toList())); log.debug("Query page: page={}, size={}, total={}, records={}", @@ -281,13 +218,7 @@ public ResPage queryPage(ReqPage reqPage) { return resPage; } - /** - * 反射读取条件对象非空字段,构建等值 {@link CriteriaQuery}。 - * - * @param condition 条件对象 - * @return 已填充等值 Criteria 的 Query - */ - private Query buildQuery(T condition) { + private Query buildQuery(P condition) { Criteria criteria = new Criteria(); try { Field[] fields = getAllFields(condition.getClass()); @@ -295,7 +226,6 @@ private Query buildQuery(T condition) { field.setAccessible(true); Object value = field.get(condition); if (value != null) { - // 通过 and 链式拼接多个字段的等值条件 criteria = criteria.and(Criteria.where(field.getName()).is(value)); } } @@ -305,12 +235,6 @@ private Query buildQuery(T condition) { return new CriteriaQuery(criteria); } - /** - * 收集类及其所有父类(直到 Object)的声明字段。 - * - * @param clazz 起始类 - * @return 全部字段数组 - */ private Field[] getAllFields(Class clazz) { List fields = new java.util.ArrayList<>(); while (clazz != null && clazz != Object.class) { @@ -320,42 +244,27 @@ private Field[] getAllFields(Class clazz) { return fields.toArray(new Field[0]); } - /** - * 批量保存实体(逐条 save)。 - * - * @param entities 实体列表,为 null 或空时返回空列表 - * @return 保存后的实体列表 - */ @Override - public List saveBatch(List entities) { + public List saveBatch(List entities) { if (entities == null || entities.isEmpty()) { return List.of(); } return entities.stream() + .map(this::toPo) .map(elasticsearchOperations::save) + .map(this::toEntity) .collect(Collectors.toList()); } - /** - * 根据主键列表批量删除(逐条 delete,ID 转 String)。 - * - * @param ids 主键列表,为 null 或空时不执行任何操作 - */ @Override public void removeBatchByIds(List ids) { if (ids != null && !ids.isEmpty()) { - ids.forEach(id -> elasticsearchOperations.delete(String.valueOf(id), entityClass)); + ids.forEach(id -> elasticsearchOperations.delete(String.valueOf(id), poClass)); } } - /** - * 根据主键列表批量查询(逐条 get 并过滤 null)。 - * - * @param ids 主键列表,为 null 或空时返回空列表 - * @return 匹配的实体列表 - */ @Override - public List listByIds(List ids) { + public List listByIds(List ids) { if (ids == null || ids.isEmpty()) { return List.of(); } @@ -365,31 +274,51 @@ public List listByIds(List ids) { .collect(Collectors.toList()); } - /** - * 按条件统计记录数。 - * - * @param condition 条件对象,为 null 时统计全部文档 - * @return 匹配的记录数 - */ @Override - public long count(T condition) { + public long count(E condition) { if (condition == null) { - // 匹配全部文档 Query query = new CriteriaQuery(Criteria.where("*").exists()); - return elasticsearchOperations.count(query, entityClass); + return elasticsearchOperations.count(query, poClass); } - Query query = buildQuery(condition); - return elasticsearchOperations.count(query, entityClass); + P poCondition = toPo(condition); + Query query = buildQuery(poCondition); + return elasticsearchOperations.count(query, poClass); } - /** - * 判断是否存在匹配条件的记录。 - * - * @param condition 条件对象 - * @return 存在返回 true,否则 false - */ @Override - public boolean exists(T condition) { + public boolean exists(E condition) { return count(condition) > 0; } -} + + protected E toEntity(P po) { + if (po == null) { + return null; + } + if (entityClass == null) { + return (E) po; + } + try { + E entity = entityClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(po, entity); + return entity; + } catch (Exception e) { + throw new RuntimeException("Failed to convert PO to entity", e); + } + } + + protected P toPo(E entity) { + if (entity == null) { + return null; + } + if (poClass == null) { + return (P) entity; + } + try { + P po = poClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(entity, po); + return po; + } catch (Exception e) { + throw new RuntimeException("Failed to convert entity to PO", e); + } + } +} \ No newline at end of file diff --git a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/configuration/JpaAutoConfiguration.java b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/configuration/JpaAutoConfiguration.java index 5ddaf2b..9d7955b 100644 --- a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/configuration/JpaAutoConfiguration.java +++ b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/configuration/JpaAutoConfiguration.java @@ -1,85 +1,13 @@ package cn.structure.infra.jpa.configuration; -import cn.structure.infra.jpa.repository.JpaDelegateBeanPostProcessor; -import cn.structure.infra.jpa.repository.JpaDelegateFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.context.annotation.Bean; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.transaction.annotation.EnableTransactionManagement; -import jakarta.persistence.EntityManager; - -/** - * JPA 自动配置类 - *

- * 当检测到 JPA 相关依赖({@link org.springframework.data.jpa.repository.JpaRepository})时自动配置, - * 注册 JPA 持久化所需的核心组件,使其与仓储框架无缝集成。 - *

- * 注册的 Bean: - *

- *

- * 工作机制: - *

    - *
  1. 当 {@link cn.structure.infra.repository.RepositoryFacade} 需要获取 RepositoryDelegate 时, - * 会通过 {@link cn.structure.infra.repository.RepositoryBeanPostProcessor} 查找匹配的 Delegate
  2. - *
  3. 若未找到用户自定义的 Delegate,会通过 JpaDelegateFactory 自动创建
  4. - *
  5. DelegateBeanPostProcessor 确保用户自定义的 Delegate 实现能正确注入 EntityManager
  6. - *
- *

- * 额外配置: - *

- * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ @AutoConfiguration @ConditionalOnClass(name = "org.springframework.data.jpa.repository.JpaRepository") @EnableJpaRepositories @EnableTransactionManagement public class JpaAutoConfiguration { - - /** - * 创建 JPA 仓储委托工厂 - *

- * 负责根据 PO 类创建 JpaRepositoryDelegate 实例,通过 EntityManager 进行持久化操作。 - * 当 RepositoryFacade 需要获取 JPA 类型的 RepositoryDelegate 时,会通过此工厂进行创建。 - * - * @param entityManager JPA 实体管理器,用于执行数据库操作 - * @return JpaDelegateFactory 实例 - */ - @Bean - @ConditionalOnBean(EntityManager.class) - public JpaDelegateFactory jpaDelegateFactory(EntityManager entityManager) { - return new JpaDelegateFactory(entityManager); - } - - /** - * 创建 JPA 委托 Bean 后处理器 - *

- * 在 Bean 初始化完成后,自动为 JpaRepositoryDelegate 实现类注入 EntityManager 和实体类。 - *

- * 处理逻辑: - * 1. 扫描所有 Bean,筛选出 JpaRepositoryDelegate 的实例 - * 2. 从 Spring 上下文获取 EntityManager 并注入到 Delegate 实例中 - * 3. 检查是否存在 {@link cn.structure.infra.annotations.DelegateFor} 注解 - * 4. 将注解中指定的 PO 类设置到 Delegate 实例中 - * - * @return JpaDelegateBeanPostProcessor 实例 - */ - @Bean - @ConditionalOnClass(name = "jakarta.persistence.EntityManager") - public JpaDelegateBeanPostProcessor jpaDelegateBeanPostProcessor() { - return new JpaDelegateBeanPostProcessor(); - } } diff --git a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaDelegateBeanPostProcessor.java b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaDelegateBeanPostProcessor.java deleted file mode 100644 index 95d4871..0000000 --- a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaDelegateBeanPostProcessor.java +++ /dev/null @@ -1,117 +0,0 @@ -package cn.structure.infra.jpa.repository; - -import cn.structure.infra.annotations.DelegateFor; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; - -/** - * JPA RepositoryDelegate 的 BeanPostProcessor,负责为用户自定义 Delegate 子类自动注入 EntityManager 与实体类型。 - *

- * 在仓储框架中,业务方可继承 {@link JpaRepositoryDelegate} 实现自定义 Delegate,并通过 - * {@link DelegateFor} 注解声明其服务的 PO 类型。本后处理器在 Bean 初始化完成后: - *

    - *
  1. 识别所有 {@link JpaRepositoryDelegate} 类型的 Bean
  2. - *
  3. 按"by name → by type → create"顺序解析并注入 {@link EntityManager}
  4. - *
  5. 读取 {@link DelegateFor#po()} 指定的 PO 类型并通过 setter 注入
  6. - *
- *

- * 与 {@link JpaDelegateFactory} 的分工:工厂负责"无自定义 Delegate 时自动创建", - * 本处理器负责"已有自定义 Delegate 时补齐依赖",二者协同保证 RepositoryFacade 总能拿到可用的 Delegate。 - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -@Slf4j -public class JpaDelegateBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { - - /** Spring 上下文,用于解析 EntityManager */ - private ApplicationContext applicationContext; - - /** - * 注入 Spring 应用上下文,供后续按类型/名称查询 Bean。 - * - * @param applicationContext Spring 应用上下文 - * @throws BeansException 上下文注入异常 - */ - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - /** - * 在 Bean 初始化完成后,对自定义 JpaRepositoryDelegate 实现类注入 EntityManager 与实体类型。 - *

- * 仅当 Bean 是 {@link JpaRepositoryDelegate} 实例时执行注入;EntityManager 解析失败仅告警不抛异常。 - * - * @param bean 待处理的 Bean 实例 - * @param beanName Bean 名称 - * @return 原始 Bean(已注入依赖),未匹配类型时原样返回 - * @throws BeansException 处理过程中的异常 - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof JpaRepositoryDelegate) { - JpaRepositoryDelegate delegate = (JpaRepositoryDelegate) bean; - - // 解析 EntityManager:by name → by type → create - EntityManager entityManager = getEntityManager(); - if (entityManager != null) { - delegate.setEntityManager(entityManager); - log.info("Injected EntityManager into JpaRepositoryDelegate: {}", beanName); - } else { - log.warn("No EntityManager available to inject into JpaRepositoryDelegate: {}", beanName); - } - - // 读取 @DelegateFor 注解,识别该 Delegate 服务的 PO 类型并注入 - DelegateFor annotation = bean.getClass().getAnnotation(DelegateFor.class); - if (annotation != null && annotation.po() != void.class) { - delegate.setEntityClass(annotation.po()); - log.info("Injected entityClass {} into JpaRepositoryDelegate: {}", annotation.po().getSimpleName(), beanName); - } - } - return bean; - } - - /** - * 解析 {@link EntityManager} 实例。 - *

- * 解析顺序(按优先级): - *

    - *
  1. by name:从容器中获取名为 "entityManager" 的 Bean
  2. - *
  3. by type:从 {@link EntityManagerFactory} 创建新的 EntityManager
  4. - *
  5. 都失败时返回 null
  6. - *
- * - * @return EntityManager 实例,无法解析时返回 null - */ - private EntityManager getEntityManager() { - // 1) by name:优先按 "entityManager" 名称获取已注册的容器 Bean - try { - Object bean = applicationContext.getBean("entityManager"); - if (bean instanceof EntityManager) { - return (EntityManager) bean; - } - } catch (Exception e) { - log.debug("entityManager bean not found by name"); - } - - // 2) by type → create:通过 EntityManagerFactory 创建新的 EntityManager - try { - EntityManagerFactory factory = applicationContext.getBean(EntityManagerFactory.class); - if (factory != null) { - return factory.createEntityManager(); - } - } catch (Exception e) { - log.debug("EntityManagerFactory bean not found"); - } - - return null; - } -} \ No newline at end of file diff --git a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaDelegateFactory.java b/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaDelegateFactory.java deleted file mode 100644 index 14ed7bf..0000000 --- a/structure-infra-jpa-starter/src/main/java/cn/structure/infra/jpa/repository/JpaDelegateFactory.java +++ /dev/null @@ -1,66 +0,0 @@ -package cn.structure.infra.jpa.repository; - -import cn.structure.infra.repository.RepositoryDelegate; -import cn.structure.infra.repository.RepositoryDelegateFactory; -import cn.structure.infra.repository.RepositoryType; - -import jakarta.persistence.EntityManager; - -/** - * JPA 仓储委托工厂 - *

- * 实现 {@link RepositoryDelegateFactory} SPI,自动创建 {@link JpaRepositoryDelegate} 实例。 - * 在仓储框架中,当 {@code RepositoryFacade} 找不到用户自定义的 Delegate 时,会通过本工厂按 PO 类型 - * 创建默认 Delegate 实例(依赖容器中的 {@link EntityManager})。 - *

- * 与 {@link JpaDelegateBeanPostProcessor} 的分工:本工厂负责"无自定义 Delegate 时创建默认实现", - * BeanPostProcessor 负责"已有自定义子类时补齐依赖"。 - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -public class JpaDelegateFactory implements RepositoryDelegateFactory { - - /** JPA 实体管理器,由容器注入并共享给所有 Delegate 实例 */ - private final EntityManager entityManager; - - /** - * 构造工厂,注入 EntityManager。 - * - * @param entityManager JPA 实体管理器,用于执行持久化操作 - */ - public JpaDelegateFactory(EntityManager entityManager) { - this.entityManager = entityManager; - } - - /** - * 返回该工厂支持的仓储类型,用于 SPI 路由匹配。 - * - * @return 固定返回 {@link RepositoryType#JPA} - */ - @Override - public RepositoryType getType() { - return RepositoryType.JPA; - } - - /** - * 为指定 PO 类型创建 {@link JpaRepositoryDelegate} 实例。 - *

- * JPA 实现不依赖 Mapper 查找,直接以入参 PO 类型构造 Delegate,因此失败概率较低; - * 出现异常时返回 null,由上层 RepositoryFacade 继续尝试其他工厂或抛出异常。 - * - * @param poClass PO 实体类型 - * @param idClass 主键类型(当前实现未使用,保留以匹配 SPI 签名) - * @return 已注入 EntityManager 的 Delegate 实例;构造异常时返回 null - */ - @Override - @SuppressWarnings({"unchecked", "rawtypes"}) - public RepositoryDelegate createDelegate(Class poClass, Class idClass) { - try { - return new JpaRepositoryDelegate(entityManager, poClass); - } catch (Exception e) { - return null; - } - } -} 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 6ce7983..168399b 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 @@ -2,11 +2,11 @@ import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; +import cn.structure.infra.repository.GenericTypeResolver; import cn.structure.infra.repository.RepositoryDelegate; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; @@ -14,10 +14,12 @@ import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import jakarta.persistence.Id; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; /** * 基于 JPA 的 RepositoryDelegate 适配实现 @@ -36,259 +38,234 @@ *

  • 使用 Jakarta 命名空间({@code jakarta.persistence.*}),适用于 Spring Boot 3.x
  • *
  • 查询条件通过反射读取实体非空字段,组装为 Criteria API 的 {@link Predicate}(等值匹配)
  • *
  • save 委托给 {@link EntityManager#merge(Object)},由 JPA 自动判断新增或更新
  • - *
  • 分页采用"内存分页":先 findAll 取全量再切片,适用于中小数据量; - * 大数据量场景建议用户自定义 Delegate 子类覆盖 queryPage 使用原生 SQL 分页
  • + *
  • 分页使用 CriteriaBuilder 创建 COUNT 查询和物理分页,避免内存分页性能问题
  • + *
  • ID 字段名通过 PO 类的 {@link Id} 注解自动识别,默认为 "id"
  • + *
  • Entity ↔ PO 转换在此层完成,Facade 层只操作领域实体
  • * * - * @param 实体(PO)类型 + * @param 领域实体类型 + * @param

    持久化对象类型(JPA Entity) * @param 主键类型 * @author chuck - * @version 1.0.1 + * @version 1.0.3 * @since 2026/6/28 */ @Slf4j -public class JpaRepositoryDelegate implements RepositoryDelegate { +public class JpaRepositoryDelegate implements RepositoryDelegate { - /** JPA 实体管理器,承担实际持久化操作 */ + @Autowired protected EntityManager entityManager; - /** PO 实体类型,用于 Criteria API 与 find */ - protected Class entityClass; - - /** - * 默认构造器,用于用户自定义子类场景。 - *

    - * 创建后由 {@link JpaDelegateBeanPostProcessor} 通过 setter 注入依赖。 - */ + protected Class entityClass; + protected Class

    poClass; + protected Class idClass; + protected String idFieldName = "id"; + 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; } - /** - * 全参构造器,工厂自动创建场景使用。 - * - * @param entityManager JPA 实体管理器 - * @param entityClass PO 实体类型 - */ - public JpaRepositoryDelegate(EntityManager entityManager, Class entityClass) { - this.entityManager = entityManager; - this.entityClass = entityClass; - log.info("JpaRepositoryDelegate initialized for entity: {}", entityClass.getSimpleName()); + @Override + public Class getEntityClass() { + return entityClass; } - /** - * 注入 EntityManager,供 BeanPostProcessor 在自定义子类上调用。 - * - * @param entityManager JPA 实体管理器 - */ - public void setEntityManager(EntityManager entityManager) { - this.entityManager = entityManager; + @Override + public Class getPoClass() { + return poClass; } - /** - * 注入 PO 实体类型,供 BeanPostProcessor 在自定义子类上调用。 - * - * @param entityClass PO 实体类型 - */ - public void setEntityClass(Class entityClass) { - this.entityClass = entityClass; + @Override + public Class getIdClass() { + return idClass; } - /** - * 保存或更新实体。 - *

    - * 委托给 {@link EntityManager#merge(Object)},由 JPA 根据实体主键自动判断新增或更新。 - * - * @param entity 实体对象,为 null 或依赖未就绪时返回 null - * @return merge 后的实体实例(可能是新对象引用) - */ @Override - public T save(T entity) { - if (entity == null || entityManager == null || entityClass == null) { + public String getIdFieldName() { + return idFieldName; + } + + @Override + public E save(E entity) { + if (entity == null || entityManager == null || poClass == null) { return null; } - T saved = entityManager.merge(entity); - log.debug("Saved entity: {}", saved); - return saved; + P po = toPo(entity); + P savedPo = entityManager.merge(po); + log.debug("Saved entity: {}", savedPo); + return toEntity(savedPo); } - /** - * 根据主键删除记录。 - *

    - * JPA 删除前必须先 find 出受管实体再 remove,无法直接按 ID 删除。 - * - * @param id 主键值,为 null 时不执行任何操作 - */ @Override public void removeById(ID id) { - if (id != null) { - // JPA 删除需先加载受管实体再 remove - T entity = findById(id); - if (entity != null) { - entityManager.remove(entity); + if (id != null && entityManager != null && poClass != null) { + P po = entityManager.find(poClass, id); + if (po != null) { + entityManager.remove(po); log.debug("Removed entity: id={}", id); } } } - /** - * 根据主键查询实体。 - * - * @param id 主键值,为 null 时返回 null - * @return 实体对象,未找到时返回 null - */ @Override - public T findById(ID id) { - if (id == null) { + public E findById(ID id) { + if (id == null || entityManager == null || poClass == null) { return null; } - T entity = entityManager.find(entityClass, id); - log.debug("Find by id: id={}, found={}", id, entity != null); - return entity; + P po = entityManager.find(poClass, id); + log.debug("Find by id: id={}, found={}", id, po != null); + return toEntity(po); } - /** - * 根据主键查询(与 findById 等价,语义上用于"读模型")。 - * - * @param id 主键值 - * @return 实体对象,未找到时返回 null - */ @Override - public T queryById(ID id) { + public E queryById(ID id) { return findById(id); } - /** - * 根据主键查询并以 {@link Optional} 包装返回。 - * - * @param id 主键值 - * @return 包含实体的 Optional,未找到时为 {@link Optional#empty()} - */ @Override - public Optional queryByIdOptional(ID id) { - return Optional.ofNullable(findById(id)); + public Optional queryByIdOptional(ID id) { + return Optional.ofNullable(queryById(id)); } - /** - * 根据非空字段等值匹配查询单条记录。 - *

    - * 通过 Criteria API 构建等值条件,取结果集首条;多于一条时仅返回首条。 - * - * @param condition 查询条件对象,为 null 时返回 null - * @return 首条匹配记录,无匹配时返回 null - */ @Override - public T queryOne(T condition) { + public E queryOne(E condition) { if (condition == null) { return null; } - List results = queryList(condition); + List results = queryList(condition); return results.isEmpty() ? null : results.get(0); } - /** - * 根据条件查询单条记录,并以 {@link Optional} 包装返回。 - * - * @param condition 查询条件对象 - * @return 包含首条匹配记录的 Optional - */ @Override - public Optional queryOneOptional(T condition) { + public Optional queryOneOptional(E condition) { return Optional.ofNullable(queryOne(condition)); } - /** - * 根据条件查询列表。 - *

    - * 条件为 null 时查询全部;否则按非空字段构建 Criteria 等值条件。 - * - * @param condition 查询条件对象,可为 null - * @return 匹配的实体列表,无匹配时返回空列表 - */ @Override - public List queryList(T condition) { + public List queryList(E condition) { if (condition == null) { return findAll(); } return queryByCondition(condition); } - /** - * 分页查询(内存分页)。 - *

    - * 注意:JPA 不支持原生分页时使用内存分页——先 findAll 取全量结果, - * 再按 subList 切片返回当前页。该实现适用于中小数据量;大数据量场景 - * 建议用户自定义 Delegate 子类覆盖本方法,使用原生 SQL 分页。 - * - * @param reqPage 分页请求(页码从 1 开始、每页大小,为 null 时取默认 1/10) - * @return 分页结果,含当前页、总页数、总条数、当前页记录 - */ @Override - public ResPage queryPage(ReqPage reqPage) { - // JPA 页码从 0 开始,业务页码从 1 开始,需减 1 转换 + public ResPage queryPage(ReqPage reqPage) { + if (entityManager == null || poClass == null) { + ResPage emptyPage = new ResPage<>(); + emptyPage.setCurrent(1L); + emptyPage.setPages(0L); + emptyPage.setSize(10L); + emptyPage.setTotal(0L); + emptyPage.setRecords(List.of()); + return emptyPage; + } + int pageNum = reqPage.getPage() != null ? reqPage.getPage() - 1 : 0; int pageSize = reqPage.getSize() != null ? reqPage.getSize() : 10; - // 内存分页:先取全量再切片(大数据量场景应覆盖此方法) - List allResults = findAll(); - int start = pageNum * pageSize; - int end = Math.min(start + pageSize, allResults.size()); + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + long total = executeCountQuery(cb); - List pageContent = start < allResults.size() ? allResults.subList(start, end) : List.of(); + List pageContent; + if (total > 0) { + CriteriaQuery

    query = cb.createQuery(poClass); + query.from(poClass); - ResPage resPage = new ResPage<>(); - // 返回业务侧时页码再加回 1 + TypedQuery

    typedQuery = entityManager.createQuery(query); + typedQuery.setFirstResult(pageNum * pageSize); + typedQuery.setMaxResults(pageSize); + + pageContent = typedQuery.getResultList().stream() + .map(this::toEntity) + .collect(Collectors.toList()); + } else { + pageContent = List.of(); + } + + ResPage resPage = new ResPage<>(); resPage.setCurrent((long) (pageNum + 1)); - // 总页数向上取整 - resPage.setPages((long) ((allResults.size() + pageSize - 1) / pageSize)); + resPage.setPages(total > 0 ? (total + pageSize - 1) / pageSize : 0); resPage.setSize((long) pageSize); - resPage.setTotal((long) allResults.size()); + resPage.setTotal(total); resPage.setRecords(pageContent); log.debug("Query page: page={}, size={}, total={}, records={}", - pageNum + 1, pageSize, allResults.size(), pageContent.size()); + pageNum + 1, pageSize, total, pageContent.size()); return resPage; } - /** - * 通过 Criteria API 查询全部实体。 - * - * @return 全部实体列表 - */ - private List findAll() { + private long executeCountQuery(CriteriaBuilder cb) { + CriteriaQuery countQuery = cb.createQuery(Long.class); + countQuery.select(cb.count(countQuery.from(poClass))); + return entityManager.createQuery(countQuery).getSingleResult(); + } + + private List findAll() { + if (entityManager == null || poClass == null) { + return List.of(); + } CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery query = cb.createQuery(entityClass); - query.from(entityClass); - return entityManager.createQuery(query).getResultList(); + CriteriaQuery

    query = cb.createQuery(poClass); + query.from(poClass); + return entityManager.createQuery(query).getResultList().stream() + .map(this::toEntity) + .collect(Collectors.toList()); } - /** - * 通过 Criteria API 按条件等值查询。 - * - * @param condition 条件对象 - * @return 匹配的实体列表 - */ - private List queryByCondition(T condition) { + private List queryByCondition(E condition) { + if (entityManager == null || poClass == null) { + return List.of(); + } CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery query = cb.createQuery(entityClass); - Root root = query.from(entityClass); + CriteriaQuery

    query = cb.createQuery(poClass); + Root

    root = query.from(poClass); - // 构建等值 Predicate 数组并拼接到 WHERE 子句 Predicate[] predicates = buildPredicates(cb, root, condition); if (predicates.length > 0) { query.where(predicates); } - return entityManager.createQuery(query).getResultList(); + return entityManager.createQuery(query).getResultList().stream() + .map(this::toEntity) + .collect(Collectors.toList()); } - /** - * 反射读取条件对象非空字段,构建等值 {@link Predicate} 数组。 - * - * @param cb CriteriaBuilder - * @param root 查询根 - * @param condition 条件对象 - * @return 等值 Predicate 数组 - */ - private Predicate[] buildPredicates(CriteriaBuilder cb, Root root, T condition) { + private Predicate[] buildPredicates(CriteriaBuilder cb, Root

    root, E condition) { List predicates = new java.util.ArrayList<>(); try { Field[] fields = getAllFields(condition.getClass()); @@ -296,7 +273,6 @@ private Predicate[] buildPredicates(CriteriaBuilder cb, Root root, T conditio field.setAccessible(true); Object value = field.get(condition); if (value != null) { - // 直接以字段名作为属性路径,等值匹配 predicates.add(cb.equal(root.get(field.getName()), value)); } } @@ -306,12 +282,6 @@ private Predicate[] buildPredicates(CriteriaBuilder cb, Root root, T conditio return predicates.toArray(new Predicate[0]); } - /** - * 收集类及其所有父类(直到 Object)的声明字段。 - * - * @param clazz 起始类 - * @return 全部字段数组 - */ private Field[] getAllFields(Class clazz) { List fields = new java.util.ArrayList<>(); while (clazz != null && clazz != Object.class) { @@ -321,27 +291,18 @@ private Field[] getAllFields(Class clazz) { return fields.toArray(new Field[0]); } - /** - * 批量保存实体(逐条 merge)。 - * - * @param entities 实体列表,为 null 或空时返回空列表 - * @return merge 后的实体列表 - */ @Override - public List saveBatch(List entities) { + public List saveBatch(List entities) { if (entities == null || entities.isEmpty()) { return List.of(); } return entities.stream() + .map(this::toPo) .map(entityManager::merge) - .toList(); + .map(this::toEntity) + .collect(Collectors.toList()); } - /** - * 根据主键列表批量删除(逐条 find + remove)。 - * - * @param ids 主键列表,为 null 时不执行任何操作 - */ @Override public void removeBatchByIds(List ids) { if (ids != null) { @@ -349,47 +310,81 @@ public void removeBatchByIds(List ids) { } } - /** - * 根据主键列表批量查询(逐条 find 并过滤 null)。 - * - * @param ids 主键列表,为 null 或空时返回空列表 - * @return 匹配的实体列表 - */ @Override - public List listByIds(List ids) { - if (ids == null || ids.isEmpty()) { + public List listByIds(List ids) { + if (ids == null || ids.isEmpty() || entityManager == null || poClass == null) { return List.of(); } - return ids.stream() - .map(this::findById) - .filter(java.util.Objects::nonNull) - .toList(); + + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery

    query = cb.createQuery(poClass); + Root

    root = query.from(poClass); + + query.where(root.get(idFieldName).in(ids)); + + return entityManager.createQuery(query).getResultList().stream() + .map(this::toEntity) + .collect(Collectors.toList()); } - /** - * 按条件统计记录数。 - *

    - * 当前实现通过查询结果列表的 size 计数(未走 COUNT 查询),适用于中小数据量。 - * - * @param condition 条件对象,为 null 时统计全表 - * @return 匹配的记录数 - */ @Override - public long count(T condition) { + public long count(E condition) { + if (entityManager == null || poClass == null) { + return 0; + } + + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + if (condition == null) { - return findAll().size(); + return executeCountQuery(cb); } - return queryList(condition).size(); + + CriteriaQuery countQuery = cb.createQuery(Long.class); + Root

    root = countQuery.from(poClass); + countQuery.select(cb.count(root)); + + Predicate[] predicates = buildPredicates(cb, root, condition); + if (predicates.length > 0) { + countQuery.where(predicates); + } + + return entityManager.createQuery(countQuery).getSingleResult(); } - /** - * 判断是否存在匹配条件的记录。 - * - * @param condition 条件对象 - * @return 存在返回 true,否则 false - */ @Override - public boolean exists(T condition) { + public boolean exists(E condition) { return count(condition) > 0; } + + protected E toEntity(P po) { + if (po == null) { + return null; + } + if (entityClass == null) { + return (E) po; + } + try { + E entity = entityClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(po, entity); + return entity; + } catch (Exception e) { + throw new RuntimeException("Failed to convert PO to entity", e); + } + } + + protected P toPo(E entity) { + if (entity == null) { + return null; + } + if (poClass == null) { + return (P) entity; + } + try { + P po = poClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(entity, po); + return po; + } catch (Exception e) { + throw new RuntimeException("Failed to convert entity to PO", e); + } + } } \ No newline at end of file diff --git a/structure-infra-mongodb-starter/pom.xml b/structure-infra-mongodb-starter/pom.xml index ba7785e..789d10e 100644 --- a/structure-infra-mongodb-starter/pom.xml +++ b/structure-infra-mongodb-starter/pom.xml @@ -24,6 +24,10 @@ org.springframework.boot spring-boot-starter-data-mongodb + + jakarta.persistence + jakarta.persistence-api + diff --git a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/configuration/MongoAutoConfiguration.java b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/configuration/MongoAutoConfiguration.java index 65209b9..8d6deec 100644 --- a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/configuration/MongoAutoConfiguration.java +++ b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/configuration/MongoAutoConfiguration.java @@ -1,89 +1,13 @@ package cn.structure.infra.mongodb.configuration; -import cn.structure.infra.mongodb.repository.MongoDelegateBeanPostProcessor; -import cn.structure.infra.mongodb.repository.MongoDelegateFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; -/** - * MongoDB 自动配置类 - *

    - * 当检测到 MongoDB 相关依赖({@link org.springframework.data.mongodb.core.MongoTemplate})时自动配置, - * 注册 MongoDB 文档操作所需的核心组件,使其与仓储框架无缝集成。 - *

    - * 注册的 Bean: - *

      - *
    • {@link cn.structure.infra.mongodb.repository.MongoDelegateFactory} - 仓储委托工厂, - * 负责根据 PO 类创建 {@link cn.structure.infra.mongodb.repository.MongoRepositoryDelegate} 实例,依赖 {@link org.springframework.data.mongodb.core.MongoTemplate}
    • - *
    • {@link cn.structure.infra.mongodb.repository.MongoDelegateBeanPostProcessor} - Bean 后处理器, - * 为自定义的 MongoRepositoryDelegate 实现类自动注入 MongoTemplate 和实体类
    • - *
    - *

    - * 工作机制: - *

      - *
    1. 当 {@link cn.structure.infra.repository.RepositoryFacade} 需要获取 RepositoryDelegate 时, - * 会通过 {@link cn.structure.infra.repository.RepositoryBeanPostProcessor} 查找匹配的 Delegate
    2. - *
    3. 若未找到用户自定义的 Delegate,会通过 MongoDelegateFactory 自动创建
    4. - *
    5. DelegateBeanPostProcessor 确保用户自定义的 Delegate 实现能正确注入 MongoTemplate
    6. - *
    - *

    - * 配置方式: - *

      - *
    • 默认自动启用(matchIfMissing = true)
    • - *
    • 可通过 `structure.infra.type=MONGODB` 显式指定
    • - *
    - *

    - * 额外配置: - *

      - *
    • {@link EnableMongoRepositories} - 启用 Spring Data MongoDB 仓库扫描
    • - *
    - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ @AutoConfiguration @ConditionalOnClass(name = "org.springframework.data.mongodb.core.MongoTemplate") @ConditionalOnProperty(prefix = "structure.infra", name = "type", havingValue = "MONGODB", matchIfMissing = true) @EnableMongoRepositories public class MongoAutoConfiguration { - - /** - * 创建 MongoDB 仓储委托工厂 - *

    - * 负责根据 PO 类创建 MongoRepositoryDelegate 实例,通过 MongoTemplate 进行文档操作。 - * 当 RepositoryFacade 需要获取 MongoDB 类型的 RepositoryDelegate 时,会通过此工厂进行创建。 - * - * @param mongoTemplate MongoDB 操作模板,用于执行增删改查等操作 - * @return MongoDelegateFactory 实例 - */ - @Bean - @ConditionalOnBean(MongoTemplate.class) - public MongoDelegateFactory mongoDelegateFactory(MongoTemplate mongoTemplate) { - return new MongoDelegateFactory(mongoTemplate); - } - - /** - * 创建 MongoDB 委托 Bean 后处理器 - *

    - * 在 Bean 初始化完成后,自动为 MongoRepositoryDelegate 实现类注入 MongoTemplate 和实体类。 - *

    - * 处理逻辑: - * 1. 扫描所有 Bean,筛选出 MongoRepositoryDelegate 的实例 - * 2. 从 Spring 上下文获取 MongoTemplate 并注入到 Delegate 实例中 - * 3. 检查是否存在 {@link cn.structure.infra.annotations.DelegateFor} 注解 - * 4. 将注解中指定的 PO 类设置到 Delegate 实例中 - * - * @return MongoDelegateBeanPostProcessor 实例 - */ - @Bean - @ConditionalOnBean(MongoTemplate.class) - public MongoDelegateBeanPostProcessor mongoDelegateBeanPostProcessor() { - return new MongoDelegateBeanPostProcessor(); - } } diff --git a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoDelegateBeanPostProcessor.java b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoDelegateBeanPostProcessor.java deleted file mode 100644 index f71088f..0000000 --- a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoDelegateBeanPostProcessor.java +++ /dev/null @@ -1,79 +0,0 @@ -package cn.structure.infra.mongodb.repository; - -import cn.structure.infra.annotations.DelegateFor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.data.mongodb.core.MongoTemplate; - -/** - * MongoDB RepositoryDelegate 的 BeanPostProcessor,负责为用户自定义 Delegate 子类自动注入 MongoTemplate 与实体类型。 - *

    - * 在仓储框架中,业务方可继承 {@link MongoRepositoryDelegate} 实现自定义 Delegate,并通过 - * {@link DelegateFor} 注解声明其服务的 PO 类型。本后处理器在 Bean 初始化完成后: - *

      - *
    1. 识别所有 {@link MongoRepositoryDelegate} 类型的 Bean
    2. - *
    3. 按类型从容器获取 {@link MongoTemplate} 并注入
    4. - *
    5. 读取 {@link DelegateFor#po()} 指定的 PO 类型并通过 setter 注入
    6. - *
    - *

    - * 与 {@link MongoDelegateFactory} 的分工:工厂负责"无自定义 Delegate 时自动创建", - * 本处理器负责"已有自定义 Delegate 时补齐依赖",二者协同保证 RepositoryFacade 总能拿到可用的 Delegate。 - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -@Slf4j -public class MongoDelegateBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { - - /** Spring 上下文,用于按类型获取 MongoTemplate */ - private ApplicationContext applicationContext; - - /** - * 注入 Spring 应用上下文,供后续按类型查询 Bean。 - * - * @param applicationContext Spring 应用上下文 - * @throws BeansException 上下文注入异常 - */ - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - /** - * 在 Bean 初始化完成后,对自定义 MongoRepositoryDelegate 实现类注入 MongoTemplate 与实体类型。 - *

    - * 仅当 Bean 是 {@link MongoRepositoryDelegate} 实例时执行注入;MongoTemplate 解析失败仅告警不抛异常。 - * - * @param bean 待处理的 Bean 实例 - * @param beanName Bean 名称 - * @return 原始 Bean(已注入依赖),未匹配类型时原样返回 - * @throws BeansException 处理过程中的异常 - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof MongoRepositoryDelegate) { - MongoRepositoryDelegate delegate = (MongoRepositoryDelegate) bean; - try { - // 按类型从容器获取 MongoTemplate 并注入 - MongoTemplate mongoTemplate = applicationContext.getBean(MongoTemplate.class); - delegate.setMongoTemplate(mongoTemplate); - - // 读取 @DelegateFor 注解,识别该 Delegate 服务的 PO 类型并注入 - DelegateFor annotation = bean.getClass().getAnnotation(DelegateFor.class); - if (annotation != null && annotation.po() != void.class) { - delegate.setEntityClass(annotation.po()); - } - - log.info("Injected MongoTemplate into MongoRepositoryDelegate: {}", beanName); - } catch (Exception e) { - log.warn("Failed to inject MongoTemplate into MongoRepositoryDelegate {}: {}", beanName, e.getMessage()); - } - } - return bean; - } -} \ No newline at end of file diff --git a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoDelegateFactory.java b/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoDelegateFactory.java deleted file mode 100644 index d7cb15a..0000000 --- a/structure-infra-mongodb-starter/src/main/java/cn/structure/infra/mongodb/repository/MongoDelegateFactory.java +++ /dev/null @@ -1,65 +0,0 @@ -package cn.structure.infra.mongodb.repository; - -import cn.structure.infra.repository.RepositoryDelegate; -import cn.structure.infra.repository.RepositoryDelegateFactory; -import cn.structure.infra.repository.RepositoryType; -import org.springframework.data.mongodb.core.MongoTemplate; - -/** - * MongoDB 仓储委托工厂 - *

    - * 实现 {@link RepositoryDelegateFactory} SPI,自动创建 {@link MongoRepositoryDelegate} 实例。 - * 在仓储框架中,当 {@code RepositoryFacade} 找不到用户自定义的 Delegate 时,会通过本工厂按 PO 类型 - * 创建默认 Delegate 实例(依赖容器中的 {@link MongoTemplate})。 - *

    - * 与 {@link MongoDelegateBeanPostProcessor} 的分工:本工厂负责"无自定义 Delegate 时创建默认实现", - * BeanPostProcessor 负责"已有自定义子类时补齐依赖"。 - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -public class MongoDelegateFactory implements RepositoryDelegateFactory { - - /** MongoDB 操作模板,由容器注入并共享给所有 Delegate 实例 */ - private final MongoTemplate mongoTemplate; - - /** - * 构造工厂,注入 MongoTemplate。 - * - * @param mongoTemplate MongoDB 操作模板 - */ - public MongoDelegateFactory(MongoTemplate mongoTemplate) { - this.mongoTemplate = mongoTemplate; - } - - /** - * 返回该工厂支持的仓储类型,用于 SPI 路由匹配。 - * - * @return 固定返回 {@link RepositoryType#MONGODB} - */ - @Override - public RepositoryType getType() { - return RepositoryType.MONGODB; - } - - /** - * 为指定 PO 类型创建 {@link MongoRepositoryDelegate} 实例。 - *

    - * MongoDB 实现不依赖 Mapper 查找,直接以入参 PO 类型构造 Delegate,因此失败概率较低; - * 出现异常时返回 null,由上层 RepositoryFacade 继续尝试其他工厂或抛出异常。 - * - * @param poClass PO 实体类型 - * @param idClass 主键类型(当前实现未使用,保留以匹配 SPI 签名) - * @return 已注入 MongoTemplate 的 Delegate 实例;构造异常时返回 null - */ - @Override - @SuppressWarnings({"unchecked", "rawtypes"}) - public RepositoryDelegate createDelegate(Class poClass, Class idClass) { - try { - return new MongoRepositoryDelegate(mongoTemplate, poClass); - } catch (Exception e) { - return 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 f1afbbf..a447592 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 @@ -2,18 +2,23 @@ import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; +import cn.structure.infra.repository.GenericTypeResolver; import cn.structure.infra.repository.RepositoryDelegate; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; +import jakarta.persistence.Id; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; /** * 基于 MongoDB 的 RepositoryDelegate 适配实现 @@ -30,229 +35,171 @@ * 实现说明: *

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

    持久化对象类型(MongoDB Document) * @param 主键类型 * @author chuck - * @version 1.0.1 + * @version 1.0.3 * @since 2026/6/28 */ @Slf4j -public class MongoRepositoryDelegate implements RepositoryDelegate { +public class MongoRepositoryDelegate implements RepositoryDelegate { - /** MongoDB 操作模板,承担实际文档操作 */ + @Autowired protected MongoTemplate mongoTemplate; - /** PO 实体类型,用于反射读取字段与 findOne/find */ - protected Class entityClass; - /** 主键字段名,默认 "id",可被子类覆盖 */ - protected String idFieldName; - - /** - * 默认构造器,用于用户自定义子类场景。 - *

    - * 创建后由 {@link MongoDelegateBeanPostProcessor} 通过 setter 注入依赖。 - */ + 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); + } + + @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(); + } + } } - /** - * 以默认主键字段名 "id" 构造 Delegate。 - * - * @param mongoTemplate MongoDB 操作模板 - * @param entityClass PO 实体类型 - */ - public MongoRepositoryDelegate(MongoTemplate mongoTemplate, Class entityClass) { - this(mongoTemplate, entityClass, "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; } - /** - * 全参构造器,工厂自动创建场景使用。 - * - * @param mongoTemplate MongoDB 操作模板 - * @param entityClass PO 实体类型 - * @param idFieldName 主键字段名(用于构建按 ID 查询的 Criteria) - */ - public MongoRepositoryDelegate(MongoTemplate mongoTemplate, Class entityClass, String idFieldName) { - this.mongoTemplate = mongoTemplate; - this.entityClass = entityClass; - this.idFieldName = idFieldName; - log.info("MongoRepositoryDelegate initialized for entity: {}", entityClass.getSimpleName()); + @Override + public Class getEntityClass() { + return entityClass; } - /** - * 注入 MongoTemplate,供 BeanPostProcessor 在自定义子类上调用。 - * - * @param mongoTemplate MongoDB 操作模板 - */ - public void setMongoTemplate(MongoTemplate mongoTemplate) { - this.mongoTemplate = mongoTemplate; + @Override + public Class getPoClass() { + return poClass; } - /** - * 注入 PO 实体类型,供 BeanPostProcessor 在自定义子类上调用。 - * - * @param entityClass PO 实体类型 - */ - public void setEntityClass(Class entityClass) { - this.entityClass = entityClass; + @Override + public Class getIdClass() { + return idClass; } - /** - * 设置主键字段名,供自定义子类覆盖默认 "id"。 - * - * @param idFieldName 主键字段名 - */ - public void setIdFieldName(String idFieldName) { - this.idFieldName = idFieldName; + @Override + public String getIdFieldName() { + return idFieldName; } - /** - * 保存或更新文档。 - *

    - * 委托给 {@link MongoTemplate#save(Object)},由 MongoDB 依据 _id 自动判断新增或更新。 - * - * @param entity 实体对象,为 null 时返回 null - * @return 保存后的实体(与入参同一引用) - */ @Override - public T save(T entity) { + public E save(E entity) { if (entity == null) { return null; } - T saved = mongoTemplate.save(entity); - log.debug("Saved entity: {}", saved); - return saved; + P po = toPo(entity); + P savedPo = mongoTemplate.save(po); + log.debug("Saved entity: {}", savedPo); + return toEntity(savedPo); } - /** - * 根据主键删除文档。 - * - * @param id 主键值,为 null 时不执行任何操作 - */ @Override public void removeById(ID id) { - if (id != null) { - // 按主键字段构建等值条件并删除 + if (id != null && poClass != null) { Query query = new Query(Criteria.where(idFieldName).is(id)); - mongoTemplate.remove(query, entityClass); + mongoTemplate.remove(query, poClass); log.debug("Removed entity: id={}", id); } } - /** - * 根据主键查询文档。 - * - * @param id 主键值,为 null 时返回 null - * @return 实体对象,未找到时返回 null - */ @Override - public T findById(ID id) { - if (id == null) { + public E findById(ID id) { + if (id == null || poClass == null) { return null; } Query query = new Query(Criteria.where(idFieldName).is(id)); - T entity = mongoTemplate.findOne(query, entityClass); - log.debug("Find by id: id={}, found={}", id, entity != null); - return entity; + P po = mongoTemplate.findOne(query, poClass); + log.debug("Find by id: id={}, found={}", id, po != null); + return toEntity(po); } - /** - * 根据主键查询(与 findById 等价,语义上用于"读模型")。 - * - * @param id 主键值 - * @return 实体对象,未找到时返回 null - */ @Override - public T queryById(ID id) { + public E queryById(ID id) { return findById(id); } - /** - * 根据主键查询并以 {@link Optional} 包装返回。 - * - * @param id 主键值 - * @return 包含实体的 Optional,未找到时为 {@link Optional#empty()} - */ @Override - public Optional queryByIdOptional(ID id) { + public Optional queryByIdOptional(ID id) { return Optional.ofNullable(queryById(id)); } - /** - * 根据非空字段等值匹配查询单条记录。 - *

    - * 通过反射构建 {@link Query},取首条匹配;多于一条时仅返回首条。 - * - * @param condition 查询条件对象,为 null 时返回 null - * @return 首条匹配记录,无匹配时返回 null - */ @Override - public T queryOne(T condition) { + public E queryOne(E condition) { if (condition == null) { return null; } Query query = buildQuery(condition); - return mongoTemplate.findOne(query, entityClass); + P po = mongoTemplate.findOne(query, poClass); + return toEntity(po); } - /** - * 根据条件查询单条记录,并以 {@link Optional} 包装返回。 - * - * @param condition 查询条件对象 - * @return 包含首条匹配记录的 Optional - */ @Override - public Optional queryOneOptional(T condition) { + public Optional queryOneOptional(E condition) { return Optional.ofNullable(queryOne(condition)); } - /** - * 根据条件查询列表。 - *

    - * 条件为 null 时查询全部;否则按非空字段构建等值 {@link Query}。 - * - * @param condition 查询条件对象,可为 null - * @return 匹配的实体列表,无匹配时返回空列表 - */ @Override - public List queryList(T condition) { + public List queryList(E condition) { if (condition == null) { - return mongoTemplate.findAll(entityClass); + return mongoTemplate.findAll(poClass).stream() + .map(this::toEntity) + .collect(Collectors.toList()); } Query query = buildQuery(condition); - return mongoTemplate.find(query, entityClass); + return mongoTemplate.find(query, poClass).stream() + .map(this::toEntity) + .collect(Collectors.toList()); } - /** - * 分页查询。 - *

    - * 通过 {@link MongoTemplate#count(Query, Class)} 获取总数, - * 再用 {@link PageRequest} 切片查询当前页记录。 - * - * @param reqPage 分页请求(页码从 1 开始、每页大小,为 null 时取默认 1/10) - * @return 分页结果,含当前页、总页数、总条数、当前页记录 - */ @Override - public ResPage queryPage(ReqPage reqPage) { - // MongoTemplate 页码从 0 开始,业务页码从 1 开始,需减 1 + public ResPage queryPage(ReqPage reqPage) { int pageNum = reqPage.getPage() != null ? reqPage.getPage() - 1 : 0; int pageSize = reqPage.getSize() != null ? reqPage.getSize() : 10; Query query = new Query(); - long total = mongoTemplate.count(query, entityClass); + long total = mongoTemplate.count(query, poClass); - // 在原 Query 上叠加分页参数 Query pageQuery = query.with(PageRequest.of(pageNum, pageSize, Sort.unsorted())); - List records = mongoTemplate.find(pageQuery, entityClass); + List records = mongoTemplate.find(pageQuery, poClass).stream() + .map(this::toEntity) + .collect(Collectors.toList()); - ResPage resPage = new ResPage<>(); - // 返回业务侧时页码再加回 1 + 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); @@ -263,13 +210,7 @@ public ResPage queryPage(ReqPage reqPage) { return resPage; } - /** - * 反射读取条件对象非空字段,构建等值 {@link Query}。 - * - * @param condition 条件对象 - * @return 已填充等值 Criteria 的 Query - */ - private Query buildQuery(T condition) { + private Query buildQuery(E condition) { Query query = new Query(); try { Field[] fields = getAllFields(condition.getClass()); @@ -277,7 +218,6 @@ private Query buildQuery(T condition) { field.setAccessible(true); Object value = field.get(condition); if (value != null) { - // 直接以字段名作为 key,等值匹配 query.addCriteria(Criteria.where(field.getName()).is(value)); } } @@ -287,12 +227,6 @@ private Query buildQuery(T condition) { return query; } - /** - * 收集类及其所有父类(直到 Object)的声明字段。 - * - * @param clazz 起始类 - * @return 全部字段数组 - */ private Field[] getAllFields(Class clazz) { List fields = new java.util.ArrayList<>(); while (clazz != null && clazz != Object.class) { @@ -302,73 +236,80 @@ private Field[] getAllFields(Class clazz) { return fields.toArray(new Field[0]); } - /** - * 批量保存实体(逐条 save)。 - * - * @param entities 实体列表,为 null 或空时返回空列表 - * @return 保存后的实体列表 - */ @Override - public List saveBatch(List entities) { + public List saveBatch(List entities) { if (entities == null || entities.isEmpty()) { return List.of(); } return entities.stream() + .map(this::toPo) .map(mongoTemplate::save) - .toList(); + .map(this::toEntity) + .collect(Collectors.toList()); } - /** - * 根据主键列表批量删除(使用 $in 一次性删除)。 - * - * @param ids 主键列表,为 null 或空时不执行任何操作 - */ @Override public void removeBatchByIds(List ids) { if (ids != null && !ids.isEmpty()) { Query query = new Query(Criteria.where(idFieldName).in(ids)); - mongoTemplate.remove(query, entityClass); + mongoTemplate.remove(query, poClass); } } - /** - * 根据主键列表批量查询(使用 $in 一次性查询)。 - * - * @param ids 主键列表,为 null 或空时返回空列表 - * @return 匹配的实体列表 - */ @Override - public List listByIds(List ids) { + 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, entityClass); + return mongoTemplate.find(query, poClass).stream() + .map(this::toEntity) + .collect(Collectors.toList()); } - /** - * 按条件统计记录数。 - * - * @param condition 条件对象,为 null 时统计全部 - * @return 匹配的记录数 - */ @Override - public long count(T condition) { + public long count(E condition) { if (condition == null) { - return mongoTemplate.count(new Query(), entityClass); + return mongoTemplate.count(new Query(), poClass); } Query query = buildQuery(condition); - return mongoTemplate.count(query, entityClass); + return mongoTemplate.count(query, poClass); } - /** - * 判断是否存在匹配条件的记录。 - * - * @param condition 条件对象 - * @return 存在返回 true,否则 false - */ @Override - public boolean exists(T condition) { + public boolean exists(E condition) { return count(condition) > 0; } -} + + protected E toEntity(P po) { + if (po == null) { + return null; + } + if (entityClass == null) { + return (E) po; + } + try { + E entity = entityClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(po, entity); + return entity; + } catch (Exception e) { + throw new RuntimeException("Failed to convert PO to entity", e); + } + } + + protected P toPo(E entity) { + if (entity == null) { + return null; + } + if (poClass == null) { + return (P) entity; + } + try { + P po = poClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(entity, po); + return po; + } catch (Exception e) { + throw new RuntimeException("Failed to convert entity to PO", e); + } + } +} \ No newline at end of file diff --git a/structure-infra-mybatis-plus-starter/pom.xml b/structure-infra-mybatis-plus-starter/pom.xml index 060c5cd..35f3958 100644 --- a/structure-infra-mybatis-plus-starter/pom.xml +++ b/structure-infra-mybatis-plus-starter/pom.xml @@ -45,6 +45,10 @@ cn.structured structure-tenant-starter + + jakarta.persistence + jakarta.persistence-api + \ No newline at end of file diff --git a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/configuration/MybatisPlusAutoConfiguration.java b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/configuration/MybatisPlusAutoConfiguration.java index 004267d..1c8c70f 100644 --- a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/configuration/MybatisPlusAutoConfiguration.java +++ b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/configuration/MybatisPlusAutoConfiguration.java @@ -1,80 +1,11 @@ package cn.structure.infra.mybatis.plus.configuration; -import cn.structure.infra.mybatis.plus.repository.MybatisPlusDelegateBeanPostProcessor; -import cn.structure.infra.mybatis.plus.repository.MybatisPlusDelegateFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.ApplicationContext; -/** - * MyBatis Plus 自动配置类 - *

    - * 当检测到 MyBatis Plus 相关依赖({@link com.baomidou.mybatisplus.core.mapper.BaseMapper})时自动配置, - * 注册 MyBatis Plus 持久化所需的核心组件,使其与仓储框架无缝集成。 - *

    - * 注册的 Bean: - *

      - *
    • {@link cn.structure.infra.mybatis.plus.repository.MybatisPlusDelegateFactory} - 仓储委托工厂, - * 负责根据 PO 类自动查找对应的 BaseMapper 并创建 {@link cn.structure.infra.mybatis.plus.repository.MybatisPlusRepositoryDelegate} 实例
    • - *
    • {@link cn.structure.infra.mybatis.plus.repository.MybatisPlusDelegateBeanPostProcessor} - Bean 后处理器, - * 为自定义的 MybatisPlusRepositoryDelegate 实现类自动注入 BaseMapper
    • - *
    - *

    - * 工作机制: - *

      - *
    1. 当 {@link cn.structure.infra.repository.RepositoryFacade} 需要获取 RepositoryDelegate 时, - * 会通过 {@link cn.structure.infra.repository.RepositoryBeanPostProcessor} 查找匹配的 Delegate
    2. - *
    3. 若未找到用户自定义的 Delegate,会通过 MybatisPlusDelegateFactory 自动创建
    4. - *
    5. DelegateBeanPostProcessor 确保用户自定义的 Delegate 实现能正确注入 BaseMapper
    6. - *
    - *

    - * 配置方式: - *

      - *
    • 默认自动启用(matchIfMissing = true)
    • - *
    • 可通过 `structure.infra.type=MYBATIS_PLUS` 显式指定
    • - *
    - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ @AutoConfiguration @ConditionalOnClass(name = "com.baomidou.mybatisplus.core.mapper.BaseMapper") @ConditionalOnProperty(prefix = "structure.infra", name = "type", havingValue = "MYBATIS_PLUS", matchIfMissing = true) public class MybatisPlusAutoConfiguration { - - /** - * 创建 MyBatis Plus 仓储委托工厂 - *

    - * 负责根据 PO 类自动查找对应的 BaseMapper,并创建 MybatisPlusRepositoryDelegate 实例。 - * 当 RepositoryFacade 需要获取特定类型的 RepositoryDelegate 时,会通过此工厂进行创建。 - * - * @param applicationContext Spring 应用上下文,用于查找 Mapper Bean - * @return MybatisPlusDelegateFactory 实例 - */ - @Bean - public MybatisPlusDelegateFactory mybatisPlusDelegateFactory(ApplicationContext applicationContext) { - return new MybatisPlusDelegateFactory(applicationContext); - } - - /** - * 创建 MyBatis Plus 委托 Bean 后处理器 - *

    - * 在 Bean 初始化完成后,自动为带有 {@link cn.structure.infra.annotations.DelegateFor} 注解的 - * MybatisPlusRepositoryDelegate 实现类注入对应的 BaseMapper。 - *

    - * 处理逻辑: - * 1. 扫描所有 Bean,筛选出 MybatisPlusRepositoryDelegate 的实例 - * 2. 检查是否存在 {@link cn.structure.infra.annotations.DelegateFor} 注解 - * 3. 根据注解中指定的 PO 类查找对应的 BaseMapper - * 4. 将找到的 BaseMapper 注入到 Delegate 实例中 - * - * @return MybatisPlusDelegateBeanPostProcessor 实例 - */ - @Bean - public MybatisPlusDelegateBeanPostProcessor mybatisPlusDelegateBeanPostProcessor() { - return new MybatisPlusDelegateBeanPostProcessor(); - } } diff --git a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusDelegateBeanPostProcessor.java b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusDelegateBeanPostProcessor.java deleted file mode 100644 index 4486a52..0000000 --- a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusDelegateBeanPostProcessor.java +++ /dev/null @@ -1,121 +0,0 @@ -package cn.structure.infra.mybatis.plus.repository; - -import cn.structure.infra.annotations.DelegateFor; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; - -/** - * MyBatis Plus RepositoryDelegate 的 BeanPostProcessor,负责为用户自定义 Delegate 子类自动注入 BaseMapper。 - *

    - * 在仓储框架中,业务方可继承 {@link MybatisPlusRepositoryDelegate} 实现自定义 Delegate,并通过 - * {@link DelegateFor} 注解声明其服务的 PO 类型。本后处理器在 Bean 初始化完成后: - *

      - *
    1. 识别所有 {@link MybatisPlusRepositoryDelegate} 类型的 Bean
    2. - *
    3. 读取其 {@link DelegateFor#po()} 指定的 PO 类型
    4. - *
    5. 按 PO 包名约定({@code .po.} → {@code .mapper.}、PO 后缀 → Mapper)或 Bean 名称查找对应 BaseMapper
    6. - *
    7. 通过 setter 反向注入 BaseMapper 与 PO 类型,使自定义 Delegate 可正常工作
    8. - *
    - *

    - * 与 {@link MybatisPlusDelegateFactory} 的分工:工厂负责"无自定义 Delegate 时自动创建", - * 本处理器负责"已有自定义 Delegate 时补齐依赖",二者协同保证 RepositoryFacade 总能拿到可用的 Delegate。 - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -@Slf4j -public class MybatisPlusDelegateBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { - - /** Spring 上下文,用于按类型或名称查找 BaseMapper Bean */ - private ApplicationContext applicationContext; - - /** - * 注入 Spring 应用上下文,供后续按类型/名称查询 Bean。 - * - * @param applicationContext Spring 应用上下文 - * @throws BeansException 上下文注入异常 - */ - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - /** - * 在 Bean 初始化完成后,对自定义 MybatisPlusRepositoryDelegate 实现类注入 BaseMapper。 - *

    - * 仅当 Bean 同时满足:是 {@link MybatisPlusRepositoryDelegate} 实例、且类上标注了 - * {@link DelegateFor} 注解、注解显式指定了 PO 类型时,才执行注入。 - * - * @param bean 待处理的 Bean 实例 - * @param beanName Bean 名称 - * @return 原始 Bean(已注入依赖),未匹配类型时原样返回 - * @throws BeansException 处理过程中的异常 - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof MybatisPlusRepositoryDelegate) { - MybatisPlusRepositoryDelegate delegate = (MybatisPlusRepositoryDelegate) bean; - // 读取 @DelegateFor 注解,识别该 Delegate 服务的 PO 类型 - DelegateFor annotation = bean.getClass().getAnnotation(DelegateFor.class); - if (annotation != null && annotation.po() != void.class) { - try { - // 按 PO 类型约定查找对应的 BaseMapper - Object mapper = findMapperByPoClass(annotation.po()); - if (mapper != null) { - // 反向注入 BaseMapper 与 PO 类型,使自定义 Delegate 可正常工作 - delegate.setBaseMapper((BaseMapper) mapper); - delegate.setEntityClass(annotation.po()); - log.info("Injected BaseMapper into MybatisPlusRepositoryDelegate: {}", beanName); - } else { - log.warn("No BaseMapper found for PO class {} in MybatisPlusRepositoryDelegate {}", annotation.po().getSimpleName(), beanName); - } - } catch (Exception e) { - log.warn("Failed to inject BaseMapper into MybatisPlusRepositoryDelegate {}: {}", beanName, e.getMessage()); - } - } - } - return bean; - } - - /** - * 根据 PO 类查找对应的 BaseMapper Bean。 - *

    - * 查找策略(按优先级): - *

      - *
    1. 约定包名转换:将 {@code xxx.po.XxxPO} 推导为 {@code xxx.mapper.XxxMapper},按类型获取 Bean
    2. - *
    3. 若类型不存在,再用简单名(如 {@code XxxMapper})按 Bean 名称获取
    4. - *
    - * - * @param poClass PO 类型 - * @return 对应的 BaseMapper Bean,未找到返回 null - */ - private Object findMapperByPoClass(Class poClass) { - // 约定:po 包下的 XxxPO 对应 mapper 包下的 XxxMapper - String poClassName = poClass.getName(); - String mapperClassName = poClassName.replace(".po.", ".mapper.") - .replace("PO", "Mapper"); - try { - Class mapperClass = Class.forName(mapperClassName); - return applicationContext.getBean(mapperClass); - } catch (ClassNotFoundException e) { - log.debug("Mapper class not found: {}", mapperClassName); - } catch (Exception e) { - log.debug("Failed to get mapper bean: {}", e.getMessage()); - } - - // 兜底:按 Bean 简单名查找(如 "XxxMapper") - String simpleMapperName = poClass.getSimpleName().replace("PO", "Mapper"); - try { - return applicationContext.getBean(simpleMapperName); - } catch (Exception e) { - log.debug("Failed to get mapper by name: {}", simpleMapperName); - } - - return null; - } -} \ No newline at end of file diff --git a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusDelegateFactory.java b/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusDelegateFactory.java deleted file mode 100644 index 64b7c26..0000000 --- a/structure-infra-mybatis-plus-starter/src/main/java/cn/structure/infra/mybatis/plus/repository/MybatisPlusDelegateFactory.java +++ /dev/null @@ -1,102 +0,0 @@ -package cn.structure.infra.mybatis.plus.repository; - -import cn.structure.infra.repository.RepositoryDelegate; -import cn.structure.infra.repository.RepositoryDelegateFactory; -import cn.structure.infra.repository.RepositoryType; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import org.springframework.context.ApplicationContext; - -/** - * MyBatis Plus 仓储委托工厂 - *

    - * 实现 {@link RepositoryDelegateFactory} SPI,自动创建 {@link MybatisPlusRepositoryDelegate} 实例。 - * 在仓储框架中,当 {@code RepositoryFacade} 找不到用户自定义的 Delegate 时,会通过本工厂按 PO 类型 - * 查找对应的 {@link BaseMapper},并创建默认 Delegate 实例。 - *

    - * 与 {@link MybatisPlusDelegateBeanPostProcessor} 的分工:本工厂负责"无自定义 Delegate 时创建默认实现", - * BeanPostProcessor 负责"已有自定义子类时补齐依赖"。 - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -public class MybatisPlusDelegateFactory implements RepositoryDelegateFactory { - - /** Spring 上下文,用于按类型/名称查找 BaseMapper Bean */ - private final ApplicationContext applicationContext; - - /** - * 构造工厂,注入 Spring 应用上下文。 - * - * @param applicationContext Spring 应用上下文,用于查找 Mapper Bean - */ - public MybatisPlusDelegateFactory(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - - /** - * 返回该工厂支持的仓储类型,用于 SPI 路由匹配。 - * - * @return 固定返回 {@link RepositoryType#MYBATIS_PLUS} - */ - @Override - public RepositoryType getType() { - return RepositoryType.MYBATIS_PLUS; - } - - /** - * 为指定 PO 类型创建 {@link MybatisPlusRepositoryDelegate} 实例。 - *

    - * 内部按约定({@code .po.} → {@code .mapper.}、PO 后缀 → Mapper)查找对应 BaseMapper, - * 找不到时返回 null(由上层 RepositoryFacade 继续尝试其他工厂或抛出异常)。 - * - * @param poClass PO 实体类型 - * @param idClass 主键类型(当前实现未使用,保留以匹配 SPI 签名) - * @return 已注入 BaseMapper 的 Delegate 实例;未找到 Mapper 时返回 null - */ - @Override - @SuppressWarnings({"unchecked", "rawtypes"}) - public RepositoryDelegate createDelegate(Class poClass, Class idClass) { - try { - BaseMapper mapper = (BaseMapper) findMapperByPoClass(poClass); - if (mapper == null) { - return null; - } - return new MybatisPlusRepositoryDelegate(mapper, poClass); - } catch (Exception e) { - return null; - } - } - - /** - * 根据 PO 类查找对应的 BaseMapper Bean。 - *

    - * 查找策略(按优先级): - *

      - *
    1. 约定包名转换:将 {@code xxx.po.XxxPO} 推导为 {@code xxx.mapper.XxxMapper},按类型获取 Bean
    2. - *
    3. 失败时遍历所有 Bean 名称,匹配以简单 Mapper 名(如 {@code XxxMapper})结尾的 Bean
    4. - *
    - * - * @param poClass PO 类型 - * @return 对应的 BaseMapper Bean,未找到返回 null - */ - private Object findMapperByPoClass(Class poClass) { - // 约定:po 包下的 XxxPO 对应 mapper 包下的 XxxMapper - String poClassName = poClass.getName(); - String mapperClassName = poClassName.replace(".po.", ".mapper.") - .replace("PO", "Mapper"); - try { - Class mapperClass = Class.forName(mapperClassName); - return applicationContext.getBean(mapperClass); - } catch (Exception e) { - // 兜底:遍历 Bean 名称,匹配以 XxxMapper 结尾的 Bean - String simpleMapperName = poClass.getSimpleName().replace("PO", "Mapper"); - for (String beanName : applicationContext.getBeanDefinitionNames()) { - if (beanName.endsWith(simpleMapperName)) { - return applicationContext.getBean(beanName); - } - } - return 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 55202f2..fa79939 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 @@ -2,18 +2,23 @@ import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; +import cn.structure.infra.repository.GenericTypeResolver; import cn.structure.infra.repository.RepositoryDelegate; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; 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 java.io.Serializable; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; /** * 基于 MyBatis Plus 的 RepositoryDelegate 适配实现 @@ -31,115 +36,104 @@ *
      *
    • 查询条件通过反射读取实体非空字段,按"等值匹配"组装 {@link QueryWrapper},并将驼峰字段名 * 转为下划线列名以匹配数据库列
    • - *
    • ID 字段名默认为 "id",可通过构造器或 setter 自定义
    • + *
    • ID 字段名通过 PO 类的 {@link Id} 注解自动识别,默认为 "id"
    • *
    • save 方法根据 ID 是否为空自动区分 insert / update
    • *
    • 分页委托给 MyBatis Plus 的 {@link Page},由分页拦截器生成方言相关 SQL
    • *
    + *

    + * 注意:Entity ↔ PO 转换在此层完成,Facade 层只操作领域实体。 * - * @param 实体(PO)类型 + * @param 领域实体类型(Entity) + * @param

    持久化对象类型(PO) * @param 主键类型 * @author chuck - * @version 1.0.1 + * @version 1.0.3 * @since 2026/6/28 */ @Slf4j -public class MybatisPlusRepositoryDelegate implements RepositoryDelegate { - - /** 底层 MyBatis Plus Mapper,由工厂或 BeanPostProcessor 注入 */ - protected BaseMapper baseMapper; - /** PO 实体类型,用于反射读取字段 */ - protected Class entityClass; - /** 主键字段名,默认 "id",可被子类覆盖 */ - protected String idFieldName; - - /** - * 默认构造器,用于用户自定义子类场景。 - *

    - * 创建后由 {@link MybatisPlusDelegateBeanPostProcessor} 通过 setter 注入依赖。 - */ +public class MybatisPlusRepositoryDelegate implements RepositoryDelegate { + + @Autowired + protected BaseMapper

    baseMapper; + protected Class entityClass; + protected Class

    poClass; + protected Class idClass; + protected String idFieldName = "id"; + 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); } - /** - * 以默认主键字段名 "id" 构造 Delegate。 - * - * @param baseMapper MyBatis Plus 的 BaseMapper,承担实际 CRUD - * @param entityClass PO 实体类型 - */ - public MybatisPlusRepositoryDelegate(BaseMapper baseMapper, Class entityClass) { - this(baseMapper, entityClass, "id"); + @SuppressWarnings("unchecked") + protected void resolveGenericTypes() { + this.entityClass = (Class) GenericTypeResolver.resolveEntityClass(getClass()); + this.poClass = (Class

    ) GenericTypeResolver.resolvePoClass(getClass()); + this.idClass = (Class) GenericTypeResolver.resolveIdClass(getClass()); } - /** - * 全参构造器,工厂自动创建场景使用。 - * - * @param baseMapper MyBatis Plus 的 BaseMapper - * @param entityClass PO 实体类型 - * @param idFieldName 主键字段名(用于反射读取 ID 值) - */ - public MybatisPlusRepositoryDelegate(BaseMapper baseMapper, Class entityClass, String idFieldName) { - this.baseMapper = baseMapper; - this.entityClass = entityClass; - this.idFieldName = idFieldName; - log.info("MybatisPlusRepositoryDelegate initialized for entity: {}", entityClass.getSimpleName()); + protected void resolveIdFieldName() { + if (poClass != null) { + Field idField = findFieldWithAnnotation(poClass, Id.class); + if (idField != null) { + this.idFieldName = idField.getName(); + } + } } - /** - * 注入 BaseMapper,供 BeanPostProcessor 在自定义子类上调用。 - * - * @param baseMapper MyBatis Plus 的 BaseMapper - */ - public void setBaseMapper(BaseMapper baseMapper) { - this.baseMapper = baseMapper; + 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; } - /** - * 注入 PO 实体类型,供 BeanPostProcessor 在自定义子类上调用。 - * - * @param entityClass PO 实体类型 - */ - public void setEntityClass(Class entityClass) { - this.entityClass = entityClass; + @Override + public Class getEntityClass() { + return entityClass; } - /** - * 设置主键字段名,供自定义子类覆盖默认 "id"。 - * - * @param idFieldName 主键字段名 - */ - public void setIdFieldName(String idFieldName) { - this.idFieldName = idFieldName; + @Override + public Class getPoClass() { + return poClass; + } + + @Override + public Class getIdClass() { + return idClass; + } + + @Override + public String getIdFieldName() { + return idFieldName; } - /** - * 保存或更新实体。 - *

    - * 根据 ID 字段值是否为空自动选择策略:ID 为空执行 insert,否则执行 updateById。 - * - * @param entity 实体对象,为 null 时直接返回 null - * @return 保存后的实体(与入参同一引用) - */ @Override - public T save(T entity) { + public E save(E entity) { if (entity == null) { return null; } - // 反射读取主键值,决定走新增还是更新分支 - ID id = getIdValue(entity); + P po = toPo(entity); + ID id = getIdValue(po); if (id == null) { - baseMapper.insert(entity); + baseMapper.insert(po); } else { - baseMapper.updateById(entity); + baseMapper.updateById(po); } log.debug("Saved entity: id={}, entity={}", id, entity); - return entity; + return toEntity(po); } - /** - * 根据主键删除记录。 - * - * @param id 主键值,为 null 时不执行任何操作 - */ @Override public void removeById(ID id) { if (id != null) { @@ -148,137 +142,128 @@ public void removeById(ID id) { } } - /** - * 根据主键查询实体。 - * - * @param id 主键值,为 null 时返回 null - * @return 实体对象,未找到时返回 null - */ @Override - public T findById(ID id) { + public E findById(ID id) { if (id == null) { return null; } - T entity = baseMapper.selectById((Serializable) id); - log.debug("Find by id: id={}, found={}", id, entity != null); - return entity; + P po = baseMapper.selectById((Serializable) id); + log.debug("Find by id: id={}, found={}", id, po != null); + return toEntity(po); } - /** - * 根据主键查询(与 findById 等价,语义上用于"读模型")。 - * - * @param id 主键值 - * @return 实体对象,未找到时返回 null - */ @Override - public T queryById(ID id) { + public E queryById(ID id) { return findById(id); } - /** - * 根据主键查询并以 {@link Optional} 包装返回,避免空指针。 - * - * @param id 主键值 - * @return 包含实体的 Optional,未找到时为 {@link Optional#empty()} - */ @Override - public Optional queryByIdOptional(ID id) { + public Optional queryByIdOptional(ID id) { return Optional.ofNullable(queryById(id)); } - /** - * 根据非空字段等值匹配查询单条记录。 - *

    - * 将条件对象非空字段组装为 {@link QueryWrapper},取结果集第一条;多于一条时仅返回首条。 - * - * @param condition 查询条件对象,为 null 时返回 null - * @return 首条匹配记录,无匹配时返回 null - */ @Override - public T queryOne(T condition) { + public E queryOne(E condition) { if (condition == null) { return null; } - QueryWrapper queryWrapper = buildQueryWrapper(condition); - List results = baseMapper.selectList(queryWrapper); - return results.isEmpty() ? null : results.get(0); + P poCondition = toPo(condition); + QueryWrapper

    queryWrapper = buildQueryWrapper(poCondition); + List

    results = baseMapper.selectList(queryWrapper); + P po = results.isEmpty() ? null : results.get(0); + return toEntity(po); } - /** - * 根据条件查询单条记录,并以 {@link Optional} 包装返回。 - * - * @param condition 查询条件对象 - * @return 包含首条匹配记录的 Optional - */ @Override - public Optional queryOneOptional(T condition) { + public Optional queryOneOptional(E condition) { return Optional.ofNullable(queryOne(condition)); } - /** - * 根据条件查询列表。 - *

    - * 条件为 null 时等价于全表查询;否则按非空字段等值匹配。 - * - * @param condition 查询条件对象,可为 null - * @return 匹配的实体列表,无匹配时返回空列表 - */ @Override - public List queryList(T condition) { + public List queryList(E condition) { if (condition == null) { - return baseMapper.selectList(null); + List

    poList = baseMapper.selectList(null); + return toEntityList(poList); } - QueryWrapper queryWrapper = buildQueryWrapper(condition); - return baseMapper.selectList(queryWrapper); + P poCondition = toPo(condition); + QueryWrapper

    queryWrapper = buildQueryWrapper(poCondition); + List

    poList = baseMapper.selectList(queryWrapper); + return toEntityList(poList); } - /** - * 分页查询。 - *

    - * 委托 MyBatis Plus 的 {@link Page} 执行分页,实际分页 SQL 由分页拦截器按方言生成。 - * - * @param reqPage 分页请求(页码、每页大小,为 null 时取默认 1/10) - * @return 分页结果,包含当前页、总页数、总条数、当前页记录 - */ @Override - public ResPage queryPage(ReqPage reqPage) { - // 页码与每页大小兜底,避免 NPE + public ResPage queryPage(ReqPage reqPage) { long pageNum = reqPage.getPage() != null ? reqPage.getPage() : 1; long pageSize = reqPage.getSize() != null ? reqPage.getSize() : 10; - Page page = new Page<>(pageNum, pageSize); - IPage result = baseMapper.selectPage(page, null); + Page

    page = new Page<>(pageNum, pageSize); + IPage

    result = baseMapper.selectPage(page, null); - // 将 MyBatis Plus 分页结果转写为统一 ResPage - ResPage resPage = new ResPage<>(); + ResPage resPage = new ResPage<>(); resPage.setCurrent(result.getCurrent()); resPage.setPages(result.getPages()); resPage.setSize(result.getSize()); resPage.setTotal(result.getTotal()); - resPage.setRecords(result.getRecords()); + resPage.setRecords(toEntityList(result.getRecords())); log.debug("Query page: page={}, size={}, total={}, records={}", pageNum, pageSize, result.getTotal(), result.getRecords().size()); return resPage; } - /** - * 根据条件对象的非空字段构建等值查询 {@link QueryWrapper}。 - *

    - * 反射读取所有字段(含父类),将驼峰字段名转为下划线列名后拼接 eq 条件。 - * - * @param condition 条件对象 - * @return 已填充等值条件的 QueryWrapper - */ - private QueryWrapper buildQueryWrapper(T condition) { - QueryWrapper queryWrapper = new QueryWrapper<>(); + @Override + public List saveBatch(List entities) { + if (entities == null || entities.isEmpty()) { + return List.of(); + } + List

    poList = toPoList(entities); + poList.forEach(baseMapper::insert); + return toEntityList(poList); + } + + @Override + public void removeBatchByIds(List ids) { + if (ids != null && !ids.isEmpty()) { + baseMapper.deleteBatchIds(ids.stream() + .map(id -> (Serializable) id) + .toList()); + } + } + + @Override + public List listByIds(List ids) { + if (ids == null || ids.isEmpty()) { + return List.of(); + } + List

    poList = baseMapper.selectBatchIds(ids.stream() + .map(id -> (Serializable) id) + .toList()); + return toEntityList(poList); + } + + @Override + public long count(E condition) { + if (condition == null) { + return baseMapper.selectCount(null); + } + P poCondition = toPo(condition); + QueryWrapper

    queryWrapper = buildQueryWrapper(poCondition); + return baseMapper.selectCount(queryWrapper); + } + + @Override + public boolean exists(E condition) { + return count(condition) > 0; + } + + private QueryWrapper

    buildQueryWrapper(P condition) { + QueryWrapper

    queryWrapper = new QueryWrapper<>(); try { Field[] fields = getAllFields(condition.getClass()); for (Field field : fields) { field.setAccessible(true); Object value = field.get(condition); if (value != null) { - // 字段名驼峰转下划线,以匹配数据库列名 queryWrapper.eq(camelToUnderline(field.getName()), value); } } @@ -288,12 +273,6 @@ private QueryWrapper buildQueryWrapper(T condition) { return queryWrapper; } - /** - * 收集类及其所有父类(直到 Object)的声明字段。 - * - * @param clazz 起始类 - * @return 全部字段数组 - */ private Field[] getAllFields(Class clazz) { List fields = new java.util.ArrayList<>(); while (clazz != null && clazz != Object.class) { @@ -303,19 +282,13 @@ private Field[] getAllFields(Class clazz) { return fields.toArray(new Field[0]); } - /** - * 反射读取实体的主键字段值。 - * - * @param entity 实体对象 - * @return 主键值,无法读取时返回 null - */ @SuppressWarnings("unchecked") - private ID getIdValue(T entity) { + private ID getIdValue(P po) { try { - Field field = findIdField(entity.getClass()); + Field field = findIdField(po.getClass()); if (field != null) { field.setAccessible(true); - return (ID) field.get(entity); + return (ID) field.get(po); } } catch (Exception e) { log.warn("Error getting id value: {}", e.getMessage()); @@ -323,18 +296,11 @@ private ID getIdValue(T entity) { return null; } - /** - * 沿继承链递归查找主键字段。 - * - * @param clazz 起始类 - * @return 主键 Field,未找到返回 null - */ private Field findIdField(Class clazz) { try { Field field = clazz.getDeclaredField(idFieldName); return field; } catch (NoSuchFieldException e) { - // 当前类未声明 ID 字段,继续向父类递归 if (clazz.getSuperclass() != null && clazz.getSuperclass() != Object.class) { return findIdField(clazz.getSuperclass()); } @@ -342,12 +308,6 @@ private Field findIdField(Class clazz) { } } - /** - * 驼峰命名转下划线命名(如 userName → user_name),用于对齐数据库列名。 - * - * @param param 原始字段名 - * @return 下划线命名,入参为空时返回空字符串 - */ private String camelToUnderline(String param) { if (param == null || "".equals(param.trim())) { return ""; @@ -366,74 +326,47 @@ private String camelToUnderline(String param) { return sb.toString(); } - /** - * 批量保存实体(逐条 insert)。 - * - * @param entities 实体列表,为 null 或空时返回空列表 - * @return 入参列表引用(已写入数据库) - */ - @Override - public List saveBatch(List entities) { - if (entities == null || entities.isEmpty()) { - return List.of(); + protected E toEntity(P po) { + if (po == null) { + return null; } - entities.forEach(baseMapper::insert); - return entities; - } - - /** - * 根据主键列表批量删除。 - * - * @param ids 主键列表,为 null 或空时不执行任何操作 - */ - @Override - public void removeBatchByIds(List ids) { - if (ids != null && !ids.isEmpty()) { - baseMapper.deleteBatchIds(ids.stream() - .map(id -> (Serializable) id) - .toList()); + try { + E entity = entityClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(po, entity); + return entity; + } catch (Exception e) { + throw new RuntimeException("Failed to convert PO to entity", e); } } - /** - * 根据主键列表批量查询。 - * - * @param ids 主键列表,为 null 或空时返回空列表 - * @return 匹配的实体列表 - */ - @Override - public List listByIds(List ids) { - if (ids == null || ids.isEmpty()) { + protected List toEntityList(List

    poList) { + if (poList == null || poList.isEmpty()) { return List.of(); } - return baseMapper.selectBatchIds(ids.stream() - .map(id -> (Serializable) id) - .toList()); + return poList.stream() + .map(this::toEntity) + .collect(Collectors.toList()); } - /** - * 按条件统计记录数。 - * - * @param condition 条件对象,为 null 时统计全表 - * @return 匹配的记录数 - */ - @Override - public long count(T condition) { - if (condition == null) { - return baseMapper.selectCount(null); + protected P toPo(E entity) { + if (entity == null) { + return null; + } + try { + P po = poClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(entity, po); + return po; + } catch (Exception e) { + throw new RuntimeException("Failed to convert entity to PO", e); } - QueryWrapper queryWrapper = buildQueryWrapper(condition); - return baseMapper.selectCount(queryWrapper); } - /** - * 判断是否存在匹配条件的记录。 - * - * @param condition 条件对象 - * @return 存在返回 true,否则 false - */ - @Override - public boolean exists(T condition) { - return count(condition) > 0; + protected List

    toPoList(List entityList) { + if (entityList == null || entityList.isEmpty()) { + return List.of(); + } + return entityList.stream() + .map(this::toPo) + .collect(Collectors.toList()); } -} +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java index 8250b28..3510f05 100644 --- a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java +++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/po/UserPO.java @@ -1,25 +1,24 @@ package cn.structure.infra.sample.infra.po; -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import jakarta.persistence.*; +import jakarta.persistence.Id; import lombok.Data; -import org.springframework.data.mongodb.core.mapping.Document; import java.time.LocalDateTime; +/** + * 用户持久化对象(基础版本) + *

    + * 仅包含基础字段定义,不包含任何框架特定的注解。 + * 各持久化技术模块应创建各自的专用 PO,继承或参考此类。 + * + * @author chuck + * @version 1.0.2 + * @since 2026/6/28 + */ @Data -@TableName("t_user") -@Entity -@Table(name = "t_user") -@Document(collection = "t_user") -@org.springframework.data.elasticsearch.annotations.Document(indexName = "t_user") public class UserPO { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @TableId(type = IdType.AUTO) private Long id; private String username; diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java index 20fef2c..293f574 100644 --- a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java +++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java @@ -3,23 +3,23 @@ import cn.structure.infra.repository.RepositoryFacade; import cn.structure.infra.sample.domain.entity.UserEntity; import cn.structure.infra.sample.domain.repository.UserRepository; -import cn.structure.infra.sample.infra.po.UserPO; import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate; /** * 用户仓储基类 *

    - * 提供通用的仓储实现,各存储技术模块可以继承此类并指定具体的存储类型 + * 提供通用的仓储实现,各存储技术模块可以继承此类并指定具体的存储类型。 + *

    + * 注意:PO 类型已从 Facade 层移除,Entity ↔ PO 转换由具体的 Delegate 实现负责。 * * @author chuck - * @version 1.0.1 + * @version 1.0.2 * @since 2026/6/28 */ -public abstract class AbstractUserRepositoryImpl extends RepositoryFacade implements UserRepository { +public abstract class AbstractUserRepositoryImpl extends RepositoryFacade implements UserRepository { @Override public UserEntity findByName(String name) { - UserPO po = this.baseDelegate.finByName(name); - return this.toEntity(po); + return ((UserRepositoryDelegate) getDelegate()).findByName(name); } } diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java index 5fdb293..0f4df81 100644 --- a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java +++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java @@ -1,18 +1,19 @@ package cn.structure.infra.sample.infra.repository.delegate; import cn.structure.infra.repository.RepositoryDelegate; -import cn.structure.infra.sample.infra.po.UserPO; +import cn.structure.infra.sample.domain.entity.UserEntity; /** - *

    * 用户仓储代理 - *

    + *

    + * 定义用户仓储的委托接口,面向领域实体(UserEntity)。 + * 具体实现负责内部的 Entity ↔ PO 转换。 * * @author chuck - * @version 1.0.1 + * @version 1.0.2 * @since 2026/6/28 */ -public interface UserRepositoryDelegate extends RepositoryDelegate { +public interface UserRepositoryDelegate extends RepositoryDelegate { - UserPO finByName(String name); + UserEntity findByName(String name); } diff --git a/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/delegate/read/UserReadDelegate.java b/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/delegate/read/UserReadDelegate.java index 9ed0aee..5af1c6c 100644 --- a/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/delegate/read/UserReadDelegate.java +++ b/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/delegate/read/UserReadDelegate.java @@ -1,40 +1,20 @@ package cn.structure.infra.sample.cqrs.infra.delegate.read; -import cn.structure.infra.annotations.DelegateFor; +import cn.structure.infra.annotations.ReadDelegate; import cn.structure.infra.elasticsearch.repository.ElasticsearchRepositoryDelegate; -import cn.structure.infra.repository.DelegateType; -import cn.structure.infra.repository.RepositoryType; -import cn.structure.infra.sample.infra.po.UserPO; +import cn.structure.infra.sample.domain.entity.UserEntity; +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.stereotype.Component; -/** - * 用户读代理(READ) - *

    - * 模拟 Elasticsearch 等读数据源,只实现读操作,不实现写操作。 - *

    - * 特点: - * - 只实现 IQueryDelegate 接口(只读能力) - * - 不实现 RepositoryDelegate 接口(无写操作) - * - 可以与写代理是完全不同的类型 - * - 如果读代理执行失败,自动回退到写代理(baseDelegate) - *

    - * delegateType = READ 表示这是读代理 - */ @Slf4j @Component -@DelegateFor( - name = "userCqrsRepository", - po = UserPO.class, - delegateType = DelegateType.READ, - type = RepositoryType.ELASTICSEARCH - -) -public class UserReadDelegate extends ElasticsearchRepositoryDelegate implements UserRepositoryDelegate { +@ReadDelegate +public class UserReadDelegate extends ElasticsearchRepositoryDelegate implements UserRepositoryDelegate { @Override - public UserPO finByName(String name) { + public UserEntity findByName(String name) { return null; } -} \ No newline at end of file +} 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 be26c11..b86f62d 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 @@ -1,10 +1,8 @@ package cn.structure.infra.sample.cqrs.infra.delegate.write; -import cn.structure.infra.annotations.DelegateFor; import cn.structure.infra.mybatis.plus.repository.MybatisPlusRepositoryDelegate; -import cn.structure.infra.repository.DelegateType; -import cn.structure.infra.repository.RepositoryType; -import cn.structure.infra.sample.infra.po.UserPO; +import cn.structure.infra.sample.domain.entity.UserEntity; +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.stereotype.Component; @@ -19,21 +17,13 @@ * - removeBatchByIds *

    * 同时也可以处理读操作(作为读操作的兜底) - *

    - * delegateType = BASE 表示这是基础/写代理 */ @Slf4j @Component -@DelegateFor( - name = "userCqrsRepository", - po = UserPO.class, - delegateType = DelegateType.BASE, - type = RepositoryType.MYBATIS_PLUS -) -public class UserWriteDelegate extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate { +public class UserWriteDelegate extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate { @Override - public UserPO finByName(String name) { + public UserEntity findByName(String name) { return null; } -} \ No newline at end of file +} diff --git a/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repositoory/UserCqrsRepository.java b/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repositoory/UserCqrsRepository.java deleted file mode 100644 index 906080c..0000000 --- a/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repositoory/UserCqrsRepository.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.structure.infra.sample.cqrs.infra.repositoory; - -import cn.structure.infra.annotations.Repository; -import cn.structure.infra.repository.InMemoryRepositoryDelegate; -import cn.structure.infra.repository.RepositoryFacade; -import cn.structure.infra.sample.cqrs.infra.delegate.read.UserReadDelegate; -import cn.structure.infra.sample.cqrs.infra.delegate.write.UserWriteDelegate; -import cn.structure.infra.sample.domain.entity.UserEntity; -import cn.structure.infra.sample.domain.repository.UserRepository; -import cn.structure.infra.sample.infra.po.UserPO; -import org.springframework.stereotype.Component; - -/** - * 用户 CQRS 仓储 - *

    - * 启用了 CQRS 读写分离模式: - * - 写操作:通过 baseDelegate(写代理)执行 - * - 读操作:通过 readDelegate(读代理)执行,失败自动回退到 baseDelegate - *

    - * 使用条件(必须同时满足): - * 1. cqrs = true - * 2. readDelegateClass 指定了读代理类 - */ -@Repository( - entity = UserEntity.class, - po = UserPO.class, - cqrs = true, - readDelegateClass = UserReadDelegate.class -) -@Component("userCqrsRepository") -public class UserCqrsRepository extends RepositoryFacade implements UserRepository { - @Override - public UserEntity findByName(String name) { - return null; - } -} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repository/UserCqrsRepository.java b/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repository/UserCqrsRepository.java new file mode 100644 index 0000000..9419aed --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/repository/UserCqrsRepository.java @@ -0,0 +1,32 @@ +package cn.structure.infra.sample.cqrs.infra.repository; + +import cn.structure.infra.repository.CqrsRepositoryFacade; +import cn.structure.infra.sample.cqrs.infra.delegate.read.UserReadDelegate; +import cn.structure.infra.sample.cqrs.infra.delegate.write.UserWriteDelegate; +import cn.structure.infra.sample.domain.entity.UserEntity; +import cn.structure.infra.sample.domain.repository.UserRepository; +import org.springframework.stereotype.Component; + +/** + * 用户 CQRS 仓储实现 + *

    + * 继承 CqrsRepositoryFacade,实现读写分离模式: + * - 写操作通过 UserWriteDelegate(MyBatis Plus)执行 + * - 读操作通过 UserReadDelegate(Elasticsearch)执行 + * - 读操作失败时自动回退到写代理 + *

    + * 注意:通过泛型参数和 @ReadDelegate 注解自动匹配对应的代理实现, + * 无需额外的 @Repository 注解配置。 + * + * @author chuck + * @version 1.0.2 + * @since 2026/6/28 + */ +@Component("userCqrsRepository") +public class UserCqrsRepository extends CqrsRepositoryFacade implements UserRepository { + + @Override + public UserEntity findByName(String name) { + return getReadDelegate().findByName(name); + } +} \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/UserCqrsRepositoryTest.java b/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/UserCqrsRepositoryTest.java index 8ea0fa6..ca980c1 100644 --- a/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/UserCqrsRepositoryTest.java +++ b/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/UserCqrsRepositoryTest.java @@ -1,14 +1,13 @@ package cn.structure.infra.sample.cqrs; +import cn.structure.common.vo.ReqPage; +import cn.structure.common.vo.ResPage; +import cn.structure.infra.repository.IQueryDelegate; import cn.structure.infra.sample.cqrs.config.CqrsTestConfig; import cn.structure.infra.sample.cqrs.infra.delegate.write.UserWriteDelegate; -import cn.structure.infra.sample.cqrs.infra.repositoory.UserCqrsRepository; -import cn.structure.infra.repository.IQueryDelegate; -import cn.structure.infra.sample.infra.po.UserPO; +import cn.structure.infra.sample.cqrs.infra.repository.UserCqrsRepository; import cn.structure.infra.sample.domain.entity.UserEntity; import cn.structure.infra.sample.domain.repository.UserRepository; -import cn.structure.common.vo.ReqPage; -import cn.structure.common.vo.ResPage; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -20,7 +19,8 @@ import java.util.List; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * CQRS 仓储测试类 @@ -97,7 +97,7 @@ void testReadDelegateInjection() { assertNotNull(userCqrsRepository, "CQRS 仓储应该被注入"); // 验证 READ 代理存在 - IQueryDelegate readDelegate = userCqrsRepository.getReadDelegate(); + IQueryDelegate readDelegate = userCqrsRepository.getReadDelegate(); assertNotNull(readDelegate, "READ 读代理应该被注入"); log.info("✓ READ 读代理注入成功: {}", readDelegate.getClass().getName()); } @@ -109,7 +109,7 @@ void testMultipleDelegatesLoaded() { // 验证同时加载了两种代理 UserWriteDelegate baseDelegate = userCqrsRepository.getBaseDelegate(); - IQueryDelegate readDelegate = userCqrsRepository.getReadDelegate(); + IQueryDelegate readDelegate = userCqrsRepository.getReadDelegate(); assertNotNull(baseDelegate, "BASE 代理应该被加载"); assertNotNull(readDelegate, "READ 代理应该被加载"); 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 9ec2754..cc25c26 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 @@ -1,6 +1,6 @@ package cn.structure.infra.sample.cqrs.config; -import cn.structure.infra.elasticsearch.repository.ElasticsearchDelegateBeanPostProcessor; + import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; @@ -71,17 +71,4 @@ public MybatisPlusInterceptor mybatisPlusInterceptor() { interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2)); return interceptor; } - - /** - * Elasticsearch 委托 Bean 后处理器 - *

    - * 显式注册,确保 Mock 环境下 ElasticsearchOperations 能正确注入到 Delegate 中。 - * 由于测试环境下 @ConditionalOnBean 可能因注册顺序问题不满足,因此手动注册。 - * - * @return ElasticsearchDelegateBeanPostProcessor 实例 - */ - @Bean - public ElasticsearchDelegateBeanPostProcessor elasticsearchDelegateBeanPostProcessor() { - return new ElasticsearchDelegateBeanPostProcessor(); - } } \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/config/MockElasticsearchConfiguration.java b/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/config/MockElasticsearchConfiguration.java index 8cea874..74261a6 100644 --- a/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/config/MockElasticsearchConfiguration.java +++ b/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/config/MockElasticsearchConfiguration.java @@ -6,7 +6,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate; import org.springframework.data.elasticsearch.core.SearchHit; -import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.SearchHitsImpl; import org.springframework.data.elasticsearch.core.TotalHitsRelation; import org.springframework.data.elasticsearch.core.query.Query; diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/UserElasticsearchRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/UserElasticsearchRepositoryImpl.java index 5be19be..d0a5c06 100644 --- a/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/UserElasticsearchRepositoryImpl.java +++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/UserElasticsearchRepositoryImpl.java @@ -1,19 +1,19 @@ package cn.structure.infra.sample.infra.repository; -import cn.structure.infra.annotations.Repository; -import cn.structure.infra.repository.RepositoryType; -import cn.structure.infra.sample.domain.entity.UserEntity; -import cn.structure.infra.sample.infra.po.UserPO; import org.springframework.stereotype.Component; /** * 用户仓储 Elasticsearch 实现 + *

    + * 继承基类 {@link AbstractUserRepositoryImpl},使用 Elasticsearch 作为持久化技术。 + *

    + * 注意:通过命名约定和泛型参数自动匹配对应的 Delegate 实现, + * 无需额外的 @Repository 注解配置。 * * @author chuck - * @version 1.0.1 + * @version 1.0.2 * @since 2026/6/28 */ -@Repository(value = "用户仓储", type = RepositoryType.ELASTICSEARCH, entity = UserEntity.class, po = UserPO.class) @Component("userRepository") public class UserElasticsearchRepositoryImpl extends AbstractUserRepositoryImpl { } diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java index a4ef840..d595357 100644 --- a/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java +++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/main/java/cn/structure/infra/sample/infra/repository/elasticsearch/UserEsDelegate.java @@ -1,31 +1,42 @@ package cn.structure.infra.sample.infra.repository.elasticsearch; -import cn.structure.infra.annotations.DelegateFor; import cn.structure.infra.elasticsearch.repository.ElasticsearchRepositoryDelegate; -import cn.structure.infra.repository.RepositoryType; +import cn.structure.infra.sample.domain.entity.UserEntity; import cn.structure.infra.sample.infra.po.UserPO; import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.stereotype.Component; -@Component -@DelegateFor( - name = "userRepository", - type = RepositoryType.ELASTICSEARCH, - po = UserPO.class, - description = "用户仓储 Elasticsearch 实现", - priority = 10 -) +/** + * 用户仓储 Elasticsearch 委托实现 + *

    + * 负责 UserEntity 与 UserPO 之间的转换, + * 内部使用 Spring Data Elasticsearch 进行持久化操作。 + *

    + * 注意:通过命名约定和泛型参数自动匹配对应的 RepositoryFacade, + * 无需额外的 @DelegateFor 注解配置。 + * + * @author chuck + * @version 1.0.2 + * @since 2026/6/28 + */ @Slf4j -public class UserEsDelegate extends ElasticsearchRepositoryDelegate implements UserRepositoryDelegate { +@Component +public class UserEsDelegate extends ElasticsearchRepositoryDelegate implements UserRepositoryDelegate { + /** + * 根据用户名查询用户 + *

    + * 使用 ElasticsearchOperations 执行条件查询,将 PO 结果转换为 Entity 返回。 + * + * @param name 用户名 + * @return 用户实体,不存在时返回 null + */ @Override - public UserPO finByName(String name) { - log.info("使用 Elasticsearch 实现 finByName"); - UserPO condition = new UserPO(); + public UserEntity findByName(String name) { + log.info("使用 Elasticsearch 实现 findByName"); + UserEntity condition = new UserEntity(); condition.setUsername(name); return queryOne(condition); } - } diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchDetailRepositoryTest.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchDetailRepositoryTest.java index 861a4e1..fbbe3aa 100644 --- a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchDetailRepositoryTest.java +++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchDetailRepositoryTest.java @@ -1,11 +1,11 @@ package cn.structure.infra.sample.elasticsearch; +import cn.structure.common.vo.ReqPage; +import cn.structure.common.vo.ResPage; import cn.structure.infra.sample.domain.entity.UserEntity; import cn.structure.infra.sample.domain.repository.UserRepository; import cn.structure.infra.sample.elasticsearch.config.ElasticsearchTestConfig; import cn.structure.infra.sample.elasticsearch.config.MockElasticsearchConfiguration; -import cn.structure.common.vo.ReqPage; -import cn.structure.common.vo.ResPage; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java index dcbe92d..43a9962 100644 --- a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java +++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/UserElasticsearchRepositoryTest.java @@ -1,6 +1,5 @@ package cn.structure.infra.sample.elasticsearch; -import cn.structure.infra.sample.domain.entity.UserEntity; import cn.structure.infra.sample.domain.repository.UserRepository; import cn.structure.infra.sample.elasticsearch.config.ElasticsearchTestConfig; import org.junit.jupiter.api.DisplayName; @@ -9,7 +8,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; @SpringBootTest(classes = ElasticsearchTestConfig.class) @ActiveProfiles("es-test") diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchLowCodeTestConfig.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchLowCodeTestConfig.java index 4120fb4..0299924 100644 --- a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchLowCodeTestConfig.java +++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/ElasticsearchLowCodeTestConfig.java @@ -1,7 +1,7 @@ package cn.structure.infra.sample.elasticsearch.config; -import cn.structure.infra.lowcode.configuration.LowCodeAutoConfiguration; import cn.structure.infra.elasticsearch.lowcode.ElasticsearchLowCodeAutoConfiguration; +import cn.structure.infra.lowcode.configuration.LowCodeAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Import; diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/MockElasticsearchConfiguration.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/MockElasticsearchConfiguration.java index 0b8e328..c1d1cf9 100644 --- a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/MockElasticsearchConfiguration.java +++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/config/MockElasticsearchConfiguration.java @@ -6,7 +6,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate; import org.springframework.data.elasticsearch.core.SearchHit; -import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.SearchHitsImpl; import org.springframework.data.elasticsearch.core.TotalHitsRelation; import org.springframework.data.elasticsearch.core.query.Query; diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/lowcode/ElasticsearchLowCodeRepositoryTest.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/lowcode/ElasticsearchLowCodeRepositoryTest.java index 30f58fe..0497468 100644 --- a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/lowcode/ElasticsearchLowCodeRepositoryTest.java +++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/lowcode/ElasticsearchLowCodeRepositoryTest.java @@ -2,12 +2,7 @@ import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; -import cn.structure.infra.lowcode.model.AutoFillType; -import cn.structure.infra.lowcode.model.FieldSchema; -import cn.structure.infra.lowcode.model.FieldType; -import cn.structure.infra.lowcode.model.RepositoryConfig; -import cn.structure.infra.lowcode.model.ResourceSchema; -import cn.structure.infra.lowcode.model.StorageType; +import cn.structure.infra.lowcode.model.*; import cn.structure.infra.lowcode.repository.LowCodeRepository; import cn.structure.infra.lowcode.router.LowCodeRepositoryRouter; import org.junit.jupiter.api.BeforeEach; @@ -28,17 +23,14 @@ import org.springframework.data.elasticsearch.core.query.Query; import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.atomic.AtomicLong; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Elasticsearch 低代码仓储测试 - 通过 LowCodeRepository 接口测试 diff --git a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/lowcode/ElasticsearchLowCodeStorageTest.java b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/lowcode/ElasticsearchLowCodeStorageTest.java index 4a8d573..9d1ec28 100644 --- a/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/lowcode/ElasticsearchLowCodeStorageTest.java +++ b/structure-infra-sample/structure-infra-sample-elasticsearch/src/test/java/cn/structure/infra/sample/elasticsearch/lowcode/ElasticsearchLowCodeStorageTest.java @@ -34,7 +34,8 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/po/JpaUserPO.java b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/po/JpaUserPO.java new file mode 100644 index 0000000..c249849 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/po/JpaUserPO.java @@ -0,0 +1,41 @@ +package cn.structure.infra.sample.infra.po; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 用户持久化对象(JPA 专用) + *

    + * JPA 框架专用的 PO,包含 JPA 注解。 + * + * @author chuck + * @version 1.0.2 + * @since 2026/6/28 + */ +@Data +@Entity +@Table(name = "t_user") +public class JpaUserPO { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + 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-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java index b5fb7de..47e1da8 100644 --- a/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java +++ b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/UserJpaRepositoryImpl.java @@ -1,19 +1,19 @@ package cn.structure.infra.sample.infra.repository; -import cn.structure.infra.annotations.Repository; -import cn.structure.infra.repository.RepositoryType; -import cn.structure.infra.sample.domain.entity.UserEntity; -import cn.structure.infra.sample.infra.po.UserPO; import org.springframework.stereotype.Component; /** * 用户仓储 JPA 实现 + *

    + * 继承基类 {@link AbstractUserRepositoryImpl},使用 JPA 作为持久化技术。 + *

    + * 注意:通过命名约定和泛型参数自动匹配对应的 Delegate 实现, + * 无需额外的 @Repository 注解配置。 * * @author chuck - * @version 1.0.1 + * @version 1.0.2 * @since 2026/6/28 */ -@Repository(value = "用户仓储", type = RepositoryType.JPA, entity = UserEntity.class, po = UserPO.class) @Component("userRepository") public class UserJpaRepositoryImpl extends AbstractUserRepositoryImpl { } diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java index a7bc0e9..084ca7c 100644 --- a/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java +++ b/structure-infra-sample/structure-infra-sample-jpa/src/main/java/cn/structure/infra/sample/infra/repository/jpa/UserJpaDelegate.java @@ -1,28 +1,41 @@ package cn.structure.infra.sample.infra.repository.jpa; -import cn.structure.infra.annotations.DelegateFor; import cn.structure.infra.jpa.repository.JpaRepositoryDelegate; -import cn.structure.infra.repository.RepositoryType; -import cn.structure.infra.sample.infra.po.UserPO; +import cn.structure.infra.sample.domain.entity.UserEntity; +import cn.structure.infra.sample.infra.po.JpaUserPO; import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate; -import jakarta.persistence.EntityManager; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +/** + * 用户仓储 JPA 委托实现 + *

    + * 负责 UserEntity 与 UserPO 之间的转换, + * 内部使用 JPA EntityManager 进行持久化操作。 + *

    + * 注意:通过命名约定和泛型参数自动匹配对应的 RepositoryFacade, + * 无需额外的 @DelegateFor 注解配置。 + * + * @author chuck + * @version 1.0.2 + * @since 2026/6/28 + */ +@Slf4j @Component -@DelegateFor( - name = "userRepository", - type = RepositoryType.JPA, - po = UserPO.class, - description = "用户仓储 JPA 实现", - priority = 10 -) -public class UserJpaDelegate extends JpaRepositoryDelegate implements UserRepositoryDelegate { +public class UserJpaDelegate extends JpaRepositoryDelegate implements UserRepositoryDelegate { + /** + * 根据用户名查询用户 + *

    + * 使用 EntityManager 执行条件查询,将 PO 结果转换为 Entity 返回。 + * + * @param name 用户名 + * @return 用户实体,不存在时返回 null + */ @Override - public UserPO finByName(String name) { - UserPO condition = new UserPO(); + public UserEntity findByName(String name) { + UserEntity condition = new UserEntity(); condition.setUsername(name); return queryOne(condition); } - } diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java index 67fa66c..12be945 100644 --- a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java +++ b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/UserJpaRepositoryTest.java @@ -1,10 +1,10 @@ package cn.structure.infra.sample.jpa; +import cn.structure.common.vo.ReqPage; +import cn.structure.common.vo.ResPage; import cn.structure.infra.sample.domain.entity.UserEntity; import cn.structure.infra.sample.domain.repository.UserRepository; import cn.structure.infra.sample.jpa.config.JpaTestConfig; -import cn.structure.common.vo.ReqPage; -import cn.structure.common.vo.ResPage; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java index 4b0b8c0..3f7cc96 100644 --- a/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java +++ b/structure-infra-sample/structure-infra-sample-jpa/src/test/java/cn/structure/infra/sample/jpa/config/JpaTestConfig.java @@ -1,5 +1,7 @@ package cn.structure.infra.sample.jpa.config; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -14,8 +16,6 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import javax.sql.DataSource; import java.util.Properties; @@ -29,7 +29,10 @@ "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration", "org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration", "org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration", - "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration" + "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration", + "org.springframework.cloud.stream.config.BindingServiceConfiguration", + "org.springframework.cloud.stream.function.FunctionConfiguration", + "cn.structured.datascope.message.config.DataScopeMessageAutoConfiguration" }) @ComponentScan(basePackages = { "cn.structure.infra.sample", @@ -55,7 +58,7 @@ public DataSource dataSource() { public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource); - em.setPackagesToScan("cn.structure.infra.sample.infra.po"); + em.setPackagesToScan("cn.structure.infra.sample.infra.po", "cn.structure.infra.sample"); HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); em.setJpaVendorAdapter(vendorAdapter); diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/UserMongoRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/UserMongoRepositoryImpl.java index e39c8e8..7de7d70 100644 --- a/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/UserMongoRepositoryImpl.java +++ b/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/UserMongoRepositoryImpl.java @@ -1,19 +1,19 @@ package cn.structure.infra.sample.infra.repository; -import cn.structure.infra.annotations.Repository; -import cn.structure.infra.repository.RepositoryType; -import cn.structure.infra.sample.domain.entity.UserEntity; -import cn.structure.infra.sample.infra.po.UserPO; import org.springframework.stereotype.Component; /** * 用户仓储 MongoDB 实现 + *

    + * 继承基类 {@link AbstractUserRepositoryImpl},使用 MongoDB 作为持久化技术。 + *

    + * 注意:通过命名约定和泛型参数自动匹配对应的 Delegate 实现, + * 无需额外的 @Repository 注解配置。 * * @author chuck - * @version 1.0.1 + * @version 1.0.2 * @since 2026/6/28 */ -@Repository(value = "用户仓储", type = RepositoryType.MONGODB, entity = UserEntity.class, po = UserPO.class) @Component("userRepository") public class UserMongoRepositoryImpl extends AbstractUserRepositoryImpl { } diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java b/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java index 923c4db..f73e2e8 100644 --- a/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java +++ b/structure-infra-sample/structure-infra-sample-mongodb/src/main/java/cn/structure/infra/sample/infra/repository/mongodb/UserMongoDelegate.java @@ -1,26 +1,40 @@ package cn.structure.infra.sample.infra.repository.mongodb; -import cn.structure.infra.annotations.DelegateFor; import cn.structure.infra.mongodb.repository.MongoRepositoryDelegate; -import cn.structure.infra.repository.RepositoryType; +import cn.structure.infra.sample.domain.entity.UserEntity; import cn.structure.infra.sample.infra.po.UserPO; import cn.structure.infra.sample.infra.repository.delegate.UserRepositoryDelegate; -import org.springframework.data.mongodb.core.MongoTemplate; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +/** + * 用户仓储 MongoDB 委托实现 + *

    + * 负责 UserEntity 与 UserPO 之间的转换, + * 内部使用 Spring Data MongoDB 进行持久化操作。 + *

    + * 注意:通过命名约定和泛型参数自动匹配对应的 RepositoryFacade, + * 无需额外的 @DelegateFor 注解配置。 + * + * @author chuck + * @version 1.0.2 + * @since 2026/6/28 + */ +@Slf4j @Component -@DelegateFor( - name = "userRepository", - type = RepositoryType.MONGODB, - po = UserPO.class, - description = "用户仓储 MongoDB 实现", - priority = 10 -) -public class UserMongoDelegate extends MongoRepositoryDelegate implements UserRepositoryDelegate { +public class UserMongoDelegate extends MongoRepositoryDelegate implements UserRepositoryDelegate { + /** + * 根据用户名查询用户 + *

    + * 使用 MongoTemplate 执行条件查询,将 PO 结果转换为 Entity 返回。 + * + * @param name 用户名 + * @return 用户实体,不存在时返回 null + */ @Override - public UserPO finByName(String name) { - UserPO condition = new UserPO(); + public UserEntity findByName(String name) { + UserEntity condition = new UserEntity(); condition.setUsername(name); return queryOne(condition); } diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryInjectionTest.java b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryInjectionTest.java index 9b1ea04..f6089d0 100644 --- a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryInjectionTest.java +++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryInjectionTest.java @@ -8,7 +8,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; @SpringBootTest(classes = MongoTestConfig.class) @ActiveProfiles("mongo-test") diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java index 96c3424..6bc8e39 100644 --- a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java +++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/UserMongoRepositoryTest.java @@ -1,11 +1,11 @@ package cn.structure.infra.sample.mongodb; +import cn.structure.common.vo.ReqPage; +import cn.structure.common.vo.ResPage; import cn.structure.infra.sample.domain.entity.UserEntity; import cn.structure.infra.sample.domain.repository.UserRepository; -import cn.structure.infra.sample.mongodb.config.MongoTestConfig; import cn.structure.infra.sample.mongodb.config.MockMongoConfiguration; -import cn.structure.common.vo.ReqPage; -import cn.structure.common.vo.ResPage; +import cn.structure.infra.sample.mongodb.config.MongoTestConfig; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; 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 74df5c3..270f1f2 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 @@ -17,11 +17,7 @@ import org.springframework.data.mongodb.core.query.Update; import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; diff --git a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/lowcode/MongoLowCodeRepositoryTest.java b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/lowcode/MongoLowCodeRepositoryTest.java index 67c5fb0..6360236 100644 --- a/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/lowcode/MongoLowCodeRepositoryTest.java +++ b/structure-infra-sample/structure-infra-sample-mongodb/src/test/java/cn/structure/infra/sample/mongodb/lowcode/MongoLowCodeRepositoryTest.java @@ -2,12 +2,7 @@ import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; -import cn.structure.infra.lowcode.model.AutoFillType; -import cn.structure.infra.lowcode.model.FieldSchema; -import cn.structure.infra.lowcode.model.FieldType; -import cn.structure.infra.lowcode.model.RepositoryConfig; -import cn.structure.infra.lowcode.model.ResourceSchema; -import cn.structure.infra.lowcode.model.StorageType; +import cn.structure.infra.lowcode.model.*; import cn.structure.infra.lowcode.repository.LowCodeRepository; import cn.structure.infra.lowcode.router.LowCodeRepositoryRouter; import org.junit.jupiter.api.BeforeEach; @@ -25,11 +20,7 @@ import org.springframework.data.mongodb.core.query.Update; import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.atomic.AtomicLong; import static org.junit.jupiter.api.Assertions.*; diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/mapper/UserMapper.java b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/mapper/UserMapper.java index 6d0c47b..909a074 100644 --- a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/mapper/UserMapper.java +++ b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/mapper/UserMapper.java @@ -1,9 +1,16 @@ package cn.structure.infra.sample.infra.mapper; -import cn.structure.infra.sample.infra.po.UserPO; +import cn.structure.infra.sample.infra.po.MybatisUserPO; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; +/** + * 用户 Mapper(MyBatis Plus) + * + * @author chuck + * @version 1.0.2 + * @since 2026/6/28 + */ @Mapper -public interface UserMapper extends BaseMapper { +public interface UserMapper extends BaseMapper { } diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/po/MybatisUserPO.java b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/po/MybatisUserPO.java new file mode 100644 index 0000000..f86fba9 --- /dev/null +++ b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/po/MybatisUserPO.java @@ -0,0 +1,39 @@ +package cn.structure.infra.sample.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; + +/** + * 用户持久化对象(MyBatis Plus 专用) + *

    + * 仅包含 MyBatis Plus 框架的注解,不依赖其他持久化框架。 + * + * @author chuck + * @version 1.0.2 + * @since 2026/6/28 + */ +@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; +} diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/UserRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/UserRepositoryImpl.java index a7d15c7..f0aa780 100644 --- a/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/UserRepositoryImpl.java +++ b/structure-infra-sample/structure-infra-sample-mybatis/src/main/java/cn/structure/infra/sample/infra/repository/UserRepositoryImpl.java @@ -1,12 +1,19 @@ package cn.structure.infra.sample.infra.repository; -import cn.structure.infra.annotations.Repository; -import cn.structure.infra.repository.RepositoryType; -import cn.structure.infra.sample.domain.entity.UserEntity; -import cn.structure.infra.sample.infra.po.UserPO; import org.springframework.stereotype.Component; -@Repository(value = "用户仓储", type = RepositoryType.MYBATIS_PLUS, entity = UserEntity.class, po = UserPO.class) +/** + * 用户仓储 MyBatis Plus 实现 + *

    + * 继承基类 {@link AbstractUserRepositoryImpl},使用 MyBatis Plus 作为持久化技术。 + *

    + * 注意:通过命名约定和泛型参数自动匹配对应的 Delegate 实现, + * 无需额外的 @Repository 注解配置。 + * + * @author chuck + * @version 1.0.2 + * @since 2026/6/28 + */ @Component("userRepository") public class UserRepositoryImpl extends AbstractUserRepositoryImpl { -} \ No newline at end of file +} 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 2ae4074..9b7a3db 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 @@ -1,30 +1,46 @@ package cn.structure.infra.sample.infra.repository.mybatis; -import cn.structure.infra.annotations.DelegateFor; import cn.structure.infra.mybatis.plus.repository.MybatisPlusRepositoryDelegate; -import cn.structure.infra.repository.RepositoryType; +import cn.structure.infra.sample.domain.entity.UserEntity; import cn.structure.infra.sample.infra.mapper.UserMapper; -import cn.structure.infra.sample.infra.po.UserPO; +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; +/** + * 用户仓储 MyBatis Plus 委托实现 + *

    + * 负责 UserEntity 与 MybatisUserPO 之间的转换, + * 内部使用 MyBatis Plus 进行持久化操作。 + *

    + * 注意:通过命名约定和泛型参数自动匹配对应的 RepositoryFacade, + * 无需额外的 @DelegateFor 注解配置。 + * + * @author chuck + * @version 1.0.2 + * @since 2026/6/28 + */ +@Slf4j @Component -@DelegateFor( - name = "userRepository", - type = RepositoryType.MYBATIS_PLUS, - po = UserPO.class, - description = "用户仓储 MyBatis Plus 实现", - priority = 10 -) @AllArgsConstructor -public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate { +public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate { private final UserMapper userMapper; + /** + * 根据用户名查询用户 + *

    + * 使用 UserMapper 执行条件查询,将 PO 结果转换为 Entity 返回。 + * + * @param name 用户名 + * @return 用户实体,不存在时返回 null + */ @Override - public UserPO finByName(String name) { - return userMapper.selectOne(Wrappers.lambdaQuery().eq(UserPO::getUsername, name)); + public UserEntity findByName(String name) { + MybatisUserPO po = userMapper.selectOne(Wrappers.lambdaQuery().eq(MybatisUserPO::getUsername, name)); + return toEntity(po); } -} \ No newline at end of file +} diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/TestConfig.java b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/TestConfig.java index 2bdc198..45e8fd6 100644 --- a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/TestConfig.java +++ b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/config/TestConfig.java @@ -15,6 +15,6 @@ "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration" }) @ComponentScan(basePackages = "cn.structure.infra.sample") -@MapperScan("cn.structure.infra.sample.infra.repository.mybatis.mapper") +@MapperScan("cn.structure.infra.sample.infra.mapper") public class TestConfig { } \ No newline at end of file diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/LowCodeRepositoryTest.java b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/LowCodeRepositoryTest.java index f815f0f..3a3db6f 100644 --- a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/LowCodeRepositoryTest.java +++ b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/LowCodeRepositoryTest.java @@ -4,7 +4,6 @@ import cn.structure.common.vo.ResPage; import cn.structure.infra.lowcode.repository.LowCodeRepository; import cn.structure.infra.sample.lowcode.LowCodeTestConfig; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/UserRepositoryTest.java b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/UserRepositoryTest.java index 22e7c43..19f35c9 100644 --- a/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/UserRepositoryTest.java +++ b/structure-infra-sample/structure-infra-sample-mybatis/src/test/java/cn/structure/infra/sample/repository/UserRepositoryTest.java @@ -1,9 +1,9 @@ package cn.structure.infra.sample.repository; -import cn.structure.infra.sample.domain.entity.UserEntity; -import cn.structure.infra.sample.config.MybatisOnlyConfig; import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; +import cn.structure.infra.sample.config.MybatisOnlyConfig; +import cn.structure.infra.sample.domain.entity.UserEntity; import cn.structure.infra.sample.domain.repository.UserRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/handler/DemoTaskHandlers.java b/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/handler/DemoTaskHandlers.java index 29039c6..ab9c898 100644 --- a/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/handler/DemoTaskHandlers.java +++ b/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/handler/DemoTaskHandlers.java @@ -1,6 +1,5 @@ package cn.structure.infra.sample.schedule.handler; -import cn.structure.infra.schedule.TaskHandler; import cn.structure.infra.schedule.TaskHandlerRegistry; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; diff --git a/structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/controller/JobManagerController.java b/structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/controller/JobManagerController.java index 6afc01f..ea8190c 100644 --- a/structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/controller/JobManagerController.java +++ b/structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/controller/JobManagerController.java @@ -3,14 +3,7 @@ import cn.structure.infra.schedule.ScheduleTask; import cn.structure.infra.schedule.TaskScheduler; import jakarta.annotation.Resource; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; diff --git a/structure-infra-starter/pom.xml b/structure-infra-starter/pom.xml index dd85241..a6388b6 100644 --- a/structure-infra-starter/pom.xml +++ b/structure-infra-starter/pom.xml @@ -32,11 +32,7 @@ org.springframework.boot spring-boot-data-commons - - cn.structured - structure-infra-schedule-starter - ${revision} - + \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/annotations/DelegateFor.java b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/DelegateFor.java deleted file mode 100644 index a6843e4..0000000 --- a/structure-infra-starter/src/main/java/cn/structure/infra/annotations/DelegateFor.java +++ /dev/null @@ -1,83 +0,0 @@ -package cn.structure.infra.annotations; - -import cn.structure.infra.repository.DelegateType; -import cn.structure.infra.repository.RepositoryType; - -import java.lang.annotation.*; - -/** - * 仓储委托实现标记注解 - *

    - * 标注在具体的 RepositoryDelegate 实现类上 - *

    - * 示例: - *

    - * @DelegateFor(name = "userRepository", po = UserPO.class, delegateType = DelegateType.BASE)
    - * public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate<UserPO, Long> {
    - *     // 实现
    - * }
    - * 
    - *

    - * CQRS 模式下可以指定读代理: - *

    - * @DelegateFor(name = "userRepository", po = UserPO.class, delegateType = DelegateType.READ)
    - * public class UserReadDelegate extends ElasticsearchRepositoryDelegate<UserPO, Long> {
    - *     // 读操作实现
    - * }
    - * 
    - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -@Inherited -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface DelegateFor { - - /** - * 仓储名称,对应 RepositoryFacade 的 Bean 名称 - * - * @return 仓储名称 - */ - String name() default ""; - - /** - * 存储类型 - * - * @return 仓储类型 - */ - RepositoryType type() default RepositoryType.AUTO; - - /** - * 持久化对象类型 - * - * @return PO 类 - */ - Class po() default Object.class; - - /** - * 描述 - * - * @return 描述信息 - */ - String description() default ""; - - /** - * 优先级,多个同类型 Delegate 时使用优先级高的 - * - * @return 优先级,数字越大优先级越高 - */ - int priority() default 0; - - /** - * 委托类型 - *

    - * - BASE: 基础代理,承担写操作和默认读操作 - * - READ: 读代理,专门承担读操作(CQRS 模式下使用) - * - * @return 委托类型 - */ - DelegateType delegateType() default DelegateType.BASE; -} \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/annotations/ReadDelegate.java b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/ReadDelegate.java new file mode 100644 index 0000000..04601da --- /dev/null +++ b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/ReadDelegate.java @@ -0,0 +1,12 @@ +package cn.structure.infra.annotations; + +import org.springframework.beans.factory.annotation.Qualifier; + +import java.lang.annotation.*; + +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ReadDelegate { +} diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java deleted file mode 100644 index 4273fe6..0000000 --- a/structure-infra-starter/src/main/java/cn/structure/infra/annotations/Repository.java +++ /dev/null @@ -1,115 +0,0 @@ -package cn.structure.infra.annotations; - -import cn.structure.infra.repository.RepositoryType; - -import java.lang.annotation.*; - - -/** - * 仓储标记注解 - *

    - * 标注在 {@link cn.structure.infra.repository.RepositoryFacade} 的子类上, - * 用于声明一个领域仓储及其元数据(实体类型、PO 类型、主键类型、CQRS 配置等)。 - *

    - * 框架在启动时通过 {@link cn.structure.infra.repository.RepositoryBeanPostProcessor} - * 扫描此注解,并根据配置自动注入对应的 BASE/READ Delegate。 - *

    - * 示例: - *

    - * @Repository(value = "userRepository", entity = User.class, po = UserPO.class, id = Long.class)
    - * public class UserRepository extends RepositoryFacade<User, Long, UserPO, MybatisPlusRepositoryDelegate<UserPO, Long>> {
    - * }
    - * 
    - *

    - * 启用 CQRS 读写分离的示例: - *

    - * @Repository(value = "userRepository", entity = User.class, po = UserPO.class,
    - *              cqrs = true, readDelegateClass = ElasticsearchRepositoryDelegate.class)
    - * public class UserRepository extends RepositoryFacade<User, Long, UserPO, MybatisPlusRepositoryDelegate<UserPO, Long>> {
    - * }
    - * 
    - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -@Inherited -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface Repository { - - /** - * 仓储名称(对应 Bean 名称,用于与 {@link DelegateFor#name()} 进行匹配) - * - * @return 仓储名称,默认空字符串表示使用类名 - */ - String value() default ""; - - /** - * 仓储类型,默认 {@link RepositoryType#AUTO} 由框架自动推断 - *

    - * 指定具体类型时,会优先匹配同类型的 Delegate - * - * @return 仓储类型 - */ - RepositoryType type() default RepositoryType.AUTO; - - - /** - * 领域实体类类型 - *

    - * RepositoryFacade 在执行 Entity ↔ PO 转换时使用 - * - * @return 实体类,默认 Object.class 表示从泛型参数推断 - */ - Class entity() default Object.class; - - /** - * PO 持久化对象类型 - *

    - * 用于 RepositoryFacade 中的 Entity ↔ PO 转换,以及 Delegate 匹配 - * - * @return PO 类,默认 Object.class 表示从泛型参数推断 - */ - Class po() default Object.class; - - /** - * 主键类型 - *

    - * 默认 Long,如果需要指定其他类型可配置 - * - * @return 主键类型 - */ - Class id() default Long.class; - - /** - * 仓储描述 - * - * @return 描述信息 - */ - String description() default ""; - - - /** - * 是否启用 CQRS 读写分离 - *

    - * 启用后,读操作使用 readDelegate,写操作使用 baseDelegate - *

    - * 必须与 readDelegateClass 配合使用,两者同时成立时才启用读代理 - * - * @return true 启用 CQRS - */ - boolean cqrs() default false; - - /** - * 读代理类 - *

    - * 指定读操作使用的代理类,用于 CQRS 读写分离 - *

    - * 必须与 cqrs=true 配合使用,两者同时成立时才启用读代理 - * - * @return 读代理类 - */ - Class readDelegateClass() default Object.class; - -} 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 13d2c6c..e7304f9 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,52 +1,22 @@ package cn.structure.infra.configuration; -import cn.structure.infra.repository.RepositoryBeanPostProcessor; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 仓储自动装配配置类 *

    - * 负责注册仓储框架的核心组件,实现 RepositoryDelegate 到 RepositoryFacade 的自动注入机制。 + * 负责注册仓储框架的核心组件。 *

    * 核心功能: - * 1. 注册 {@link RepositoryBeanPostProcessor},在 Spring 容器初始化过程中扫描所有 Delegate 和 Facade - * 2. 自动将匹配的 RepositoryDelegate 注入到对应的 RepositoryFacade 中 - * 3. 支持 CQRS 模式,区分 BASE(写)和 READ(读)两种 Delegate - * 4. 当找不到匹配的 Delegate 时,自动尝试通过 DelegateFactory 创建,或使用默认的内存实现 + * 1. RepositoryDelegate 通过 Spring @Autowired 自动注入到 RepositoryFacade + * 2. 支持 CQRS 模式,通过 @ReadDelegate 限定注解区分读代理 + * 3. 当找不到匹配的 Delegate 时,由各存储模块的 AutoConfiguration 通过 @ConditionalOnMissingBean 提供默认实现 * * @author chuck - * @version 1.0.1 + * @version 1.0.2 * @since 2026/6/28 */ @Configuration public class AutoRepositoryConfiguration { - /** - * 注册 Repository Bean 后处理器 - *

    - * 这是仓储框架的核心组件,负责在 Spring 容器启动过程中完成以下工作: - *

    - * 1. **收集阶段(postProcessBeforeInitialization)**: - * - 扫描所有带有 {@link cn.structure.infra.annotations.DelegateFor} 注解的 Bean - * - 收集 RepositoryDelegate 和 IQueryDelegate 的元信息 - * - 按 priority 优先级排序 - *

    - * 2. **识别阶段(postProcessAfterInitialization)**: - * - 识别所有 RepositoryFacade 的实例 - * - 记录 Facade 的 Bean 名称和引用 - *

    - * 3. **注入阶段(ContextRefreshedEvent)**: - * - 根据泛型参数和注解配置,为每个 Facade 查找匹配的 Delegate - * - 优先使用用户自定义的 Delegate(通过 @DelegateFor 声明) - * - 若无匹配,自动通过 {@link cn.structure.infra.repository.RepositoryDelegateFactory} 创建 - * - 最后回退到默认的 {@link cn.structure.infra.repository.InMemoryRepositoryDelegate} - * - 支持 CQRS 模式,分别注入 BASE 和 READ Delegate - * - * @return RepositoryBeanPostProcessor 实例 - */ - @Bean - public static RepositoryBeanPostProcessor repositoryBeanPostProcessor() { - return new RepositoryBeanPostProcessor(); - } -} +} \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoScheduleConfiguration.java b/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoScheduleConfiguration.java deleted file mode 100644 index 28ec00e..0000000 --- a/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoScheduleConfiguration.java +++ /dev/null @@ -1,85 +0,0 @@ -package cn.structure.infra.configuration; - -import cn.structure.infra.properties.InfraProperties; -import cn.structure.infra.schedule.DefaultTaskHandlerRegistry; -import cn.structure.infra.schedule.LocalThreadTaskScheduler; -import cn.structure.infra.schedule.SpringTaskSchedulerAdapter; -import cn.structure.infra.schedule.TaskHandlerRegistry; -import cn.structure.infra.schedule.TaskScheduler; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * 调度子系统自动装配配置类 - *

    - * 负责注册任务调度体系的核心组件,将框架自有的 {@link TaskScheduler} 与 Spring 的 - * {@link org.springframework.scheduling.TaskScheduler} 进行桥接。 - *

    - * 自动注册的 Bean: - *

      - *
    • {@link TaskHandlerRegistry} —— 任务处理器注册表(仅当容器中缺失时)
    • - *
    • {@link TaskScheduler} —— 本地线程任务调度器(仅当容器中缺失时)
    • - *
    • {@link SpringTaskSchedulerAdapter} —— 适配 Spring 调度接口的桥接器
    • - *
    - * 线程池大小通过 {@link InfraProperties#getSchedulePoolSize()} 配置。 - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -@Configuration -@EnableConfigurationProperties(InfraProperties.class) -public class AutoScheduleConfiguration { - - /** - * 注册任务处理器注册表 - *

    - * 当容器中不存在 {@link TaskHandlerRegistry} 时使用默认实现 {@link DefaultTaskHandlerRegistry} - * - * @return 任务处理器注册表 - */ - @Bean - @ConditionalOnMissingBean(TaskHandlerRegistry.class) - public TaskHandlerRegistry taskHandlerRegistry() { - return new DefaultTaskHandlerRegistry(); - } - - /** - * 注册任务调度器 - *

    - * 默认使用基于本地线程池的 {@link LocalThreadTaskScheduler},线程池大小取自 - * {@link InfraProperties#getSchedulePoolSize()},未配置时回退到 CPU 核心数。 - * - * @param infraProperties 框架配置属性 - * @param handlerRegistry 任务处理器注册表 - * @return 任务调度器实例 - */ - @Bean - @ConditionalOnMissingBean(TaskScheduler.class) - public TaskScheduler taskScheduler(InfraProperties infraProperties, TaskHandlerRegistry handlerRegistry) { - Integer poolSize = infraProperties.getSchedulePoolSize(); - return new LocalThreadTaskScheduler(poolSize != null ? poolSize : Runtime.getRuntime().availableProcessors(), handlerRegistry); - } - - /** - * 注册 Spring 任务调度器适配器 - *

    - * 将框架自有的 {@link LocalThreadTaskScheduler} 适配为 Spring 标准的 - * {@link org.springframework.scheduling.TaskScheduler},便于 @Scheduled 等场景复用。 - * 若自有调度器不是 {@link LocalThreadTaskScheduler} 类型,则返回 null 表示不适配。 - * - * @param taskScheduler 框架自有任务调度器 - * @param handlerRegistry 任务处理器注册表 - * @return Spring 标准任务调度器适配器,无法适配时返回 null - */ - @Bean - @ConditionalOnMissingBean(name = "springTaskScheduler") - public org.springframework.scheduling.TaskScheduler springTaskScheduler(TaskScheduler taskScheduler, TaskHandlerRegistry handlerRegistry) { - if (taskScheduler instanceof LocalThreadTaskScheduler) { - return new SpringTaskSchedulerAdapter((LocalThreadTaskScheduler) taskScheduler, handlerRegistry); - } - return null; - } -} \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java new file mode 100644 index 0000000..2acf114 --- /dev/null +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java @@ -0,0 +1,223 @@ +package cn.structure.infra.repository; + +import cn.structure.common.vo.ReqPage; +import cn.structure.common.vo.ResPage; +import cn.structure.infra.annotations.ReadDelegate; +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; + +/** + * CQRS 仓储门面 + *

    + * 支持 CQRS 读写分离模式的仓储门面实现,继承自 RepositoryFacade。 + *

    + * 维护两个代理: + * - baseDelegate: RepositoryDelegate,承担写操作和默认读操作 + * - readDelegate: IQueryDelegate,承担读操作(CQRS 模式下使用) + *

    + * 读操作回退机制: + * - 如果配置了 readDelegate,读操作优先使用 readDelegate + * - 如果 readDelegate 执行失败(抛出异常),自动回退到 baseDelegate 执行 + * - baseDelegate 是最后的兜底 + *

    + * 继承层次: + *

    + * RepositoryFacade<T, ID>                   // 基础门面(单代理模式)
    + *     │
    + *     └── CqrsRepositoryFacade<T, ID, D, RD> // CQRS门面(读写分离,两个代理)
    + * 
    + *

    + * 职责划分: + *

      + *
    • 维护基础代理(baseDelegate)和读代理(readDelegate)
    • + *
    • 写操作始终走 baseDelegate(通过父类 delegate 字段)
    • + *
    • 读操作优先走 readDelegate,失败回退到 baseDelegate
    • + *
    • 提供统一的读操作执行框架(executeReadOperation)
    • + *
    + * + * @param 领域实体类型 + * @param 主键类型 + * @param 基础委托类型(继承 RepositoryDelegate) + * @param 读委托类型(继承 IQueryDelegate) + * @author chuck + * @version 1.0.2 + * @since 2026/7/21 + */ +@Setter +@Getter +@Slf4j +public class CqrsRepositoryFacade, RD extends IQueryDelegate> + extends RepositoryFacade { + + @Autowired + protected D baseDelegate; + + @Autowired + @ReadDelegate + protected RD readDelegate; + + public CqrsRepositoryFacade() { + super(); + } + + public D getBaseDelegate() { + return baseDelegate; + } + + public void setBaseDelegate(D baseDelegate) { + this.baseDelegate = baseDelegate; + super.setDelegate(baseDelegate); + } + + /** + * 根据主键查询(读操作,支持 CQRS 路由) + *

    + * 优先走 readDelegate,失败回退到 baseDelegate + * + * @param id 主键 + * @return 实体对象,不存在时返回 null + */ + @Override + public T queryById(ID id) { + return executeReadOperation( + () -> readDelegate.queryById(id), + () -> baseDelegate.queryById(id)); + } + + /** + * 根据主键查询(Optional 包装,读操作,支持 CQRS 路由) + * + * @param id 主键 + * @return Optional 包装的实体 + */ + @Override + public Optional queryByIdOptional(ID id) { + T entity = executeReadOperation( + () -> readDelegate.queryById(id), + () -> baseDelegate.queryById(id)); + return Optional.ofNullable(entity); + } + + /** + * 条件查询单条记录(读操作,支持 CQRS 路由) + * + * @param entity 查询条件(非空属性作为等值条件) + * @return 单条实体,不存在时返回 null + */ + @Override + public T queryOne(T entity) { + return executeReadOperation( + () -> readDelegate.queryOne(entity), + () -> baseDelegate.queryOne(entity)); + } + + /** + * 条件查询单条记录(Optional 包装,读操作,支持 CQRS 路由) + * + * @param entity 查询条件 + * @return Optional 包装的实体 + */ + @Override + public Optional queryOneOptional(T entity) { + return executeReadOperation( + () -> readDelegate.queryOneOptional(entity), + () -> baseDelegate.queryOneOptional(entity)); + } + + /** + * 条件查询列表(读操作,支持 CQRS 路由) + * + * @param entity 查询条件,为 null 时查询全部 + * @return 实体列表,永远不为 null + */ + @Override + public List queryList(T entity) { + List entityList = executeReadOperation( + () -> readDelegate.queryList(entity), + () -> baseDelegate.queryList(entity)); + if (entityList == null || entityList.isEmpty()) { + return List.of(); + } + return entityList; + } + + /** + * 分页查询(读操作,支持 CQRS 路由) + * + * @param reqPage 分页参数 + * @return 分页结果 + */ + @Override + public ResPage queryPage(ReqPage reqPage) { + return executeReadOperation( + () -> readDelegate.queryPage(reqPage), + () -> baseDelegate.queryPage(reqPage)); + } + + /** + * 根据主键列表批量查询(读操作,支持 CQRS 路由) + * + * @param ids 主键列表 + * @return 实体列表,永远不为 null + */ + @Override + public List listByIds(List ids) { + List entityList = executeReadOperation( + () -> readDelegate.listByIds(ids), + () -> baseDelegate.listByIds(ids)); + if (entityList == null || entityList.isEmpty()) { + return List.of(); + } + return entityList; + } + + /** + * 统计数量(读操作,支持 CQRS 路由) + * + * @param entity 查询条件 + * @return 记录数量 + */ + @Override + public long count(T entity) { + return executeReadOperation( + () -> readDelegate.count(entity), + () -> baseDelegate.count(entity)); + } + + /** + * 执行读操作,带回退机制 + *

    + * 优先使用 readDelegate 执行,如果 readDelegate 未配置或执行失败, + * 自动回退到 baseDelegate 执行。 + * + * @param readOperation 读代理操作 + * @param fallbackOperation 基础代理回退操作 + * @param 返回类型 + * @return 操作结果 + */ + protected R executeReadOperation(ReadOperation readOperation, ReadOperation fallbackOperation) { + if (readDelegate != null) { + try { + return readOperation.execute(); + } catch (Exception e) { + log.warn("Read delegate operation failed, falling back to base delegate: {}", e.getMessage()); + } + } + return fallbackOperation.execute(); + } + + /** + * 读操作接口 + * + * @param 返回类型 + */ + @FunctionalInterface + protected interface ReadOperation { + R execute(); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..1c1e35a --- /dev/null +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/GenericTypeResolver.java @@ -0,0 +1,63 @@ +package cn.structure.infra.repository; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public class GenericTypeResolver { + + public static Class resolveType(Class clazz, Class targetInterface, int index) { + Type genericSuperclass = clazz.getGenericSuperclass(); + while (genericSuperclass != null) { + if (genericSuperclass instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass; + Type rawType = parameterizedType.getRawType(); + if (targetInterface.isAssignableFrom((Class) rawType)) { + Type typeArg = parameterizedType.getActualTypeArguments()[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(); + } + + Type[] interfaces = clazz.getGenericInterfaces(); + for (Type iface : interfaces) { + if (iface instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) iface; + Type rawType = parameterizedType.getRawType(); + if (targetInterface.isAssignableFrom((Class) rawType)) { + Type typeArg = parameterizedType.getActualTypeArguments()[index]; + if (typeArg instanceof Class) { + return (Class) typeArg; + } else if (typeArg instanceof ParameterizedType) { + return (Class) ((ParameterizedType) typeArg).getRawType(); + } + } + } + } + + return null; + } + + public static Class resolveEntityClass(Class clazz) { + return resolveType(clazz, RepositoryDelegate.class, 0); + } + + public static Class resolvePoClass(Class clazz) { + return resolveType(clazz, RepositoryDelegate.class, 1); + } + + public static Class resolveIdClass(Class clazz) { + return resolveType(clazz, RepositoryDelegate.class, 2); + } + + private GenericTypeResolver() { + } +} diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/IQueryDelegate.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/IQueryDelegate.java index ad1e04b..a6d75ec 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/IQueryDelegate.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/IQueryDelegate.java @@ -11,18 +11,18 @@ * 用于 CQRS 模式下,读代理只需要实现读操作,不需要实现写操作。 *

    * 这是防腐层(ACL)的核心组件之一: - * - 对外:由 RepositoryFacade 调用,面向领域模型 - * - 对内:操作持久化模型(PO),与具体存储技术交互 + * - 对外:由 RepositoryFacade 调用,面向领域模型,接收和返回领域实体 + * - 对内:操作持久化模型(PO),与具体存储技术交互,内部完成转换 *

    * 与 RepositoryDelegate 的关系: * - RepositoryDelegate:继承 ICrudRepository + IQueryDelegate,包含完整的 CRUD 操作(写+读) * - IQueryDelegate:只继承 IQueryRepository + 补充方法,只有读操作 * - RepositoryDelegate 是 IQueryDelegate 的子类型,因此所有 RepositoryDelegate 都可以作为读代理使用 * - * @param 持久化对象类型(PO) + * @param 领域实体类型(Entity) * @param 主键类型 * @author chuck - * @version 1.0.1 + * @version 1.0.2 * @since 2026/6/28 */ public interface IQueryDelegate extends IQueryRepository { diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/InMemoryRepositoryDelegate.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/InMemoryRepositoryDelegate.java index 94afbe1..888e136 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/InMemoryRepositoryDelegate.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/InMemoryRepositoryDelegate.java @@ -16,11 +16,13 @@ *

    * 基于 ConcurrentHashMap 实现的内存仓储,用于示例、测试和开发阶段 * 支持基本的 CRUD、条件查询、分页查询 + *

    + * 注意:此实现直接操作领域实体(Entity),不涉及 PO 转换。 * - * @param 实体类型 + * @param 实体类型(领域实体) * @param 主键类型 * @author chuck - * @version 1.0.1 + * @version 1.0.2 * @since 2026/6/28 */ @Slf4j @@ -29,15 +31,25 @@ public class InMemoryRepositoryDelegate implements RepositoryDelegate storage = new ConcurrentHashMap<>(); private final AtomicLong idGenerator = new AtomicLong(1); private final Class entityClass; + private final Class idClass; private final String idFieldName; + /** + * 默认构造函数(用于无参实例化场景) + */ + public InMemoryRepositoryDelegate() { + this.entityClass = null; + this.idClass = null; + this.idFieldName = "id"; + } + /** * 构造内存仓储委托,默认主键字段名为 "id" * * @param entityClass 实体类型 */ public InMemoryRepositoryDelegate(Class entityClass) { - this(entityClass, "id"); + this(entityClass, null, "id"); } /** @@ -47,9 +59,21 @@ public InMemoryRepositoryDelegate(Class entityClass) { * @param idFieldName 主键字段名 */ public InMemoryRepositoryDelegate(Class entityClass, String idFieldName) { + this(entityClass, null, idFieldName); + } + + /** + * 构造内存仓储委托 + * + * @param entityClass 实体类型 + * @param idClass 主键类型 + * @param idFieldName 主键字段名 + */ + public InMemoryRepositoryDelegate(Class entityClass, Class idClass, String idFieldName) { this.entityClass = entityClass; + this.idClass = idClass; this.idFieldName = idFieldName; - log.info("InMemoryRepositoryDelegate initialized for entity: {}", entityClass.getSimpleName()); + log.info("InMemoryRepositoryDelegate initialized for entity: {}", entityClass != null ? entityClass.getSimpleName() : "null"); } /** @@ -376,4 +400,24 @@ public long count(T condition) { public boolean exists(T condition) { return count(condition) > 0; } + + @Override + public Class getEntityClass() { + return entityClass; + } + + @Override + public Class getPoClass() { + return entityClass; + } + + @Override + public Class getIdClass() { + return idClass; + } + + @Override + public String getIdFieldName() { + return idFieldName; + } } 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 deleted file mode 100644 index 4487b15..0000000 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java +++ /dev/null @@ -1,535 +0,0 @@ -package cn.structure.infra.repository; - -import cn.structure.infra.annotations.DelegateFor; -import cn.structure.infra.annotations.Repository; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; - -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - -/** - * 仓储 Bean 后处理器 - *

    - * 仓储框架的核心装配器,负责在 Spring 容器启动过程中完成 Delegate 收集、Facade 识别 - * 以及 Delegate → Facade 的自动注入。是 Facade + Delegate 模式的"装配枢纽"。 - *

    - * 工作流程分为三个阶段: - *

      - *
    1. 收集阶段({@link #postProcessBeforeInitialization}): - * 扫描所有带 {@link DelegateFor} 注解的 Bean,提取 Delegate 元信息并按 priority 降序排序
    2. - *
    3. 识别阶段({@link #postProcessAfterInitialization}): - * 识别所有 {@link RepositoryFacade} 实例并记录其 Bean 名称
    4. - *
    5. 注入阶段({@link #onApplicationEvent}): - * 容器刷新后,对每个 Facade 执行 6 步匹配查找 BASE Delegate, - * 并在 CQRS 模式下查找 READ Delegate,完成注入
    6. - *
    - *

    - * 当无任何匹配的 Delegate 时,依次尝试: - *

      - *
    1. 通过 {@link RepositoryDelegateFactory} 自动创建
    2. - *
    3. 回退到 {@link InMemoryRepositoryDelegate}(仅用于开发/测试)
    4. - *
    - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -@Slf4j -public class RepositoryBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware, ApplicationListener { - - /** - * Spring 应用上下文,用于查找 {@link RepositoryDelegateFactory} Bean - */ - private ApplicationContext applicationContext; - - /** - * 已收集的 Delegate 元信息列表(按收集顺序,查找时按 priority 降序排序) - */ - private final List delegateInfos = new ArrayList<>(); - - /** - * 已识别的 RepositoryFacade 信息列表 - */ - private final List facadeInfos = new ArrayList<>(); - - /** - * 注入 Spring 应用上下文 - * - * @param applicationContext Spring 应用上下文 - * @throws BeansException 注入失败时抛出 - */ - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - - /** - * Bean 初始化前置处理:收集阶段 - *

    - * 检测 Bean 是否带有 {@link DelegateFor} 注解,若有则提取元信息: - *

      - *
    • 若是 {@link RepositoryDelegate},同时记录 delegate 和 queryDelegate 引用
    • - *
    • 若是 {@link IQueryDelegate}(仅实现查询接口),记录 queryDelegate 引用
    • - *
    - * - * @param bean 待处理的 Bean 实例 - * @param beanName Bean 名称 - * @return 原始 Bean(不进行任何修改) - * @throws BeansException 处理异常时抛出 - */ - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - DelegateFor annotation = bean.getClass().getAnnotation(DelegateFor.class); - if (annotation != null) { - // 提取 @DelegateFor 注解的元数据 - DelegateInfo info = new DelegateInfo(); - info.beanName = beanName; - info.name = annotation.name(); - info.type = annotation.type(); - info.poClass = annotation.po(); - info.priority = annotation.priority(); - info.description = annotation.description(); - info.delegateClass = bean.getClass(); - info.delegateType = annotation.delegateType(); - - // 同时是 RepositoryDelegate 的,记录完整 delegate 引用 - if (bean instanceof RepositoryDelegate) { - info.delegate = (RepositoryDelegate) bean; - if (bean instanceof IQueryDelegate) { - info.queryDelegate = (IQueryDelegate) bean; - } - log.info("Found RepositoryDelegate: name={}, type={}, delegateType={}, poClass={}, delegateClass={}, priority={}", - info.name, info.type, info.delegateType, - info.poClass != null ? info.poClass.getSimpleName() : "null", - info.delegateClass.getSimpleName(), - info.priority); - } - // 仅实现 IQueryDelegate(如专用的读代理),仅记录 queryDelegate 引用 - if (bean instanceof IQueryDelegate) { - info.queryDelegate = (IQueryDelegate) bean; - log.info("Found IQueryDelegate: name={}, type={}, delegateType={}, poClass={}, delegateClass={}, priority={}", - info.name, info.type, info.delegateType, - info.poClass != null ? info.poClass.getSimpleName() : "null", - info.delegateClass.getSimpleName(), - info.priority); - } - delegateInfos.add(info); - } - return bean; - } - - /** - * Bean 初始化后置处理:识别阶段 - *

    - * 识别 {@link RepositoryFacade} 实例并记录引用,待容器刷新时统一注入 Delegate。 - * - * @param bean 待处理的 Bean 实例 - * @param beanName Bean 名称 - * @return 原始 Bean - * @throws BeansException 处理异常时抛出 - */ - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof RepositoryFacade) { - RepositoryFacadeInfo info = new RepositoryFacadeInfo(); - info.facade = (RepositoryFacade) bean; - info.beanName = beanName; - facadeInfos.add(info); - } - return bean; - } - - /** - * 容器刷新事件处理:注入阶段 - *

    - * 容器刷新完成后,遍历所有 Facade,依次执行 Delegate 匹配与注入。 - * - * @param event 容器刷新事件 - */ - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - for (RepositoryFacadeInfo facadeInfo : facadeInfos) { - injectDelegatesToFacade(facadeInfo.facade, facadeInfo.beanName); - } - } - - /** - * 为单个 RepositoryFacade 注入 Delegate - *

    - * 完整流程: - *

      - *
    1. 解析 Facade 子类的 4 个泛型参数(entity/id/po/delegateClass)
    2. - *
    3. 读取 @Repository 注解配置(type/cqrs/readDelegateClass)
    4. - *
    5. 查找 BASE Delegate:先匹配用户自定义 → 再尝试工厂自动创建 → 最后回退 InMemory
    6. - *
    7. 当 cqrs=true 且指定 readDelegateClass 时,查找 READ Delegate
    8. - *
    9. 注入 entityClass/poClass 到 Facade
    10. - *
    - * - * @param facade 待注入的 RepositoryFacade - * @param beanName Facade 的 Bean 名称 - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - private void injectDelegatesToFacade(RepositoryFacade facade, String beanName) { - try { - // 步骤 1:解析 Facade 子类继承 RepositoryFacade 时的泛型实参 - Class[] genericTypes = getGenericTypes(facade.getClass()); - if (genericTypes.length < 4) { - log.debug("RepositoryFacade '{}' has insufficient generic types (need 4, got {})", beanName, genericTypes.length); - return; - } - - Class entityClass = genericTypes[0]; - Class idClass = genericTypes[1]; - Class poClass = genericTypes[2]; - Class delegateClass = genericTypes[3]; - - // 步骤 2:读取 @Repository 注解配置(type/cqrs/readDelegateClass) - Repository repositoryAnnotation = facade.getClass().getAnnotation(Repository.class); - RepositoryType targetType = repositoryAnnotation != null ? repositoryAnnotation.type() : RepositoryType.AUTO; - boolean cqrsEnabled = repositoryAnnotation != null && repositoryAnnotation.cqrs(); - Class readDelegateClass = repositoryAnnotation != null ? repositoryAnnotation.readDelegateClass() : Object.class; - - log.debug("RepositoryFacade '{}' requires: entity={}, id={}, po={}, delegateClass={}, type={}, cqrs={}, readDelegateClass={}", - beanName, entityClass.getSimpleName(), idClass.getSimpleName(), - poClass.getSimpleName(), delegateClass.getSimpleName(), targetType, cqrsEnabled, - readDelegateClass != null ? readDelegateClass.getSimpleName() : "null"); - - // 步骤 3:查找 BASE Delegate(6 步匹配) - DelegateInfo baseDelegateInfo = findBaseDelegate(beanName, poClass, delegateClass, targetType); - - if (baseDelegateInfo != null && baseDelegateInfo.delegate != null) { - // 3a. 命中用户自定义 Delegate,直接注入 - facade.setBaseDelegate(baseDelegateInfo.delegate); - log.info("Injected BASE delegate '{}' (type={}) into RepositoryFacade '{}'", - baseDelegateInfo.beanName, baseDelegateInfo.type, beanName); - } else { - // 3b. 未命中,尝试通过 RepositoryDelegateFactory 自动创建 - log.debug("No matching BASE delegate found for RepositoryFacade '{}', trying to auto-create delegate via factory", beanName); - RepositoryDelegate autoDelegate = autoCreateDelegate(poClass, idClass, targetType); - - if (autoDelegate != null) { - facade.setBaseDelegate(autoDelegate); - log.info("Auto-created BASE delegate (type={}) for RepositoryFacade '{}'", targetType, beanName); - } else { - // 3c. 工厂也无法创建,回退到 InMemoryRepositoryDelegate(仅用于开发/测试) - log.warn("No matching BASE delegate found for RepositoryFacade '{}', using default InMemoryRepositoryDelegate", beanName); - RepositoryDelegate defaultDelegate = new InMemoryRepositoryDelegate(poClass); - facade.setBaseDelegate(defaultDelegate); - } - } - - // 步骤 4:CQRS 模式下查找 READ Delegate - boolean shouldEnableReadDelegate = cqrsEnabled && readDelegateClass != null && readDelegateClass != Object.class; - if (shouldEnableReadDelegate) { - // READ 代理使用 AUTO 类型匹配,因为 CQRS 模式下读代理可能与写代理类型不同 - // 例如:写代理用 MyBatis Plus,读代理用 Elasticsearch - DelegateInfo readDelegateInfo = findReadDelegate(beanName, poClass, readDelegateClass, RepositoryType.AUTO); - - if (readDelegateInfo != null && readDelegateInfo.queryDelegate != null) { - facade.setReadDelegate(readDelegateInfo.queryDelegate); - log.info("Injected READ delegate '{}' (type={}) into RepositoryFacade '{}'", - readDelegateInfo.beanName, readDelegateInfo.type, beanName); - } else { - log.warn("No matching READ delegate found for RepositoryFacade '{}' (readDelegateClass={}), read operations will use BASE delegate", - beanName, readDelegateClass.getSimpleName()); - } - } - - // 步骤 5:注入 entityClass/poClass,供 Facade 做 Entity ↔ PO 反射转换 - facade.setEntityClass(entityClass); - facade.setPoClass(poClass); - - } catch (Exception e) { - log.warn("Error injecting delegates to RepositoryFacade '{}': {}", beanName, e.getMessage()); - } - } - - /** - * 通过 {@link RepositoryDelegateFactory} 自动创建 Delegate - *

    - * 当容器中存在工厂 Bean 且无自定义 Delegate 时使用。 - *

      - *
    • 指定了非 AUTO 类型:仅向同类型工厂请求创建
    • - *
    • 指定为 AUTO 类型:依次尝试所有工厂,首个成功即返回
    • - *
    - * - * @param poClass PO 类型 - * @param idClass 主键类型 - * @param targetType 期望的仓储类型 - * @return 创建成功的 Delegate,无法创建时返回 null - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - private RepositoryDelegate autoCreateDelegate(Class poClass, Class idClass, RepositoryType targetType) { - try { - Map factories = applicationContext.getBeansOfType(RepositoryDelegateFactory.class); - - if (factories.isEmpty()) { - log.debug("No RepositoryDelegateFactory beans found in application context"); - return null; - } - - // 指定具体类型:仅匹配同类型工厂 - if (targetType != RepositoryType.AUTO) { - for (RepositoryDelegateFactory factory : factories.values()) { - if (factory.getType() == targetType) { - RepositoryDelegate delegate = factory.createDelegate(poClass, idClass); - if (delegate != null) { - return delegate; - } - } - } - } else { - // AUTO 类型:依次尝试所有工厂,首个成功即返回 - for (RepositoryDelegateFactory factory : factories.values()) { - try { - RepositoryDelegate delegate = factory.createDelegate(poClass, idClass); - if (delegate != null) { - log.debug("Auto-created delegate using factory for type: {}", factory.getType()); - return delegate; - } - } catch (Exception e) { - log.debug("Factory {} failed to create delegate: {}", factory.getType(), e.getMessage()); - } - } - } - } catch (Exception e) { - log.debug("Failed to auto-create delegate: {}", e.getMessage()); - } - return null; - } - - /** - * 查找 BASE Delegate(6 步匹配,按优先级从严格到宽松) - *

    - * 候选集合:delegateType == BASE 且 delegate != null,按 priority 降序排序。 - * 匹配步骤(命中即返回,越靠前匹配条件越严格): - *

      - *
    1. delegateClass 兼容 + name 匹配 + type 匹配(最严格,三者全中)
    2. - *
    3. delegateClass 兼容 + type 匹配(忽略 name)
    4. - *
    5. delegateClass 兼容 + name 匹配(忽略 type)
    6. - *
    7. 仅 delegateClass 兼容(仅按类型)
    8. - *
    9. 仅 name 匹配(仅按名称)
    10. - *
    11. 仅 poClass 匹配(最宽松,按 PO 类型兜底)
    12. - *
    - * - * @param beanName Facade 的 Bean 名称 - * @param poClass PO 类型 - * @param delegateClass Delegate 类型(Facade 第四个泛型实参) - * @param targetType 期望的仓储类型(AUTO 表示不限制) - * @return 匹配的 DelegateInfo,无匹配时返回 null - */ - private DelegateInfo findBaseDelegate(String beanName, Class poClass, Class delegateClass, RepositoryType targetType) { - // 候选集合:BASE 类型且 delegate 已就绪,按 priority 降序 - List filtered = delegateInfos.stream() - .filter(info -> info.delegateType == DelegateType.BASE && info.delegate != null) - .sorted(Comparator.comparingInt((DelegateInfo i) -> i.priority).reversed()) - .toList(); - - // 第 1 步:delegateClass 兼容 + name 匹配 + type 匹配(最严格) - for (DelegateInfo info : filtered) { - if (delegateClass.isAssignableFrom(info.delegateClass) - && info.name != null && !info.name.isEmpty() && info.name.equals(beanName) - && (targetType == RepositoryType.AUTO || targetType == info.type)) { - return info; - } - } - - // 第 2 步:delegateClass 兼容 + type 匹配(忽略 name) - for (DelegateInfo info : filtered) { - if (delegateClass.isAssignableFrom(info.delegateClass) - && (targetType == RepositoryType.AUTO || targetType == info.type)) { - return info; - } - } - - // 第 3 步:delegateClass 兼容 + name 匹配(忽略 type) - for (DelegateInfo info : filtered) { - if (delegateClass.isAssignableFrom(info.delegateClass) - && info.name != null && !info.name.isEmpty() && info.name.equals(beanName)) { - return info; - } - } - - // 第 4 步:仅 delegateClass 兼容 - for (DelegateInfo info : filtered) { - if (delegateClass.isAssignableFrom(info.delegateClass)) { - return info; - } - } - - // 第 5 步:仅 name 匹配 - for (DelegateInfo info : filtered) { - if (info.name != null && !info.name.isEmpty() && info.name.equals(beanName)) { - return info; - } - } - - // 第 6 步:仅 poClass 匹配(最宽松兜底) - for (DelegateInfo info : filtered) { - if (info.poClass != null && info.poClass.equals(poClass)) { - return info; - } - } - - return null; - } - - /** - * 查找 READ Delegate(6 步匹配,与 {@link #findBaseDelegate} 结构一致) - *

    - * 候选集合:delegateType == READ 且 queryDelegate != null,按 priority 降序排序。 - * 用于 CQRS 模式下匹配读代理,匹配步骤与 BASE 完全一致,区别仅在于候选类型。 - * - * @param beanName Facade 的 Bean 名称 - * @param poClass PO 类型 - * @param readDelegateClass 读代理类型(@Repository#readDelegateClass) - * @param targetType 期望的仓储类型 - * @return 匹配的 DelegateInfo,无匹配时返回 null - */ - private DelegateInfo findReadDelegate(String beanName, Class poClass, Class readDelegateClass, RepositoryType targetType) { - // 候选集合:READ 类型且 queryDelegate 已就绪,按 priority 降序 - List filtered = delegateInfos.stream() - .filter(info -> info.delegateType == DelegateType.READ && info.queryDelegate != null) - .sorted(Comparator.comparingInt((DelegateInfo i) -> i.priority).reversed()) - .toList(); - - // 第 1 步:readDelegateClass 兼容 + name 匹配 + type 匹配 - for (DelegateInfo info : filtered) { - if (readDelegateClass.isAssignableFrom(info.delegateClass) - && info.name != null && !info.name.isEmpty() && info.name.equals(beanName) - && (targetType == RepositoryType.AUTO || targetType == info.type)) { - return info; - } - } - - // 第 2 步:readDelegateClass 兼容 + type 匹配 - for (DelegateInfo info : filtered) { - if (readDelegateClass.isAssignableFrom(info.delegateClass) - && (targetType == RepositoryType.AUTO || targetType == info.type)) { - return info; - } - } - - // 第 3 步:readDelegateClass 兼容 + name 匹配 - for (DelegateInfo info : filtered) { - if (readDelegateClass.isAssignableFrom(info.delegateClass) - && info.name != null && !info.name.isEmpty() && info.name.equals(beanName)) { - return info; - } - } - - // 第 4 步:仅 readDelegateClass 兼容 - for (DelegateInfo info : filtered) { - if (readDelegateClass.isAssignableFrom(info.delegateClass)) { - return info; - } - } - - // 第 5 步:仅 name 匹配 - for (DelegateInfo info : filtered) { - if (info.name != null && !info.name.isEmpty() && info.name.equals(beanName)) { - return info; - } - } - - // 第 6 步:仅 poClass 匹配 - for (DelegateInfo info : filtered) { - if (info.poClass != null && info.poClass.equals(poClass)) { - return info; - } - } - - return null; - } - - /** - * 反射解析类继承 RepositoryFacade 时的泛型实参 - *

    - * 沿父类链向上查找首个参数化继承 RepositoryFacade 的位置, - * 返回其实际类型参数数组(长度应为 4:entity/id/po/delegate)。 - * 支持嵌套 ParameterizedType 场景。 - * - * @param clazz 待解析的类 - * @return 泛型实参数组,无法解析时返回空数组 - */ - private Class[] getGenericTypes(Class clazz) { - Class currentClass = clazz; - while (currentClass != null && currentClass != Object.class) { - Type superclass = currentClass.getGenericSuperclass(); - if (superclass instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) superclass; - Type rawType = parameterizedType.getRawType(); - // 定位到 RepositoryFacade 的参数化父类 - if (rawType instanceof Class && RepositoryFacade.class.isAssignableFrom((Class) rawType)) { - Type[] typeArgs = parameterizedType.getActualTypeArguments(); - Class[] classes = new Class[typeArgs.length]; - for (int i = 0; i < typeArgs.length; i++) { - // 普通类型实参直接使用 - if (typeArgs[i] instanceof Class) { - classes[i] = (Class) typeArgs[i]; - } else if (typeArgs[i] instanceof ParameterizedType) { - // 嵌套泛型(如 Delegate)取其原始类型 - Type raw = ((ParameterizedType) typeArgs[i]).getRawType(); - if (raw instanceof Class) { - classes[i] = (Class) raw; - } - } - } - return classes; - } - } - currentClass = currentClass.getSuperclass(); - } - return new Class[0]; - } - - /** - * Delegate 元信息内部载体 - *

    - * 封装从 {@link DelegateFor} 注解提取的所有元数据,以及对应的 Bean 引用。 - */ - private static class DelegateInfo { - /** Bean 名称 */ - String beanName; - /** @DelegateFor#name() 声明的仓储名称 */ - String name; - /** 存储类型 */ - RepositoryType type; - /** PO 类型 */ - Class poClass; - /** 优先级(数字越大越优先) */ - int priority; - /** 描述信息 */ - String description; - /** Delegate 实现类 */ - Class delegateClass; - /** RepositoryDelegate 引用(若 Bean 实现了该接口) */ - RepositoryDelegate delegate; - /** IQueryDelegate 引用(若 Bean 实现了该接口) */ - IQueryDelegate queryDelegate; - /** 委托类型(BASE/READ) */ - DelegateType delegateType; - } - - /** - * RepositoryFacade 元信息内部载体 - *

    - * 封装 Facade 实例及其 Bean 名称,供容器刷新时批量注入使用。 - */ - private static class RepositoryFacadeInfo { - /** Facade 实例 */ - RepositoryFacade facade; - /** Bean 名称 */ - String beanName; - } -} \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java index e2fce1f..de39d54 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDefinition.java @@ -1,18 +1,16 @@ package cn.structure.infra.repository; -import cn.structure.infra.annotations.Repository; -import cn.structure.infra.repository.RepositoryType; import lombok.Data; - - /** * 仓储定义元数据 *

    - * 封装 @Repository 注解的配置信息 + * 封装仓储的配置信息。 + *

    + * 注意:PO 类型已从 Facade 层移除,Entity ↔ PO 转换由 Delegate 实现负责。 * * @author chuck - * @version 1.0.1 + * @version 1.0.2 * @since 2026/6/28 */ @Data @@ -33,11 +31,6 @@ public class RepositoryDefinition { */ private Class entityClass; - /** - * PO持久化对象类型 - */ - private Class poClass; - /** * 主键类型 */ @@ -66,40 +59,13 @@ public class RepositoryDefinition { */ private Class readDelegateClass; - /** - * 原始注解 - */ - private Repository annotation; - - /** - * 从注解构建仓储定义 - * - * @param annotation @Repository注解实例 - * @param beanName Bean名称 - * @return 仓储定义 - */ - public static RepositoryDefinition fromAnnotation(Repository annotation, String beanName) { - RepositoryDefinition definition = new RepositoryDefinition(); - definition.setName(beanName); - definition.setType(annotation.type()); - definition.setEntityClass(annotation.entity()); - definition.setPoClass(annotation.po()); - definition.setIdClass(annotation.id()); - definition.setDescription(annotation.description()); - definition.setCqrs(annotation.cqrs()); - definition.setReadDelegateClass(annotation.readDelegateClass()); - definition.setAnnotation(annotation); - return definition; - } - /** * 验证配置是否有效 * * @return true if valid */ public boolean isValid() { - return entityClass != null && entityClass != Object.class - && poClass != null && poClass != Object.class; + return entityClass != null && entityClass != Object.class; } -} +} \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java index 96b40b6..e2e01d5 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegate.java @@ -5,12 +5,13 @@ /** * 仓储委托接口 *

    - * 定义持久化层的操作契约,面向持久化对象(PO)。 - * 不同的持久化技术(MyBatis、JPA、MongoDB等)提供各自的实现。 + * 定义持久化层的操作契约,面向领域实体(Entity)。 + * 不同的持久化技术(MyBatis、JPA、MongoDB等)提供各自的实现, + * 每个实现负责内部的 Entity ↔ PO 转换。 *

    * 这是防腐层(ACL)的核心组件之一: - * - 对外:由 RepositoryFacade 调用,面向领域模型 - * - 对内:操作持久化模型(PO),与具体存储技术交互 + * - 对外:由 RepositoryFacade 调用,面向领域模型,接收和返回领域实体 + * - 对内:操作持久化模型(PO),与具体存储技术交互,内部完成转换 *

    * DDD 场景下,用户可以自定义实现此接口来满足特殊的持久化需求。 *

    @@ -21,12 +22,42 @@ * 使 RepositoryDelegate 可直接作为 CQRS 模式下的 READ 代理使用 * * - * @param 持久化对象类型(PO) + * @param 领域实体类型(Entity) * @param 主键类型 * @author chuck - * @version 1.0.1 + * @version 1.0.3 * @since 2026/6/28 */ public interface RepositoryDelegate extends ICrudRepository, IQueryDelegate { + /** + * 获取领域实体类型 + * + * @return 领域实体类型 + */ + Class getEntityClass(); + + /** + * 获取持久化对象类型(PO) + * + * @return 持久化对象类型 + */ + Class getPoClass(); + + /** + * 获取主键类型 + * + * @return 主键类型 + */ + Class getIdClass(); + + /** + * 获取 ID 字段名称 + *

    + * 通过 PO 类的 @Id 注解自动识别,默认为 "id" + * + * @return ID 字段名称 + */ + String getIdFieldName(); + } \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegateFactory.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegateFactory.java deleted file mode 100644 index 9a86ec4..0000000 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryDelegateFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -package cn.structure.infra.repository; - -import cn.structure.infra.repository.RepositoryType; - -/** - * 仓储委托工厂接口 - *

    - * 各个持久化技术的 starter 模块实现此接口, - * 用于自动创建对应的 RepositoryDelegate 实例。 - *

    - * 基础模块通过 Spring 容器查找所有实现类, - * 当找不到用户自定义的 delegate 时,使用工厂自动创建。 - * - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -public interface RepositoryDelegateFactory { - - /** - * 获取支持的仓储类型 - * - * @return 仓储类型 - */ - RepositoryType getType(); - - /** - * 创建仓储委托实例 - * - * @param poClass PO 类 - * @param idClass ID 类 - * @return 仓储委托实例,如果无法创建则返回 null - */ - RepositoryDelegate createDelegate(Class poClass, Class idClass); -} 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 fdf2f99..e3fdb8c 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,8 +6,7 @@ import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.BeanUtils; - +import org.springframework.beans.factory.annotation.Autowired; import java.util.List; import java.util.Optional; @@ -17,233 +16,144 @@ *

    * 作为领域层与持久化层之间的防腐层(ACL),提供统一的 CRUD 操作契约。 *

    - * 支持两种代理: - * - baseDelegate: RepositoryDelegate,承担写操作和默认读操作 - * - readDelegate: IQueryDelegate,承担读操作(CQRS 模式下使用) + * 单代理模式:维护单一的 RepositoryDelegate,所有操作(读和写)都通过该代理执行。 + *

    + * 注意:Entity ↔ PO 的转换逻辑已下移到 Delegate 实现层, + * Facade 层只操作领域实体(Entity),不感知持久化对象(PO)。 *

    - * 读操作回退机制: - * - 如果配置了 readDelegate 且 cqrs=true,读操作优先使用 readDelegate - * - 如果 readDelegate 执行失败(抛出异常),自动回退到 baseDelegate 执行 - * - baseDelegate 是最后的兜底 + * 继承层次: + *

    + * RepositoryFacade<T, ID>                   // 基础门面(单代理模式)
    + *     │
    + *     └── CqrsRepositoryFacade<T, ID, D, RD> // CQRS门面(读写分离,两个代理)
    + * 
    * * @param 领域实体类型 * @param 主键类型 - * @param

    持久化对象类型(PO) - * @param 基础委托类型 * @author chuck - * @version 1.0.1 + * @version 1.0.4 * @since 2026/6/28 */ @Setter @Getter @Slf4j -public class RepositoryFacade> implements ICrudRepository { - - - - /** - * 基础代理 - *

    - * 承担所有写操作:save、removeById、saveBatch、removeBatchByIds - * 同时作为读操作的默认代理和兜底代理 - */ - protected D baseDelegate; +public class RepositoryFacade implements ICrudRepository { - /** - * 读代理(新增) - *

    - * 承担所有读操作:findById、queryList、queryPage 等 - * 如果未配置或执行失败,读操作会回退到使用 baseDelegate - * 类型为 IQueryDelegate,可以与 baseDelegate 类型不同 - */ - protected IQueryDelegate readDelegate; + @Autowired + protected RepositoryDelegate delegate; - /** - * 领域实体类型,用于 Entity ↔ PO 反射转换 - */ protected Class entityClass; - /** - * 持久化对象类型,用于 Entity ↔ PO 反射转换 - */ - protected Class

    poClass; - - /** - * 默认构造函数(用于无参实例化场景,需后续手动设置 entityClass/poClass) - */ public RepositoryFacade() { } /** - * 根据实体类和 PO 类构造 RepositoryFacade - * - * @param entityClass 领域实体类型 - * @param poClass 持久化对象类型 - */ - public RepositoryFacade(Class entityClass, Class

    poClass) { - this.entityClass = entityClass; - this.poClass = poClass; - } - - /** - * 获取基础代理(原有方法保持不变) - * - * @return 基础代理 - */ - public D getBaseDelegate() { - return baseDelegate; - } - - /** - * 保存实体(写操作,走 baseDelegate) + * 保存实体(写操作,走 delegate) *

    - * 流程:Entity → PO → baseDelegate.save → PO → Entity + * 流程:Entity → delegate.save(内部转换为 PO)→ 返回 Entity * * @param entity 领域实体 * @return 保存后的实体(包含可能生成的主键) */ @Override public T save(T entity) { - // Entity → PO 转换后交给基础代理持久化,再回转为 Entity - P save = baseDelegate.save(toPo(entity)); - return toEntity(save); + return delegate.save(entity); } /** - * 根据主键删除(写操作,走 baseDelegate) + * 根据主键删除(写操作,走 delegate) * * @param id 主键 */ @Override public void removeById(ID id) { - baseDelegate.removeById(id); + delegate.removeById(id); } /** - * 根据主键查询(写操作路径,走 baseDelegate) - *

    - * 此方法对应 ICrudRepository 契约,不参与 CQRS 路由,始终走基础代理。 + * 根据主键查询(写操作路径,走 delegate) * * @param id 主键 * @return 实体对象,不存在时返回 null */ @Override public T findById(ID id) { - P po = baseDelegate.findById(id); - return toEntity(po); + return delegate.findById(id); } /** - * 根据主键查询(读操作,支持 CQRS 路由) - *

    - * 优先走 readDelegate,失败回退到 baseDelegate + * 根据主键查询(读操作,走 delegate) * * @param id 主键 * @return 实体对象,不存在时返回 null */ @Override public T queryById(ID id) { - // 读操作:优先 readDelegate,失败回退 baseDelegate - P po = executeReadOperation( - () -> readDelegate.queryById(id), - () -> baseDelegate.queryById(id)); - return toEntity(po); + return delegate.queryById(id); } /** - * 根据主键查询(Optional 包装,读操作,支持 CQRS 路由) + * 根据主键查询(Optional 包装,读操作,走 delegate) * * @param id 主键 * @return Optional 包装的实体 */ @Override public Optional queryByIdOptional(ID id) { - P po = executeReadOperation( - () -> readDelegate.queryById(id), - () -> baseDelegate.queryById(id)); - return Optional.ofNullable(toEntity(po)); + T entity = delegate.queryById(id); + return Optional.ofNullable(entity); } /** - * 条件查询单条记录(读操作,支持 CQRS 路由) + * 条件查询单条记录(读操作,走 delegate) * * @param entity 查询条件(非空属性作为等值条件) * @return 单条实体,不存在时返回 null */ @Override public T queryOne(T entity) { - P p = executeReadOperation( - () -> readDelegate.queryOne(toPo(entity)), - () -> baseDelegate.queryOne(toPo(entity))); - return toEntity(p); + return delegate.queryOne(entity); } /** - * 条件查询单条记录(Optional 包装,读操作,支持 CQRS 路由) + * 条件查询单条记录(Optional 包装,读操作,走 delegate) * * @param entity 查询条件 * @return Optional 包装的实体 */ @Override public Optional queryOneOptional(T entity) { - Optional

    p = executeReadOperation( - () -> readDelegate.queryOneOptional(toPo(entity)), - () -> baseDelegate.queryOneOptional(toPo(entity))); - return p.map(this::toEntity); + return delegate.queryOneOptional(entity); } /** - * 条件查询列表(读操作,支持 CQRS 路由) + * 条件查询列表(读操作,走 delegate) * * @param entity 查询条件,为 null 时查询全部 * @return 实体列表,永远不为 null */ @Override public List queryList(T entity) { - List

    poList = executeReadOperation( - () -> readDelegate.queryList(toPo(entity)), - () -> baseDelegate.queryList(toPo(entity))); - if (poList == null || poList.isEmpty()) { + List entityList = delegate.queryList(entity); + if (entityList == null || entityList.isEmpty()) { return List.of(); } - // PO 列表批量转换为 Entity 列表 - return poList.stream() - .map(this::toEntity) - .toList(); + return entityList; } /** - * 分页查询(读操作,支持 CQRS 路由) - *

    - * 由于分页结果结构 {@link ResPage} 与实体类型绑定,需要逐项转换 PO → Entity。 + * 分页查询(读操作,走 delegate) * * @param reqPage 分页参数 - * @return 分页结果,代理返回 null 时本方法也返回 null + * @return 分页结果 */ @Override public ResPage queryPage(ReqPage reqPage) { - ResPage

    poPage = executeReadOperation( - () -> readDelegate.queryPage(reqPage), - () -> baseDelegate.queryPage(reqPage)); - if (poPage == null) { - return null; - } - // 复制分页元数据,仅对 records 进行 PO → Entity 转换 - ResPage tPage = new ResPage<>(); - tPage.setCurrent(poPage.getCurrent()); - tPage.setPages(poPage.getPages()); - tPage.setSize(poPage.getSize()); - tPage.setTotal(poPage.getTotal()); - if (poPage.getRecords() != null) { - tPage.setRecords(poPage.getRecords().stream() - .map(this::toEntity) - .toList()); - } - return tPage; + return delegate.queryPage(reqPage); } /** - * 批量保存(写操作,走 baseDelegate) + * 批量保存(写操作,走 delegate) * * @param entities 实体列表 * @return 保存后的实体列表 @@ -253,144 +163,53 @@ public List saveBatch(List entities) { if (entities == null || entities.isEmpty()) { return List.of(); } - // Entity 列表 → PO 列表,批量持久化后再回转 - List

    poList = entities.stream() - .map(this::toPo) - .toList(); - List

    savedPoList = baseDelegate.saveBatch(poList); - return savedPoList.stream() - .map(this::toEntity) - .toList(); + return delegate.saveBatch(entities); } /** - * 根据主键批量删除(写操作,走 baseDelegate) + * 根据主键批量删除(写操作,走 delegate) * * @param ids 主键列表 */ @Override public void removeBatchByIds(List ids) { - baseDelegate.removeBatchByIds(ids); + delegate.removeBatchByIds(ids); } /** - * 根据主键列表批量查询(读操作,支持 CQRS 路由) + * 根据主键列表批量查询(读操作,走 delegate) * * @param ids 主键列表 * @return 实体列表,永远不为 null */ @Override public List listByIds(List ids) { - List

    poList = executeReadOperation( - () -> readDelegate.listByIds(ids), - () -> baseDelegate.listByIds(ids)); - if (poList == null || poList.isEmpty()) { + List entityList = delegate.listByIds(ids); + if (entityList == null || entityList.isEmpty()) { return List.of(); } - return poList.stream() - .map(this::toEntity) - .toList(); + return entityList; } /** - * 统计数量(读操作,支持 CQRS 路由) + * 统计数量(读操作,走 delegate) * * @param entity 查询条件 * @return 记录数量 */ @Override public long count(T entity) { - return executeReadOperation( - () -> readDelegate.count(toPo(entity)), - () -> baseDelegate.count(toPo(entity))); + return delegate.count(entity); } /** - * 判断是否存在(写操作路径,走 baseDelegate) + * 判断是否存在(写操作路径,走 delegate) * * @param entity 查询条件 * @return true 表示存在 */ @Override public boolean exists(T entity) { - return baseDelegate.exists(toPo(entity)); - } - - /** - * 执行读操作,带回退机制(新增方法) - *

    - * 优先使用 readDelegate 执行,如果 readDelegate 未配置或执行失败, - * 自动回退到 baseDelegate 执行。 - * - * @param readOperation 读代理操作 - * @param fallbackOperation 基础代理回退操作 - * @param 返回类型 - * @return 操作结果 - */ - protected R executeReadOperation(ReadOperation readOperation, ReadOperation fallbackOperation) { - if (readDelegate != null) { - try { - return readOperation.execute(); - } catch (Exception e) { - log.warn("Read delegate operation failed, falling back to base delegate: {}", e.getMessage()); - } - } - return fallbackOperation.execute(); - } - - /** - * 读操作接口(新增接口) - * - * @param 返回类型 - */ - @FunctionalInterface - protected interface ReadOperation { - R execute(); - } - - /** - * PO → Entity 转换 - *

    - * 通过反射调用无参构造函数创建 Entity 实例,并使用 {@link BeanUtils#copyProperties(Object, Object)} - * 复制同名属性。子类可重写以实现自定义映射逻辑。 - * - * @param po 持久化对象,为 null 时返回 null - * @return 领域实体 - * @throws RuntimeException 反射创建实例或属性复制失败时抛出 - */ - protected T toEntity(P po) { - if (po == null) { - return null; - } - try { - T entity = entityClass.getDeclaredConstructor().newInstance(); - BeanUtils.copyProperties(po, entity); - return entity; - } catch (Exception e) { - throw new RuntimeException("Failed to convert PO to entity", e); - } - } - - /** - * Entity → PO 转换 - *

    - * 通过反射调用无参构造函数创建 PO 实例,并使用 {@link BeanUtils#copyProperties(Object, Object)} - * 复制同名属性。子类可重写以实现自定义映射逻辑。 - * - * @param entity 领域实体,为 null 时返回 null - * @return 持久化对象 - * @throws RuntimeException 反射创建实例或属性复制失败时抛出 - */ - protected P toPo(T entity) { - if (entity == null) { - return null; - } - try { - P po = poClass.getDeclaredConstructor().newInstance(); - BeanUtils.copyProperties(entity, po); - return po; - } catch (Exception e) { - throw new RuntimeException("Failed to convert entity to PO", e); - } + return delegate.exists(entity); } } \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacadeFactoryBean.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacadeFactoryBean.java deleted file mode 100644 index 813ef6e..0000000 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacadeFactoryBean.java +++ /dev/null @@ -1,105 +0,0 @@ -package cn.structure.infra.repository; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.FactoryBean; - -/** - * RepositoryFacade 的 Spring {@link FactoryBean} 实现 - *

    - * 用于以编程方式注册 {@link RepositoryFacade} Bean,封装了创建过程所需的元数据 - * (entity 类型、PO 类型、Delegate 类型、{@link RepositoryDefinition})。 - *

    - * 实际的 Delegate 注入不在此处完成,而是由 {@link RepositoryBeanPostProcessor} - * 在容器刷新事件中统一处理。 - * - * @param 领域实体类型 - * @param 主键类型 - * @param

    持久化对象类型(PO) - * @param 基础委托类型 - * @author chuck - * @version 1.0.1 - * @since 2026/6/28 - */ -@Slf4j -public class RepositoryFacadeFactoryBean> implements FactoryBean> { - - /** - * 领域实体类型 - */ - private final Class entityClass; - - /** - * 持久化对象类型 - */ - private final Class

    poClass; - - /** - * 基础委托类型,用于在 BeanPostProcessor 中匹配 Delegate - */ - private final Class delegateClass; - - /** - * 仓储定义元数据,封装 @Repository 注解的配置信息 - */ - private final RepositoryDefinition definition; - - /** - * 构造 FactoryBean - * - * @param entityClass 领域实体类型 - * @param poClass 持久化对象类型 - * @param delegateClass 基础委托类型 - * @param definition 仓储定义元数据 - */ - public RepositoryFacadeFactoryBean(Class entityClass, Class

    poClass, Class delegateClass, RepositoryDefinition definition) { - this.entityClass = entityClass; - this.poClass = poClass; - this.delegateClass = delegateClass; - this.definition = definition; - } - - /** - * 创建 RepositoryFacade 实例 - *

    - * 仅创建 Facade 本身并注入 entityClass/poClass,Delegate 的注入由 - * {@link RepositoryBeanPostProcessor#onApplicationEvent} 完成。 - * - * @return RepositoryFacade 实例 - */ - @Override - public RepositoryFacade getObject() { - log.debug("Creating RepositoryFacade for entity: {}, po: {}, delegate: {}", - entityClass.getName(), poClass.getName(), delegateClass.getName()); - return new RepositoryFacade<>(entityClass, poClass); - } - - /** - * 返回 FactoryBean 产出的对象类型 - * - * @return RepositoryFacade 类型 - */ - @Override - @SuppressWarnings("unchecked") - public Class getObjectType() { - return RepositoryFacade.class; - } - - /** - * 声明为单例 Bean - * - * @return 始终返回 true - */ - @Override - public boolean isSingleton() { - return true; - } - - /** - * 获取仓储定义元数据 - * - * @return 仓储定义 - */ - public RepositoryDefinition getDefinition() { - return definition; - } -} \ No newline at end of file From ab9170a93c5977f14d47a9558f64e697a58b0911 Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Tue, 21 Jul 2026 15:24:36 +0800 Subject: [PATCH 5/9] =?UTF-8?q?refactor(repository):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BB=93=E5=82=A8=E9=97=A8=E9=9D=A2=E5=AE=9E=E7=8E=B0=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=B3=9B=E5=9E=8B=E5=A7=94=E6=89=98=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 RepositoryFacade 类添加泛型参数 D extends RepositoryDelegate - 移除 CqrsRepositoryFacade 中的 baseDelegate 字段和相关 setter 方法 - 简化 CqrsRepositoryFacade 的读操作实现,直接调用父类方法作为回退 - 更新 AbstractUserRepositoryImpl 继承带泛型的 RepositoryFacade - 为 UserRepositoryDelegate 添加 IQueryDelegate 接口继承 - 调整依赖注入策略,使用 required = false 避免注入失败 - 优化组件扫描配置,添加 PO 和 Mapper 包路径 --- .../AbstractUserRepositoryImpl.java | 4 +- .../delegate/UserRepositoryDelegate.java | 1 + .../sample/cqrs/config/CqrsTestConfig.java | 3 +- .../repository/CqrsRepositoryFacade.java | 116 +++-------------- .../infra/repository/RepositoryFacade.java | 120 +----------------- 5 files changed, 27 insertions(+), 217 deletions(-) diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java index 293f574..c7640bc 100644 --- a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java +++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/AbstractUserRepositoryImpl.java @@ -16,10 +16,10 @@ * @version 1.0.2 * @since 2026/6/28 */ -public abstract class AbstractUserRepositoryImpl extends RepositoryFacade implements UserRepository { +public abstract class AbstractUserRepositoryImpl extends RepositoryFacade implements UserRepository { @Override public UserEntity findByName(String name) { - return ((UserRepositoryDelegate) getDelegate()).findByName(name); + return getDelegate().findByName(name); } } diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java index 0f4df81..2fa5976 100644 --- a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java +++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/repository/delegate/UserRepositoryDelegate.java @@ -1,5 +1,6 @@ package cn.structure.infra.sample.infra.repository.delegate; +import cn.structure.infra.repository.IQueryDelegate; import cn.structure.infra.repository.RepositoryDelegate; import cn.structure.infra.sample.domain.entity.UserEntity; 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 cc25c26..8cea5f4 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 @@ -45,7 +45,8 @@ }) @ComponentScan(basePackages = { "cn.structure.infra.sample.cqrs", - "cn.structure.infra.sample.infra", + "cn.structure.infra.sample.infra.po", + "cn.structure.infra.sample.infra.mapper", "cn.structure.infra.repository", "cn.structure.infra.mybatis.plus", "cn.structure.infra.elasticsearch" diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java index 2acf114..d443e3e 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java @@ -2,9 +2,7 @@ import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; -import cn.structure.infra.annotations.ReadDelegate; import lombok.Getter; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -42,164 +40,91 @@ * * @param 领域实体类型 * @param 主键类型 - * @param 基础委托类型(继承 RepositoryDelegate) * @param 读委托类型(继承 IQueryDelegate) * @author chuck * @version 1.0.2 * @since 2026/7/21 */ -@Setter @Getter @Slf4j public class CqrsRepositoryFacade, RD extends IQueryDelegate> - extends RepositoryFacade { + extends RepositoryFacade { - @Autowired - protected D baseDelegate; - - @Autowired - @ReadDelegate + @Autowired(required = false) protected RD readDelegate; - public CqrsRepositoryFacade() { - super(); - } - - public D getBaseDelegate() { - return baseDelegate; - } - - public void setBaseDelegate(D baseDelegate) { - this.baseDelegate = baseDelegate; - super.setDelegate(baseDelegate); - } - - /** - * 根据主键查询(读操作,支持 CQRS 路由) - *

    - * 优先走 readDelegate,失败回退到 baseDelegate - * - * @param id 主键 - * @return 实体对象,不存在时返回 null - */ @Override public T queryById(ID id) { return executeReadOperation( () -> readDelegate.queryById(id), - () -> baseDelegate.queryById(id)); + () -> super.queryById(id)); } - /** - * 根据主键查询(Optional 包装,读操作,支持 CQRS 路由) - * - * @param id 主键 - * @return Optional 包装的实体 - */ @Override public Optional queryByIdOptional(ID id) { T entity = executeReadOperation( () -> readDelegate.queryById(id), - () -> baseDelegate.queryById(id)); + () -> super.queryById(id)); return Optional.ofNullable(entity); } - /** - * 条件查询单条记录(读操作,支持 CQRS 路由) - * - * @param entity 查询条件(非空属性作为等值条件) - * @return 单条实体,不存在时返回 null - */ @Override public T queryOne(T entity) { return executeReadOperation( () -> readDelegate.queryOne(entity), - () -> baseDelegate.queryOne(entity)); + () -> super.queryOne(entity)); } - /** - * 条件查询单条记录(Optional 包装,读操作,支持 CQRS 路由) - * - * @param entity 查询条件 - * @return Optional 包装的实体 - */ @Override public Optional queryOneOptional(T entity) { return executeReadOperation( () -> readDelegate.queryOneOptional(entity), - () -> baseDelegate.queryOneOptional(entity)); + () -> super.queryOneOptional(entity)); } - /** - * 条件查询列表(读操作,支持 CQRS 路由) - * - * @param entity 查询条件,为 null 时查询全部 - * @return 实体列表,永远不为 null - */ @Override public List queryList(T entity) { List entityList = executeReadOperation( () -> readDelegate.queryList(entity), - () -> baseDelegate.queryList(entity)); + () -> super.queryList(entity)); if (entityList == null || entityList.isEmpty()) { return List.of(); } return entityList; } - /** - * 分页查询(读操作,支持 CQRS 路由) - * - * @param reqPage 分页参数 - * @return 分页结果 - */ @Override public ResPage queryPage(ReqPage reqPage) { return executeReadOperation( () -> readDelegate.queryPage(reqPage), - () -> baseDelegate.queryPage(reqPage)); + () -> super.queryPage(reqPage)); } - /** - * 根据主键列表批量查询(读操作,支持 CQRS 路由) - * - * @param ids 主键列表 - * @return 实体列表,永远不为 null - */ @Override public List listByIds(List ids) { List entityList = executeReadOperation( () -> readDelegate.listByIds(ids), - () -> baseDelegate.listByIds(ids)); + () -> super.listByIds(ids)); if (entityList == null || entityList.isEmpty()) { return List.of(); } return entityList; } - /** - * 统计数量(读操作,支持 CQRS 路由) - * - * @param entity 查询条件 - * @return 记录数量 - */ @Override public long count(T entity) { return executeReadOperation( () -> readDelegate.count(entity), - () -> baseDelegate.count(entity)); + () -> super.count(entity)); + } + + @Override + public boolean exists(T entity) { + return executeReadOperation( + () -> readDelegate.exists(entity), + () -> super.exists(entity)); } - /** - * 执行读操作,带回退机制 - *

    - * 优先使用 readDelegate 执行,如果 readDelegate 未配置或执行失败, - * 自动回退到 baseDelegate 执行。 - * - * @param readOperation 读代理操作 - * @param fallbackOperation 基础代理回退操作 - * @param 返回类型 - * @return 操作结果 - */ protected R executeReadOperation(ReadOperation readOperation, ReadOperation fallbackOperation) { if (readDelegate != null) { try { @@ -211,13 +136,8 @@ protected R executeReadOperation(ReadOperation readOperation, ReadOperati return fallbackOperation.execute(); } - /** - * 读操作接口 - * - * @param 返回类型 - */ @FunctionalInterface protected interface ReadOperation { R execute(); } -} \ No newline at end of file +} 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 e3fdb8c..3bb59f1 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 @@ -4,134 +4,57 @@ import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; 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; -/** - * 仓储门面 - *

    - * 作为领域层与持久化层之间的防腐层(ACL),提供统一的 CRUD 操作契约。 - *

    - * 单代理模式:维护单一的 RepositoryDelegate,所有操作(读和写)都通过该代理执行。 - *

    - * 注意:Entity ↔ PO 的转换逻辑已下移到 Delegate 实现层, - * Facade 层只操作领域实体(Entity),不感知持久化对象(PO)。 - *

    - * 继承层次: - *

    - * RepositoryFacade<T, ID>                   // 基础门面(单代理模式)
    - *     │
    - *     └── CqrsRepositoryFacade<T, ID, D, RD> // CQRS门面(读写分离,两个代理)
    - * 
    - * - * @param 领域实体类型 - * @param 主键类型 - * @author chuck - * @version 1.0.4 - * @since 2026/6/28 - */ -@Setter @Getter @Slf4j -public class RepositoryFacade implements ICrudRepository { +public class RepositoryFacade> implements ICrudRepository { - @Autowired - protected RepositoryDelegate delegate; + @Autowired(required = false) + protected D delegate; protected Class entityClass; - public RepositoryFacade() { - } - - /** - * 保存实体(写操作,走 delegate) - *

    - * 流程:Entity → delegate.save(内部转换为 PO)→ 返回 Entity - * - * @param entity 领域实体 - * @return 保存后的实体(包含可能生成的主键) - */ @Override public T save(T entity) { return delegate.save(entity); } - /** - * 根据主键删除(写操作,走 delegate) - * - * @param id 主键 - */ @Override public void removeById(ID id) { delegate.removeById(id); } - /** - * 根据主键查询(写操作路径,走 delegate) - * - * @param id 主键 - * @return 实体对象,不存在时返回 null - */ @Override public T findById(ID id) { return delegate.findById(id); } - /** - * 根据主键查询(读操作,走 delegate) - * - * @param id 主键 - * @return 实体对象,不存在时返回 null - */ @Override public T queryById(ID id) { return delegate.queryById(id); } - /** - * 根据主键查询(Optional 包装,读操作,走 delegate) - * - * @param id 主键 - * @return Optional 包装的实体 - */ @Override public Optional queryByIdOptional(ID id) { T entity = delegate.queryById(id); return Optional.ofNullable(entity); } - /** - * 条件查询单条记录(读操作,走 delegate) - * - * @param entity 查询条件(非空属性作为等值条件) - * @return 单条实体,不存在时返回 null - */ @Override public T queryOne(T entity) { return delegate.queryOne(entity); } - /** - * 条件查询单条记录(Optional 包装,读操作,走 delegate) - * - * @param entity 查询条件 - * @return Optional 包装的实体 - */ @Override public Optional queryOneOptional(T entity) { return delegate.queryOneOptional(entity); } - /** - * 条件查询列表(读操作,走 delegate) - * - * @param entity 查询条件,为 null 时查询全部 - * @return 实体列表,永远不为 null - */ @Override public List queryList(T entity) { List entityList = delegate.queryList(entity); @@ -141,23 +64,11 @@ public List queryList(T entity) { return entityList; } - /** - * 分页查询(读操作,走 delegate) - * - * @param reqPage 分页参数 - * @return 分页结果 - */ @Override public ResPage queryPage(ReqPage reqPage) { return delegate.queryPage(reqPage); } - /** - * 批量保存(写操作,走 delegate) - * - * @param entities 实体列表 - * @return 保存后的实体列表 - */ @Override public List saveBatch(List entities) { if (entities == null || entities.isEmpty()) { @@ -166,22 +77,11 @@ public List saveBatch(List entities) { return delegate.saveBatch(entities); } - /** - * 根据主键批量删除(写操作,走 delegate) - * - * @param ids 主键列表 - */ @Override public void removeBatchByIds(List ids) { delegate.removeBatchByIds(ids); } - /** - * 根据主键列表批量查询(读操作,走 delegate) - * - * @param ids 主键列表 - * @return 实体列表,永远不为 null - */ @Override public List listByIds(List ids) { List entityList = delegate.listByIds(ids); @@ -191,25 +91,13 @@ public List listByIds(List ids) { return entityList; } - /** - * 统计数量(读操作,走 delegate) - * - * @param entity 查询条件 - * @return 记录数量 - */ @Override public long count(T entity) { return delegate.count(entity); } - /** - * 判断是否存在(写操作路径,走 delegate) - * - * @param entity 查询条件 - * @return true 表示存在 - */ @Override public boolean exists(T entity) { return delegate.exists(entity); } -} \ No newline at end of file +} From 611be071dbc825a97f263ea4ae4fb55233af2c51 Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Tue, 21 Jul 2026 16:04:43 +0800 Subject: [PATCH 6/9] =?UTF-8?q?feat(repository):=20=E5=AE=9E=E7=8E=B0CQRS?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E6=B3=A8=E5=85=A5=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 WriteDelegate 注解用于标记写代理组件 - 实现 RepositoryBeanPostProcessor 自动注入读写代理 - 更新 RepositoryFacade 和 CqrsRepositoryFacade 的注入逻辑 - 修复 NoUniqueBeanDefinitionException 代理冲突问题 - 添加详细的 CQRS 代理注入设计方案文档 - 更新 pom.xml 配置以支持参数编译 - 优化代理查找策略,支持注解优先级匹配 --- .trae/documents/cqrs_injection_fix_plan.md | 230 ++++++++++++++++++ .../structure-infra-sample-cqrs/pom.xml | 13 + .../delegate/write/UserWriteDelegate.java | 4 + .../sample/cqrs/UserCqrsRepositoryTest.java | 15 +- .../infra/annotations/WriteDelegate.java | 12 + .../AutoRepositoryConfiguration.java | 10 +- .../repository/CqrsRepositoryFacade.java | 10 +- .../RepositoryBeanPostProcessor.java | 197 +++++++++++++++ .../infra/repository/RepositoryFacade.java | 4 +- 9 files changed, 473 insertions(+), 22 deletions(-) create mode 100644 .trae/documents/cqrs_injection_fix_plan.md create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/annotations/WriteDelegate.java create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java diff --git a/.trae/documents/cqrs_injection_fix_plan.md b/.trae/documents/cqrs_injection_fix_plan.md new file mode 100644 index 0000000..91bbff6 --- /dev/null +++ b/.trae/documents/cqrs_injection_fix_plan.md @@ -0,0 +1,230 @@ +# CQRS 代理注入问题修复方案 + +## 问题分析 + +### 错误描述 + +Spring Boot 启动时抛出 `NoUniqueBeanDefinitionException`: + +``` +Field delegate in cn.structure.infra.repository.RepositoryFacade required a single bean, but 3 were found: + - userReadDelegate + - userWriteDelegate + - userMybatisPlusDelegate +``` + +### 根本原因 + +`RepositoryFacade` 的 `delegate` 字段使用 `@Autowired(required = false)` 注入,但当存在多个匹配类型的 Bean 时,Spring 无法确定注入哪一个。`required = false` 仅在没有匹配 Bean 时生效,无法解决多 Bean 歧义问题。 + +### 当前状态 + +| 组件 | 注解 | 问题 | +| ----------------------------------- | ------------------------------ | ----------- | +| `UserReadDelegate` | `@Component` + `@ReadDelegate` | 已标记读代理 | +| `UserWriteDelegate` | `@Component` | 未标记写代理 | +| `RepositoryFacade.delegate` | `@Autowired(required = false)` | 无限定符,无法区分读写 | +| `CqrsRepositoryFacade.readDelegate` | `@Autowired(required = false)` | 无限定符,无法精准注入 | + +*** + +## 解决方案 + +### 设计思路 + +通过**注解标记 + 自定义限定符**机制,让 Spring 能够根据代理类型(读/写)精准注入: + +1. **新增** **`@WriteDelegate`** **注解**:标记写代理/基础代理 +2. **修改** **`RepositoryFacade`**:`delegate` 字段注入写代理(排除 `@ReadDelegate`) +3. **修改** **`CqrsRepositoryFacade`**:`readDelegate` 字段注入读代理(仅 `@ReadDelegate`) +4. **保留** **`@ReadDelegate`** **注解**:保持现有功能不变 + +### 架构设计 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Spring Context │ +│ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────┐ │ +│ │ @Component │ │ @Component │ │ @Component │ │ +│ │ @ReadDelegate │ │ @WriteDelegate │ │ (无标记) │ │ +│ │ UserReadDelegate │ │ UserWriteDelegate│ │ Others... │ │ +│ └────────┬─────────┘ └────────┬─────────┘ └───────────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ RepositoryBeanPostProcessor │ │ +│ │ - 根据注解筛选 Bean,注入到 Facade 对应字段 │ │ +│ └───────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +*** + +## 修改步骤 + +### 步骤 1:新增 `@WriteDelegate` 注解 + +**文件位置**:`structure-infra-starter/src/main/java/cn/structure/infra/annotations/WriteDelegate.java` + +**内容**: + +```java +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WriteDelegate { +} +``` + +*** + +### 步骤 2:修改 `RepositoryFacade` 注入逻辑 + +**文件位置**:`structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryFacade.java` + +**修改内容**: + +* 将 `@Autowired(required = false)` 替换为自定义注入逻辑 + +* 通过 `@Qualifier` 或 BeanPostProcessor 确保只注入写代理(无 `@ReadDelegate` 标记的 Bean) + +*** + +### 步骤 3:修改 `CqrsRepositoryFacade` 注入逻辑 + +**文件位置**:`structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java` + +**修改内容**: + +* 将 `readDelegate` 字段添加 `@ReadDelegate` 限定符 + +* 确保 `delegate` 字段(继承自父类)注入写代理 + +*** + +### 步骤 4:创建 `RepositoryBeanPostProcessor` + +**文件位置**:`structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java` + +**功能**: + +* 遍历所有 `RepositoryFacade` 和 `CqrsRepositoryFacade` 实例 + +* 根据泛型类型和注解标记,精准注入对应的 delegate + +* 优先注入标记了 `@WriteDelegate` 的 Bean 作为 baseDelegate + +* 优先注入标记了 `@ReadDelegate` 的 Bean 作为 readDelegate + +*** + +### 步骤 5:更新 `AutoRepositoryConfiguration` + +**文件位置**:`structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoRepositoryConfiguration.java` + +**修改内容**: + +* 注册 `RepositoryBeanPostProcessor` + +*** + +### 步骤 6:更新 `UserWriteDelegate` 示例 + +**文件位置**:`structure-infra-sample/structure-infra-sample-cqrs/src/main/java/cn/structure/infra/sample/cqrs/infra/delegate/write/UserWriteDelegate.java` + +**修改内容**: + +* 添加 `@WriteDelegate` 注解 + +*** + +## 关键代码设计 + +### 1. RepositoryBeanPostProcessor 核心逻辑 + +```java +public class RepositoryBeanPostProcessor implements BeanPostProcessor { + + @Autowired + private ListableBeanFactory beanFactory; + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof CqrsRepositoryFacade) { + processCqrsFacade((CqrsRepositoryFacade) bean); + } else if (bean instanceof RepositoryFacade) { + processRepositoryFacade((RepositoryFacade) bean); + } + return bean; + } + + private void processCqrsFacade(CqrsRepositoryFacade facade) { + // 通过泛型获取代理类型 + Type[] types = GenericTypeResolver.resolveTypeArguments( + facade.getClass(), CqrsRepositoryFacade.class); + + // 注入写代理(优先 @WriteDelegate,其次无 @ReadDelegate 的 Bean) + Object writeDelegate = findDelegate((Class) types[2], true); + // 注入读代理(优先 @ReadDelegate 标记的 Bean) + Object readDelegate = findDelegate((Class) types[3], false); + + // 通过反射设置字段 + } +} +``` + +### 2. delegate 查找策略 + +| 场景 | 优先级 | 说明 | +| ------ | ----------------------- | -------- | +| 写代理注入 | 1. `@WriteDelegate` 标记 | 显式标记的写代理 | +|
    | 2. 无 `@ReadDelegate` 标记 | 默认作为写代理 | +|
    | 3. 任意匹配类型 | 兜底策略 | +| 读代理注入 | 1. `@ReadDelegate` 标记 | 显式标记的读代理 | +|
    | 2. 任意匹配类型 | 兜底策略 | + +*** + +## 风险与注意事项 + +### 风险点 + +1. **泛型类型解析失败**:如果子类未正确声明泛型参数,可能导致注入失败 +2. **多 Bean 冲突**:如果同一类型同时存在多个 `@WriteDelegate` 或 `@ReadDelegate`,仍会冲突 +3. **性能影响**:BeanPostProcessor 在每个 Bean 创建时都会执行 + +### 注意事项 + +1. 确保所有 Delegate 实现类都标记了正确的注解 +2. 对于非 CQRS 场景(单代理),不需要标记 `@WriteDelegate`,框架会自动选择无 `@ReadDelegate` 标记的 Bean +3. 保留 `required = false` 的容错机制,确保非 CQRS 场景也能正常运行 + +*** + +## 测试验证 + +### 验证步骤 + +1. **编译测试**:`mvn clean compile` +2. **单元测试**:`mvn test -pl structure-infra-sample/structure-infra-sample-cqrs` +3. **集成测试**:启动 CQRS 示例应用,验证代理正确注入 + +### 预期结果 + +* `UserCqrsRepository` 的 `delegate` 字段注入 `UserWriteDelegate` + +* `UserCqrsRepository` 的 `readDelegate` 字段注入 `UserReadDelegate` + +* 单代理模式的 `UserRepositoryImpl` 正常工作,不受影响 + +*** + +## 兼容性说明 + +| 场景 | 兼容 | 说明 | +| ------------ | -- | ------------------------------------------ | +| 单代理模式(现有代码) | 是 | 无需修改,框架自动选择无 `@ReadDelegate` 的 Bean | +| CQRS 模式(修改后) | 是 | 需要添加 `@WriteDelegate` 和 `@ReadDelegate` 注解 | +| 混合模式(多个模块) | 是 | 通过注解标记区分不同用途的代理 | + diff --git a/structure-infra-sample/structure-infra-sample-cqrs/pom.xml b/structure-infra-sample/structure-infra-sample-cqrs/pom.xml index 19c931f..79f333c 100644 --- a/structure-infra-sample/structure-infra-sample-cqrs/pom.xml +++ b/structure-infra-sample/structure-infra-sample-cqrs/pom.xml @@ -74,4 +74,17 @@ provided + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -parameters + + + + + \ No newline at end of file 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 b86f62d..0107b15 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 @@ -1,5 +1,6 @@ package cn.structure.infra.sample.cqrs.infra.delegate.write; +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.po.MybatisUserPO; @@ -17,9 +18,12 @@ * - removeBatchByIds *

    * 同时也可以处理读操作(作为读操作的兜底) + *

    + * 使用 @WriteDelegate 注解标记为写代理,便于 RepositoryBeanPostProcessor 精准注入 */ @Slf4j @Component +@WriteDelegate public class UserWriteDelegate extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate { @Override diff --git a/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/UserCqrsRepositoryTest.java b/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/UserCqrsRepositoryTest.java index ca980c1..53327f6 100644 --- a/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/UserCqrsRepositoryTest.java +++ b/structure-infra-sample/structure-infra-sample-cqrs/src/test/java/cn/structure/infra/sample/cqrs/UserCqrsRepositoryTest.java @@ -46,9 +46,6 @@ class UserCqrsRepositoryTest { @Autowired private UserCqrsRepository userCqrsRepository; - @Autowired(required = false) - private UserRepository userRepository; - /** * 创建测试用户实体 * @@ -86,7 +83,7 @@ void testBaseDelegateInjection() { assertNotNull(userCqrsRepository, "CQRS 仓储应该被注入"); // 验证 BASE 代理存在 - UserWriteDelegate baseDelegate = userCqrsRepository.getBaseDelegate(); + UserWriteDelegate baseDelegate = userCqrsRepository.getDelegate(); assertNotNull(baseDelegate, "BASE 写代理应该被注入"); log.info("✓ BASE 写代理注入成功: {}", baseDelegate.getClass().getName()); } @@ -108,7 +105,7 @@ void testMultipleDelegatesLoaded() { assertNotNull(userCqrsRepository, "CQRS 仓储应该被注入"); // 验证同时加载了两种代理 - UserWriteDelegate baseDelegate = userCqrsRepository.getBaseDelegate(); + UserWriteDelegate baseDelegate = userCqrsRepository.getDelegate(); IQueryDelegate readDelegate = userCqrsRepository.getReadDelegate(); assertNotNull(baseDelegate, "BASE 代理应该被加载"); @@ -270,12 +267,4 @@ void testFullCqrsCrudFlow() { log.info("✓ CQRS 完整 CRUD 流程测试完成"); } - - @Test - @DisplayName("测试 UserRepository 接口注入") - void testUserRepositoryInterfaceInjection() { - // 验证接口注入 - assertNotNull(userRepository, "UserRepository 接口应该被注入"); - log.info("✓ UserRepository 接口注入成功: {}", userRepository.getClass().getName()); - } } \ No newline at end of file diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/annotations/WriteDelegate.java b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/WriteDelegate.java new file mode 100644 index 0000000..c26a3fe --- /dev/null +++ b/structure-infra-starter/src/main/java/cn/structure/infra/annotations/WriteDelegate.java @@ -0,0 +1,12 @@ +package cn.structure.infra.annotations; + +import org.springframework.beans.factory.annotation.Qualifier; + +import java.lang.annotation.*; + +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WriteDelegate { +} \ 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 e7304f9..d978ce4 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 @@ -8,12 +8,14 @@ * 负责注册仓储框架的核心组件。 *

    * 核心功能: - * 1. RepositoryDelegate 通过 Spring @Autowired 自动注入到 RepositoryFacade - * 2. 支持 CQRS 模式,通过 @ReadDelegate 限定注解区分读代理 - * 3. 当找不到匹配的 Delegate 时,由各存储模块的 AutoConfiguration 通过 @ConditionalOnMissingBean 提供默认实现 + * 1. RepositoryDelegate 通过 RepositoryBeanPostProcessor 自动注入到 RepositoryFacade + * 2. 支持 CQRS 模式,通过 @WriteDelegate 和 @ReadDelegate 注解区分读写代理 + * 3. 写代理注入优先级:@WriteDelegate 标记 > 无 @ReadDelegate 标记 > 任意匹配 + * 4. 读代理注入优先级:@ReadDelegate 标记 > 任意匹配 + * 5. 当找不到匹配的 Delegate 时,由各存储模块的 AutoConfiguration 通过 @ConditionalOnMissingBean 提供默认实现 * * @author chuck - * @version 1.0.2 + * @version 1.0.3 * @since 2026/6/28 */ @Configuration diff --git a/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java b/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java index d443e3e..04bc0ef 100644 --- a/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/CqrsRepositoryFacade.java @@ -3,8 +3,8 @@ import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; 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; @@ -37,20 +37,24 @@ *

  • 读操作优先走 readDelegate,失败回退到 baseDelegate
  • *
  • 提供统一的读操作执行框架(executeReadOperation)
  • * + *

    + * 注入机制: + * - delegate(写代理):由 RepositoryBeanPostProcessor 根据泛型类型和 @WriteDelegate/@ReadDelegate 注解注入 + * - readDelegate(读代理):由 RepositoryBeanPostProcessor 根据泛型类型和 @ReadDelegate 注解注入 * * @param 领域实体类型 * @param 主键类型 * @param 读委托类型(继承 IQueryDelegate) * @author chuck - * @version 1.0.2 + * @version 1.0.3 * @since 2026/7/21 */ @Getter +@Setter @Slf4j public class CqrsRepositoryFacade, RD extends IQueryDelegate> extends RepositoryFacade { - @Autowired(required = false) protected RD readDelegate; @Override 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 new file mode 100644 index 0000000..80f2562 --- /dev/null +++ b/structure-infra-starter/src/main/java/cn/structure/infra/repository/RepositoryBeanPostProcessor.java @@ -0,0 +1,197 @@ +package cn.structure.infra.repository; + +import cn.structure.infra.annotations.ReadDelegate; +import cn.structure.infra.annotations.WriteDelegate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Qualifier; +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.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.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class RepositoryBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { + + private DefaultListableBeanFactory beanFactory; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof CqrsRepositoryFacade) { + processCqrsFacade((CqrsRepositoryFacade) bean, beanName); + } else if (bean instanceof RepositoryFacade) { + processRepositoryFacade((RepositoryFacade) bean, beanName); + } + return bean; + } + + private void processCqrsFacade(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) { + setDelegate(facade, "readDelegate", readDelegate); + log.info("Injected read delegate [{}] to CqrsRepositoryFacade [{}]", + readDelegate.getClass().getSimpleName(), beanName); + } + } + } + + 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()) { + return null; + } + + List candidates = new ArrayList<>(beans.values()); + + Object writeDelegate = findByAnnotation(candidates, WriteDelegate.class); + if (writeDelegate != null) { + return writeDelegate; + } + + Object noReadDelegate = findWithoutAnnotation(candidates, ReadDelegate.class); + if (noReadDelegate != null) { + return noReadDelegate; + } + + return candidates.get(0); + } + + private Object findReadDelegate(Class delegateType) { + Map beans = beanFactory.getBeansOfType(delegateType); + if (beans.isEmpty()) { + return null; + } + + List candidates = new ArrayList<>(beans.values()); + + Object readDelegate = findByAnnotation(candidates, ReadDelegate.class); + if (readDelegate != null) { + return readDelegate; + } + + return candidates.get(0); + } + + private Object findByAnnotation(List candidates, Class annotationClass) { + for (Object candidate : candidates) { + if (AnnotationUtils.findAnnotation(candidate.getClass(), annotationClass) != null) { + return candidate; + } + } + return null; + } + + private Object findWithoutAnnotation(List candidates, Class annotationClass) { + for (Object candidate : candidates) { + if (AnnotationUtils.findAnnotation(candidate.getClass(), annotationClass) == null) { + return candidate; + } + } + return null; + } + + private void setDelegate(Object facade, String fieldName, Object delegate) { + try { + Field field = findField(facade.getClass(), fieldName); + if (field != null) { + field.setAccessible(true); + field.set(facade, delegate); + } + } catch (Exception e) { + log.error("Failed to inject delegate to field [{}] in [{}]: {}", + fieldName, facade.getClass().getSimpleName(), e.getMessage()); + } + } + + private Field findField(Class clazz, String fieldName) { + while (clazz != null) { + try { + return clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } + return null; + } + + 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; + } +} \ No newline at end of file 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 3bb59f1..b5784e3 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 @@ -4,17 +4,17 @@ import cn.structure.common.vo.ReqPage; import cn.structure.common.vo.ResPage; 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 { - @Autowired(required = false) protected D delegate; protected Class entityClass; From 327331618adde3d615983ce796951eae4c7f546e Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Tue, 21 Jul 2026 16:06:46 +0800 Subject: [PATCH 7/9] =?UTF-8?q?chore(project):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E7=89=88=E6=9C=AC=E5=8F=B7=E4=BB=8E=201.1.1?= =?UTF-8?q?=20=E5=88=B0=201.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 revision 属性从 1.1.1-SNAPSHOT 更新为 1.2.0-SNAPSHOT - 保持其他依赖版本不变以确保兼容性 - 为下个开发迭代准备新的版本标识 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4a5d2ac..0a03bca 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ pro项目父工程 - 1.1.1-SNAPSHOT + 1.2.0-SNAPSHOT 4.0.6 5.0.0 3.5.16 From 103aadc3444e9e547fcfb1d9582aa084540fff18 Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Tue, 21 Jul 2026 16:27:12 +0800 Subject: [PATCH 8/9] =?UTF-8?q?refactor(repository):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BB=93=E5=82=A8=E7=B3=BB=E7=BB=9F=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=B3=9B=E5=9E=8B=E8=A7=A3=E6=9E=90=E5=92=8CCQRS=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 @Repository 和 @DelegateFor 注解,改用 @WriteDelegate 和 @ReadDelegate 注解 - 通过泛型解析设置 entityClass,替代 @DelegateFor(po = XxxPO.class) 注解方式 - 更新 RepositoryFacade 泛型参数结构,简化实体和PO的映射关系 - 新增 CqrsRepositoryFacade 支持读写分离模式 - 重构 delegate 实现类的继承结构,统一使用 RepositoryDelegate 泛型 - 更新文档中的使用示例和配置方式,适配新的注解和泛型结构 - 调整 RepositoryBeanPostProcessor 的匹配策略,基于泛型类型和注解标记进行注入 - 完善 CQRS 模式的回退机制,确保读操作失败时能正确回退到写代理 --- README.md | 229 +++++++++------- .../README.md | 45 ++-- structure-infra-jpa-starter/README.md | 30 +-- structure-infra-mongodb-starter/README.md | 14 +- .../README.md | 102 +++++--- structure-infra-starter/README.md | 246 ++++++++++++------ 6 files changed, 421 insertions(+), 245 deletions(-) diff --git a/README.md b/README.md index 9532e0f..6953fdc 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ ``` structure-pro-infra/ ├── structure-infra-starter/ # 核心模块(仓储抽象、低代码、事件管理、调度集成) -│ ├── annotations/ # @Repository / @DelegateFor 注解 +│ ├── annotations/ # @WriteDelegate / @ReadDelegate 注解 │ ├── configuration/ # 自动配置(仓储、事件、调度) │ ├── event/ # 事件管理抽象 │ ├── lowcode/ # 低代码仓储子系统 @@ -296,16 +296,36 @@ public class UserPO { } ``` -### 3. 创建 RepositoryFacade +### 3. 创建 RepositoryFacade(单代理模式) ```java -@Repository(entity = User.class, po = UserPO.class, type = RepositoryType.MYBATIS_PLUS) -public class UserRepository extends RepositoryFacade> { - // 可添加自定义方法 +@Component("userRepository") +public class UserRepositoryImpl extends RepositoryFacade + implements UserRepository { } ``` -### 4. 使用仓储 +### 4. 创建 Delegate 实现 + +```java +@Slf4j +@Component +public class UserMybatisPlusDelegate + extends MybatisPlusRepositoryDelegate + implements UserRepositoryDelegate { + + private final UserMapper userMapper; + + @Override + public UserEntity findByName(String name) { + UserPO po = userMapper.selectOne( + Wrappers.lambdaQuery().eq(UserPO::getUsername, name)); + return toEntity(po); + } +} +``` + +### 5. 使用仓储 ```java @Service @@ -314,75 +334,82 @@ public class UserService { @Autowired private UserRepository userRepository; - public User saveUser(User user) { + public UserEntity saveUser(UserEntity user) { return userRepository.save(user); } - public User getUserById(Long id) { + public UserEntity getUserById(Long id) { return userRepository.findById(id); } - public List findUsers(User condition) { + public List findUsers(UserEntity condition) { return userRepository.queryList(condition); } - public ResPage findUsersPage(ReqPage reqPage) { + public ResPage findUsersPage(ReqPage reqPage) { return userRepository.queryPage(reqPage); } } ``` -### 5. CQRS 读写分离模式(可选) +### 6. CQRS 读写分离模式(可选) -当需要读写分离时,可以为同一个仓储配置两个代理。**必须同时满足 `cqrs=true` 和 `readDelegateClass` 指定才会启用读代理**。 +当需要读写分离时,使用 `CqrsRepositoryFacade` 并为写代理和读代理分别添加 `@WriteDelegate` 和 `@ReadDelegate` 注解。 -**1) 启用 CQRS** +**1) 定义写代理** ```java -@Repository( - entity = User.class, - po = UserPO.class, - type = RepositoryType.MYBATIS_PLUS, - cqrs = true, // 启用 CQRS - readDelegateClass = UserReadDelegate.class // 指定读代理类 -) -public class UserRepository extends RepositoryFacade> { +@Slf4j +@Component +@WriteDelegate +public class UserWriteDelegate + extends MybatisPlusRepositoryDelegate + implements UserRepositoryDelegate { + + private final UserMapper userMapper; } ``` -**2) 定义写代理(BASE)** +**2) 定义读代理** ```java -@DelegateFor( - name = "userRepository", - po = UserPO.class, - type = RepositoryType.MYBATIS_PLUS, - delegateType = DelegateType.BASE // 写代理 -) -public class UserWriteDelegate extends MybatisPlusRepositoryDelegate { +@Slf4j +@Component +@ReadDelegate +public class UserReadDelegate + extends ElasticsearchRepositoryDelegate + implements UserRepositoryDelegate { + + @Override + public UserEntity findByName(String name) { + // Elasticsearch 查询实现 + return null; + } } ``` -**3) 定义读代理(READ)** +**3) 创建 CqrsRepositoryFacade** ```java -@DelegateFor( - name = "userRepository", - po = UserPO.class, - type = RepositoryType.ELASTICSEARCH, - delegateType = DelegateType.READ // 读代理 -) -public class UserReadDelegate extends ElasticsearchRepositoryDelegate { +@Component("userCqrsRepository") +public class UserCqrsRepository + extends CqrsRepositoryFacade + implements UserRepository { + + @Override + public UserEntity findByName(String name) { + return getReadDelegate().findByName(name); + } } ``` **读操作回退机制**: -- 写操作(save、removeById、saveBatch、removeBatchByIds)始终走 BASE 代理(MyBatis Plus) -- 读操作(findById、queryList、queryPage、count、exists 等)优先走 READ 代理(Elasticsearch) -- 如果 READ 代理执行失败(抛出异常),自动回退到 BASE 代理执行 -- BASE 代理是最后的兜底,确保读操作始终可用 +- 写操作(save、removeById、saveBatch、removeBatchByIds)始终走写代理(`delegate`) +- 读操作(queryById、queryList、queryPage、count、exists 等)优先走读代理(`readDelegate`) +- 如果读代理执行失败(抛出异常),自动回退到写代理执行 +- `findById` 始终走写代理(严格的实体查询) -### 6. 低代码仓储使用 +### 7. 低代码仓储使用 低代码仓储无需定义实体类,通过资源名称和 Map 操作数据。 @@ -535,36 +562,31 @@ public class UserEventPublisher { ## 注解说明 -### @Repository - -标注在 `RepositoryFacade` 子类上,配置仓储属性: - -| 属性 | 类型 | 默认值 | 说明 | -|-----|------|-------|------| -| `value` | String | "" | 仓储名称 | -| `type` | RepositoryType | AUTO | 仓储类型 | -| `entity` | Class | Object.class | 领域实体类 | -| `po` | Class | Object.class | 持久化对象类 | -| `id` | Class | Long.class | 主键类型 | -| `description` | String | "" | 仓储描述 | -| `cache` | boolean | false | 是否启用缓存 | -| `cacheTime` | long | 60 | 缓存时间 | -| `cacheTimeUnit` | TimeUnit | SECONDS | 缓存时间单位 | -| `cqrs` | boolean | false | 是否启用 CQRS 读写分离 | -| `readDelegateClass` | Class | Object.class | 读代理类(CQRS 模式下使用) | - -### @DelegateFor - -标注在自定义 `RepositoryDelegate` 实现类上,用于注册委托: - -| 属性 | 类型 | 默认值 | 说明 | -|-----|------|-------|------| -| `name` | String | "" | 仓储名称(对应 RepositoryFacade 的 Bean 名称) | -| `type` | RepositoryType | AUTO | 存储类型 | -| `po` | Class | Object.class | 持久化对象类型 | -| `description` | String | "" | 描述 | -| `priority` | int | 0 | 优先级(数字越大优先级越高) | -| `delegateType` | DelegateType | BASE | 委托类型(BASE=写/默认,READ=读) | +### @WriteDelegate + +标注在 `RepositoryDelegate` 实现类上,标记为写代理/基础代理。当存在多个同类型的 delegate bean 时,`RepositoryBeanPostProcessor` 优先选择带有此注解的 bean 作为写代理。 + +```java +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WriteDelegate { +} +``` + +### @ReadDelegate + +标注在 `IQueryDelegate` / `RepositoryDelegate` 实现类上,标记为读代理。`RepositoryBeanPostProcessor` 使用此注解识别读代理,用于 CQRS 模式下的读操作。 + +```java +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ReadDelegate { +} +``` ## 配置项 @@ -574,7 +596,7 @@ public class UserEventPublisher { structure: infra: default-event-channel: SPRING_EVENT # 默认事件通道:DEFAULT, SPRING_EVENT, MESSAGE_EVENT - cqrs: false # 是否开启 CQRS(仅作建议,以 @Repository 注解为准) + cqrs: false # 是否开启 CQRS(仅作建议) cache-time: 60 # 默认缓存时间 cache-time-unit: SECONDS # 默认缓存时间单位 schedule-pool-size: 8 # 调度线程池大小(默认 CPU 核心数) @@ -655,18 +677,25 @@ structure: 当默认实现无法满足需求时,可以自定义委托实现: ```java -@DelegateFor(name = "userRepository", po = UserPO.class, type = RepositoryType.MYBATIS_PLUS) -public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate { +@Slf4j +@Component +public class UserMybatisPlusDelegate + extends MybatisPlusRepositoryDelegate + implements UserRepositoryDelegate { + + private final UserMapper userMapper; - // 自定义查询方法 - public List findByEmailLike(String emailPattern) { - QueryWrapper wrapper = new QueryWrapper<>(); - wrapper.like("email", emailPattern); - return baseMapper.selectList(wrapper); + @Override + public UserEntity findByEmailLike(String emailPattern) { + UserPO po = userMapper.selectOne( + Wrappers.lambdaQuery().like(UserPO::getEmail, emailPattern)); + return toEntity(po); } } ``` +> 注意:Delegate 实现类需要通过泛型参数指定领域实体类型(T)、持久化对象类型(PO)和主键类型(ID),内部自动处理 PO 到 Entity 的转换。 + ### 新增持久化技术支持 1. 创建 `RepositoryDelegate` 实现类 @@ -684,22 +713,38 @@ public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate`,其中 `D` 是 `RepositoryDelegate` 的实现类。`RepositoryBeanPostProcessor` 根据泛型参数 `D` 查找对应的 delegate bean 并注入。 + +### CQRS 模式(CqrsRepositoryFacade) -### BASE 代理匹配优先级 +继承自 `CqrsRepositoryFacade`,其中: +- `D`:写代理类型(继承 `RepositoryDelegate`) +- `RD`:读代理类型(继承 `IQueryDelegate`) -1. 匹配 delegate 类型 + 名称 + 仓储类型 -2. 匹配 delegate 类型 + 仓储类型 -3. 匹配 delegate 类型 + 名称 -4. 匹配 delegate 类型 -5. 匹配名称 -6. 匹配 PO 类型 -7. 自动创建(通过 RepositoryDelegateFactory) -8. 使用默认的 InMemoryRepositoryDelegate +`RepositoryBeanPostProcessor` 根据泛型参数分别查找写代理和读代理: +- 写代理:通过 `@WriteDelegate` 注解或类型匹配查找 +- 读代理:通过 `@ReadDelegate` 注解或类型匹配查找 -### READ 代理匹配优先级(CQRS 启用时) +### 委托创建兜底 -与 BASE 代理匹配策略相同,但只匹配 `delegateType = READ` 的委托。如果未找到 READ 代理,读操作会回退到使用 BASE 代理。 +当未找到用户自定义的 delegate bean 时,`RepositoryDelegateFactory` 会按约定自动创建: +- 根据 PO 类名推导 Mapper 类(`xxx.po.UserPO` → `xxx.mapper.UserMapper`) +- 从 `ApplicationContext` 查找该 Mapper bean +- 找到则创建对应的 `RepositoryDelegate` 实例 +- 未找到则使用默认的 `InMemoryRepositoryDelegate`(仅用于测试或无持久化场景) ## 技术栈 diff --git a/structure-infra-elasticsearch-starter/README.md b/structure-infra-elasticsearch-starter/README.md index a40b87f..c3c4358 100644 --- a/structure-infra-elasticsearch-starter/README.md +++ b/structure-infra-elasticsearch-starter/README.md @@ -102,7 +102,7 @@ spring: 1. 检测 Bean 是否为 `ElasticsearchRepositoryDelegate` 实例 2. 从 Spring 上下文获取 `ElasticsearchOperations` 并注入 -3. 读取 `@DelegateFor(po = XxxPO.class)` 注解,设置 `entityClass` +3. 通过泛型解析设置 `entityClass` ### 2. 低代码仓储 @@ -166,14 +166,12 @@ public class UserPO { } // 2. 仓储接口 -public interface UserRepository extends Repository {} +public interface UserRepository extends ICrudRepository {} // 3. 仓储实现,继承 RepositoryFacade -@Repository(value = "用户仓储", type = RepositoryType.ELASTICSEARCH, - entity = UserEntity.class, po = UserPO.class) -@Component +@Component("userRepository") public class UserRepositoryImpl - extends RepositoryFacade + extends RepositoryFacade implements UserRepository { // 未提供自定义 Delegate 时,框架会通过 ElasticsearchDelegateFactory 自动创建 @@ -183,10 +181,9 @@ public class UserRepositoryImpl ### 方式二:自定义 Delegate ```java -@DelegateFor(po = UserPO.class) @Component public class UserElasticsearchRepositoryDelegate - extends ElasticsearchRepositoryDelegate + extends ElasticsearchRepositoryDelegate implements UserRepositoryDelegate { // 可覆写 queryOne/queryList 等方法实现自定义查询逻辑 @@ -250,18 +247,30 @@ structure: Elasticsearch 常作为读侧高速检索引擎,与 MyBatis Plus / MongoDB 组合实现 CQRS: ```java -// 写操作走 MyBatis Plus,读操作走 Elasticsearch -@Repository(value = "用户仓储", type = RepositoryType.MYBATIS_PLUS, - entity = UserEntity.class, po = UserPO.class, - readType = RepositoryType.ELASTICSEARCH, - readPo = UserPO.class) +// 写代理(MyBatis Plus) @Component -public class UserRepositoryImpl - extends RepositoryFacade +@WriteDelegate +public class UserWriteDelegate + extends MybatisPlusRepositoryDelegate + implements UserRepositoryDelegate { +} + +// 读代理(Elasticsearch) +@Component +@ReadDelegate +public class UserReadDelegate + extends ElasticsearchRepositoryDelegate + implements UserRepositoryDelegate { +} + +// CQRS 仓储 +@Component("userCqrsRepository") +public class UserCqrsRepository + extends CqrsRepositoryFacade implements UserRepository { - // 写操作 → MyBatis Plus Delegate(BASE) - // 读操作 → Elasticsearch Delegate(READ) - // 读失败自动回退到 BASE 代理 + // 写操作 → UserWriteDelegate(MyBatis Plus) + // 读操作 → UserReadDelegate(Elasticsearch) + // 读失败自动回退到 UserWriteDelegate } ``` diff --git a/structure-infra-jpa-starter/README.md b/structure-infra-jpa-starter/README.md index 09bfa7c..08c2580 100644 --- a/structure-infra-jpa-starter/README.md +++ b/structure-infra-jpa-starter/README.md @@ -15,7 +15,7 @@ - **条件激活**:仅当 classpath 存在 `JpaRepository` 且上下文有 `EntityManager` bean 时激活 - **启用 Spring Data JPA**:自动应用 `@EnableJpaRepositories` 与 `@EnableTransactionManagement` - **自动创建 delegate**:`JpaDelegateFactory` 按需为任意 PO 类实例化 `JpaRepositoryDelegate` -- **自动注入 EntityManager**:`JpaDelegateBeanPostProcessor` 发现用户自定义的 `JpaRepositoryDelegate` bean,注入 `EntityManager` 与从 `@DelegateFor.po()` 解析的实体类 +- **自动注入 EntityManager**:`JpaDelegateBeanPostProcessor` 发现用户自定义的 `JpaRepositoryDelegate` bean,注入 `EntityManager` 与泛型解析的实体类 - **完整 CRUD + 分页 + 条件查询**:基于 JPA Criteria API 实现,通过反射读取条件对象的非空字段构建等值谓词 - **CQRS 兼容**:可作为 BASE 代理或 READ 代理,与 `RepositoryFacade` 的读写分离机制无缝配合 @@ -180,8 +180,8 @@ public class UserPO { ### 4. 定义 delegate 接口(可选,用于自定义查询) ```java -public interface UserRepositoryDelegate extends RepositoryDelegate { - UserPO finByName(String name); +public interface UserRepositoryDelegate extends RepositoryDelegate { + UserEntity findByName(String name); } ``` @@ -189,28 +189,25 @@ public interface UserRepositoryDelegate extends RepositoryDelegate ```java public abstract class AbstractUserRepositoryImpl - extends RepositoryFacade + extends RepositoryFacade implements UserRepository { @Override public UserEntity findByName(String name) { - UserPO po = this.baseDelegate.finByName(name); - return this.toEntity(po); + return getDelegate().findByName(name); } } ``` -### 6. 声明具体 JPA 仓储(标注 `@Repository`) +### 6. 声明具体 JPA 仓储 ```java -@Repository(value = "用户仓储", type = RepositoryType.JPA, - entity = UserEntity.class, po = UserPO.class) @Component("userRepository") public class UserJpaRepositoryImpl extends AbstractUserRepositoryImpl { } ``` -仓储体为空 — 所有 CRUD 行为由继承的 `RepositoryFacade` 与自动创建的 `JpaRepositoryDelegate` 提供。用户只需通过 `@Repository` 注解声明类型元数据。 +仓储体为空 — 所有 CRUD 行为由继承的 `RepositoryFacade` 与自动创建的 `JpaRepositoryDelegate` 提供。`RepositoryBeanPostProcessor` 通过泛型解析自动注入 delegate。 ### 7. 业务层使用 @@ -249,13 +246,14 @@ public EntityManager entityManager(EntityManagerFactory entityManagerFactory) { 2. `JpaAutoConfiguration` 激活(`JpaRepository` 在 classpath 上) 3. `EntityManager` bean 就绪后,`JpaDelegateFactory` 注册为 `RepositoryDelegateFactory` 类型为 `JPA` 4. `JpaDelegateBeanPostProcessor` 注册 -5. 用户的 `@Repository(type = JPA, po = UserPO.class)` 注解类实例化为 `RepositoryFacade` 子类 -6. `structure-infra-starter` 中的 `RepositoryBeanPostProcessor` 在 `ContextRefreshedEvent` 触发: - - 查找用户定义的 `@DelegateFor` BASE delegate;若未找到,调用 `JpaDelegateFactory.createDelegate(UserPO.class, Long.class)` 返回完整构造的 `JpaRepositoryDelegate(entityManager, UserPO.class)` - - 通过 `facade.setBaseDelegate(delegate)` 注入 -7. 若用户定义了 `JpaRepositoryDelegate` 子类并标注 `@DelegateFor(po = UserPO.class)`,`JpaDelegateBeanPostProcessor` 在初始化后处理: +5. 用户的 `RepositoryFacade` 子类实例化为 bean +6. `structure-infra-starter` 中的 `RepositoryBeanPostProcessor` 在 Bean 初始化后触发: + - 解析 `RepositoryFacade` 泛型参数获取 delegate 类型 + - 查找用户定义的 delegate bean;若未找到,调用 `JpaDelegateFactory.createDelegate(UserPO.class, Long.class)` 返回完整构造的 `JpaRepositoryDelegate(entityManager, UserPO.class)` + - 通过 `facade.setDelegate(delegate)` 注入 +7. 若用户定义了 `JpaRepositoryDelegate` 子类并标注 `@Component`,`JpaDelegateBeanPostProcessor` 在初始化后处理: - 解析并注入 `EntityManager` - - 从 `@DelegateFor.po()` 读取并注入 `entityClass` + - 通过泛型解析注入 `entityClass` 8. 应用调用 `userRepository.save(entity)` → `RepositoryFacade` 转换为 PO → `JpaRepositoryDelegate.save(po)` → `entityManager.merge(po)` → 转换回 entity ## 注意事项 diff --git a/structure-infra-mongodb-starter/README.md b/structure-infra-mongodb-starter/README.md index ac262a9..c4b420d 100644 --- a/structure-infra-mongodb-starter/README.md +++ b/structure-infra-mongodb-starter/README.md @@ -105,7 +105,7 @@ spring: 1. 检测 Bean 是否为 `MongoRepositoryDelegate` 实例 2. 从 Spring 上下文获取 `MongoTemplate` 并注入 -3. 读取 `@DelegateFor(po = XxxPO.class)` 注解,设置 `entityClass` +3. 通过泛型解析设置 `entityClass` ### 2. 低代码仓储 @@ -166,14 +166,12 @@ public class UserPO { } // 2. 仓储接口 -public interface UserRepository extends Repository {} +public interface UserRepository extends ICrudRepository {} // 3. 仓储实现,继承 RepositoryFacade -@Repository(value = "用户仓储", type = RepositoryType.MONGODB, - entity = UserEntity.class, po = UserPO.class) -@Component +@Component("userRepository") public class UserRepositoryImpl - extends RepositoryFacade + extends RepositoryFacade implements UserRepository { // 未提供自定义 Delegate 时,框架会通过 MongoDelegateFactory 自动创建 @@ -183,9 +181,9 @@ public class UserRepositoryImpl ### 方式二:自定义 Delegate ```java -@DelegateFor(po = UserPO.class) @Component -public class UserMongoRepositoryDelegate extends MongoRepositoryDelegate +public class UserMongoRepositoryDelegate + extends MongoRepositoryDelegate implements UserRepositoryDelegate { // 可覆写 queryOne/queryList 等方法实现自定义查询逻辑 diff --git a/structure-infra-mybatis-plus-starter/README.md b/structure-infra-mybatis-plus-starter/README.md index d46cc09..e44a793 100644 --- a/structure-infra-mybatis-plus-starter/README.md +++ b/structure-infra-mybatis-plus-starter/README.md @@ -2,17 +2,18 @@ [MyBatis-Plus](https://baomidou.com/) 接入 `structure-pro-infra` 仓储抽象层的适配模块,提供: -1. **类型化仓储**:通过 `RepositoryFacade` + `MybatisPlusRepositoryDelegate` 透明地以 MyBatis-Plus `BaseMapper` 操作 PO +1. **类型化仓储**:通过 `RepositoryFacade` + `MybatisPlusRepositoryDelegate` 透明地以 MyBatis-Plus `BaseMapper` 操作领域实体 2. **低代码 MySQL/H2 存储**:通过 `LowCodeStorage` 实现基于 MyBatis `SqlSession` 的动态表/SQL,无需定义实体类 ## 功能特性 ### 类型化仓储层 -- **自动 delegate 创建**:当 `RepositoryFacade` 需要某个 PO 的 delegate 但无用户自定义 bean 时,`MybatisPlusDelegateFactory` 自动发现匹配的 `BaseMapper` 并实例化 `MybatisPlusRepositoryDelegate` -- **自定义 delegate 支持**:用户可继承 `MybatisPlusRepositoryDelegate` 并标注 `@DelegateFor`,`MybatisPlusDelegateBeanPostProcessor` 会自动注入 `BaseMapper` 与 PO 类型 +- **自动 delegate 创建**:当 `RepositoryFacade` 需要某个实体的 delegate 但无用户自定义 bean 时,`MybatisPlusDelegateFactory` 自动发现匹配的 `BaseMapper` 并实例化 `MybatisPlusRepositoryDelegate` +- **自定义 delegate 支持**:用户可继承 `MybatisPlusRepositoryDelegate` 并标注 `@WriteDelegate` 或 `@ReadDelegate`,`MybatisPlusDelegateBeanPostProcessor` 会自动注入 `BaseMapper` 与 PO 类型 - **约定优于配置的 Mapper 发现**:从 PO 类名推导 Mapper 类(`xxx.po.UserPO` → `xxx.mapper.UserMapper`,`PO` 后缀替换为 `Mapper`);失败时回退到 bean 名称后缀匹配 -- **CQRS 支持**:可与 `ElasticsearchRepositoryDelegate` 等组合实现读写分离 +- **CQRS 支持**:可与 `ElasticsearchRepositoryDelegate` 等组合实现读写分离,通过 `@WriteDelegate` / `@ReadDelegate` 注解区分代理类型 +- **Entity ↔ PO 转换**:由 delegate 内部处理领域实体与持久化对象之间的转换 ### 低代码 MySQL/H2 存储层 @@ -29,7 +30,7 @@ cn.structured structure-infra-mybatis-plus-starter - 1.0.0-SNAPSHOT + 1.1.1-SNAPSHOT ``` @@ -61,14 +62,26 @@ cn.structure.infra.mybatis.plus.lowcode.configuration.MybatisPlusLowCodeAutoConf ### 类型化仓储层 -#### `MybatisPlusRepositoryDelegate` +#### `MybatisPlusRepositoryDelegate` -实现 `RepositoryDelegate`,包装 `BaseMapper` 提供: +实现 `RepositoryDelegate`,包装 `BaseMapper` 提供: - `save` / `removeById` / `saveBatch` / `removeBatchByIds` — 写操作 - `findById` / `queryById` / `queryByIdOptional` / `queryOne` / `queryOneOptional` / `queryList` / `queryPage` / `listByIds` / `count` / `exists` — 读操作 - 通过反射读取条件对象的非空字段构建 `QueryWrapper`(驼峰转下划线) -- 支持无参构造 + setter,便于子类被 `@DelegateFor` 标注后由后置处理器注入 +- 内部处理 `T`(领域实体)与 `PO`(持久化对象)之间的转换 +- 支持无参构造 + setter,便于子类被 `@WriteDelegate` / `@ReadDelegate` 标注后由后置处理器注入 + +**泛型参数**: +- `T` - 领域实体类型(Domain Entity) +- `PO` - 持久化对象类型(Persistent Object) +- `ID` - 主键类型 + +**关键方法**: +- `toEntity(PO po)` - 将 PO 转换为领域实体 +- `toPo(T entity)` - 将领域实体转换为 PO +- `getEntityClass()` - 获取领域实体类型 +- `getPoClass()` - 获取持久化对象类型 #### `MybatisPlusDelegateFactory` @@ -76,14 +89,14 @@ cn.structure.infra.mybatis.plus.lowcode.configuration.MybatisPlusLowCodeAutoConf 1. 按约定推导 Mapper 类(`po` 包段替换为 `mapper`,`PO` 后缀替换为 `Mapper`) 2. 从 `ApplicationContext` 查找该 Mapper bean -3. 找到则返回 `new MybatisPlusRepositoryDelegate<>(mapper, poClass)`,否则返回 `null` +3. 找到则返回 `new MybatisPlusRepositoryDelegate<>(entityClass, poClass, mapper)`,否则返回 `null` #### `MybatisPlusDelegateBeanPostProcessor` -`BeanPostProcessor`,对每个 `instanceof MybatisPlusRepositoryDelegate` 且带 `@DelegateFor` 注解的 bean: +`BeanPostProcessor`,对每个 `instanceof MybatisPlusRepositoryDelegate` 的 bean: -- 根据 `@DelegateFor.po()` 解析 Mapper bean -- 调用 `setBaseMapper(mapper)` 与 `setEntityClass(poClass)` 完成注入 +- 根据泛型参数解析 Mapper bean +- 调用 `setBaseMapper(mapper)` 与 `setEntityClass(entityClass)` / `setPoClass(poClass)` 完成注入 ### 低代码存储层 @@ -211,8 +224,8 @@ public interface UserRepository extends ICrudRepository { #### 3. 定义 delegate 接口(可选,用于自定义查询) ```java -public interface UserRepositoryDelegate extends RepositoryDelegate { - UserPO finByName(String name); +public interface UserRepositoryDelegate extends RepositoryDelegate { + UserEntity findByName(String name); } ``` @@ -220,21 +233,19 @@ public interface UserRepositoryDelegate extends RepositoryDelegate ```java public abstract class AbstractUserRepositoryImpl - extends RepositoryFacade + extends RepositoryFacade implements UserRepository { @Override public UserEntity findByName(String name) { - return toEntity(this.baseDelegate.finByName(name)); + return getDelegate().findByName(name); } } ``` -#### 5. 定义具体仓储(标注 `@Repository`) +#### 5. 定义具体仓储 ```java -@Repository(value = "用户仓储", type = RepositoryType.MYBATIS_PLUS, - entity = UserEntity.class, po = UserPO.class) @Component("userRepository") public class UserRepositoryImpl extends AbstractUserRepositoryImpl { } @@ -243,32 +254,28 @@ public class UserRepositoryImpl extends AbstractUserRepositoryImpl { #### 6. 提供自定义 delegate(可选,用于自定义查询) ```java +@Slf4j @Component -@DelegateFor( - name = "userRepository", - type = RepositoryType.MYBATIS_PLUS, - po = UserPO.class, - description = "用户仓储 MyBatis Plus 实现", - priority = 10 -) +@WriteDelegate @AllArgsConstructor public class UserMybatisPlusDelegate - extends MybatisPlusRepositoryDelegate + extends MybatisPlusRepositoryDelegate implements UserRepositoryDelegate { private final UserMapper userMapper; @Override - public UserPO finByName(String name) { - return userMapper.selectOne( + public UserEntity findByName(String name) { + UserPO po = userMapper.selectOne( Wrappers.lambdaQuery().eq(UserPO::getUsername, name)); + return toEntity(po); } } ``` -`MybatisPlusDelegateBeanPostProcessor` 会基于 `@DelegateFor.po()` 自动调用 `setBaseMapper(userMapper)` 与 `setEntityClass(UserPO.class)`,因此继承的 CRUD 方法开箱即用。 +`MybatisPlusDelegateBeanPostProcessor` 会自动调用 `setBaseMapper(userMapper)` 与 `setPoClass(UserPO.class)`,因此继承的 CRUD 方法开箱即用。 -> 若未提供自定义 delegate,`MybatisPlusDelegateFactory.createDelegate(UserPO.class, Long.class)` 会按约定发现 `UserMapper` 并自动创建 `MybatisPlusRepositoryDelegate`。 +> 若未提供自定义 delegate,`MybatisPlusDelegateFactory.createDelegate(UserEntity.class, Long.class)` 会按约定发现 `UserMapper` 并自动创建 `MybatisPlusRepositoryDelegate`。 #### 7. 业务层使用 @@ -290,6 +297,37 @@ ResPage page = userRepository.queryPage(reqPage); userRepository.removeById(id); ``` +### CQRS 读写分离 + +```java +// 写代理 +@Slf4j +@Component +@WriteDelegate +public class UserWriteDelegate + extends MybatisPlusRepositoryDelegate + implements UserRepositoryDelegate { + + private final UserMapper userMapper; +} + +// 读代理(使用 Elasticsearch) +@Slf4j +@Component +@ReadDelegate +public class UserReadDelegate + extends ElasticsearchRepositoryDelegate + implements UserRepositoryDelegate { +} + +// CQRS 仓储 +@Component("userCqrsRepository") +public class UserCqrsRepository + extends CqrsRepositoryFacade + implements UserRepository { +} +``` + ### 低代码仓储 #### YAML 声明资源 @@ -379,4 +417,4 @@ mvn test -pl structure-infra-sample/structure-infra-sample-mybatis \ ## License -Apache License 2.0 +Apache License 2.0 \ No newline at end of file diff --git a/structure-infra-starter/README.md b/structure-infra-starter/README.md index 64c0e92..770ef79 100644 --- a/structure-infra-starter/README.md +++ b/structure-infra-starter/README.md @@ -7,7 +7,7 @@ 本模块是其他所有 `structure-infra-*-starter` 的依赖基础,定义了以下抽象: - **仓储 Facade/Delegate 抽象**:领域层通过 `RepositoryFacade` 操作领域实体,底层由 `RepositoryDelegate` 与具体持久化技术交互 -- **自动装配机制**:通过 `RepositoryBeanPostProcessor` 在启动时扫描 `@Repository` / `@DelegateFor` 注解,自动匹配并注入委托 +- **自动装配机制**:通过 `RepositoryBeanPostProcessor` 在启动时扫描 `@WriteDelegate` / `@ReadDelegate` 注解,自动匹配并注入委托 - **CQRS 读写分离**:支持为每个仓储配置 BASE 写代理和 READ 读代理,读操作失败自动回退到写代理 - **低代码仓储子系统**:通过 YAML 定义资源 schema,运行时路由到不同存储引擎,无需编写实体类 - **事件管理**:统一 `EventManager` 抽象,支持 Spring 事件和消息中间件两种通道 @@ -38,7 +38,7 @@ cn.structure.infra.lowcode.configuration.LowCodeAutoConfiguration ``` cn.structure.infra -├── annotations/ @Repository / @DelegateFor +├── annotations/ @WriteDelegate / @ReadDelegate ├── configuration/ AutoEventConfiguration / AutoRepositoryConfiguration / AutoScheduleConfiguration ├── event/ Event / EventManager / EventChannel / DefaultEventManagerImpl ├── lowcode/ 低代码子系统 @@ -51,6 +51,15 @@ cn.structure.infra │ └── router/ LowCodeRepositoryRouter ├── properties/ InfraProperties └── repository/ 仓储核心接口与实现 + ├── RepositoryDelegate.java + ├── IQueryDelegate.java + ├── RepositoryFacade.java + ├── CqrsRepositoryFacade.java + ├── RepositoryBeanPostProcessor.java + ├── GenericTypeResolver.java + ├── DelegateType.java + ├── RepositoryType.java + └── InMemoryRepositoryDelegate.java ``` ## 仓储子系统 @@ -80,7 +89,13 @@ cn.structure.infra #### `RepositoryDelegate` -继承自 `ICrudRepository` 与 `IQueryDelegate`,定义对 PO 的完整 CRUD 操作。各持久化 starter 提供具体实现。 +继承自 `ICrudRepository` 与 `IQueryDelegate`,定义对领域实体的完整 CRUD 操作。各持久化 starter 提供具体实现。 + +**关键方法**: +- `getEntityClass()` - 获取领域实体类型 +- `getPoClass()` - 获取持久化对象类型(PO) +- `getIdClass()` - 获取主键类型 +- `getIdFieldName()` - 获取 ID 字段名称 #### `IQueryDelegate` @@ -95,14 +110,23 @@ RepositoryDelegate createDelegate(Class poClass, Class idClass); 每个持久化 starter 实现此 SPI,用于在无用户自定义 delegate 时自动创建。 -#### `RepositoryFacade>` +#### `RepositoryFacade>` 用户侧门面,职责: -- 通过 `BeanUtils.copyProperties` 完成 Entity ↔ PO 转换 -- 写操作(`save` / `removeById` / `saveBatch` / `removeBatchByIds` / `exists`)始终走 `baseDelegate` -- 读操作(`queryById` / `queryOne` / `queryList` / `queryPage` / `listByIds` / `count`)优先走 `readDelegate`,失败回退到 `baseDelegate` -- `findById` 始终走 `baseDelegate`(严格的 PO 抓取) +- 通过泛型参数指定领域实体类型、主键类型和委托类型 +- 写操作(`save` / `removeById` / `saveBatch` / `removeBatchByIds`)始终走 `delegate` +- 读操作(`queryById` / `queryOne` / `queryList` / `queryPage` / `listByIds` / `count` / `exists`)走 `delegate` +- `findById` 始终走 `delegate`(严格的实体查询) + +#### `CqrsRepositoryFacade, RD extends IQueryDelegate>` + +CQRS 模式门面,继承自 `RepositoryFacade`: + +- 维护两个代理:`delegate`(写代理)和 `readDelegate`(读代理) +- 写操作始终走 `delegate`(通过父类字段) +- 读操作优先走 `readDelegate`,失败回退到 `delegate` +- 通过 `RepositoryBeanPostProcessor` 根据泛型类型和注解标记自动注入 #### `InMemoryRepositoryDelegate` @@ -110,78 +134,99 @@ RepositoryDelegate createDelegate(Class poClass, Class idClass); ### 注解 -#### `@Repository` +#### `@WriteDelegate` -标注在 `RepositoryFacade` 子类上: +标注在 `RepositoryDelegate` 实现类上,标记为写代理/基础代理: -| 属性 | 类型 | 默认值 | 说明 | -|-----|------|-------|------| -| `value` | String | `""` | 仓储名称 | -| `type` | `RepositoryType` | `AUTO` | 持久化类型 | -| `entity` | `Class` | `Object.class` | 领域实体类 | -| `po` | `Class` | `Object.class` | 持久化对象类 | -| `id` | `Class` | `Long.class` | 主键类型 | -| `description` | String | `""` | 描述 | -| `cqrs` | boolean | `false` | 是否启用 CQRS | -| `readDelegateClass` | `Class` | `Object.class` | 读代理类(CQRS 时必填) | +```java +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface WriteDelegate { +} +``` -#### `@DelegateFor` +#### `@ReadDelegate` -标注在 `RepositoryDelegate` / `IQueryDelegate` 实现类上: +标注在 `IQueryDelegate` / `RepositoryDelegate` 实现类上,标记为读代理: -| 属性 | 类型 | 默认值 | 说明 | -|-----|------|-------|------| -| `name` | String | `""` | 目标 facade bean 名称 | -| `type` | `RepositoryType` | `AUTO` | 存储类型 | -| `po` | `Class` | `Object.class` | PO 类型 | -| `description` | String | `""` | 描述 | -| `priority` | int | `0` | 优先级(数字越大越优先) | -| `delegateType` | `DelegateType` | `BASE` | `BASE` / `READ` | +```java +@Qualifier +@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ReadDelegate { +} +``` ### 委托匹配策略 -`RepositoryBeanPostProcessor` 在 `ContextRefreshedEvent` 时执行匹配: - -1. 解析 facade 子类的泛型签名 `` -2. 读取 facade 上的 `@Repository` 注解获取 `type` / `cqrs` / `readDelegateClass` -3. **BASE 代理搜索**(按 priority 降序,逐级尝试): - 1. delegate 类型匹配 + 名称匹配 + 类型匹配 - 2. delegate 类型匹配 + 类型匹配 - 3. delegate 类型匹配 + 名称匹配 - 4. delegate 类型匹配 - 5. 名称匹配 - 6. PO 类型匹配 -4. 若未找到 BASE delegate,调用 `RepositoryDelegateFactory.createDelegate(...)` 自动创建 -5. 仍创建失败则回退到 `InMemoryRepositoryDelegate` -6. **READ 代理搜索**(仅 `cqrs=true && readDelegateClass != Object.class` 时):同上 6 步匹配,仅过滤 `delegateType == READ`,类型固定为 `AUTO`(允许读写使用不同存储技术) +`RepositoryBeanPostProcessor` 在 Bean 初始化后执行匹配: + +1. **解析泛型签名**:从 `RepositoryFacade` / `CqrsRepositoryFacade` 子类的泛型参数中提取委托类型 +2. **写代理搜索**(按优先级): + 1. 类型匹配 + `@WriteDelegate` 标记 + 2. 类型匹配 + 无 `@ReadDelegate` 标记 + 3. 类型匹配(兜底) +3. **读代理搜索**(仅 `CqrsRepositoryFacade`): + 1. 类型匹配 + `@ReadDelegate` 标记 + 2. 类型匹配(兜底) ## CQRS 读写分离 -### 类型化 CQRS(RepositoryFacade) +### 类型化 CQRS(CqrsRepositoryFacade) ```java -@Repository( - entity = User.class, po = UserPO.class, id = Long.class, - cqrs = true, readDelegateClass = UserReadDelegate.class) -public class UserRepository - extends RepositoryFacade> { +@Component("userCqrsRepository") +public class UserCqrsRepository + extends CqrsRepositoryFacade + implements UserRepository { + + @Override + public UserEntity findByName(String name) { + return getReadDelegate().findByName(name); + } } -@DelegateFor(name = "userRepository", po = UserPO.class, - type = RepositoryType.MYBATIS_PLUS, delegateType = DelegateType.BASE) +@Slf4j @Component -public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate {} +@WriteDelegate +public class UserWriteDelegate + extends MybatisPlusRepositoryDelegate + implements UserRepositoryDelegate { +} -@DelegateFor(name = "userRepository", po = UserPO.class, - type = RepositoryType.ELASTICSEARCH, delegateType = DelegateType.READ) +@Slf4j @Component -public class UserReadDelegate extends ElasticsearchRepositoryDelegate {} +@ReadDelegate +public class UserReadDelegate + extends ElasticsearchRepositoryDelegate + implements UserRepositoryDelegate { +} ``` 读操作执行流程: - `readDelegate != null` → 尝试执行读操作 -- 异常 → 记录 warning,回退到 `baseDelegate` 兜底 -- `findById` 与写操作始终走 `baseDelegate` +- 异常 → 记录 warning,回退到 `delegate` 兜底 +- `findById` 与写操作始终走 `delegate` + +### 单代理模式(RepositoryFacade) + +```java +@Component("userRepository") +public class UserRepositoryImpl + extends RepositoryFacade + implements UserRepository { +} + +@Slf4j +@Component +public class UserMybatisPlusDelegate + extends MybatisPlusRepositoryDelegate + implements UserRepositoryDelegate { +} +``` ### 低代码 CQRS(LowCodeRepositoryRouter) @@ -381,7 +426,7 @@ public class UserEventListener { | 属性 | 类型 | 默认值 | 说明 | |-----|------|-------|------| | `defaultEventChannel` | `EventChannel` | `SPRING_EVENT` | `DEFAULT` 事件的默认通道 | -| `cqrs` | `Boolean` | `false` | 全局 CQRS 开关(仅作建议,以 `@Repository` 注解为准) | +| `cqrs` | `Boolean` | `false` | 全局 CQRS 开关(仅作建议) | | `schedulePoolSize` | `Integer` | `Runtime.availableProcessors()` | 调度线程池大小 | | `type` | `RepositoryType` | - | 默认持久化类型(用于触发各 starter 的条件装配) | @@ -430,7 +475,7 @@ structure: ```java // 领域实体(无任何持久化注解) -public class User { +public class UserEntity { private Long id; private String username; private String email; @@ -447,58 +492,101 @@ public class UserPO { } ``` -### 2. 声明 RepositoryFacade +### 2. 声明 RepositoryFacade(单代理模式) ```java -@Repository(entity = User.class, po = UserPO.class, id = Long.class, - type = RepositoryType.MYBATIS_PLUS) -public class UserRepository - extends RepositoryFacade> { +@Component("userRepository") +public class UserRepositoryImpl + extends RepositoryFacade + implements UserRepository { } ``` -### 3. 提供自定义 delegate(可选) +### 3. 声明 CqrsRepositoryFacade(CQRS 模式) ```java -@DelegateFor(name = "userRepository", po = UserPO.class, - type = RepositoryType.MYBATIS_PLUS, delegateType = DelegateType.BASE) +@Component("userCqrsRepository") +public class UserCqrsRepository + extends CqrsRepositoryFacade + implements UserRepository { + + @Override + public UserEntity findByName(String name) { + return getReadDelegate().findByName(name); + } +} +``` + +### 4. 提供 delegate 实现 + +```java +// 写代理(单代理模式或 CQRS 模式的写代理) +@Slf4j +@Component +@WriteDelegate +public class UserMybatisPlusDelegate + extends MybatisPlusRepositoryDelegate + implements UserRepositoryDelegate { + + private final UserMapper userMapper; + + @Override + public UserEntity findByName(String name) { + UserPO po = userMapper.selectOne( + Wrappers.lambdaQuery().eq(UserPO::getUsername, name)); + return toEntity(po); + } +} + +// 读代理(仅 CQRS 模式) +@Slf4j @Component -public class UserMybatisPlusDelegate extends MybatisPlusRepositoryDelegate { - public UserMybatisPlusDelegate() { super(UserPO.class); } +@ReadDelegate +public class UserReadDelegate + extends ElasticsearchRepositoryDelegate + implements UserRepositoryDelegate { + + @Override + public UserEntity findByName(String name) { + // Elasticsearch 查询实现 + return null; + } } ``` -### 4. 业务层使用 +### 5. 业务层使用 ```java @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; + private final UserCqrsRepository userCqrsRepository; - public User create(String username, String email) { - User u = new User(); + public UserEntity create(String username, String email) { + UserEntity u = new UserEntity(); u.setUsername(username); u.setEmail(email); return userRepository.save(u); } - public Optional findByUsername(String username) { - User probe = new User(); + public Optional findByUsername(String username) { + UserEntity probe = new UserEntity(); probe.setUsername(username); return userRepository.queryOneOptional(probe); } - public ResPage page(int page, int size) { + public ResPage page(int page, int size) { ReqPage reqPage = new ReqPage(); reqPage.setPage(page); reqPage.setSize(size); - return userRepository.queryPage(reqPage); + // CQRS 模式:读操作走 readDelegate + return userCqrsRepository.queryPage(reqPage); } } ``` -### 5. 低代码使用 +### 6. 低代码使用 ```java @Service @@ -523,4 +611,4 @@ mvn test -pl structure-infra-starter ## License -Apache License 2.0 +Apache License 2.0 \ No newline at end of file From 947570597a5b9190924040cf2d391706cca2f1fa Mon Sep 17 00:00:00 2001 From: chuck <361648887@qq.com> Date: Tue, 21 Jul 2026 16:28:23 +0800 Subject: [PATCH 9/9] =?UTF-8?q?chore(docs):=20=E6=9B=B4=E6=96=B0=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E7=89=88=E6=9C=AC=E5=8F=B7=E4=BB=8E=201.0.0-SNAPSHOT?= =?UTF-8?q?=20=E5=88=B0=201.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 structure-infra-starter 版本到 1.2.0 - 更新 structure-infra-mybatis-plus-starter 版本到 1.2.0 - 更新 structure-infra-jpa-starter 版本到 1.2.0 - 更新 structure-infra-mongodb-starter 版本到 1.2.0 - 更新 structure-infra-elasticsearch-starter 版本到 1.2.0 - 更新 structure-infra-schedule-starter 版本到 1.2.0 - 更新 structure-infra-xxljob-starter 版本到 1.2.0 - 更新 structure-infra-stream-starter 版本到 1.2.0 --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6953fdc..6a30457 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.0.0-SNAPSHOT + 1.2.0 cn.structured structure-infra-mybatis-plus-starter - 1.0.0-SNAPSHOT + 1.2.0 cn.structured structure-infra-jpa-starter - 1.0.0-SNAPSHOT + 1.2.0 cn.structured structure-infra-mongodb-starter - 1.0.0-SNAPSHOT + 1.2.0 cn.structured structure-infra-elasticsearch-starter - 1.0.0-SNAPSHOT + 1.2.0 cn.structured structure-infra-schedule-starter - 1.0.0-SNAPSHOT + 1.2.0 cn.structured structure-infra-xxljob-starter - 1.0.0-SNAPSHOT + 1.2.0 cn.structured structure-infra-stream-starter - 1.0.0-SNAPSHOT + 1.2.0 ```