From 0c3774e8035bd7d5ae15cf4e4af54b4032856e28 Mon Sep 17 00:00:00 2001
From: chuck <361648887@qq.com>
Date: Sat, 4 Jul 2026 01:26:57 +0800
Subject: [PATCH 1/8] =?UTF-8?q?feat(schedule):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E8=B0=83=E5=BA=A6=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 schedule starter 模块提供基础调度能力
- 实现本地线程池任务调度器 LocalThreadTaskScheduler
- 添加任务处理器注册表 TaskHandlerRegistry
- 提供默认任务处理器实现 DefaultTaskHandlerRegistry
- 集成 XXL-Job 分布式任务调度框架
- 添加 Stream 事件流处理功能
- 实现配置驱动的事件路由机制
- 提供动态监听器注册与管理功能
- 添加 RabbitMQ 消息队列集成支持
- 配置 MyBatis-Plus 数据库访问层
- 添加示例项目验证各项功能完整性
---
pom.xml | 82 +++
structure-infra-sample/pom.xml | 3 +
.../config/SampleCoreAutoConfiguration.java | 18 +
...ot.autoconfigure.AutoConfiguration.imports | 1 +
.../src/main/resources/application.yml | 18 +
.../src/main/resources/schema.sql | 9 +
.../structure-infra-sample-schedule/pom.xml | 52 ++
.../schedule/ScheduleSampleApplication.java | 12 +
.../schedule/config/ScheduleDemoConfig.java | 51 ++
.../controller/JobManagerController.java | 100 ++++
.../schedule/handler/DemoTaskHandlers.java | 56 ++
.../src/main/resources/application.yml | 11 +
.../schedule/ScheduleIntegrationTest.java | 304 ++++++++++
.../structure-infra-sample-stream/pom.xml | 84 +++
.../sample/stream/StreamApplication.java | 11 +
.../sample/stream/config/SecurityConfig.java | 24 +
.../consumer/StreamMessageConsumer.java | 53 ++
.../stream/controller/StreamController.java | 73 +++
.../sample/stream/event/DeliveryEvent.java | 22 +
.../infra/sample/stream/event/OrderEvent.java | 22 +
.../sample/stream/event/PaymentEvent.java | 22 +
.../listener/DeliveryEventListener.java | 26 +
.../stream/listener/OrderEventListener.java | 31 ++
.../stream/listener/PaymentEventListener.java | 26 +
.../src/main/resources/application.yml | 26 +
.../sample/stream/StreamEventManagerTest.java | 375 +++++++++++++
.../sample/stream/StreamEventRouterTest.java | 314 +++++++++++
.../stream/config/StreamTestConfig.java | 18 +
.../controller/StreamControllerTest.java | 155 ++++++
.../stream/dynamic/DynamicListenerDemo.java | 161 ++++++
.../sample/stream/event/DeliveryEvent.java | 86 +++
.../infra/sample/stream/event/OrderEvent.java | 22 +
.../sample/stream/event/PaymentEvent.java | 22 +
.../listener/DeliveryEventListener.java | 33 ++
.../stream/listener/OrderEventListener.java | 51 ++
.../stream/listener/PaymentEventListener.java | 35 ++
.../stream/lowcode/LowCodeConfigDemo.java | 32 ++
.../stream/lowcode/LowCodeRouteConfig.java | 94 ++++
.../src/test/resources/application.yml | 20 +
.../structure-infra-sample-xxljob/pom.xml | 52 ++
.../infra/sample/xxljob/SampleXxlJob.java | 34 ++
.../sample/xxljob/XxlJobApplication.java | 12 +
.../controller/JobManagerController.java | 107 ++++
.../src/main/resources/application.yaml | 9 +
.../resources/application-xxljob-test.yml | 15 +
structure-infra-schedule-starter/pom.xml | 43 ++
.../AutoScheduleConfiguration.java | 37 ++
.../infra/properties/ScheduleProperties.java | 11 +
.../schedule/DefaultTaskHandlerRegistry.java | 37 ++
.../schedule/LocalThreadTaskScheduler.java | 184 ++++++
.../infra/schedule/ScheduleTask.java | 51 ++
.../schedule/SpringTaskSchedulerAdapter.java | 229 ++++++++
.../structure/infra/schedule/TaskHandler.java | 7 +
.../infra/schedule/TaskHandlerRegistry.java | 12 +
.../infra/schedule/TaskScheduler.java | 20 +
...ot.autoconfigure.AutoConfiguration.imports | 1 +
.../DefaultTaskHandlerRegistryTest.java | 79 +++
.../LocalThreadTaskSchedulerTest.java | 315 +++++++++++
.../SpringTaskSchedulerAdapterTest.java | 150 +++++
structure-infra-starter/pom.xml | 9 +
.../AutoScheduleConfiguration.java | 39 ++
.../infra/properties/InfraProperties.java | 5 +
structure-infra-stream-starter/README.md | 526 ++++++++++++++++++
structure-infra-stream-starter/pom.xml | 60 ++
.../annotation/StreamEventListener.java | 32 ++
.../stream/annotation/StreamRouteHandler.java | 22 +
.../StreamAutoConfiguration.java | 49 ++
.../infra/stream/event/StreamEvent.java | 181 ++++++
.../stream/handler/StreamEventHandler.java | 7 +
.../DefaultStreamEventManagerImpl.java | 231 ++++++++
.../stream/manager/ListenerRegistration.java | 121 ++++
.../stream/manager/StreamEventManager.java | 49 ++
.../EventListenerBeanPostProcessor.java | 160 ++++++
...StreamBindingBeanFactoryPostProcessor.java | 182 ++++++
.../stream/properties/StreamProperties.java | 156 ++++++
.../router/ConfigurableRouteInitializer.java | 82 +++
.../router/DefaultStreamEventRouterImpl.java | 142 +++++
.../router/RouteHandlerBeanPostProcessor.java | 81 +++
.../stream/router/RouteRegistration.java | 119 ++++
.../infra/stream/router/RouterProperties.java | 104 ++++
.../stream/router/StreamEventRouter.java | 31 ++
...ot.autoconfigure.AutoConfiguration.imports | 1 +
structure-infra-xxljob-starter/pom.xml | 44 ++
.../AutoXxlJobConfiguration.java | 34 ++
.../infra/properties/XxlJobProperties.java | 14 +
.../schedule/xxljob/XxlJobTaskScheduler.java | 163 ++++++
.../infra/schedule/xxljob/XxlJobTemplate.java | 16 +
.../schedule/xxljob/XxlJobTemplateImpl.java | 101 ++++
...ot.autoconfigure.AutoConfiguration.imports | 1 +
89 files changed, 6722 insertions(+)
create mode 100644 structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/config/SampleCoreAutoConfiguration.java
create mode 100644 structure-infra-sample/structure-infra-sample-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/main/resources/application.yml
create mode 100644 structure-infra-sample/structure-infra-sample-mybatis/src/main/resources/schema.sql
create mode 100644 structure-infra-sample/structure-infra-sample-schedule/pom.xml
create mode 100644 structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/ScheduleSampleApplication.java
create mode 100644 structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/config/ScheduleDemoConfig.java
create mode 100644 structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/controller/JobManagerController.java
create mode 100644 structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/handler/DemoTaskHandlers.java
create mode 100644 structure-infra-sample/structure-infra-sample-schedule/src/main/resources/application.yml
create mode 100644 structure-infra-sample/structure-infra-sample-schedule/src/test/java/cn/structure/infra/sample/schedule/ScheduleIntegrationTest.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/pom.xml
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/StreamApplication.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/config/SecurityConfig.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/consumer/StreamMessageConsumer.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/controller/StreamController.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/DeliveryEvent.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/OrderEvent.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/PaymentEvent.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/DeliveryEventListener.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/OrderEventListener.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/main/resources/application.yml
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/StreamEventManagerTest.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/StreamEventRouterTest.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/config/StreamTestConfig.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/controller/StreamControllerTest.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/dynamic/DynamicListenerDemo.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/DeliveryEvent.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/OrderEvent.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/PaymentEvent.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/DeliveryEventListener.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/OrderEventListener.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/lowcode/LowCodeConfigDemo.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/lowcode/LowCodeRouteConfig.java
create mode 100644 structure-infra-sample/structure-infra-sample-stream/src/test/resources/application.yml
create mode 100644 structure-infra-sample/structure-infra-sample-xxljob/pom.xml
create mode 100644 structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/SampleXxlJob.java
create mode 100644 structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/XxlJobApplication.java
create mode 100644 structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/controller/JobManagerController.java
create mode 100644 structure-infra-sample/structure-infra-sample-xxljob/src/main/resources/application.yaml
create mode 100644 structure-infra-sample/structure-infra-sample-xxljob/src/test/resources/application-xxljob-test.yml
create mode 100644 structure-infra-schedule-starter/pom.xml
create mode 100644 structure-infra-schedule-starter/src/main/java/cn/structure/infra/configuration/AutoScheduleConfiguration.java
create mode 100644 structure-infra-schedule-starter/src/main/java/cn/structure/infra/properties/ScheduleProperties.java
create mode 100644 structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/DefaultTaskHandlerRegistry.java
create mode 100644 structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/LocalThreadTaskScheduler.java
create mode 100644 structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/ScheduleTask.java
create mode 100644 structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/SpringTaskSchedulerAdapter.java
create mode 100644 structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskHandler.java
create mode 100644 structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskHandlerRegistry.java
create mode 100644 structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskScheduler.java
create mode 100644 structure-infra-schedule-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
create mode 100644 structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/DefaultTaskHandlerRegistryTest.java
create mode 100644 structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/LocalThreadTaskSchedulerTest.java
create mode 100644 structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/SpringTaskSchedulerAdapterTest.java
create mode 100644 structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoScheduleConfiguration.java
create mode 100644 structure-infra-stream-starter/README.md
create mode 100644 structure-infra-stream-starter/pom.xml
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/annotation/StreamEventListener.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/annotation/StreamRouteHandler.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/configuration/StreamAutoConfiguration.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/event/StreamEvent.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/handler/StreamEventHandler.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/DefaultStreamEventManagerImpl.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/ListenerRegistration.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/StreamEventManager.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/processor/EventListenerBeanPostProcessor.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/processor/StreamBindingBeanFactoryPostProcessor.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/properties/StreamProperties.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/ConfigurableRouteInitializer.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/DefaultStreamEventRouterImpl.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouteHandlerBeanPostProcessor.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouteRegistration.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouterProperties.java
create mode 100644 structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/StreamEventRouter.java
create mode 100644 structure-infra-stream-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
create mode 100644 structure-infra-xxljob-starter/pom.xml
create mode 100644 structure-infra-xxljob-starter/src/main/java/cn/structure/infra/configuration/AutoXxlJobConfiguration.java
create mode 100644 structure-infra-xxljob-starter/src/main/java/cn/structure/infra/properties/XxlJobProperties.java
create mode 100644 structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTaskScheduler.java
create mode 100644 structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTemplate.java
create mode 100644 structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTemplateImpl.java
create mode 100644 structure-infra-xxljob-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
diff --git a/pom.xml b/pom.xml
index 6e9190a..0461c07 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,20 +20,47 @@
1.0.0-SNAPSHOT
4.0.6
+ 5.0.0
3.5.16
1.4.3
1.1.4
1.0.3
+ 2.0.0
+ 1.18.32
structure-infra-starter
+ structure-infra-schedule-starter
+ structure-infra-xxljob-starter
structure-infra-mybatis-plus-starter
structure-infra-jpa-starter
structure-infra-mongodb-starter
structure-infra-elasticsearch-starter
+ structure-infra-stream-starter
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 17
+ 17
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+
+
+
+
sample
@@ -138,6 +165,37 @@
${structure-datascope.version}
+
+
+ org.springframework.cloud
+ spring-cloud-stream
+ ${spring-cloud-stream.version}
+
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.springframework.cloud
+ spring-cloud-stream-binder-kafka
+ ${spring-cloud-stream.version}
+
+
+
+ org.springframework.cloud
+ spring-cloud-stream-binder-rabbit
+ ${spring-cloud-stream.version}
+ provided
+
+
+ org.springframework.cloud
+ spring-cloud-stream-test-support
+ ${spring-cloud-stream.version}
+
+
jakarta.persistence
@@ -151,11 +209,35 @@
${revision}
+
+ cn.structured
+ structure-infra-schedule-starter
+ ${revision}
+
+
+
+ cn.structured
+ structure-infra-xxljob-starter
+ ${revision}
+
+
cn.structured
structure-infra-mybatis-plus-starter
${revision}
+
+
+ cn.structured
+ structure-infra-stream-starter
+ ${revision}
+
+
+
+ cn.structured
+ structure-job-starter
+ ${structure-job.version}
+
diff --git a/structure-infra-sample/pom.xml b/structure-infra-sample/pom.xml
index a6d2157..d6a52a4 100644
--- a/structure-infra-sample/pom.xml
+++ b/structure-infra-sample/pom.xml
@@ -27,6 +27,9 @@
structure-infra-sample-mongodb
structure-infra-sample-elasticsearch
structure-infra-sample-cqrs
+ structure-infra-sample-stream
+ structure-infra-sample-xxljob
+ structure-infra-sample-schedule
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/config/SampleCoreAutoConfiguration.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/config/SampleCoreAutoConfiguration.java
new file mode 100644
index 0000000..f298469
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/config/SampleCoreAutoConfiguration.java
@@ -0,0 +1,18 @@
+package cn.structure.infra.sample.infra.config;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
+import org.springframework.context.annotation.Bean;
+
+@AutoConfiguration
+@EnableCaching
+public class SampleCoreAutoConfiguration {
+
+ @Bean
+ public CacheManager cacheManager() {
+ return new ConcurrentMapCacheManager();
+ }
+
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/structure-infra-sample/structure-infra-sample-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..0701755
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+cn.structure.infra.sample.infra.config.SampleCoreAutoConfiguration
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/resources/application.yml b/structure-infra-sample/structure-infra-sample-mybatis/src/main/resources/application.yml
new file mode 100644
index 0000000..3846b8a
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/main/resources/application.yml
@@ -0,0 +1,18 @@
+spring:
+ datasource:
+ driver-class-name: org.h2.Driver
+ url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
+ username: sa
+ password:
+ sql:
+ init:
+ mode: always
+ schema-locations: classpath:schema.sql
+
+mybatis-plus:
+ configuration:
+ map-underscore-to-camel-case: true
+ log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+ global-config:
+ db-config:
+ id-type: auto
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-mybatis/src/main/resources/schema.sql b/structure-infra-sample/structure-infra-sample-mybatis/src/main/resources/schema.sql
new file mode 100644
index 0000000..3b13648
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-mybatis/src/main/resources/schema.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS t_user (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ username VARCHAR(255) NOT NULL,
+ password VARCHAR(255),
+ email VARCHAR(255),
+ age INT,
+ create_time TIMESTAMP,
+ update_time TIMESTAMP
+);
diff --git a/structure-infra-sample/structure-infra-sample-schedule/pom.xml b/structure-infra-sample/structure-infra-sample-schedule/pom.xml
new file mode 100644
index 0000000..9301a9f
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-schedule/pom.xml
@@ -0,0 +1,52 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-infra-sample
+ ${revision}
+ ../pom.xml
+
+
+ structure-infra-sample-schedule
+ structure-infra-sample-schedule
+ 调度模块示例
+ jar
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ cn.structured
+ structure-infra-schedule-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/ScheduleSampleApplication.java b/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/ScheduleSampleApplication.java
new file mode 100644
index 0000000..a2774a4
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/ScheduleSampleApplication.java
@@ -0,0 +1,12 @@
+package cn.structure.infra.sample.schedule;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class ScheduleSampleApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(ScheduleSampleApplication.class, args);
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/config/ScheduleDemoConfig.java b/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/config/ScheduleDemoConfig.java
new file mode 100644
index 0000000..d5c4550
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/config/ScheduleDemoConfig.java
@@ -0,0 +1,51 @@
+package cn.structure.infra.sample.schedule.config;
+
+import cn.structure.infra.schedule.ScheduleTask;
+import cn.structure.infra.schedule.TaskScheduler;
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.DependsOn;
+
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Configuration
+@DependsOn("demoTaskHandlers")
+public class ScheduleDemoConfig {
+
+ @Autowired
+ private TaskScheduler taskScheduler;
+
+ @PostConstruct
+ public void initScheduledTasks() {
+ ScheduleTask fixedRateTask = ScheduleTask.builder()
+ .taskId("demo-fixed-rate-task")
+ .taskName("固定速率示例任务")
+ .handlerName("demo-fixed-rate-handler")
+ .handlerParam("fixed-rate-param")
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(3000L)
+ .initialDelay(1000L)
+ .timeUnit(TimeUnit.MILLISECONDS)
+ .build();
+ taskScheduler.schedule(fixedRateTask);
+ log.info("已启动固定速率任务: {}", fixedRateTask.getTaskName());
+
+ ScheduleTask fixedDelayTask = ScheduleTask.builder()
+ .taskId("demo-fixed-delay-task")
+ .taskName("固定延迟示例任务")
+ .handlerName("demo-fixed-delay-handler")
+ .handlerParam("fixed-delay-param")
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_DELAY)
+ .delay(2000L)
+ .initialDelay(2000L)
+ .timeUnit(TimeUnit.MILLISECONDS)
+ .build();
+ taskScheduler.schedule(fixedDelayTask);
+ log.info("已启动固定延迟任务: {}", fixedDelayTask.getTaskName());
+
+ log.info("所有示例调度任务已启动");
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/controller/JobManagerController.java b/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/controller/JobManagerController.java
new file mode 100644
index 0000000..f3c6110
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/controller/JobManagerController.java
@@ -0,0 +1,100 @@
+package cn.structure.infra.sample.schedule.controller;
+
+import cn.structure.infra.schedule.ScheduleTask;
+import cn.structure.infra.schedule.TaskScheduler;
+import jakarta.annotation.Resource;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/job")
+public class JobManagerController {
+
+ @Resource
+ private TaskScheduler taskScheduler;
+
+ @PostMapping("/add")
+ public ScheduleTask add(
+ @RequestParam("taskId") String taskId,
+ @RequestParam("taskName") String taskName,
+ @RequestParam("handlerName") String handlerName,
+ @RequestParam(value = "handlerParam", required = false) String handlerParam,
+ @RequestParam(value = "cronExpression", defaultValue = "0/5 * * * * ?") String cronExpression) {
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId(taskId)
+ .taskName(taskName)
+ .handlerName(handlerName)
+ .handlerParam(handlerParam)
+ .scheduleType(ScheduleTask.ScheduleType.CRON)
+ .cronExpression(cronExpression)
+ .build();
+
+ taskScheduler.schedule(task);
+ return taskScheduler.getTaskInfo(taskId);
+ }
+
+ @PutMapping("/update/{taskId}")
+ public ScheduleTask update(
+ @PathVariable("taskId") String taskId,
+ @RequestParam(value = "taskName", required = false) String taskName,
+ @RequestParam(value = "handlerName", required = false) String handlerName,
+ @RequestParam(value = "handlerParam", required = false) String handlerParam,
+ @RequestParam(value = "cronExpression", required = false) String cronExpression) {
+
+ ScheduleTask existingTask = taskScheduler.getTaskInfo(taskId);
+ if (existingTask == null) {
+ throw new IllegalArgumentException("Task not found: " + taskId);
+ }
+
+ ScheduleTask.ScheduleTaskBuilder builder = ScheduleTask.builder()
+ .taskId(taskId)
+ .taskName(taskName != null ? taskName : existingTask.getTaskName())
+ .handlerName(handlerName != null ? handlerName : existingTask.getHandlerName())
+ .handlerParam(handlerParam != null ? handlerParam : existingTask.getHandlerParam())
+ .scheduleType(existingTask.getScheduleType());
+
+ if (cronExpression != null) {
+ builder.cronExpression(cronExpression);
+ } else if (existingTask.getCronExpression() != null) {
+ builder.cronExpression(existingTask.getCronExpression());
+ }
+
+ ScheduleTask task = builder.build();
+ taskScheduler.update(task);
+ return taskScheduler.getTaskInfo(taskId);
+ }
+
+ @DeleteMapping("/remove/{taskId}")
+ public String remove(@PathVariable("taskId") String taskId) {
+ ScheduleTask task = taskScheduler.getTaskInfo(taskId);
+ if (task == null) {
+ return "Task not found: " + taskId;
+ }
+ taskScheduler.remove(taskId);
+ return "Removed task: " + taskId;
+ }
+
+ @PutMapping("/pause/{taskId}")
+ public ScheduleTask pause(@PathVariable("taskId") String taskId) {
+ taskScheduler.pause(taskId);
+ return taskScheduler.getTaskInfo(taskId);
+ }
+
+ @PutMapping("/resume/{taskId}")
+ public ScheduleTask resume(@PathVariable("taskId") String taskId) {
+ taskScheduler.resume(taskId);
+ return taskScheduler.getTaskInfo(taskId);
+ }
+
+ @GetMapping("/info/{taskId}")
+ public ScheduleTask getTaskInfo(@PathVariable("taskId") String taskId) {
+ return taskScheduler.getTaskInfo(taskId);
+ }
+
+ @GetMapping("/list")
+ public List getAllTasks() {
+ return taskScheduler.getAllTasks();
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..29039c6
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-schedule/src/main/java/cn/structure/infra/sample/schedule/handler/DemoTaskHandlers.java
@@ -0,0 +1,56 @@
+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;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+@Slf4j
+@Component
+public class DemoTaskHandlers {
+
+ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ @Autowired
+ private TaskHandlerRegistry handlerRegistry;
+
+ @PostConstruct
+ public void registerHandlers() {
+ handlerRegistry.register("demo-fixed-rate-handler", this::fixedRateHandler);
+
+ handlerRegistry.register("demo-fixed-delay-handler", this::fixedDelayHandler);
+
+ handlerRegistry.register("demo-param-handler", this::paramHandler);
+
+ handlerRegistry.register("demo-error-handler", this::errorHandler);
+
+ log.info("所有示例 TaskHandler 已注册");
+ }
+
+ public void fixedRateHandler(String param) {
+ log.info("固定速率任务执行 - 当前时间: {}, 参数: {}", LocalDateTime.now().format(FORMATTER), param);
+ }
+
+ public void fixedDelayHandler(String param) {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ log.info("固定延迟任务执行 - 当前时间: {}, 参数: {}", LocalDateTime.now().format(FORMATTER), param);
+ }
+
+ public void paramHandler(String param) {
+ log.info("带参数任务执行 - 参数: {}", param);
+ }
+
+ public void errorHandler(String param) {
+ log.info("错误处理任务执行 - 参数: {}", param);
+ throw new RuntimeException("模拟任务执行异常");
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-schedule/src/main/resources/application.yml b/structure-infra-sample/structure-infra-sample-schedule/src/main/resources/application.yml
new file mode 100644
index 0000000..0ab1521
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-schedule/src/main/resources/application.yml
@@ -0,0 +1,11 @@
+server:
+ port: 8086
+
+logging:
+ level:
+ cn.structure.infra.schedule: DEBUG
+ cn.structure.infra.sample.schedule: INFO
+
+structure:
+ schedule:
+ pool-size: 4
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-schedule/src/test/java/cn/structure/infra/sample/schedule/ScheduleIntegrationTest.java b/structure-infra-sample/structure-infra-sample-schedule/src/test/java/cn/structure/infra/sample/schedule/ScheduleIntegrationTest.java
new file mode 100644
index 0000000..be5a9f5
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-schedule/src/test/java/cn/structure/infra/sample/schedule/ScheduleIntegrationTest.java
@@ -0,0 +1,304 @@
+package cn.structure.infra.sample.schedule;
+
+import cn.structure.infra.schedule.ScheduleTask;
+import cn.structure.infra.schedule.TaskHandlerRegistry;
+import cn.structure.infra.schedule.TaskScheduler;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@Slf4j
+@SpringBootTest(classes = ScheduleSampleApplication.class)
+class ScheduleIntegrationTest {
+
+ @Autowired
+ private TaskScheduler taskScheduler;
+
+ @Autowired
+ private TaskHandlerRegistry handlerRegistry;
+
+ @BeforeEach
+ void setUp() {
+ taskScheduler.getAllTasks().forEach(task -> taskScheduler.remove(task.getTaskId()));
+ }
+
+ @Test
+ void testFixedRateSchedule() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+ String handlerName = "test-fixed-rate-counter";
+ handlerRegistry.register(handlerName, param -> {
+ int count = counter.incrementAndGet();
+ log.info("Fixed rate task executed, count={}", count);
+ });
+
+ String taskId = "test-fixed-rate";
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId(taskId)
+ .taskName("固定速率测试任务")
+ .handlerName(handlerName)
+ .handlerParam("test-param")
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(500L)
+ .initialDelay(0L)
+ .build();
+
+ taskScheduler.schedule(task);
+
+ Thread.sleep(2000);
+
+ ScheduleTask taskInfo = taskScheduler.getTaskInfo(taskId);
+ assertNotNull(taskInfo);
+ assertEquals(ScheduleTask.TaskStatus.RUNNING, taskInfo.getStatus());
+
+ assertTrue(counter.get() >= 3, "任务应至少执行3次");
+
+ taskScheduler.pause(taskId);
+
+ Thread.sleep(1000);
+
+ int countAfterPause = counter.get();
+
+ Thread.sleep(1000);
+
+ assertEquals(countAfterPause, counter.get(), "暂停后任务不应继续执行");
+
+ taskScheduler.resume(taskId);
+
+ Thread.sleep(1000);
+
+ assertTrue(counter.get() > countAfterPause, "恢复后任务应继续执行");
+
+ taskScheduler.remove(taskId);
+ handlerRegistry.unregister(handlerName);
+
+ Thread.sleep(500);
+
+ assertNull(taskScheduler.getTaskInfo(taskId), "删除后任务信息应为空");
+ }
+
+ @Test
+ void testFixedDelaySchedule() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+ String handlerName = "test-fixed-delay-counter";
+ handlerRegistry.register(handlerName, param -> {
+ int count = counter.incrementAndGet();
+ log.info("Fixed delay task executed, count={}", count);
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ });
+
+ String taskId = "test-fixed-delay";
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId(taskId)
+ .taskName("固定延迟测试任务")
+ .handlerName(handlerName)
+ .handlerParam("test-param")
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_DELAY)
+ .delay(500L)
+ .initialDelay(0L)
+ .build();
+
+ taskScheduler.schedule(task);
+
+ Thread.sleep(2000);
+
+ assertTrue(counter.get() >= 3, "固定延迟任务应至少执行3次");
+
+ taskScheduler.remove(taskId);
+ handlerRegistry.unregister(handlerName);
+ }
+
+ @Test
+ void testUpdateTask() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+ String handlerName = "test-update-handler";
+ handlerRegistry.register(handlerName, param -> {
+ int count = counter.incrementAndGet();
+ log.info("Update test task executed, count={}", count);
+ });
+
+ String taskId = "test-update";
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId(taskId)
+ .taskName("更新测试任务")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ taskScheduler.schedule(task);
+
+ Thread.sleep(2500);
+
+ int countBeforeUpdate = counter.get();
+
+ ScheduleTask updatedTask = ScheduleTask.builder()
+ .taskId(taskId)
+ .taskName("更新测试任务-修改后")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(500L)
+ .build();
+
+ taskScheduler.update(updatedTask);
+
+ Thread.sleep(1500);
+
+ assertTrue(counter.get() > countBeforeUpdate + 1, "更新后任务应更频繁执行");
+
+ taskScheduler.remove(taskId);
+ handlerRegistry.unregister(handlerName);
+ }
+
+ @Test
+ void testGetAllTasks() throws InterruptedException {
+ String handlerName1 = "test-get-all-handler-1";
+ String handlerName2 = "test-get-all-handler-2";
+ handlerRegistry.register(handlerName1, param -> log.info("Task 1 executed"));
+ handlerRegistry.register(handlerName2, param -> log.info("Task 2 executed"));
+
+ String taskId1 = "test-get-all-1";
+ String taskId2 = "test-get-all-2";
+
+ ScheduleTask task1 = ScheduleTask.builder()
+ .taskId(taskId1)
+ .taskName("任务1")
+ .handlerName(handlerName1)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ ScheduleTask task2 = ScheduleTask.builder()
+ .taskId(taskId2)
+ .taskName("任务2")
+ .handlerName(handlerName2)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ taskScheduler.schedule(task1);
+ taskScheduler.schedule(task2);
+
+ Thread.sleep(100);
+
+ assertEquals(2, taskScheduler.getAllTasks().size());
+
+ taskScheduler.remove(taskId1);
+ taskScheduler.remove(taskId2);
+ handlerRegistry.unregister(handlerName1);
+ handlerRegistry.unregister(handlerName2);
+
+ assertEquals(0, taskScheduler.getAllTasks().size());
+ }
+
+ @Test
+ void testPauseNonExistentTask() {
+ taskScheduler.pause("non-existent-task");
+ assertNull(taskScheduler.getTaskInfo("non-existent-task"));
+ }
+
+ @Test
+ void testResumeNonExistentTask() {
+ taskScheduler.resume("non-existent-task");
+ assertNull(taskScheduler.getTaskInfo("non-existent-task"));
+ }
+
+ @Test
+ void testRemoveNonExistentTask() {
+ taskScheduler.remove("non-existent-task");
+ assertNull(taskScheduler.getTaskInfo("non-existent-task"));
+ }
+
+ @Test
+ void testScheduleWithNullTask() {
+ assertThrows(IllegalArgumentException.class, () -> taskScheduler.schedule(null));
+ }
+
+ @Test
+ void testScheduleWithNullTaskId() {
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId(null)
+ .taskName("测试任务")
+ .handlerName("test-handler")
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> taskScheduler.schedule(task));
+ }
+
+ @Test
+ void testScheduleWithNullHandlerName() {
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-null-handler")
+ .taskName("测试任务")
+ .handlerName(null)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> taskScheduler.schedule(task));
+ }
+
+ @Test
+ void testScheduleWithEmptyHandlerName() {
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-empty-handler")
+ .taskName("测试任务")
+ .handlerName("")
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> taskScheduler.schedule(task));
+ }
+
+ @Test
+ void testScheduleWithNonExistentHandler() {
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-non-existent-handler")
+ .taskName("测试任务")
+ .handlerName("non-existent-handler")
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> taskScheduler.schedule(task));
+ }
+
+ @Test
+ void testScheduleWithNullScheduleType() {
+ String handlerName = "test-null-type-handler";
+ handlerRegistry.register(handlerName, param -> {});
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-null-type")
+ .taskName("测试任务")
+ .handlerName(handlerName)
+ .scheduleType(null)
+ .period(1000L)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> taskScheduler.schedule(task));
+
+ handlerRegistry.unregister(handlerName);
+ }
+
+ @Test
+ void testAutoConfiguration() {
+ assertNotNull(taskScheduler);
+ assertNotNull(handlerRegistry);
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-stream/pom.xml b/structure-infra-sample/structure-infra-sample-stream/pom.xml
new file mode 100644
index 0000000..0280b57
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/pom.xml
@@ -0,0 +1,84 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-infra-sample
+ ${revision}
+ ../pom.xml
+
+
+ structure-infra-sample-stream
+ structure-infra-sample-stream
+ Spring Cloud Stream 示例模块
+ jar
+
+
+
+ cn.structured
+ structure-infra-sample-core
+ ${revision}
+
+
+ cn.structured
+ structure-infra-stream-starter
+ ${revision}
+
+
+ org.springframework.cloud
+ spring-cloud-stream-binder-rabbit
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.cloud
+ spring-cloud-stream-test-support
+ test
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 17
+ 17
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/StreamApplication.java b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/StreamApplication.java
new file mode 100644
index 0000000..9f9dc47
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/StreamApplication.java
@@ -0,0 +1,11 @@
+package cn.structure.infra.sample.stream;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class StreamApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(StreamApplication.class, args);
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/config/SecurityConfig.java b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/config/SecurityConfig.java
new file mode 100644
index 0000000..2d2ee0b
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/config/SecurityConfig.java
@@ -0,0 +1,24 @@
+package cn.structure.infra.sample.stream.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .authorizeHttpRequests(auth -> auth
+ .anyRequest().permitAll()
+ );
+ return http.build();
+ }
+
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..7a09f88
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/consumer/StreamMessageConsumer.java
@@ -0,0 +1,53 @@
+package cn.structure.infra.sample.stream.consumer;
+
+import cn.structure.infra.sample.stream.event.DeliveryEvent;
+import cn.structure.infra.sample.stream.event.OrderEvent;
+import cn.structure.infra.sample.stream.event.PaymentEvent;
+import cn.structure.infra.stream.manager.StreamEventManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.Message;
+
+import java.util.function.Consumer;
+
+@Configuration
+public class StreamMessageConsumer {
+
+ private static final Logger log = LoggerFactory.getLogger(StreamMessageConsumer.class);
+
+ private final StreamEventManager streamEventManager;
+
+ public StreamMessageConsumer(StreamEventManager streamEventManager) {
+ this.streamEventManager = streamEventManager;
+ }
+
+ @Bean
+ public Consumer> orderEvent() {
+ return message -> {
+ OrderEvent event = message.getPayload();
+ log.info("[消费者] 收到订单事件: {}", event);
+ streamEventManager.dispatch("orderEvent", event);
+ };
+ }
+
+ @Bean
+ public Consumer> paymentEvent() {
+ return message -> {
+ PaymentEvent event = message.getPayload();
+ log.info("[消费者] 收到支付事件: {}", event);
+ streamEventManager.dispatch("paymentEvent", event);
+ };
+ }
+
+ @Bean
+ public Consumer> deliveryEvent() {
+ return message -> {
+ DeliveryEvent event = message.getPayload();
+ log.info("[消费者] 收到配送事件: {}", event);
+ streamEventManager.dispatch("deliveryEvent", event);
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/controller/StreamController.java b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/controller/StreamController.java
new file mode 100644
index 0000000..d86ac06
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/controller/StreamController.java
@@ -0,0 +1,73 @@
+package cn.structure.infra.sample.stream.controller;
+
+import cn.structure.infra.sample.stream.event.DeliveryEvent;
+import cn.structure.infra.sample.stream.event.OrderEvent;
+import cn.structure.infra.sample.stream.event.PaymentEvent;
+import cn.structure.infra.stream.manager.StreamEventManager;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/stream")
+public class StreamController {
+
+ private final StreamEventManager streamEventManager;
+
+ public StreamController(StreamEventManager streamEventManager) {
+ this.streamEventManager = streamEventManager;
+ }
+
+ @PostMapping("/order")
+ public String sendOrderEvent(@RequestBody OrderEvent event) {
+ streamEventManager.publish("orderEvent", event);
+ return "Order event sent: " + event.getOrderId();
+ }
+
+ @PostMapping("/order/create")
+ public String sendOrderCreatedEvent() {
+ OrderEvent event = OrderEvent.builder()
+ .orderId("ORDER-" + System.currentTimeMillis())
+ .orderNo("ORD-" + System.currentTimeMillis())
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+ streamEventManager.publish("orderEvent", event);
+ return "Order created event sent: " + event.getOrderId();
+ }
+
+ @PostMapping("/order/pay")
+ public String sendOrderPaidEvent(@RequestBody OrderEvent event) {
+ event.setStatus("PAID");
+ streamEventManager.publish("orderEvent", event);
+ return "Order paid event sent: " + event.getOrderId();
+ }
+
+ @PostMapping("/payment")
+ public String sendPaymentEvent(@RequestBody PaymentEvent event) {
+ streamEventManager.publish("paymentEvent", event);
+ return "Payment event sent: " + event.getPaymentId();
+ }
+
+ @PostMapping("/payment/success")
+ public String sendPaymentSuccessEvent(@RequestBody PaymentEvent event) {
+ event.setPaymentStatus("SUCCESS");
+ streamEventManager.publish("paymentEvent", event);
+ return "Payment success event sent: " + event.getPaymentId();
+ }
+
+ @PostMapping("/delivery")
+ public String sendDeliveryEvent(@RequestBody DeliveryEvent event) {
+ streamEventManager.publish("deliveryEvent", event);
+ return "Delivery event sent: " + event.getDeliveryId();
+ }
+
+ @PostMapping("/delivery/start")
+ public String sendDeliveryStartedEvent(@RequestBody DeliveryEvent event) {
+ event.setStatus("STARTED");
+ streamEventManager.publish("deliveryEvent", event);
+ return "Delivery started event sent: " + event.getDeliveryId();
+ }
+
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/DeliveryEvent.java b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/DeliveryEvent.java
new file mode 100644
index 0000000..9d95836
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/DeliveryEvent.java
@@ -0,0 +1,22 @@
+package cn.structure.infra.sample.stream.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeliveryEvent {
+
+ private String deliveryId;
+
+ private String orderId;
+
+ private String status;
+
+ private String address;
+
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/OrderEvent.java b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/OrderEvent.java
new file mode 100644
index 0000000..3016f9d
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/OrderEvent.java
@@ -0,0 +1,22 @@
+package cn.structure.infra.sample.stream.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OrderEvent {
+
+ private String orderId;
+
+ private String orderNo;
+
+ private String status;
+
+ private Double amount;
+
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/PaymentEvent.java b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/PaymentEvent.java
new file mode 100644
index 0000000..38884fb
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/event/PaymentEvent.java
@@ -0,0 +1,22 @@
+package cn.structure.infra.sample.stream.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PaymentEvent {
+
+ private String paymentId;
+
+ private String orderId;
+
+ private String paymentStatus;
+
+ private Double amount;
+
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/DeliveryEventListener.java b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/DeliveryEventListener.java
new file mode 100644
index 0000000..685f669
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/DeliveryEventListener.java
@@ -0,0 +1,26 @@
+package cn.structure.infra.sample.stream.listener;
+
+import cn.structure.infra.sample.stream.event.DeliveryEvent;
+import cn.structure.infra.stream.annotation.StreamEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DeliveryEventListener {
+
+ private static final Logger log = LoggerFactory.getLogger(DeliveryEventListener.class);
+
+ @StreamEventListener(bindingName = "deliveryEvent", destination = "delivery-exchange", group = "delivery-group")
+ public void handleDeliveryEvent(DeliveryEvent event) {
+ log.info("[配送事件] deliveryId={}, orderId={}, status={}, address={}",
+ event.getDeliveryId(), event.getOrderId(), event.getStatus(), event.getAddress());
+ }
+
+ @StreamEventListener(bindingName = "deliveryEvent", destination = "delivery-exchange", group = "delivery-group", condition = "#event.status == 'STARTED'")
+ public void handleDeliveryStarted(DeliveryEvent event) {
+ log.info("[配送开始] deliveryId={}, orderId={}, address={}",
+ event.getDeliveryId(), event.getOrderId(), event.getAddress());
+ }
+
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/OrderEventListener.java b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/OrderEventListener.java
new file mode 100644
index 0000000..69ea333
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/OrderEventListener.java
@@ -0,0 +1,31 @@
+package cn.structure.infra.sample.stream.listener;
+
+import cn.structure.infra.sample.stream.event.OrderEvent;
+import cn.structure.infra.stream.annotation.StreamEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class OrderEventListener {
+
+ private static final Logger log = LoggerFactory.getLogger(OrderEventListener.class);
+
+ @StreamEventListener(bindingName = "orderEvent", destination = "order-exchange", group = "order-group")
+ public void handleOrderEvent(OrderEvent event) {
+ log.info("[订单事件] orderId={}, orderNo={}, status={}, amount={}",
+ event.getOrderId(), event.getOrderNo(), event.getStatus(), event.getAmount());
+ }
+
+ @StreamEventListener(bindingName = "orderEvent", destination = "order-exchange", group = "order-group", condition = "#event.status == 'CREATED'")
+ public void handleOrderCreated(OrderEvent event) {
+ log.info("[订单创建] orderId={}, orderNo={}, amount={}",
+ event.getOrderId(), event.getOrderNo(), event.getAmount());
+ }
+
+ @StreamEventListener(bindingName = "orderEvent", destination = "order-exchange", group = "order-group", condition = "#event.status == 'PAID'")
+ public void handleOrderPaid(OrderEvent event) {
+ log.info("[订单支付] orderId={}, amount={}", event.getOrderId(), event.getAmount());
+ }
+
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java
new file mode 100644
index 0000000..a706e3d
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java
@@ -0,0 +1,26 @@
+package cn.structure.infra.sample.stream.listener;
+
+import cn.structure.infra.sample.stream.event.PaymentEvent;
+import cn.structure.infra.stream.annotation.StreamEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class PaymentEventListener {
+
+ private static final Logger log = LoggerFactory.getLogger(PaymentEventListener.class);
+
+ @StreamEventListener(bindingName = "paymentEvent", destination = "payment-exchange", group = "payment-group")
+ public void handlePaymentEvent(PaymentEvent event) {
+ log.info("[支付事件] paymentId={}, orderId={}, status={}, amount={}",
+ event.getPaymentId(), event.getOrderId(), event.getPaymentStatus(), event.getAmount());
+ }
+
+ @StreamEventListener(bindingName = "paymentEvent", destination = "payment-exchange", group = "payment-group", condition = "#event.paymentStatus == 'SUCCESS'")
+ public void handlePaymentSuccess(PaymentEvent event) {
+ log.info("[支付成功] paymentId={}, orderId={}, amount={}",
+ event.getPaymentId(), event.getOrderId(), event.getAmount());
+ }
+
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/resources/application.yml b/structure-infra-sample/structure-infra-sample-stream/src/main/resources/application.yml
new file mode 100644
index 0000000..f967b58
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/resources/application.yml
@@ -0,0 +1,26 @@
+server:
+ port: 8088
+
+spring:
+ application:
+ name: structure-infra-sample-stream
+ rabbitmq:
+ addresses: 172.24.20.15:5672
+ username: root
+ password: 123456
+ virtual-host: /
+ cloud:
+ stream:
+ rabbit:
+ binder:
+ admin-addresses: ${spring.rabbitmq.addresses}
+ username: ${spring.rabbitmq.username}
+ password: ${spring.rabbitmq.password}
+ virtual-host: ${spring.rabbitmq.virtual-host}
+ cache:
+ type: simple
+
+structure:
+ infra:
+ stream:
+ enabled: true
\ No newline at end of file
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
new file mode 100644
index 0000000..0a83fc3
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/StreamEventManagerTest.java
@@ -0,0 +1,375 @@
+package cn.structure.infra.sample.stream;
+
+import cn.structure.infra.sample.stream.event.OrderEvent;
+import cn.structure.infra.sample.stream.event.PaymentEvent;
+import cn.structure.infra.stream.manager.DefaultStreamEventManagerImpl;
+import cn.structure.infra.stream.manager.StreamEventManager;
+import cn.structure.infra.stream.properties.StreamProperties;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.cloud.stream.function.StreamBridge;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.mock;
+
+@Slf4j
+public class StreamEventManagerTest {
+
+ private StreamEventManager streamEventManager;
+
+ private StreamProperties streamProperties;
+
+ @BeforeEach
+ public void setUp() {
+ streamProperties = new StreamProperties();
+ StreamProperties.Binding orderBinding = new StreamProperties.Binding();
+ orderBinding.setDestination("order-exchange");
+ orderBinding.setContentType("application/json");
+ orderBinding.setGroup("order-group");
+ streamProperties.getBindings().put("orderEvent", orderBinding);
+
+ StreamProperties.Binding paymentBinding = new StreamProperties.Binding();
+ paymentBinding.setDestination("payment-exchange");
+ paymentBinding.setContentType("application/json");
+ paymentBinding.setGroup("payment-group");
+ streamProperties.getBindings().put("paymentEvent", paymentBinding);
+
+ StreamBridge mockStreamBridge = mock(StreamBridge.class);
+ streamEventManager = new DefaultStreamEventManagerImpl(mockStreamBridge, streamProperties);
+ }
+
+ @Test
+ public void testStreamPropertiesConfiguration() {
+ assertNotNull(streamProperties);
+ assertTrue(streamProperties.isEnabled());
+ assertNotNull(streamProperties.getBindings());
+ assertTrue(streamProperties.getBindings().containsKey("orderEvent"));
+ assertTrue(streamProperties.getBindings().containsKey("paymentEvent"));
+
+ StreamProperties.Binding orderBinding = streamProperties.getBindings().get("orderEvent");
+ assertEquals("order-exchange", orderBinding.getDestination());
+ assertEquals("application/json", orderBinding.getContentType());
+ assertEquals("order-group", orderBinding.getGroup());
+
+ StreamProperties.Binding paymentBinding = streamProperties.getBindings().get("paymentEvent");
+ assertEquals("payment-exchange", paymentBinding.getDestination());
+ assertEquals("application/json", paymentBinding.getContentType());
+ assertEquals("payment-group", paymentBinding.getGroup());
+ }
+
+ @Test
+ public void testStreamEventManagerBeanExists() {
+ assertNotNull(streamEventManager);
+ }
+
+ @Test
+ public void testRegisterListener() {
+ assertFalse(streamEventManager.isListenerRegistered("orderEvent"));
+
+ streamEventManager.registerListener("orderEvent", OrderEvent.class, event -> {});
+
+ assertTrue(streamEventManager.isListenerRegistered("orderEvent"), "orderEvent listener should be registered");
+ }
+
+ @Test
+ public void testUnregisterListener() {
+ streamEventManager.registerListener("orderEvent", OrderEvent.class, event -> {});
+ assertTrue(streamEventManager.isListenerRegistered("orderEvent"));
+
+ streamEventManager.unregisterListener("orderEvent");
+
+ assertFalse(streamEventManager.isListenerRegistered("orderEvent"), "orderEvent listener should be unregistered");
+ }
+
+ @Test
+ public void testRegisterDynamicListener() {
+ String testBindingName = "testDynamicEvent";
+ int[] counter = {0};
+
+ streamEventManager.registerListener(testBindingName, "test-destination", "test-group", OrderEvent.class, event -> {
+ log.info("Dynamic listener received event: {}", event);
+ counter[0]++;
+ });
+
+ assertTrue(streamEventManager.isListenerRegistered(testBindingName));
+
+ streamEventManager.unregisterListener(testBindingName);
+
+ assertFalse(streamEventManager.isListenerRegistered(testBindingName));
+ }
+
+ @Test
+ public void testPublishEventWithRegisteredBinding() {
+ OrderEvent event = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ assertDoesNotThrow(() -> streamEventManager.publish("orderEvent", event));
+ }
+
+ @Test
+ public void testPublishEventWithDestination() {
+ OrderEvent event = OrderEvent.builder()
+ .orderId("ORDER-002")
+ .orderNo("ORD-2024-002")
+ .status("PAID")
+ .amount(200.0)
+ .build();
+
+ assertDoesNotThrow(() -> streamEventManager.publish("orderEvent", "order-exchange", event));
+ }
+
+ @Test
+ public void testPublishPaymentEvent() {
+ PaymentEvent event = PaymentEvent.builder()
+ .paymentId("PAY-001")
+ .orderId("ORDER-001")
+ .paymentStatus("SUCCESS")
+ .amount(100.0)
+ .build();
+
+ assertDoesNotThrow(() -> streamEventManager.publish("paymentEvent", event));
+ }
+
+ @Test
+ public void testPublishEventWithNonExistentBinding() {
+ OrderEvent event = OrderEvent.builder()
+ .orderId("ORDER-003")
+ .orderNo("ORD-2024-003")
+ .status("CANCELLED")
+ .amount(50.0)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> streamEventManager.publish("nonExistentBinding", event));
+ }
+
+ @Test
+ public void testIsListenerRegisteredForNonExistentBinding() {
+ assertFalse(streamEventManager.isListenerRegistered("nonExistentBinding"));
+ }
+
+ @Test
+ public void testRegisterAndUnregisterCycle() {
+ String bindingName = "testCycleEvent";
+
+ streamEventManager.registerListener(bindingName, "test-destination", "test-group", OrderEvent.class, event -> {});
+ assertTrue(streamEventManager.isListenerRegistered(bindingName));
+
+ streamEventManager.unregisterListener(bindingName);
+ assertFalse(streamEventManager.isListenerRegistered(bindingName));
+
+ streamEventManager.registerListener(bindingName, "test-destination", "test-group", PaymentEvent.class, event -> {});
+ assertTrue(streamEventManager.isListenerRegistered(bindingName));
+
+ streamEventManager.unregisterListener(bindingName);
+ assertFalse(streamEventManager.isListenerRegistered(bindingName));
+ }
+
+ @Test
+ public void testRegisterListenerWithAllParameters() {
+ String bindingName = "testFullParams";
+
+ streamEventManager.registerListener(bindingName, "test-destination", "test-group", OrderEvent.class, event -> {});
+
+ assertTrue(streamEventManager.isListenerRegistered(bindingName));
+
+ streamEventManager.unregisterListener(bindingName);
+
+ assertFalse(streamEventManager.isListenerRegistered(bindingName));
+ }
+
+ @Test
+ public void testMultipleListeners() {
+ streamEventManager.registerListener("listener1", "test-destination", "test-group", OrderEvent.class, event -> {});
+ streamEventManager.registerListener("listener2", "test-destination", "test-group", PaymentEvent.class, event -> {});
+ streamEventManager.registerListener("listener3", "test-destination", "test-group", OrderEvent.class, event -> {});
+
+ assertTrue(streamEventManager.isListenerRegistered("listener1"));
+ assertTrue(streamEventManager.isListenerRegistered("listener2"));
+ assertTrue(streamEventManager.isListenerRegistered("listener3"));
+
+ streamEventManager.unregisterListener("listener2");
+
+ assertTrue(streamEventManager.isListenerRegistered("listener1"));
+ assertFalse(streamEventManager.isListenerRegistered("listener2"));
+ assertTrue(streamEventManager.isListenerRegistered("listener3"));
+
+ streamEventManager.unregisterListener("listener1");
+ streamEventManager.unregisterListener("listener3");
+
+ assertFalse(streamEventManager.isListenerRegistered("listener1"));
+ assertFalse(streamEventManager.isListenerRegistered("listener3"));
+ }
+
+ @Test
+ public void testMultipleHandlersOnSameBinding() {
+ String bindingName = "orderEvent";
+ int[] counter1 = {0};
+ int[] counter2 = {0};
+
+ streamEventManager.registerListener(bindingName, "order-exchange", "order-group", OrderEvent.class, event -> {
+ counter1[0]++;
+ });
+
+ streamEventManager.registerListener(bindingName, "order-exchange", "order-group", OrderEvent.class, event -> {
+ counter2[0]++;
+ });
+
+ assertTrue(streamEventManager.isListenerRegistered(bindingName));
+ assertEquals(2, streamEventManager.getListeners(bindingName).size());
+
+ OrderEvent event = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ streamEventManager.dispatch(bindingName, event);
+
+ assertEquals(1, counter1[0]);
+ assertEquals(1, counter2[0]);
+ }
+
+ @Test
+ public void testConditionBasedRouting() {
+ String bindingName = "orderEvent";
+ int[] createdCounter = {0};
+ int[] paidCounter = {0};
+ int[] allCounter = {0};
+
+ streamEventManager.registerListener(bindingName, "order-exchange", "order-group", OrderEvent.class, "#event.status == 'CREATED'", event -> {
+ createdCounter[0]++;
+ });
+
+ streamEventManager.registerListener(bindingName, "order-exchange", "order-group", OrderEvent.class, "#event.status == 'PAID'", event -> {
+ paidCounter[0]++;
+ });
+
+ streamEventManager.registerListener(bindingName, "order-exchange", "order-group", OrderEvent.class, event -> {
+ allCounter[0]++;
+ });
+
+ OrderEvent createdEvent = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ OrderEvent paidEvent = OrderEvent.builder()
+ .orderId("ORDER-002")
+ .orderNo("ORD-2024-002")
+ .status("PAID")
+ .amount(200.0)
+ .build();
+
+ streamEventManager.dispatch(bindingName, createdEvent);
+
+ assertEquals(1, createdCounter[0]);
+ assertEquals(0, paidCounter[0]);
+ assertEquals(1, allCounter[0]);
+
+ streamEventManager.dispatch(bindingName, paidEvent);
+
+ assertEquals(1, createdCounter[0]);
+ assertEquals(1, paidCounter[0]);
+ assertEquals(2, allCounter[0]);
+ }
+
+ @Test
+ public void testDispatchToMultipleHandlersWithDifferentConditions() {
+ String bindingName = "orderEvent";
+ int[] highAmountCounter = {0};
+ int[] lowAmountCounter = {0};
+
+ streamEventManager.registerListener(bindingName, "order-exchange", "order-group", OrderEvent.class, "#event.amount > 100", event -> {
+ highAmountCounter[0]++;
+ });
+
+ streamEventManager.registerListener(bindingName, "order-exchange", "order-group", OrderEvent.class, "#event.amount <= 100", event -> {
+ lowAmountCounter[0]++;
+ });
+
+ OrderEvent highAmountEvent = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(150.0)
+ .build();
+
+ OrderEvent lowAmountEvent = OrderEvent.builder()
+ .orderId("ORDER-002")
+ .orderNo("ORD-2024-002")
+ .status("CREATED")
+ .amount(50.0)
+ .build();
+
+ streamEventManager.dispatch(bindingName, highAmountEvent);
+
+ assertEquals(1, highAmountCounter[0]);
+ assertEquals(0, lowAmountCounter[0]);
+
+ streamEventManager.dispatch(bindingName, lowAmountEvent);
+
+ assertEquals(1, highAmountCounter[0]);
+ assertEquals(1, lowAmountCounter[0]);
+ }
+
+ @Test
+ public void testUnregisterListenerById() {
+ String bindingName = "orderEvent";
+ int[] counter1 = {0};
+ int[] counter2 = {0};
+
+ streamEventManager.registerListener(bindingName, "order-exchange", "order-group", OrderEvent.class, event -> {
+ log.info("Listener 1 triggered");
+ counter1[0]++;
+ });
+
+ streamEventManager.registerListener(bindingName, "order-exchange", "order-group", OrderEvent.class, event -> {
+ counter2[0]++;
+ });
+
+ assertEquals(2, streamEventManager.getListeners(bindingName).size());
+
+ String listenerIdToRemove = streamEventManager.getListeners(bindingName).get(0).getListenerId();
+ streamEventManager.unregisterListener(bindingName, listenerIdToRemove);
+
+ assertEquals(1, streamEventManager.getListeners(bindingName).size());
+ assertTrue(streamEventManager.isListenerRegistered(bindingName));
+
+ OrderEvent event = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ streamEventManager.dispatch(bindingName, event);
+
+ assertEquals(1, counter1[0] + counter2[0]);
+ }
+
+ @Test
+ public void testGetListeners() {
+ String bindingName = "orderEvent";
+
+ assertTrue(streamEventManager.getListeners(bindingName).isEmpty());
+
+ streamEventManager.registerListener(bindingName, "order-exchange", "order-group", OrderEvent.class, event -> {});
+
+ assertEquals(1, streamEventManager.getListeners(bindingName).size());
+
+ streamEventManager.registerListener(bindingName, "order-exchange", "order-group", OrderEvent.class, "#event.status == 'PAID'", event -> {});
+
+ assertEquals(2, streamEventManager.getListeners(bindingName).size());
+
+ assertTrue(streamEventManager.getListeners("nonExistent").isEmpty());
+ }
+
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/StreamEventRouterTest.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/StreamEventRouterTest.java
new file mode 100644
index 0000000..9817155
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/StreamEventRouterTest.java
@@ -0,0 +1,314 @@
+package cn.structure.infra.sample.stream;
+
+import cn.structure.infra.sample.stream.event.OrderEvent;
+import cn.structure.infra.sample.stream.event.PaymentEvent;
+import cn.structure.infra.stream.event.StreamEvent;
+import cn.structure.infra.stream.router.DefaultStreamEventRouterImpl;
+import cn.structure.infra.stream.router.StreamEventRouter;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@Slf4j
+public class StreamEventRouterTest {
+
+ private StreamEventRouter streamEventRouter;
+
+ @BeforeEach
+ public void setUp() {
+ streamEventRouter = new DefaultStreamEventRouterImpl();
+ }
+
+ @Test
+ public void testRouteOrderCreatedEvent() {
+ int[] counter = {0};
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, (payload, event) -> {
+ counter[0]++;
+ });
+
+ OrderEvent orderEvent = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ StreamEvent streamEvent = StreamEvent.of("orderCreated", orderEvent);
+ streamEventRouter.route(streamEvent);
+
+ assertEquals(1, counter[0]);
+ }
+
+ @Test
+ public void testRoutePaymentSuccessEvent() {
+ int[] counter = {0};
+
+ streamEventRouter.registerRoute("paymentSuccess", PaymentEvent.class, (payload, event) -> {
+ counter[0]++;
+ });
+
+ PaymentEvent paymentEvent = PaymentEvent.builder()
+ .paymentId("PAY-001")
+ .orderId("ORDER-001")
+ .paymentStatus("SUCCESS")
+ .amount(100.0)
+ .build();
+
+ StreamEvent streamEvent = StreamEvent.of("paymentSuccess", paymentEvent);
+ streamEventRouter.route(streamEvent);
+
+ assertEquals(1, counter[0]);
+ }
+
+ @Test
+ public void testRouteMultipleEventTypes() {
+ int[] orderCounter = {0};
+ int[] paymentCounter = {0};
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, (payload, event) -> {
+ orderCounter[0]++;
+ });
+
+ streamEventRouter.registerRoute("paymentSuccess", PaymentEvent.class, (payload, event) -> {
+ paymentCounter[0]++;
+ });
+
+ OrderEvent orderEvent = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ PaymentEvent paymentEvent = PaymentEvent.builder()
+ .paymentId("PAY-001")
+ .orderId("ORDER-001")
+ .paymentStatus("SUCCESS")
+ .amount(100.0)
+ .build();
+
+ streamEventRouter.route(StreamEvent.of("orderCreated", orderEvent));
+ streamEventRouter.route(StreamEvent.of("paymentSuccess", paymentEvent));
+
+ assertEquals(1, orderCounter[0]);
+ assertEquals(1, paymentCounter[0]);
+ }
+
+ @Test
+ public void testRouteWithCondition() {
+ int[] highAmountCounter = {0};
+ int[] lowAmountCounter = {0};
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, "#payload.amount > 1000", (payload, event) -> {
+ highAmountCounter[0]++;
+ });
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, "#payload.amount <= 1000", (payload, event) -> {
+ lowAmountCounter[0]++;
+ });
+
+ OrderEvent highAmountOrder = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(2000.0)
+ .build();
+
+ OrderEvent lowAmountOrder = OrderEvent.builder()
+ .orderId("ORDER-002")
+ .orderNo("ORD-2024-002")
+ .status("CREATED")
+ .amount(500.0)
+ .build();
+
+ streamEventRouter.route(StreamEvent.of("orderCreated", highAmountOrder));
+ streamEventRouter.route(StreamEvent.of("orderCreated", lowAmountOrder));
+
+ assertEquals(1, highAmountCounter[0]);
+ assertEquals(1, lowAmountCounter[0]);
+ }
+
+ @Test
+ public void testRouteWithBusinessType() {
+ int[] retailCounter = {0};
+ int[] wholesaleCounter = {0};
+ int[] allCounter = {0};
+
+ streamEventRouter.registerRoute("orderCreated", "retail", OrderEvent.class, (payload, event) -> {
+ retailCounter[0]++;
+ });
+
+ streamEventRouter.registerRoute("orderCreated", "wholesale", OrderEvent.class, (payload, event) -> {
+ wholesaleCounter[0]++;
+ });
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, (payload, event) -> {
+ allCounter[0]++;
+ });
+
+ OrderEvent retailOrder = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ OrderEvent wholesaleOrder = OrderEvent.builder()
+ .orderId("ORDER-002")
+ .orderNo("ORD-2024-002")
+ .status("CREATED")
+ .amount(5000.0)
+ .build();
+
+ streamEventRouter.route(StreamEvent.of("orderCreated", "retail", retailOrder));
+ streamEventRouter.route(StreamEvent.of("orderCreated", "wholesale", wholesaleOrder));
+
+ assertEquals(1, retailCounter[0]);
+ assertEquals(1, wholesaleCounter[0]);
+ assertEquals(2, allCounter[0]);
+ }
+
+ @Test
+ public void testRouteWithMultipleHandlers() {
+ int[] handler1Counter = {0};
+ int[] handler2Counter = {0};
+ int[] handler3Counter = {0};
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, (payload, event) -> {
+ handler1Counter[0]++;
+ });
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, "#payload.amount > 500", (payload, event) -> {
+ handler2Counter[0]++;
+ });
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, "#payload.amount > 1000", (payload, event) -> {
+ handler3Counter[0]++;
+ });
+
+ OrderEvent highAmountOrder = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(2000.0)
+ .build();
+
+ streamEventRouter.route(StreamEvent.of("orderCreated", highAmountOrder));
+
+ assertEquals(1, handler1Counter[0]);
+ assertEquals(1, handler2Counter[0]);
+ assertEquals(1, handler3Counter[0]);
+ }
+
+ @Test
+ public void testUnregisterRoute() {
+ int[] counter = {0};
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, (payload, event) -> {
+ counter[0]++;
+ });
+
+ assertEquals(1, streamEventRouter.getRoutes("orderCreated").size());
+
+ streamEventRouter.unregisterRoute("orderCreated");
+
+ assertEquals(0, streamEventRouter.getRoutes("orderCreated").size());
+
+ OrderEvent orderEvent = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ streamEventRouter.route(StreamEvent.of("orderCreated", orderEvent));
+
+ assertEquals(0, counter[0]);
+ }
+
+ @Test
+ public void testUnregisterRouteById() {
+ int[] counter1 = {0};
+ int[] counter2 = {0};
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, (payload, event) -> {
+ counter1[0]++;
+ });
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, (payload, event) -> {
+ counter2[0]++;
+ });
+
+ assertEquals(2, streamEventRouter.getRoutes("orderCreated").size());
+
+ String handlerIdToRemove = streamEventRouter.getRoutes("orderCreated").get(0).getHandlerId();
+ streamEventRouter.unregisterRoute("orderCreated", handlerIdToRemove);
+
+ assertEquals(1, streamEventRouter.getRoutes("orderCreated").size());
+
+ OrderEvent orderEvent = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ streamEventRouter.route(StreamEvent.of("orderCreated", orderEvent));
+
+ assertEquals(1, counter1[0] + counter2[0]);
+ }
+
+ @Test
+ public void testGetRoutes() {
+ assertTrue(streamEventRouter.getRoutes("orderCreated").isEmpty());
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, (payload, event) -> {});
+
+ assertEquals(1, streamEventRouter.getRoutes("orderCreated").size());
+
+ streamEventRouter.registerRoute("orderCreated", OrderEvent.class, "#payload.amount > 1000", (payload, event) -> {});
+
+ assertEquals(2, streamEventRouter.getRoutes("orderCreated").size());
+
+ assertTrue(streamEventRouter.getRoutes("nonExistent").isEmpty());
+ }
+
+ @Test
+ public void testRouteNonExistentEventType() {
+ OrderEvent orderEvent = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ assertDoesNotThrow(() -> streamEventRouter.route(StreamEvent.of("nonExistent", orderEvent)));
+ }
+
+ @Test
+ public void testRouteWithNullEvent() {
+ assertDoesNotThrow(() -> streamEventRouter.route(null));
+ }
+
+ @Test
+ public void testRouteWithNullEventType() {
+ OrderEvent orderEvent = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ StreamEvent streamEvent = StreamEvent.builder()
+ .eventId("test-id")
+ .eventType(null)
+ .payload(orderEvent)
+ .build();
+
+ assertDoesNotThrow(() -> streamEventRouter.route(streamEvent));
+ }
+
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/config/StreamTestConfig.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/config/StreamTestConfig.java
new file mode 100644
index 0000000..5fdf62c
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/config/StreamTestConfig.java
@@ -0,0 +1,18 @@
+package cn.structure.infra.sample.stream.config;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@SpringBootApplication(excludeName = {
+ "org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration",
+ "org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration"
+})
+@ComponentScan(basePackages = "cn.structure.infra.sample.stream")
+public class StreamTestConfig {
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/controller/StreamControllerTest.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/controller/StreamControllerTest.java
new file mode 100644
index 0000000..21707ec
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/controller/StreamControllerTest.java
@@ -0,0 +1,155 @@
+package cn.structure.infra.sample.stream.controller;
+
+import cn.structure.infra.sample.stream.event.DeliveryEvent;
+import cn.structure.infra.sample.stream.event.OrderEvent;
+import cn.structure.infra.sample.stream.event.PaymentEvent;
+import cn.structure.infra.stream.manager.StreamEventManager;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@ExtendWith(MockitoExtension.class)
+public class StreamControllerTest {
+
+ @Mock
+ private StreamEventManager streamEventManager;
+
+ private MockMvc mockMvc;
+
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ public void setUp() {
+ StreamController controller = new StreamController(streamEventManager);
+ mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+ objectMapper = new ObjectMapper();
+ }
+
+ @Test
+ public void testSendOrderEvent() throws Exception {
+ OrderEvent event = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .status("CREATED")
+ .amount(100.0)
+ .build();
+
+ mockMvc.perform(post("/api/stream/order")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(event)))
+ .andExpect(status().isOk())
+ .andExpect(content().string("Order event sent: ORDER-001"));
+
+ verify(streamEventManager).publish(eq("orderEvent"), any(OrderEvent.class));
+ }
+
+ @Test
+ public void testSendOrderCreatedEvent() throws Exception {
+ mockMvc.perform(post("/api/stream/order/create"))
+ .andExpect(status().isOk())
+ .andExpect(content().string(org.hamcrest.Matchers.startsWith("Order created event sent: ORDER-")));
+
+ verify(streamEventManager).publish(eq("orderEvent"), any(OrderEvent.class));
+ }
+
+ @Test
+ public void testSendOrderPaidEvent() throws Exception {
+ OrderEvent event = OrderEvent.builder()
+ .orderId("ORDER-001")
+ .orderNo("ORD-2024-001")
+ .amount(200.0)
+ .build();
+
+ mockMvc.perform(post("/api/stream/order/pay")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(event)))
+ .andExpect(status().isOk())
+ .andExpect(content().string("Order paid event sent: ORDER-001"));
+
+ verify(streamEventManager).publish(eq("orderEvent"), any(OrderEvent.class));
+ }
+
+ @Test
+ public void testSendPaymentEvent() throws Exception {
+ PaymentEvent event = PaymentEvent.builder()
+ .paymentId("PAY-001")
+ .orderId("ORDER-001")
+ .paymentStatus("PENDING")
+ .amount(100.0)
+ .build();
+
+ mockMvc.perform(post("/api/stream/payment")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(event)))
+ .andExpect(status().isOk())
+ .andExpect(content().string("Payment event sent: PAY-001"));
+
+ verify(streamEventManager).publish(eq("paymentEvent"), any(PaymentEvent.class));
+ }
+
+ @Test
+ public void testSendPaymentSuccessEvent() throws Exception {
+ PaymentEvent event = PaymentEvent.builder()
+ .paymentId("PAY-001")
+ .orderId("ORDER-001")
+ .amount(100.0)
+ .build();
+
+ mockMvc.perform(post("/api/stream/payment/success")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(event)))
+ .andExpect(status().isOk())
+ .andExpect(content().string("Payment success event sent: PAY-001"));
+
+ verify(streamEventManager).publish(eq("paymentEvent"), any(PaymentEvent.class));
+ }
+
+ @Test
+ public void testSendDeliveryEvent() throws Exception {
+ DeliveryEvent event = DeliveryEvent.builder()
+ .deliveryId("DEL-001")
+ .orderId("ORDER-001")
+ .status("PREPARING")
+ .address("北京市朝阳区")
+ .build();
+
+ mockMvc.perform(post("/api/stream/delivery")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(event)))
+ .andExpect(status().isOk())
+ .andExpect(content().string("Delivery event sent: DEL-001"));
+
+ verify(streamEventManager).publish(eq("deliveryEvent"), any(DeliveryEvent.class));
+ }
+
+ @Test
+ public void testSendDeliveryStartedEvent() throws Exception {
+ DeliveryEvent event = DeliveryEvent.builder()
+ .deliveryId("DEL-001")
+ .orderId("ORDER-001")
+ .address("北京市朝阳区")
+ .build();
+
+ mockMvc.perform(post("/api/stream/delivery/start")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(event)))
+ .andExpect(status().isOk())
+ .andExpect(content().string("Delivery started event sent: DEL-001"));
+
+ verify(streamEventManager).publish(eq("deliveryEvent"), any(DeliveryEvent.class));
+ }
+
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/dynamic/DynamicListenerDemo.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/dynamic/DynamicListenerDemo.java
new file mode 100644
index 0000000..171ebac
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/dynamic/DynamicListenerDemo.java
@@ -0,0 +1,161 @@
+package cn.structure.infra.sample.stream.dynamic;
+
+import cn.structure.infra.sample.stream.event.OrderEvent;
+import cn.structure.infra.sample.stream.event.PaymentEvent;
+import cn.structure.infra.stream.event.StreamEvent;
+import cn.structure.infra.stream.handler.StreamEventHandler;
+import cn.structure.infra.stream.manager.ListenerRegistration;
+import cn.structure.infra.stream.manager.StreamEventManager;
+import cn.structure.infra.stream.router.StreamEventRouter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Component
+public class DynamicListenerDemo implements CommandLineRunner {
+
+ private static final Logger log = LoggerFactory.getLogger(DynamicListenerDemo.class);
+
+ private final StreamEventManager eventManager;
+ private final StreamEventRouter eventRouter;
+
+ public static final AtomicInteger dynamicOrderCount = new AtomicInteger(0);
+ public static final AtomicInteger dynamicPaymentCount = new AtomicInteger(0);
+ public static final AtomicInteger dynamicGenericCount = new AtomicInteger(0);
+
+ public DynamicListenerDemo(StreamEventManager eventManager, StreamEventRouter eventRouter) {
+ this.eventManager = eventManager;
+ this.eventRouter = eventRouter;
+ }
+
+ @Override
+ public void run(String... args) {
+ log.info("========== 运行时动态注册监听器示例 ==========");
+
+ registerDynamicListeners();
+ registerDynamicRoutes();
+
+ testDynamicListeners();
+
+ log.info("========== 动态注册监听器示例完成 ==========");
+ }
+
+ private void registerDynamicListeners() {
+ log.info("\n--- 方式一:通过 StreamEventManager 注册监听器 ---");
+
+ eventManager.registerBinding("dynamicOrder", "dynamic-order-exchange", "dynamic-group");
+
+ eventManager.registerListener("dynamicOrder", OrderEvent.class, new OrderEventHandler());
+
+ eventManager.registerListener("dynamicOrder", OrderEvent.class, event -> {
+ log.info("[Lambda方式] 动态监听订单: orderId={}", event.getOrderId());
+ dynamicOrderCount.incrementAndGet();
+ });
+
+ eventManager.registerListener("dynamicOrder", OrderEvent.class, "#payload.amount > 500", event -> {
+ log.info("[Lambda+条件] 动态监听大额订单: orderId={}, amount={}", event.getOrderId(), event.getAmount());
+ });
+
+ eventManager.registerListener("dynamicPayment", "dynamic-payment-exchange", "payment-group",
+ PaymentEvent.class, new PaymentEventHandler());
+ }
+
+ private void registerDynamicRoutes() {
+ log.info("\n--- 方式二:通过 StreamEventRouter 注册路由 ---");
+
+ eventRouter.registerRoute("dynamicOrderCreated", OrderEvent.class, new DynamicRouteHandler());
+
+ eventRouter.registerRoute("dynamicPaymentSuccess", PaymentEvent.class, (payload, event) -> {
+ log.info("[Lambda路由] 动态路由支付成功: paymentId={}", payload.getPaymentId());
+ dynamicPaymentCount.incrementAndGet();
+ });
+
+ eventRouter.registerRoute("dynamicGeneric", Object.class, (payload, event) -> {
+ log.info("[通用路由] 动态路由通用事件: payload={}", payload);
+ dynamicGenericCount.incrementAndGet();
+ });
+
+ eventRouter.registerRoute("dynamicWithBusiness", "retail", OrderEvent.class, (payload, event) -> {
+ log.info("[业务路由] 动态路由零售订单: orderId={}, businessType={}",
+ payload.getOrderId(), event.getBusinessType());
+ });
+ }
+
+ private void testDynamicListeners() {
+ log.info("\n--- 测试动态注册的监听器 ---");
+
+ OrderEvent orderEvent = OrderEvent.builder()
+ .orderId("DYN-001")
+ .orderNo("DYN-ORD-2024-001")
+ .status("CREATED")
+ .amount(800.0)
+ .build();
+
+ eventManager.dispatch("dynamicOrder", orderEvent);
+
+ eventRouter.route(StreamEvent.of("dynamicOrderCreated", orderEvent));
+
+ PaymentEvent paymentEvent = PaymentEvent.builder()
+ .paymentId("DYN-PAY-001")
+ .orderId("DYN-001")
+ .paymentStatus("SUCCESS")
+ .amount(800.0)
+ .build();
+
+ eventRouter.route(StreamEvent.of("dynamicPaymentSuccess", paymentEvent));
+
+ eventRouter.route(StreamEvent.of("dynamicGeneric", "test-message"));
+
+ eventRouter.route(StreamEvent.builder()
+ .eventType("dynamicWithBusiness")
+ .businessType("retail")
+ .payload(orderEvent)
+ .build());
+
+ logStats();
+ }
+
+ private void logStats() {
+ log.info("\n--- 统计信息 ---");
+ log.info("StreamEventManager 监听器数量:");
+ List> orderListeners = eventManager.getListeners("dynamicOrder");
+ log.info(" - dynamicOrder: {} 个监听器", orderListeners.size());
+
+ log.info("StreamEventRouter 路由数量:");
+ log.info(" - dynamicOrderCreated: {} 个路由", eventRouter.getRoutes("dynamicOrderCreated").size());
+ log.info(" - dynamicPaymentSuccess: {} 个路由", eventRouter.getRoutes("dynamicPaymentSuccess").size());
+
+ log.info("处理计数:");
+ log.info(" - 动态订单计数: {}", dynamicOrderCount.get());
+ log.info(" - 动态支付计数: {}", dynamicPaymentCount.get());
+ log.info(" - 动态通用计数: {}", dynamicGenericCount.get());
+ }
+
+ public static class OrderEventHandler implements StreamEventHandler {
+ @Override
+ public void handle(OrderEvent event) {
+ log.info("[实现类方式] 动态监听订单: orderId={}, status={}", event.getOrderId(), event.getStatus());
+ dynamicOrderCount.incrementAndGet();
+ }
+ }
+
+ public static class PaymentEventHandler implements StreamEventHandler {
+ @Override
+ public void handle(PaymentEvent event) {
+ log.info("[实现类方式] 动态监听支付: paymentId={}, status={}", event.getPaymentId(), event.getPaymentStatus());
+ dynamicPaymentCount.incrementAndGet();
+ }
+ }
+
+ public static class DynamicRouteHandler implements StreamEventRouter.StreamRouteHandler {
+ @Override
+ public void handle(OrderEvent payload, StreamEvent event) {
+ log.info("[实现类路由] 动态路由订单: orderId={}, eventType={}", payload.getOrderId(), event.getEventType());
+ dynamicOrderCount.incrementAndGet();
+ }
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/DeliveryEvent.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/DeliveryEvent.java
new file mode 100644
index 0000000..575537f
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/DeliveryEvent.java
@@ -0,0 +1,86 @@
+package cn.structure.infra.sample.stream.event;
+
+public class DeliveryEvent {
+
+ private String deliveryId;
+ private String orderId;
+ private String status;
+ private String address;
+
+ public DeliveryEvent() {
+ }
+
+ public DeliveryEvent(String deliveryId, String orderId, String status, String address) {
+ this.deliveryId = deliveryId;
+ this.orderId = orderId;
+ this.status = status;
+ this.address = address;
+ }
+
+ public String getDeliveryId() {
+ return deliveryId;
+ }
+
+ public void setDeliveryId(String deliveryId) {
+ this.deliveryId = deliveryId;
+ }
+
+ public String getOrderId() {
+ return orderId;
+ }
+
+ public void setOrderId(String orderId) {
+ this.orderId = orderId;
+ }
+
+ public String getStatus() {
+ return status;
+ }
+
+ public void setStatus(String status) {
+ this.status = status;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String deliveryId;
+ private String orderId;
+ private String status;
+ private String address;
+
+ public Builder deliveryId(String deliveryId) {
+ this.deliveryId = deliveryId;
+ return this;
+ }
+
+ public Builder orderId(String orderId) {
+ this.orderId = orderId;
+ return this;
+ }
+
+ public Builder status(String status) {
+ this.status = status;
+ return this;
+ }
+
+ public Builder address(String address) {
+ this.address = address;
+ return this;
+ }
+
+ public DeliveryEvent build() {
+ return new DeliveryEvent(deliveryId, orderId, status, address);
+ }
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/OrderEvent.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/OrderEvent.java
new file mode 100644
index 0000000..80dfc89
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/OrderEvent.java
@@ -0,0 +1,22 @@
+package cn.structure.infra.sample.stream.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OrderEvent {
+
+ private String orderId;
+
+ private String orderNo;
+
+ private String status;
+
+ private Double amount;
+
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/PaymentEvent.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/PaymentEvent.java
new file mode 100644
index 0000000..1bd3d4f
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/event/PaymentEvent.java
@@ -0,0 +1,22 @@
+package cn.structure.infra.sample.stream.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PaymentEvent {
+
+ private String paymentId;
+
+ private String orderId;
+
+ private String paymentStatus;
+
+ private Double amount;
+
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/DeliveryEventListener.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/DeliveryEventListener.java
new file mode 100644
index 0000000..cef46d1
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/DeliveryEventListener.java
@@ -0,0 +1,33 @@
+package cn.structure.infra.sample.stream.listener;
+
+import cn.structure.infra.sample.stream.event.DeliveryEvent;
+import cn.structure.infra.stream.annotation.StreamEventListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Component
+public class DeliveryEventListener {
+
+ private static final Logger log = LoggerFactory.getLogger(DeliveryEventListener.class);
+
+ public static final AtomicInteger deliveryCreatedCount = new AtomicInteger(0);
+ public static final AtomicInteger deliveryCompletedCount = new AtomicInteger(0);
+ public static final AtomicReference lastDeliveryEvent = new AtomicReference<>();
+
+ @StreamEventListener(bindingName = "deliveryEvent", destination = "delivery-exchange", group = "delivery-group")
+ public void handleDeliveryCreated(DeliveryEvent event) {
+ log.info("[配送创建] deliveryId={}, orderId={}, status={}",
+ event.getDeliveryId(), event.getOrderId(), event.getStatus());
+ if ("CREATED".equals(event.getStatus())) {
+ deliveryCreatedCount.incrementAndGet();
+ } else if ("COMPLETED".equals(event.getStatus())) {
+ deliveryCompletedCount.incrementAndGet();
+ }
+ lastDeliveryEvent.set(event);
+ }
+
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/OrderEventListener.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/OrderEventListener.java
new file mode 100644
index 0000000..50984cb
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/OrderEventListener.java
@@ -0,0 +1,51 @@
+package cn.structure.infra.sample.stream.listener;
+
+import cn.structure.infra.sample.stream.event.OrderEvent;
+import cn.structure.infra.stream.annotation.StreamRouteHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Component
+public class OrderEventListener {
+
+ private static final Logger log = LoggerFactory.getLogger(OrderEventListener.class);
+
+ public static final AtomicInteger orderCreatedCount = new AtomicInteger(0);
+ public static final AtomicInteger orderPaidCount = new AtomicInteger(0);
+ public static final AtomicInteger orderCancelledCount = new AtomicInteger(0);
+ public static final AtomicInteger highAmountOrderCount = new AtomicInteger(0);
+ public static final AtomicReference lastOrderEvent = new AtomicReference<>();
+
+ @StreamRouteHandler(eventType = "orderCreated")
+ public void handleOrderCreated(OrderEvent event) {
+ log.info("[订单创建] orderId={}, orderNo={}, amount={}",
+ event.getOrderId(), event.getOrderNo(), event.getAmount());
+ orderCreatedCount.incrementAndGet();
+ lastOrderEvent.set(event);
+ }
+
+ @StreamRouteHandler(eventType = "orderPaid")
+ public void handleOrderPaid(OrderEvent event) {
+ log.info("[订单支付] orderId={}, amount={}", event.getOrderId(), event.getAmount());
+ orderPaidCount.incrementAndGet();
+ lastOrderEvent.set(event);
+ }
+
+ @StreamRouteHandler(eventType = "orderCancelled")
+ public void handleOrderCancelled(OrderEvent event) {
+ log.info("[订单取消] orderId={}", event.getOrderId());
+ orderCancelledCount.incrementAndGet();
+ lastOrderEvent.set(event);
+ }
+
+ @StreamRouteHandler(eventType = "orderCreated", condition = "#payload.amount > 1000")
+ public void handleHighAmountOrder(OrderEvent event) {
+ log.info("[高额订单] orderId={}, amount={}", event.getOrderId(), event.getAmount());
+ highAmountOrderCount.incrementAndGet();
+ }
+
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java
new file mode 100644
index 0000000..748008a
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java
@@ -0,0 +1,35 @@
+package cn.structure.infra.sample.stream.listener;
+
+import cn.structure.infra.sample.stream.event.PaymentEvent;
+import cn.structure.infra.stream.annotation.StreamRouteHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Slf4j
+@Component
+public class PaymentEventListener {
+
+ public static final AtomicInteger paymentSuccessCount = new AtomicInteger(0);
+ public static final AtomicInteger paymentFailedCount = new AtomicInteger(0);
+ public static final AtomicReference lastPaymentEvent = new AtomicReference<>();
+
+ @StreamRouteHandler(eventType = "paymentSuccess")
+ public void handlePaymentSuccess(PaymentEvent event) {
+ log.info("[支付成功] paymentId={}, orderId={}, amount={}",
+ event.getPaymentId(), event.getOrderId(), event.getAmount());
+ paymentSuccessCount.incrementAndGet();
+ lastPaymentEvent.set(event);
+ }
+
+ @StreamRouteHandler(eventType = "paymentFailed")
+ public void handlePaymentFailed(PaymentEvent event) {
+ log.info("[支付失败] paymentId={}, orderId={}, status={}",
+ event.getPaymentId(), event.getOrderId(), event.getPaymentStatus());
+ paymentFailedCount.incrementAndGet();
+ lastPaymentEvent.set(event);
+ }
+
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/lowcode/LowCodeConfigDemo.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/lowcode/LowCodeConfigDemo.java
new file mode 100644
index 0000000..e8327ee
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/lowcode/LowCodeConfigDemo.java
@@ -0,0 +1,32 @@
+package cn.structure.infra.sample.stream.lowcode;
+
+import cn.structure.infra.sample.stream.event.OrderEvent;
+import cn.structure.infra.sample.stream.event.PaymentEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Component("lowCodeHandler")
+public class LowCodeConfigDemo {
+
+ private static final Logger log = LoggerFactory.getLogger(LowCodeConfigDemo.class);
+
+ public static final AtomicInteger configOrderCreatedCount = new AtomicInteger(0);
+ public static final AtomicInteger configPaymentSuccessCount = new AtomicInteger(0);
+
+ public void onOrderCreated(OrderEvent event) {
+ log.info("[配置驱动] 订单创建处理: orderId={}, amount={}", event.getOrderId(), event.getAmount());
+ configOrderCreatedCount.incrementAndGet();
+ }
+
+ public void onPaymentSuccess(PaymentEvent event) {
+ log.info("[配置驱动] 支付成功处理: paymentId={}, orderId={}", event.getPaymentId(), event.getOrderId());
+ configPaymentSuccessCount.incrementAndGet();
+ }
+
+ public void onHighAmountOrder(OrderEvent event) {
+ log.info("[配置驱动-高额订单] 订单金额超过阈值: orderId={}, amount={}", event.getOrderId(), event.getAmount());
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/lowcode/LowCodeRouteConfig.java b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/lowcode/LowCodeRouteConfig.java
new file mode 100644
index 0000000..15d3432
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/java/cn/structure/infra/sample/stream/lowcode/LowCodeRouteConfig.java
@@ -0,0 +1,94 @@
+package cn.structure.infra.sample.stream.lowcode;
+
+import cn.structure.infra.sample.stream.event.OrderEvent;
+import cn.structure.infra.sample.stream.event.PaymentEvent;
+import cn.structure.infra.stream.event.StreamEvent;
+import cn.structure.infra.stream.router.StreamEventRouter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+@Component
+public class LowCodeRouteConfig implements CommandLineRunner {
+
+ private static final Logger log = LoggerFactory.getLogger(LowCodeRouteConfig.class);
+
+ private final StreamEventRouter eventRouter;
+
+ public LowCodeRouteConfig(StreamEventRouter eventRouter) {
+ this.eventRouter = eventRouter;
+ }
+
+ @Override
+ public void run(String... args) {
+ log.info("========== 低代码路由注册示例 ==========");
+
+ registerOrderRoutes();
+ registerPaymentRoutes();
+
+ log.info("========== 低代码路由注册完成 ==========");
+
+ testLowCodeRoutes();
+ }
+
+ private void registerOrderRoutes() {
+ eventRouter.registerRoute("orderCreated", OrderEvent.class, (payload, event) -> {
+ log.info("[低代码-订单创建] orderId={}, amount={}", payload.getOrderId(), payload.getAmount());
+ });
+
+ eventRouter.registerRoute("orderCreated", OrderEvent.class, "#payload.amount > 1000", (payload, event) -> {
+ log.info("[低代码-高额订单] orderId={}, amount={}, 触发风控检查", payload.getOrderId(), payload.getAmount());
+ });
+
+ eventRouter.registerRoute("orderPaid", OrderEvent.class, (payload, event) -> {
+ log.info("[低代码-订单支付] orderId={}, status={}", payload.getOrderId(), payload.getStatus());
+ });
+
+ eventRouter.registerRoute("orderCancelled", OrderEvent.class, (payload, event) -> {
+ log.info("[低代码-订单取消] orderId={}", payload.getOrderId());
+ });
+ }
+
+ private void registerPaymentRoutes() {
+ eventRouter.registerRoute("paymentSuccess", PaymentEvent.class, (payload, event) -> {
+ log.info("[低代码-支付成功] paymentId={}, orderId={}", payload.getPaymentId(), payload.getOrderId());
+ });
+
+ eventRouter.registerRoute("paymentFailed", PaymentEvent.class, (payload, event) -> {
+ log.info("[低代码-支付失败] paymentId={}, status={}", payload.getPaymentId(), payload.getPaymentStatus());
+ });
+ }
+
+ private void testLowCodeRoutes() {
+ log.info("========== 低代码路由测试 ==========");
+
+ OrderEvent normalOrder = OrderEvent.builder()
+ .orderId("LC-001")
+ .orderNo("LC-ORD-2024-001")
+ .status("CREATED")
+ .amount(500.0)
+ .build();
+
+ OrderEvent highAmountOrder = OrderEvent.builder()
+ .orderId("LC-002")
+ .orderNo("LC-ORD-2024-002")
+ .status("CREATED")
+ .amount(2000.0)
+ .build();
+
+ eventRouter.route(StreamEvent.of("orderCreated", normalOrder));
+ eventRouter.route(StreamEvent.of("orderCreated", highAmountOrder));
+
+ PaymentEvent paymentEvent = PaymentEvent.builder()
+ .paymentId("LC-PAY-001")
+ .orderId("LC-001")
+ .paymentStatus("SUCCESS")
+ .amount(500.0)
+ .build();
+
+ eventRouter.route(StreamEvent.of("paymentSuccess", paymentEvent));
+
+ log.info("========== 低代码路由测试完成 ==========");
+ }
+}
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/test/resources/application.yml b/structure-infra-sample/structure-infra-sample-stream/src/test/resources/application.yml
new file mode 100644
index 0000000..ea514fe
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-stream/src/test/resources/application.yml
@@ -0,0 +1,20 @@
+spring:
+ autoconfigure:
+ exclude:
+ - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
+ - org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
+ - org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration
+ - org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration
+ - org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration
+ - org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration
+ cloud:
+ stream:
+ default-binder: test
+
+structure:
+ infra:
+ stream:
+ enabled: true
+ auto-binding: true
+ default-group: test-group
+ default-concurrency: 1
diff --git a/structure-infra-sample/structure-infra-sample-xxljob/pom.xml b/structure-infra-sample/structure-infra-sample-xxljob/pom.xml
new file mode 100644
index 0000000..764ebf2
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-xxljob/pom.xml
@@ -0,0 +1,52 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-infra-sample
+ ${revision}
+ ../pom.xml
+
+
+ structure-infra-sample-xxljob
+ structure-infra-sample-xxljob
+ XXL-Job 示例模块
+ jar
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ cn.structured
+ structure-infra-xxljob-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/SampleXxlJob.java b/structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/SampleXxlJob.java
new file mode 100644
index 0000000..6b84f3d
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/SampleXxlJob.java
@@ -0,0 +1,34 @@
+package cn.structure.infra.sample.xxljob;
+
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Slf4j
+@Component
+public class SampleXxlJob {
+
+ private final AtomicInteger counter = new AtomicInteger(0);
+
+ @XxlJob("sampleJobHandler")
+ public void sampleJobHandler() {
+ int count = counter.incrementAndGet();
+
+ log.info("SampleXxlJob executed, count={}", count);
+ }
+
+ @XxlJob("simpleJobHandler")
+ public void simpleJobHandler() {
+ log.info("SimpleXxlJob executed");
+ }
+
+ public int getCounter() {
+ return counter.get();
+ }
+
+ public void resetCounter() {
+ counter.set(0);
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/XxlJobApplication.java b/structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/XxlJobApplication.java
new file mode 100644
index 0000000..9eef0c0
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/XxlJobApplication.java
@@ -0,0 +1,12 @@
+package cn.structure.infra.sample.xxljob;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class XxlJobApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(XxlJobApplication.class, args);
+ }
+}
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
new file mode 100644
index 0000000..6afc01f
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-xxljob/src/main/java/cn/structure/infra/sample/xxljob/controller/JobManagerController.java
@@ -0,0 +1,107 @@
+package cn.structure.infra.sample.xxljob.controller;
+
+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 java.util.List;
+
+@RestController
+@RequestMapping("/job")
+public class JobManagerController {
+
+ @Resource
+ private TaskScheduler taskScheduler;
+
+ @PostMapping("/add")
+ public ScheduleTask add(
+ @RequestParam("taskId") String taskId,
+ @RequestParam("taskName") String taskName,
+ @RequestParam("handlerName") String handlerName,
+ @RequestParam(value = "handlerParam", required = false) String handlerParam,
+ @RequestParam(value = "cronExpression", defaultValue = "0/5 * * * * ?") String cronExpression) {
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId(taskId)
+ .taskName(taskName)
+ .handlerName(handlerName)
+ .handlerParam(handlerParam)
+ .scheduleType(ScheduleTask.ScheduleType.CRON)
+ .cronExpression(cronExpression)
+ .build();
+
+ taskScheduler.schedule(task);
+ return taskScheduler.getTaskInfo(taskId);
+ }
+
+ @PutMapping("/update/{taskId}")
+ public ScheduleTask update(
+ @PathVariable("taskId") String taskId,
+ @RequestParam(value = "taskName", required = false) String taskName,
+ @RequestParam(value = "handlerName", required = false) String handlerName,
+ @RequestParam(value = "handlerParam", required = false) String handlerParam,
+ @RequestParam(value = "cronExpression", required = false) String cronExpression) {
+
+ ScheduleTask existingTask = taskScheduler.getTaskInfo(taskId);
+ if (existingTask == null) {
+ throw new IllegalArgumentException("Task not found: " + taskId);
+ }
+
+ ScheduleTask.ScheduleTaskBuilder builder = ScheduleTask.builder()
+ .taskId(taskId)
+ .taskName(taskName != null ? taskName : existingTask.getTaskName())
+ .handlerName(handlerName != null ? handlerName : existingTask.getHandlerName())
+ .handlerParam(handlerParam != null ? handlerParam : existingTask.getHandlerParam())
+ .scheduleType(existingTask.getScheduleType());
+
+ if (cronExpression != null) {
+ builder.cronExpression(cronExpression);
+ } else if (existingTask.getCronExpression() != null) {
+ builder.cronExpression(existingTask.getCronExpression());
+ }
+
+ ScheduleTask task = builder.build();
+ taskScheduler.update(task);
+ return taskScheduler.getTaskInfo(taskId);
+ }
+
+ @DeleteMapping("/remove/{taskId}")
+ public String remove(@PathVariable("taskId") String taskId) {
+ ScheduleTask task = taskScheduler.getTaskInfo(taskId);
+ if (task == null) {
+ return "Task not found: " + taskId;
+ }
+ taskScheduler.remove(taskId);
+ return "Removed task: " + taskId;
+ }
+
+ @PutMapping("/pause/{taskId}")
+ public ScheduleTask pause(@PathVariable("taskId") String taskId) {
+ taskScheduler.pause(taskId);
+ return taskScheduler.getTaskInfo(taskId);
+ }
+
+ @PutMapping("/resume/{taskId}")
+ public ScheduleTask resume(@PathVariable("taskId") String taskId) {
+ taskScheduler.resume(taskId);
+ return taskScheduler.getTaskInfo(taskId);
+ }
+
+ @GetMapping("/info/{taskId}")
+ public ScheduleTask getTaskInfo(@PathVariable("taskId") String taskId) {
+ return taskScheduler.getTaskInfo(taskId);
+ }
+
+ @GetMapping("/list")
+ public List getAllTasks() {
+ return taskScheduler.getAllTasks();
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-xxljob/src/main/resources/application.yaml b/structure-infra-sample/structure-infra-sample-xxljob/src/main/resources/application.yaml
new file mode 100644
index 0000000..49ba714
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-xxljob/src/main/resources/application.yaml
@@ -0,0 +1,9 @@
+server:
+ port: 12001
+structure:
+ job:
+ enable: true
+ admin-address: http://localhost:8080
+ executor:
+ appname: xxl-job-executor-sample
+ access-token: xxl-job-admin-token
\ No newline at end of file
diff --git a/structure-infra-sample/structure-infra-sample-xxljob/src/test/resources/application-xxljob-test.yml b/structure-infra-sample/structure-infra-sample-xxljob/src/test/resources/application-xxljob-test.yml
new file mode 100644
index 0000000..baae450
--- /dev/null
+++ b/structure-infra-sample/structure-infra-sample-xxljob/src/test/resources/application-xxljob-test.yml
@@ -0,0 +1,15 @@
+structure:
+ schedule:
+ xxl-job:
+ enabled: true
+ admin-address: http://localhost:8080/xxl-job-admin
+ access-token: default_token
+ job-group: 1
+ executor-app-name: structure-infra-sample-xxljob
+ executor-port: 9999
+ executor-log-path: /data/applogs/xxl-job/jobhandler
+ executor-log-retention-days: 30
+
+spring:
+ application:
+ name: structure-infra-sample-xxljob
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/pom.xml b/structure-infra-schedule-starter/pom.xml
new file mode 100644
index 0000000..5833d29
--- /dev/null
+++ b/structure-infra-schedule-starter/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-pro-infra
+ ${revision}
+ ../pom.xml
+
+
+ structure-pro-schedule-starter
+ structure-infra-schedule-starter
+ structure-pro-schedule-starter
+ jar
+
+
+
+ cn.structured
+ structure-common
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/main/java/cn/structure/infra/configuration/AutoScheduleConfiguration.java b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/configuration/AutoScheduleConfiguration.java
new file mode 100644
index 0000000..819e81f
--- /dev/null
+++ b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/configuration/AutoScheduleConfiguration.java
@@ -0,0 +1,37 @@
+package cn.structure.infra.configuration;
+
+import cn.structure.infra.properties.ScheduleProperties;
+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.ConditionalOnBean;
+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;
+
+@Configuration
+@EnableConfigurationProperties(ScheduleProperties.class)
+public class AutoScheduleConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(TaskHandlerRegistry.class)
+ public TaskHandlerRegistry taskHandlerRegistry() {
+ return new DefaultTaskHandlerRegistry();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(TaskScheduler.class)
+ public TaskScheduler taskScheduler(ScheduleProperties scheduleProperties, TaskHandlerRegistry handlerRegistry) {
+ Integer poolSize = scheduleProperties.getPoolSize();
+ return new LocalThreadTaskScheduler(poolSize != null ? poolSize : Runtime.getRuntime().availableProcessors(), handlerRegistry);
+ }
+
+ @Bean
+ @ConditionalOnBean(LocalThreadTaskScheduler.class)
+ public org.springframework.scheduling.TaskScheduler springTaskScheduler(LocalThreadTaskScheduler taskScheduler, TaskHandlerRegistry handlerRegistry) {
+ return new SpringTaskSchedulerAdapter(taskScheduler, handlerRegistry);
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/main/java/cn/structure/infra/properties/ScheduleProperties.java b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/properties/ScheduleProperties.java
new file mode 100644
index 0000000..ffec34d
--- /dev/null
+++ b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/properties/ScheduleProperties.java
@@ -0,0 +1,11 @@
+package cn.structure.infra.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@Data
+@ConfigurationProperties(prefix = "structure.schedule")
+public class ScheduleProperties {
+
+ private Integer poolSize = Runtime.getRuntime().availableProcessors();
+}
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/DefaultTaskHandlerRegistry.java b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/DefaultTaskHandlerRegistry.java
new file mode 100644
index 0000000..da47820
--- /dev/null
+++ b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/DefaultTaskHandlerRegistry.java
@@ -0,0 +1,37 @@
+package cn.structure.infra.schedule;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+public class DefaultTaskHandlerRegistry implements TaskHandlerRegistry {
+
+ private final Map handlerMap = new ConcurrentHashMap<>();
+
+ @Override
+ public void register(String handlerName, TaskHandler handler) {
+ if (handlerName == null || handler == null) {
+ throw new IllegalArgumentException("Handler name and handler cannot be null");
+ }
+ handlerMap.put(handlerName, handler);
+ log.info("Registered task handler: {}", handlerName);
+ }
+
+ @Override
+ public TaskHandler get(String handlerName) {
+ return handlerMap.get(handlerName);
+ }
+
+ @Override
+ public void unregister(String handlerName) {
+ handlerMap.remove(handlerName);
+ log.info("Unregistered task handler: {}", handlerName);
+ }
+
+ @Override
+ public boolean contains(String handlerName) {
+ return handlerMap.containsKey(handlerName);
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/LocalThreadTaskScheduler.java b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/LocalThreadTaskScheduler.java
new file mode 100644
index 0000000..bc0842e
--- /dev/null
+++ b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/LocalThreadTaskScheduler.java
@@ -0,0 +1,184 @@
+package cn.structure.infra.schedule;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class LocalThreadTaskScheduler implements TaskScheduler {
+
+ private final ScheduledExecutorService executorService;
+
+ private final Map> futureMap = new ConcurrentHashMap<>();
+
+ private final Map taskMap = new ConcurrentHashMap<>();
+
+ private final TaskHandlerRegistry handlerRegistry;
+
+ public LocalThreadTaskScheduler(TaskHandlerRegistry handlerRegistry) {
+ this(Runtime.getRuntime().availableProcessors(), handlerRegistry);
+ }
+
+ public LocalThreadTaskScheduler(int poolSize, TaskHandlerRegistry handlerRegistry) {
+ this.executorService = Executors.newScheduledThreadPool(poolSize, r -> {
+ Thread thread = new Thread(r);
+ thread.setName("structure-schedule-" + thread.getId());
+ thread.setDaemon(true);
+ return thread;
+ });
+ this.handlerRegistry = handlerRegistry;
+ log.info("LocalThreadTaskScheduler initialized with pool size: {}", poolSize);
+ }
+
+ @Override
+ public void schedule(ScheduleTask task) {
+ validateTask(task);
+
+ remove(task.getTaskId());
+
+ ScheduledFuture> future;
+ Runnable wrappedRunnable = wrapRunnable(task);
+
+ switch (task.getScheduleType()) {
+ case FIXED_DELAY:
+ long delay = task.getDelay() != null ? task.getDelay() : 1000L;
+ long initialDelay = task.getInitialDelay() != null ? task.getInitialDelay() : 0L;
+ TimeUnit timeUnit = task.getTimeUnit() != null ? task.getTimeUnit() : TimeUnit.MILLISECONDS;
+ future = executorService.scheduleWithFixedDelay(wrappedRunnable, initialDelay, delay, timeUnit);
+ break;
+
+ case FIXED_RATE:
+ long period = task.getPeriod() != null ? task.getPeriod() : 1000L;
+ initialDelay = task.getInitialDelay() != null ? task.getInitialDelay() : 0L;
+ timeUnit = task.getTimeUnit() != null ? task.getTimeUnit() : TimeUnit.MILLISECONDS;
+ future = executorService.scheduleAtFixedRate(wrappedRunnable, initialDelay, period, timeUnit);
+ break;
+
+ case CRON:
+ if (task.getCronExpression() == null || task.getCronExpression().isEmpty()) {
+ throw new IllegalArgumentException("Cron expression cannot be null for CRON schedule type");
+ }
+ future = scheduleCronTask(task, wrappedRunnable);
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unsupported schedule type: " + task.getScheduleType());
+ }
+
+ futureMap.put(task.getTaskId(), future);
+ task.setStatus(ScheduleTask.TaskStatus.RUNNING);
+ taskMap.put(task.getTaskId(), task);
+
+ log.info("Scheduled task: id={}, name={}, type={}, handler={}", task.getTaskId(), task.getTaskName(), task.getScheduleType(), task.getHandlerName());
+ }
+
+ @Override
+ public void update(ScheduleTask task) {
+ validateTask(task);
+
+ ScheduleTask existingTask = taskMap.get(task.getTaskId());
+ if (existingTask == null) {
+ log.warn("Task not found for update: {}", task.getTaskId());
+ return;
+ }
+
+ schedule(task);
+ log.info("Updated task: id={}", task.getTaskId());
+ }
+
+ private void validateTask(ScheduleTask task) {
+ if (task == null || task.getTaskId() == null) {
+ throw new IllegalArgumentException("Task and taskId cannot be null");
+ }
+
+ if (task.getHandlerName() == null || task.getHandlerName().isEmpty()) {
+ throw new IllegalArgumentException("Handler name cannot be null or empty");
+ }
+
+ if (!handlerRegistry.contains(task.getHandlerName())) {
+ throw new IllegalArgumentException("Handler not found: " + task.getHandlerName());
+ }
+
+ if (task.getScheduleType() == null) {
+ throw new IllegalArgumentException("ScheduleType cannot be null");
+ }
+ }
+
+ private ScheduledFuture> scheduleCronTask(ScheduleTask task, Runnable wrappedRunnable) {
+ return executorService.scheduleAtFixedRate(() -> {
+ try {
+ wrappedRunnable.run();
+ } catch (Exception e) {
+ log.error("Cron task execution failed: id={}, error={}", task.getTaskId(), e.getMessage(), e);
+ }
+ }, 0, 1000, TimeUnit.MILLISECONDS);
+ }
+
+ private Runnable wrapRunnable(ScheduleTask task) {
+ return () -> {
+ try {
+ TaskHandler handler = handlerRegistry.get(task.getHandlerName());
+ if (handler != null) {
+ handler.execute(task.getHandlerParam());
+ } else {
+ log.error("Handler not found during execution: {}", task.getHandlerName());
+ }
+ } catch (Exception e) {
+ log.error("Task execution failed: id={}, name={}, handler={}, error={}", task.getTaskId(), task.getTaskName(), task.getHandlerName(), e.getMessage(), e);
+ }
+ };
+ }
+
+ @Override
+ public void remove(String taskId) {
+ ScheduledFuture> future = futureMap.remove(taskId);
+ if (future != null) {
+ future.cancel(false);
+ }
+
+ ScheduleTask task = taskMap.remove(taskId);
+ if (task != null) {
+ task.setStatus(ScheduleTask.TaskStatus.STOPPED);
+ }
+
+ log.info("Removed task: id={}", taskId);
+ }
+
+ @Override
+ public void pause(String taskId) {
+ ScheduledFuture> future = futureMap.get(taskId);
+ if (future != null) {
+ future.cancel(false);
+ ScheduleTask task = taskMap.get(taskId);
+ if (task != null) {
+ task.setStatus(ScheduleTask.TaskStatus.PAUSED);
+ }
+ log.info("Paused task: id={}", taskId);
+ }
+ }
+
+ @Override
+ public void resume(String taskId) {
+ ScheduleTask task = taskMap.get(taskId);
+ if (task != null && task.getStatus() == ScheduleTask.TaskStatus.PAUSED) {
+ schedule(task);
+ log.info("Resumed task: id={}", taskId);
+ }
+ }
+
+ @Override
+ public ScheduleTask getTaskInfo(String taskId) {
+ return taskMap.get(taskId);
+ }
+
+ @Override
+ public List getAllTasks() {
+ return List.copyOf(taskMap.values());
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/ScheduleTask.java b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/ScheduleTask.java
new file mode 100644
index 0000000..fbc3787
--- /dev/null
+++ b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/ScheduleTask.java
@@ -0,0 +1,51 @@
+package cn.structure.infra.schedule;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.concurrent.TimeUnit;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ScheduleTask {
+
+ private String taskId;
+
+ private String taskName;
+
+ private String handlerName;
+
+ private String handlerParam;
+
+ private ScheduleType scheduleType;
+
+ private String cronExpression;
+
+ private Long initialDelay;
+
+ private Long delay;
+
+ private Long period;
+
+ private TimeUnit timeUnit;
+
+ @Builder.Default
+ private TaskStatus status = TaskStatus.PENDING;
+
+ public enum ScheduleType {
+ CRON,
+ FIXED_DELAY,
+ FIXED_RATE
+ }
+
+ public enum TaskStatus {
+ PENDING,
+ RUNNING,
+ PAUSED,
+ STOPPED
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/SpringTaskSchedulerAdapter.java b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/SpringTaskSchedulerAdapter.java
new file mode 100644
index 0000000..0a25a03
--- /dev/null
+++ b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/SpringTaskSchedulerAdapter.java
@@ -0,0 +1,229 @@
+package cn.structure.infra.schedule;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.Trigger;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class SpringTaskSchedulerAdapter implements TaskScheduler {
+
+ private final LocalThreadTaskScheduler localThreadTaskScheduler;
+
+ private final TaskHandlerRegistry handlerRegistry;
+
+ public SpringTaskSchedulerAdapter(LocalThreadTaskScheduler localThreadTaskScheduler,
+ TaskHandlerRegistry handlerRegistry) {
+ this.localThreadTaskScheduler = localThreadTaskScheduler;
+ this.handlerRegistry = handlerRegistry;
+ }
+
+ @Override
+ public ScheduledFuture> schedule(Runnable task, Trigger trigger) {
+ String handlerName = "spring-trigger-task-" + System.currentTimeMillis();
+ handlerRegistry.register(handlerName, param -> task.run());
+
+ ScheduleTask scheduleTask = ScheduleTask.builder()
+ .taskId(handlerName)
+ .taskName("Spring Trigger Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ localThreadTaskScheduler.schedule(scheduleTask);
+
+ return new ScheduledFuture() {
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ localThreadTaskScheduler.remove(handlerName);
+ handlerRegistry.unregister(handlerName);
+ return true;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return localThreadTaskScheduler.getTaskInfo(handlerName) == null;
+ }
+
+ @Override
+ public boolean isDone() {
+ return isCancelled();
+ }
+
+ @Override
+ public Void get() {
+ return null;
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit) {
+ return null;
+ }
+
+ @Override
+ public long getDelay(TimeUnit unit) {
+ return 0;
+ }
+
+ @Override
+ public int compareTo(java.util.concurrent.Delayed other) {
+ return 0;
+ }
+ };
+ }
+
+ @Override
+ public ScheduledFuture> schedule(Runnable task, Instant startTime) {
+ String handlerName = "spring-delay-task-" + System.currentTimeMillis();
+ handlerRegistry.register(handlerName, param -> task.run());
+
+ long initialDelay = Duration.between(Instant.now(), startTime).toMillis();
+ if (initialDelay < 0) {
+ initialDelay = 0;
+ }
+
+ ScheduleTask scheduleTask = ScheduleTask.builder()
+ .taskId(handlerName)
+ .taskName("Spring Delay Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_DELAY)
+ .delay(Long.MAX_VALUE)
+ .initialDelay(initialDelay)
+ .build();
+
+ localThreadTaskScheduler.schedule(scheduleTask);
+
+ return createScheduledFuture(handlerName);
+ }
+
+ @Override
+ public ScheduledFuture> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) {
+ String handlerName = "spring-fixed-rate-task-" + System.currentTimeMillis();
+ handlerRegistry.register(handlerName, param -> task.run());
+
+ long initialDelay = Duration.between(Instant.now(), startTime).toMillis();
+ if (initialDelay < 0) {
+ initialDelay = 0;
+ }
+
+ ScheduleTask scheduleTask = ScheduleTask.builder()
+ .taskId(handlerName)
+ .taskName("Spring Fixed Rate Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(period.toMillis())
+ .initialDelay(initialDelay)
+ .build();
+
+ localThreadTaskScheduler.schedule(scheduleTask);
+
+ return createScheduledFuture(handlerName);
+ }
+
+ @Override
+ public ScheduledFuture> scheduleAtFixedRate(Runnable task, Duration period) {
+ String handlerName = "spring-fixed-rate-task-" + System.currentTimeMillis();
+ handlerRegistry.register(handlerName, param -> task.run());
+
+ ScheduleTask scheduleTask = ScheduleTask.builder()
+ .taskId(handlerName)
+ .taskName("Spring Fixed Rate Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(period.toMillis())
+ .build();
+
+ localThreadTaskScheduler.schedule(scheduleTask);
+
+ return createScheduledFuture(handlerName);
+ }
+
+ @Override
+ public ScheduledFuture> scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) {
+ String handlerName = "spring-fixed-delay-task-" + System.currentTimeMillis();
+ handlerRegistry.register(handlerName, param -> task.run());
+
+ long initialDelay = Duration.between(Instant.now(), startTime).toMillis();
+ if (initialDelay < 0) {
+ initialDelay = 0;
+ }
+
+ ScheduleTask scheduleTask = ScheduleTask.builder()
+ .taskId(handlerName)
+ .taskName("Spring Fixed Delay Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_DELAY)
+ .delay(delay.toMillis())
+ .initialDelay(initialDelay)
+ .build();
+
+ localThreadTaskScheduler.schedule(scheduleTask);
+
+ return createScheduledFuture(handlerName);
+ }
+
+ @Override
+ public ScheduledFuture> scheduleWithFixedDelay(Runnable task, Duration delay) {
+ String handlerName = "spring-fixed-delay-task-" + System.currentTimeMillis();
+ handlerRegistry.register(handlerName, param -> task.run());
+
+ ScheduleTask scheduleTask = ScheduleTask.builder()
+ .taskId(handlerName)
+ .taskName("Spring Fixed Delay Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_DELAY)
+ .delay(delay.toMillis())
+ .build();
+
+ localThreadTaskScheduler.schedule(scheduleTask);
+
+ return createScheduledFuture(handlerName);
+ }
+
+ private ScheduledFuture createScheduledFuture(String handlerName) {
+ return new ScheduledFuture() {
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ localThreadTaskScheduler.remove(handlerName);
+ handlerRegistry.unregister(handlerName);
+ return true;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return localThreadTaskScheduler.getTaskInfo(handlerName) == null;
+ }
+
+ @Override
+ public boolean isDone() {
+ return isCancelled();
+ }
+
+ @Override
+ public Void get() {
+ return null;
+ }
+
+ @Override
+ public Void get(long timeout, TimeUnit unit) {
+ return null;
+ }
+
+ @Override
+ public long getDelay(TimeUnit unit) {
+ return 0;
+ }
+
+ @Override
+ public int compareTo(java.util.concurrent.Delayed other) {
+ return 0;
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskHandler.java b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskHandler.java
new file mode 100644
index 0000000..89cfe81
--- /dev/null
+++ b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskHandler.java
@@ -0,0 +1,7 @@
+package cn.structure.infra.schedule;
+
+@FunctionalInterface
+public interface TaskHandler {
+
+ void execute(String param);
+}
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskHandlerRegistry.java b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskHandlerRegistry.java
new file mode 100644
index 0000000..0012ebe
--- /dev/null
+++ b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskHandlerRegistry.java
@@ -0,0 +1,12 @@
+package cn.structure.infra.schedule;
+
+public interface TaskHandlerRegistry {
+
+ void register(String handlerName, TaskHandler handler);
+
+ TaskHandler get(String handlerName);
+
+ void unregister(String handlerName);
+
+ boolean contains(String handlerName);
+}
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskScheduler.java b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskScheduler.java
new file mode 100644
index 0000000..41f520c
--- /dev/null
+++ b/structure-infra-schedule-starter/src/main/java/cn/structure/infra/schedule/TaskScheduler.java
@@ -0,0 +1,20 @@
+package cn.structure.infra.schedule;
+
+import java.util.List;
+
+public interface TaskScheduler {
+
+ void schedule(ScheduleTask task);
+
+ void update(ScheduleTask task);
+
+ void remove(String taskId);
+
+ void pause(String taskId);
+
+ void resume(String taskId);
+
+ ScheduleTask getTaskInfo(String taskId);
+
+ List getAllTasks();
+}
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/structure-infra-schedule-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..134f052
--- /dev/null
+++ b/structure-infra-schedule-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+cn.structure.infra.configuration.AutoScheduleConfiguration
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/DefaultTaskHandlerRegistryTest.java b/structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/DefaultTaskHandlerRegistryTest.java
new file mode 100644
index 0000000..d41d1c4
--- /dev/null
+++ b/structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/DefaultTaskHandlerRegistryTest.java
@@ -0,0 +1,79 @@
+package cn.structure.infra.schedule;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DefaultTaskHandlerRegistryTest {
+
+ private DefaultTaskHandlerRegistry registry;
+
+ @BeforeEach
+ void setUp() {
+ registry = new DefaultTaskHandlerRegistry();
+ }
+
+ @Test
+ void testRegisterAndGet() {
+ String handlerName = "test-handler";
+ TaskHandler handler = param -> {};
+
+ registry.register(handlerName, handler);
+
+ assertTrue(registry.contains(handlerName));
+ assertNotNull(registry.get(handlerName));
+ assertSame(handler, registry.get(handlerName));
+ }
+
+ @Test
+ void testRegisterWithNullHandlerName() {
+ assertThrows(IllegalArgumentException.class, () -> registry.register(null, param -> {}));
+ }
+
+ @Test
+ void testRegisterWithNullHandler() {
+ assertThrows(IllegalArgumentException.class, () -> registry.register("test-handler", null));
+ }
+
+ @Test
+ void testUnregister() {
+ String handlerName = "test-handler";
+ TaskHandler handler = param -> {};
+
+ registry.register(handlerName, handler);
+ assertTrue(registry.contains(handlerName));
+
+ registry.unregister(handlerName);
+ assertFalse(registry.contains(handlerName));
+ assertNull(registry.get(handlerName));
+ }
+
+ @Test
+ void testUnregisterNonExistent() {
+ assertDoesNotThrow(() -> registry.unregister("non-existent"));
+ }
+
+ @Test
+ void testGetNonExistent() {
+ assertNull(registry.get("non-existent"));
+ }
+
+ @Test
+ void testContainsNonExistent() {
+ assertFalse(registry.contains("non-existent"));
+ }
+
+ @Test
+ void testRegisterOverwrite() {
+ String handlerName = "test-handler";
+ TaskHandler handler1 = param -> {};
+ TaskHandler handler2 = param -> {};
+
+ registry.register(handlerName, handler1);
+ assertSame(handler1, registry.get(handlerName));
+
+ registry.register(handlerName, handler2);
+ assertSame(handler2, registry.get(handlerName));
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/LocalThreadTaskSchedulerTest.java b/structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/LocalThreadTaskSchedulerTest.java
new file mode 100644
index 0000000..4775888
--- /dev/null
+++ b/structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/LocalThreadTaskSchedulerTest.java
@@ -0,0 +1,315 @@
+package cn.structure.infra.schedule;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class LocalThreadTaskSchedulerTest {
+
+ private LocalThreadTaskScheduler scheduler;
+ private DefaultTaskHandlerRegistry registry;
+
+ @BeforeEach
+ void setUp() {
+ registry = new DefaultTaskHandlerRegistry();
+ scheduler = new LocalThreadTaskScheduler(2, registry);
+ }
+
+ @AfterEach
+ void tearDown() {
+ scheduler.getAllTasks().forEach(task -> scheduler.remove(task.getTaskId()));
+ }
+
+ @Test
+ void testScheduleFixedRateTask() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+ String handlerName = "fixed-rate-handler";
+ registry.register(handlerName, param -> counter.incrementAndGet());
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-fixed-rate")
+ .taskName("Fixed Rate Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(100L)
+ .initialDelay(0L)
+ .build();
+
+ scheduler.schedule(task);
+
+ Thread.sleep(500);
+
+ assertTrue(counter.get() >= 4, "Fixed rate task should execute at least 4 times");
+ assertEquals(ScheduleTask.TaskStatus.RUNNING, scheduler.getTaskInfo("test-fixed-rate").getStatus());
+
+ scheduler.remove("test-fixed-rate");
+ registry.unregister(handlerName);
+ }
+
+ @Test
+ void testScheduleFixedDelayTask() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+ String handlerName = "fixed-delay-handler";
+ registry.register(handlerName, param -> counter.incrementAndGet());
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-fixed-delay")
+ .taskName("Fixed Delay Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_DELAY)
+ .delay(100L)
+ .initialDelay(0L)
+ .build();
+
+ scheduler.schedule(task);
+
+ Thread.sleep(500);
+
+ assertTrue(counter.get() >= 4, "Fixed delay task should execute at least 4 times");
+
+ scheduler.remove("test-fixed-delay");
+ registry.unregister(handlerName);
+ }
+
+ @Test
+ void testPauseAndResumeTask() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+ String handlerName = "pause-resume-handler";
+ registry.register(handlerName, param -> counter.incrementAndGet());
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-pause-resume")
+ .taskName("Pause Resume Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(100L)
+ .build();
+
+ scheduler.schedule(task);
+
+ Thread.sleep(300);
+ int countBeforePause = counter.get();
+
+ scheduler.pause("test-pause-resume");
+ assertEquals(ScheduleTask.TaskStatus.PAUSED, scheduler.getTaskInfo("test-pause-resume").getStatus());
+
+ Thread.sleep(300);
+ int countAfterPause = counter.get();
+ assertEquals(countBeforePause, countAfterPause, "Task should not execute while paused");
+
+ scheduler.resume("test-pause-resume");
+ assertEquals(ScheduleTask.TaskStatus.RUNNING, scheduler.getTaskInfo("test-pause-resume").getStatus());
+
+ Thread.sleep(300);
+ assertTrue(counter.get() > countAfterPause, "Task should resume executing");
+
+ scheduler.remove("test-pause-resume");
+ registry.unregister(handlerName);
+ }
+
+ @Test
+ void testRemoveTask() throws InterruptedException {
+ String handlerName = "remove-handler";
+ registry.register(handlerName, param -> {});
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-remove")
+ .taskName("Remove Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(100L)
+ .build();
+
+ scheduler.schedule(task);
+ assertNotNull(scheduler.getTaskInfo("test-remove"));
+
+ scheduler.remove("test-remove");
+
+ assertNull(scheduler.getTaskInfo("test-remove"));
+ registry.unregister(handlerName);
+ }
+
+ @Test
+ void testGetAllTasks() throws InterruptedException {
+ String handlerName1 = "handler-1";
+ String handlerName2 = "handler-2";
+ registry.register(handlerName1, param -> {});
+ registry.register(handlerName2, param -> {});
+
+ ScheduleTask task1 = ScheduleTask.builder()
+ .taskId("task-1")
+ .taskName("Task 1")
+ .handlerName(handlerName1)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ ScheduleTask task2 = ScheduleTask.builder()
+ .taskId("task-2")
+ .taskName("Task 2")
+ .handlerName(handlerName2)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ scheduler.schedule(task1);
+ scheduler.schedule(task2);
+
+ Thread.sleep(50);
+
+ assertEquals(2, scheduler.getAllTasks().size());
+
+ scheduler.remove("task-1");
+ scheduler.remove("task-2");
+ registry.unregister(handlerName1);
+ registry.unregister(handlerName2);
+
+ assertEquals(0, scheduler.getAllTasks().size());
+ }
+
+ @Test
+ void testUpdateTask() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+ String handlerName = "update-handler";
+ registry.register(handlerName, param -> counter.incrementAndGet());
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-update")
+ .taskName("Update Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(500L)
+ .build();
+
+ scheduler.schedule(task);
+
+ Thread.sleep(1200);
+ int countBeforeUpdate = counter.get();
+
+ ScheduleTask updatedTask = ScheduleTask.builder()
+ .taskId("test-update")
+ .taskName("Updated Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(100L)
+ .build();
+
+ scheduler.update(updatedTask);
+
+ Thread.sleep(500);
+
+ assertTrue(counter.get() > countBeforeUpdate + 2, "Updated task should execute more frequently");
+
+ scheduler.remove("test-update");
+ registry.unregister(handlerName);
+ }
+
+ @Test
+ void testScheduleWithNullTask() {
+ assertThrows(IllegalArgumentException.class, () -> scheduler.schedule(null));
+ }
+
+ @Test
+ void testScheduleWithNullTaskId() {
+ String handlerName = "test-handler";
+ registry.register(handlerName, param -> {});
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId(null)
+ .taskName("Test Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> scheduler.schedule(task));
+ registry.unregister(handlerName);
+ }
+
+ @Test
+ void testScheduleWithNullHandlerName() {
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-null-handler")
+ .taskName("Test Task")
+ .handlerName(null)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> scheduler.schedule(task));
+ }
+
+ @Test
+ void testScheduleWithNonExistentHandler() {
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-non-existent")
+ .taskName("Test Task")
+ .handlerName("non-existent-handler")
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> scheduler.schedule(task));
+ }
+
+ @Test
+ void testScheduleWithNullScheduleType() {
+ String handlerName = "test-type-handler";
+ registry.register(handlerName, param -> {});
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-null-type")
+ .taskName("Test Task")
+ .handlerName(handlerName)
+ .scheduleType(null)
+ .period(1000L)
+ .build();
+
+ assertThrows(IllegalArgumentException.class, () -> scheduler.schedule(task));
+ registry.unregister(handlerName);
+ }
+
+ @Test
+ void testPauseNonExistentTask() {
+ assertDoesNotThrow(() -> scheduler.pause("non-existent"));
+ }
+
+ @Test
+ void testResumeNonExistentTask() {
+ assertDoesNotThrow(() -> scheduler.resume("non-existent"));
+ }
+
+ @Test
+ void testRemoveNonExistentTask() {
+ assertDoesNotThrow(() -> scheduler.remove("non-existent"));
+ }
+
+ @Test
+ void testResumeNotPausedTask() throws InterruptedException {
+ String handlerName = "resume-handler";
+ registry.register(handlerName, param -> {});
+
+ ScheduleTask task = ScheduleTask.builder()
+ .taskId("test-resume-running")
+ .taskName("Resume Running Task")
+ .handlerName(handlerName)
+ .scheduleType(ScheduleTask.ScheduleType.FIXED_RATE)
+ .period(1000L)
+ .build();
+
+ scheduler.schedule(task);
+
+ Thread.sleep(50);
+
+ scheduler.resume("test-resume-running");
+
+ assertNotNull(scheduler.getTaskInfo("test-resume-running"));
+
+ scheduler.remove("test-resume-running");
+ registry.unregister(handlerName);
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/SpringTaskSchedulerAdapterTest.java b/structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/SpringTaskSchedulerAdapterTest.java
new file mode 100644
index 0000000..6d6b952
--- /dev/null
+++ b/structure-infra-schedule-starter/src/test/java/cn/structure/infra/schedule/SpringTaskSchedulerAdapterTest.java
@@ -0,0 +1,150 @@
+package cn.structure.infra.schedule;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class SpringTaskSchedulerAdapterTest {
+
+ private SpringTaskSchedulerAdapter adapter;
+ private LocalThreadTaskScheduler localScheduler;
+ private DefaultTaskHandlerRegistry registry;
+
+ @BeforeEach
+ void setUp() {
+ registry = new DefaultTaskHandlerRegistry();
+ localScheduler = new LocalThreadTaskScheduler(2, registry);
+ adapter = new SpringTaskSchedulerAdapter(localScheduler, registry);
+ }
+
+ @AfterEach
+ void tearDown() {
+ localScheduler.getAllTasks().forEach(task -> localScheduler.remove(task.getTaskId()));
+ }
+
+ @Test
+ void testScheduleAtFixedRateWithDuration() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+
+ var future = adapter.scheduleAtFixedRate(() -> counter.incrementAndGet(), Duration.ofMillis(100));
+
+ Thread.sleep(500);
+
+ assertTrue(counter.get() >= 4, "Task should execute at least 4 times");
+
+ assertTrue(future.cancel(false));
+ assertTrue(future.isDone());
+ }
+
+ @Test
+ void testScheduleAtFixedRateWithStartTime() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+
+ Instant startTime = Instant.now().plusMillis(100);
+ var future = adapter.scheduleAtFixedRate(() -> counter.incrementAndGet(), startTime, Duration.ofMillis(100));
+
+ Thread.sleep(600);
+
+ assertTrue(counter.get() >= 4, "Task should execute at least 4 times");
+
+ assertTrue(future.cancel(false));
+ }
+
+ @Test
+ void testScheduleWithFixedDelay() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+
+ var future = adapter.scheduleWithFixedDelay(() -> counter.incrementAndGet(), Duration.ofMillis(100));
+
+ Thread.sleep(500);
+
+ assertTrue(counter.get() >= 4, "Task should execute at least 4 times");
+
+ assertTrue(future.cancel(false));
+ }
+
+ @Test
+ void testScheduleWithFixedDelayAndStartTime() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+
+ Instant startTime = Instant.now().plusMillis(100);
+ var future = adapter.scheduleWithFixedDelay(() -> counter.incrementAndGet(), startTime, Duration.ofMillis(100));
+
+ Thread.sleep(600);
+
+ assertTrue(counter.get() >= 4, "Task should execute at least 4 times");
+
+ assertTrue(future.cancel(false));
+ }
+
+ @Test
+ void testScheduleWithInstant() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+
+ Instant startTime = Instant.now().plusMillis(50);
+ var future = adapter.schedule(() -> counter.incrementAndGet(), startTime);
+
+ Thread.sleep(200);
+
+ assertEquals(1, counter.get(), "Task should execute once after delay");
+
+ assertTrue(future.cancel(false));
+ }
+
+ @Test
+ void testScheduleWithTrigger() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+
+ var future = adapter.schedule(() -> counter.incrementAndGet(), triggerContext -> Instant.now().plusMillis(100));
+
+ Thread.sleep(3500);
+
+ assertTrue(counter.get() >= 3, "Task should execute at least 3 times");
+
+ assertTrue(future.cancel(false));
+ }
+
+ @Test
+ void testCancelTask() throws InterruptedException {
+ AtomicInteger counter = new AtomicInteger(0);
+
+ var future = adapter.scheduleAtFixedRate(() -> counter.incrementAndGet(), Duration.ofMillis(50));
+
+ Thread.sleep(200);
+ int countBeforeCancel = counter.get();
+
+ assertTrue(future.cancel(false));
+
+ Thread.sleep(200);
+
+ assertEquals(countBeforeCancel, counter.get(), "Task should stop executing after cancel");
+ }
+
+ @Test
+ void testIsCancelled() throws InterruptedException {
+ var future = adapter.scheduleAtFixedRate(() -> {}, Duration.ofMillis(100));
+
+ assertFalse(future.isCancelled());
+
+ future.cancel(false);
+
+ assertTrue(future.isCancelled());
+ }
+
+ @Test
+ void testIsDone() throws InterruptedException {
+ var future = adapter.scheduleAtFixedRate(() -> {}, Duration.ofMillis(100));
+
+ assertFalse(future.isDone());
+
+ future.cancel(false);
+
+ assertTrue(future.isDone());
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-starter/pom.xml b/structure-infra-starter/pom.xml
index bf8264e..5a0e82c 100644
--- a/structure-infra-starter/pom.xml
+++ b/structure-infra-starter/pom.xml
@@ -28,10 +28,19 @@
cn.structured
structure-datascope-message
+
+ org.springframework.boot
+ spring-boot-data-commons
+
cn.structured
structure-datascope-cache
+
+ 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/configuration/AutoScheduleConfiguration.java b/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoScheduleConfiguration.java
new file mode 100644
index 0000000..9f334b1
--- /dev/null
+++ b/structure-infra-starter/src/main/java/cn/structure/infra/configuration/AutoScheduleConfiguration.java
@@ -0,0 +1,39 @@
+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;
+
+@Configuration
+@EnableConfigurationProperties(InfraProperties.class)
+public class AutoScheduleConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(TaskHandlerRegistry.class)
+ public TaskHandlerRegistry taskHandlerRegistry() {
+ return new DefaultTaskHandlerRegistry();
+ }
+
+ @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);
+ }
+
+ @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/properties/InfraProperties.java b/structure-infra-starter/src/main/java/cn/structure/infra/properties/InfraProperties.java
index 53be23e..7dca4f7 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
@@ -35,4 +35,9 @@ public class InfraProperties {
* @return
*/
private TimeUnit cacheTimeUnit = TimeUnit.SECONDS;
+
+ /**
+ * 调度线程池大小,默认 CPU 核心数
+ */
+ private Integer schedulePoolSize = Runtime.getRuntime().availableProcessors();
}
diff --git a/structure-infra-stream-starter/README.md b/structure-infra-stream-starter/README.md
new file mode 100644
index 0000000..4815b47
--- /dev/null
+++ b/structure-infra-stream-starter/README.md
@@ -0,0 +1,526 @@
+# Structure Infra Stream Starter
+
+基于 Spring Cloud Stream 的事件监听管理器,提供灵活的事件监听配置和统一路由能力。
+
+## 功能特性
+
+- **动态配置**:支持动态配置 Stream Binding,替代静态配置
+- **自动绑定**:根据注解自动生成 binding 配置,无需手动配置
+- **统一路由**:基于 eventType/businessType/condition 的统一事件路由
+- **声明式注册**:通过注解自动注册事件处理器
+- **运行时动态注册**:支持运行时动态注册监听器和绑定
+- **低代码支持**:支持配置文件驱动、代码动态注册等多种使用方式
+- **SpEL 条件路由**:支持 SpEL 表达式进行条件过滤
+- **业务解耦**:事件生产者和消费者完全解耦
+
+## 快速开始
+
+### 添加依赖
+
+```xml
+
+ cn.structured
+ structure-infra-stream-starter
+ 1.0.0-SNAPSHOT
+
+```
+
+### 基本配置
+
+```yaml
+structure:
+ infra:
+ stream:
+ enabled: true
+ auto-binding: true # 自动生成 binding 配置(默认开启)
+ default-group: my-service # 所有自动绑定的默认消费组
+ default-concurrency: 1 # 默认并发数
+```
+
+> **重要**:启用 `auto-binding` 后,无需手动配置 `spring.cloud.stream.bindings`,系统会根据注解自动生成!
+
+## 核心概念
+
+### StreamEvent 统一事件信封
+
+所有事件通过 `StreamEvent` 封装,包含路由元数据:
+
+```java
+public class StreamEvent {
+ private String eventId; // 事件唯一标识
+ private String eventType; // 事件类型(用于路由)
+ private String businessType; // 业务类型(可选)
+ private LocalDateTime timestamp; // 时间戳
+ private T payload; // 业务数据
+ private Map headers; // 扩展头信息
+ private String traceId; // 链路追踪ID
+}
+```
+
+## 使用方式
+
+### 方式一:注解声明式(推荐)
+
+通过 `@StreamRouteHandler` 注解声明路由,自动注册:
+
+```java
+@Component
+public class OrderEventListener {
+
+ @StreamRouteHandler(eventType = "orderCreated")
+ public void handleOrderCreated(OrderEvent event) {
+ log.info("[订单创建] orderId={}, amount={}", event.getOrderId(), event.getAmount());
+ }
+
+ @StreamRouteHandler(eventType = "orderCreated", condition = "#payload.amount > 1000")
+ public void handleHighAmountOrder(OrderEvent event) {
+ log.info("[高额订单] 触发风控检查: orderId={}, amount={}", event.getOrderId(), event.getAmount());
+ }
+
+ @StreamRouteHandler(eventType = "orderPaid")
+ public void handleOrderPaid(OrderEvent event) {
+ log.info("[订单支付] orderId={}", event.getOrderId());
+ }
+
+ @StreamRouteHandler(eventType = "orderCancelled", businessType = "retail")
+ public void handleRetailOrderCancelled(OrderEvent event) {
+ log.info("[零售订单取消] orderId={}", event.getOrderId());
+ }
+}
+```
+
+#### @StreamRouteHandler 注解参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| value / eventType | String | 是 | 事件类型,用于路由匹配 |
+| businessType | String | 否 | 业务类型,支持 `*` 通配符 |
+| condition | String | 否 | SpEL 表达式条件过滤 |
+
+### 方式二:代码动态注册
+
+通过 `StreamEventRouter` API 动态注册路由:
+
+```java
+@Component
+public class RouteConfig implements CommandLineRunner {
+
+ private final StreamEventRouter eventRouter;
+
+ @Override
+ public void run(String... args) {
+ // 注册基础路由
+ eventRouter.registerRoute("orderCreated", OrderEvent.class, (payload, event) -> {
+ log.info("[订单创建] orderId={}", payload.getOrderId());
+ });
+
+ // 注册带条件的路由
+ eventRouter.registerRoute("orderCreated", OrderEvent.class,
+ "#payload.amount > 1000", (payload, event) -> {
+ log.info("[高额订单] 触发风控检查");
+ });
+
+ // 注册带业务类型的路由
+ eventRouter.registerRoute("orderCreated", "wholesale", OrderEvent.class, (payload, event) -> {
+ log.info("[批发订单] orderId={}", payload.getOrderId());
+ });
+ }
+}
+```
+
+### 方式三:配置文件驱动
+
+通过 YAML 配置文件声明路由,真正的低代码:
+
+```yaml
+structure:
+ infra:
+ stream:
+ router:
+ enabled: true
+ routes:
+ - id: route-order-created
+ event-type: orderCreated
+ payload-type: cn.structure.infra.sample.stream.event.OrderEvent
+ handler-bean: orderHandler
+ handler-method: onOrderCreated
+ description: 处理订单创建事件
+
+ - id: route-payment-success
+ event-type: paymentSuccess
+ payload-type: cn.structure.infra.sample.stream.event.PaymentEvent
+ handler-bean: paymentHandler
+ handler-method: onPaymentSuccess
+ description: 处理支付成功事件
+
+ - id: route-high-amount-order
+ event-type: orderCreated
+ payload-type: cn.structure.infra.sample.stream.event.OrderEvent
+ condition: "#payload.amount > 1000"
+ handler-bean: orderHandler
+ handler-method: onHighAmountOrder
+ description: 处理高额订单(金额>1000)
+```
+
+业务处理器(只需写方法,无需注解):
+
+```java
+@Component("orderHandler")
+public class OrderHandler {
+
+ public void onOrderCreated(OrderEvent event) {
+ log.info("[配置驱动] 订单创建处理: orderId={}", event.getOrderId());
+ }
+
+ public void onHighAmountOrder(OrderEvent event) {
+ log.info("[配置驱动] 高额订单处理: orderId={}, amount={}", event.getOrderId(), event.getAmount());
+ }
+}
+```
+
+### 方式四:传统消息监听
+
+通过 `@StreamEventListener` 绑定到特定消息队列:
+
+```java
+@Component
+public class DeliveryEventListener {
+
+ @StreamEventListener(bindingName = "deliveryEvent",
+ destination = "delivery-exchange",
+ group = "delivery-group")
+ public void handleDelivery(DeliveryEvent event) {
+ log.info("[配送] deliveryId={}, orderId={}", event.getDeliveryId(), event.getOrderId());
+ }
+}
+```
+
+#### @StreamEventListener 注解参数
+
+### 方式五:运行时动态注册(推荐)
+
+支持在应用运行时动态注册监听器和绑定,无需重启应用。
+
+#### 通过 StreamEventManager 动态注册
+
+```java
+@Component
+public class DynamicListenerDemo implements CommandLineRunner {
+
+ private final StreamEventManager eventManager;
+
+ @Override
+ public void run(String... args) {
+ // 1. 动态注册 binding
+ eventManager.registerBinding("dynamicOrder", "dynamic-order-exchange", "dynamic-group");
+
+ // 2. 通过实现类注册监听器
+ eventManager.registerListener("dynamicOrder", OrderEvent.class, new OrderEventHandler());
+
+ // 3. 通过 Lambda 注册监听器
+ eventManager.registerListener("dynamicOrder", OrderEvent.class, event -> {
+ log.info("[Lambda] 动态监听订单: orderId={}", event.getOrderId());
+ });
+
+ // 4. 通过 Lambda + SpEL 条件注册监听器
+ eventManager.registerListener("dynamicOrder", OrderEvent.class,
+ "#payload.amount > 500", event -> {
+ log.info("[条件] 动态监听大额订单: orderId={}", event.getOrderId());
+ });
+
+ // 5. 动态发布事件(自动创建 binding)
+ eventManager.publish("dynamicPayment", "dynamic-payment-exchange", paymentEvent);
+ }
+
+ public static class OrderEventHandler implements StreamEventHandler {
+ @Override
+ public void handle(OrderEvent event) {
+ log.info("[实现类] 动态监听订单: orderId={}", event.getOrderId());
+ }
+ }
+}
+```
+
+#### 通过 StreamEventRouter 动态注册路由
+
+```java
+@Component
+public class DynamicRouteDemo implements CommandLineRunner {
+
+ private final StreamEventRouter eventRouter;
+
+ @Override
+ public void run(String... args) {
+ // 1. 通过实现类注册路由
+ eventRouter.registerRoute("orderCreated", OrderEvent.class, new OrderRouteHandler());
+
+ // 2. 通过 Lambda 注册路由
+ eventRouter.registerRoute("paymentSuccess", PaymentEvent.class, (payload, event) -> {
+ log.info("[Lambda] 支付成功: paymentId={}", payload.getPaymentId());
+ });
+
+ // 3. 注册通用路由(Object 类型)
+ eventRouter.registerRoute("genericEvent", Object.class, (payload, event) -> {
+ log.info("[通用] 事件: payload={}", payload);
+ });
+
+ // 4. 注册带业务类型的路由
+ eventRouter.registerRoute("orderCreated", "retail", OrderEvent.class, (payload, event) -> {
+ log.info("[零售] 订单: orderId={}", payload.getOrderId());
+ });
+
+ // 5. 分发事件
+ eventRouter.route(StreamEvent.of("orderCreated", orderEvent));
+ }
+
+ public static class OrderRouteHandler implements StreamEventRouter.StreamRouteHandler {
+ @Override
+ public void handle(OrderEvent payload, StreamEvent event) {
+ log.info("[实现类] 订单路由: orderId={}", payload.getOrderId());
+ }
+ }
+}
+```
+
+#### 动态注册 API 汇总
+
+| API | 说明 |
+|-----|------|
+| `eventManager.registerBinding(name, destination)` | 动态注册 binding |
+| `eventManager.registerBinding(name, destination, group)` | 动态注册 binding(指定消费组) |
+| `eventManager.registerListener(name, type, handler)` | 动态注册监听器 |
+| `eventManager.registerListener(name, type, condition, handler)` | 动态注册带条件的监听器 |
+| `eventManager.publish(name, destination, event)` | 动态发布事件(自动创建 binding) |
+| `eventManager.unregisterBinding(name)` | 注销 binding |
+| `eventManager.unregisterListener(name)` | 注销监听器 |
+| `eventRouter.registerRoute(eventType, payloadType, handler)` | 动态注册路由 |
+| `eventRouter.registerRoute(eventType, businessType, payloadType, handler)` | 动态注册带业务类型的路由 |
+| `eventRouter.unregisterRoute(eventType)` | 注销路由 |
+
+#### @StreamEventListener 注解参数
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| value / bindingName | String | 是 | Binding 名称 |
+| destination | String | 否 | 消息队列目的地 |
+| group | String | 否 | 消费组 |
+| contentType | String | 否 | 内容类型,默认 application/json |
+| eventType | Class | 否 | 事件类型 |
+| condition | String | 否 | SpEL 条件表达式 |
+
+## 事件发布
+
+### 方式一:使用 StreamEventManager 发布
+
+```java
+@Component
+public class OrderService {
+
+ private final StreamEventManager eventManager;
+
+ public void createOrder(Order order) {
+ OrderEvent event = OrderEvent.builder()
+ .orderId(order.getId())
+ .orderNo(order.getOrderNo())
+ .amount(order.getAmount())
+ .build();
+
+ // 发布到已配置的 binding
+ eventManager.publish("orderEvent", event);
+ }
+}
+```
+
+### 方式二:使用 StreamEvent 封装发布
+
+```java
+@Component
+public class EventPublisher {
+
+ private final StreamEventRouter eventRouter;
+
+ public void publishOrderCreated(OrderEvent event) {
+ // 封装为统一事件信封
+ StreamEvent streamEvent = StreamEvent.of("orderCreated", event);
+ // 路由分发
+ eventRouter.route(streamEvent);
+ }
+
+ public void publishPaymentSuccess(PaymentEvent event) {
+ StreamEvent streamEvent = StreamEvent.of("paymentSuccess", "retail", event);
+ eventRouter.route(streamEvent);
+ }
+}
+```
+
+## 配置说明
+
+### Stream 绑定配置
+
+```yaml
+structure:
+ infra:
+ stream:
+ enabled: true # 是否启用
+ bindings:
+ orderEvent: # binding 名称
+ destination: order-exchange # 消息队列目的地
+ content-type: application/json
+ group: order-group # 消费组
+ binder: kafka # 绑定器(可选)
+ concurrency: 3 # 并发数(可选)
+```
+
+### 路由配置
+
+```yaml
+structure:
+ infra:
+ stream:
+ router:
+ enabled: true # 是否启用配置驱动路由
+ routes: # 路由列表
+ - id: route-001 # 路由唯一标识
+ event-type: orderCreated # 事件类型
+ business-type: retail # 业务类型(可选)
+ payload-type: com.example.OrderEvent # 负载类型全限定名
+ condition: "#payload.amount > 1000" # SpEL 条件(可选)
+ handler-bean: orderHandler # 处理器 Bean 名称
+ handler-method: onOrderCreated # 处理器方法名
+ description: 订单创建事件处理 # 描述(可选)
+```
+
+## API 文档
+
+### StreamEventRouter
+
+| 方法 | 说明 |
+|------|------|
+| `registerRoute(eventType, payloadType, handler)` | 注册路由 |
+| `registerRoute(eventType, payloadType, condition, handler)` | 注册带条件的路由 |
+| `registerRoute(eventType, businessType, payloadType, handler)` | 注册带业务类型的路由 |
+| `registerRoute(eventType, businessType, payloadType, condition, handler)` | 注册完整路由 |
+| `unregisterRoute(eventType)` | 注销指定事件类型的所有路由 |
+| `unregisterRoute(eventType, handlerId)` | 注销指定路由 |
+| `route(event)` | 路由分发事件 |
+| `isRouteRegistered(eventType)` | 检查路由是否已注册 |
+| `getRoutes(eventType)` | 获取指定事件类型的路由列表 |
+
+### StreamEventManager
+
+| 方法 | 说明 |
+|------|------|
+| `publish(bindingName, event)` | 发布事件到指定 binding |
+| `publish(bindingName, destination, event)` | 发布事件到指定目的地 |
+| `registerListener(bindingName, eventType, handler)` | 注册监听器 |
+| `registerListener(bindingName, destination, group, eventType, handler)` | 注册完整监听器 |
+| `unregisterListener(bindingName)` | 注销监听器 |
+| `dispatch(bindingName, event)` | 分发给注册的监听器 |
+
+## 架构设计
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ 事件发布层 │
+│ StreamEventManager / StreamEventRouter.route() │
+└───────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────┐
+│ StreamEvent 统一信封 │
+│ { eventType, businessType, payload, headers, traceId } │
+└───────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────────┐
+│ StreamEventRouter 路由网关 │
+│ ┌───────────────────────────────────────────────────────────┐ │
+│ │ 路由匹配规则: │ │
+│ │ 1. eventType 精确匹配 │ │
+│ │ 2. businessType 匹配(支持 * 通配符) │ │
+│ │ 3. payloadType 类型匹配 │ │
+│ │ 4. condition SpEL 表达式匹配 │ │
+│ └───────────────────────────────────────────────────────────┘ │
+└───────────────────────────────────────────────────────────────┘
+ │
+ ┌───────────────┼───────────────┐
+ ▼ ▼ ▼
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
+ │ Handler A │ │ Handler B │ │ Handler C │
+ │ orderCreated │ │ orderCreated │ │ orderPaid │
+ │ condition: >1000│ │ business: retail│ │ │
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
+```
+
+## 目录结构
+
+```
+structure-infra-stream-starter/
+├── src/main/java/cn/structure/infra/stream/
+│ ├── annotation/ # 注解定义
+│ │ ├── StreamEventListener.java
+│ │ └── StreamRouteHandler.java
+│ ├── configuration/ # 自动配置
+│ │ └── StreamAutoConfiguration.java
+│ ├── event/ # 事件模型
+│ │ └── StreamEvent.java
+│ ├── handler/ # 处理器接口
+│ │ └── StreamEventHandler.java
+│ ├── manager/ # 事件管理器
+│ │ ├── StreamEventManager.java
+│ │ └── DefaultStreamEventManagerImpl.java
+│ ├── processor/ # Bean 后置处理器
+│ │ └── EventListenerBeanPostProcessor.java
+│ ├── properties/ # 配置属性
+│ │ └── StreamProperties.java
+│ └── router/ # 路由模块
+│ ├── StreamEventRouter.java
+│ ├── DefaultStreamEventRouterImpl.java
+│ ├── RouteHandlerBeanPostProcessor.java
+│ ├── RouterProperties.java
+│ └── ConfigurableRouteInitializer.java
+└── pom.xml
+```
+
+## 扩展能力
+
+### 自定义路由策略
+
+实现 `StreamEventRouter` 接口自定义路由逻辑:
+
+```java
+@Component
+public class CustomEventRouter implements StreamEventRouter {
+ // 实现自定义路由逻辑
+}
+```
+
+### 自定义事件类型
+
+只需创建普通的 POJO 类即可作为事件类型:
+
+```java
+public class CustomEvent {
+ private String id;
+ private String data;
+ // getter/setter
+}
+```
+
+## 测试
+
+```bash
+mvn clean test -pl structure-infra-sample/structure-infra-sample-stream -am
+```
+
+测试覆盖:
+- 事件路由分发
+- 业务类型过滤
+- SpEL 条件表达式
+- 多处理器并发处理
+- 路由注册/注销
+- 配置驱动路由
+
+## License
+
+Apache License 2.0
diff --git a/structure-infra-stream-starter/pom.xml b/structure-infra-stream-starter/pom.xml
new file mode 100644
index 0000000..ed74bb5
--- /dev/null
+++ b/structure-infra-stream-starter/pom.xml
@@ -0,0 +1,60 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-pro-infra
+ ${revision}
+ ../pom.xml
+
+
+ structure-pro-stream-starter
+ structure-infra-stream-starter
+ structure-pro-stream-starter
+ jar
+
+
+
+ cn.structured
+ structure-infra-starter
+ ${revision}
+
+
+ org.springframework.cloud
+ spring-cloud-stream
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ true
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 17
+ 17
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+
+
+
+
+
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/annotation/StreamEventListener.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/annotation/StreamEventListener.java
new file mode 100644
index 0000000..9969f78
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/annotation/StreamEventListener.java
@@ -0,0 +1,32 @@
+package cn.structure.infra.stream.annotation;
+
+import org.springframework.core.annotation.AliasFor;
+
+import java.lang.annotation.*;
+
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface StreamEventListener {
+
+ @AliasFor("bindingName")
+ String value() default "";
+
+ @AliasFor("value")
+ String bindingName() default "";
+
+ String destination() default "";
+
+ String group() default "";
+
+ String contentType() default "application/json";
+
+ Class> eventType() default Object.class;
+
+ String consumerPrefix() default "consumer";
+
+ String producerPrefix() default "producer";
+
+ String condition() default "";
+
+}
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/annotation/StreamRouteHandler.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/annotation/StreamRouteHandler.java
new file mode 100644
index 0000000..30ebc16
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/annotation/StreamRouteHandler.java
@@ -0,0 +1,22 @@
+package cn.structure.infra.stream.annotation;
+
+import org.springframework.core.annotation.AliasFor;
+
+import java.lang.annotation.*;
+
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface StreamRouteHandler {
+
+ @AliasFor("eventType")
+ String value() default "";
+
+ @AliasFor("value")
+ String eventType() default "";
+
+ String businessType() default "";
+
+ String condition() default "";
+
+}
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
new file mode 100644
index 0000000..7c8f739
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/configuration/StreamAutoConfiguration.java
@@ -0,0 +1,49 @@
+package cn.structure.infra.stream.configuration;
+
+import cn.structure.infra.stream.manager.DefaultStreamEventManagerImpl;
+import cn.structure.infra.stream.manager.StreamEventManager;
+import cn.structure.infra.stream.properties.StreamProperties;
+import cn.structure.infra.stream.processor.EventListenerBeanPostProcessor;
+import cn.structure.infra.stream.processor.StreamBindingBeanFactoryPostProcessor;
+import cn.structure.infra.stream.router.DefaultStreamEventRouterImpl;
+import cn.structure.infra.stream.router.RouterProperties;
+import cn.structure.infra.stream.router.StreamEventRouter;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.cloud.stream.function.StreamBridge;
+import org.springframework.context.annotation.Bean;
+
+@AutoConfiguration
+@ConditionalOnClass({StreamBridge.class})
+@ConditionalOnProperty(prefix = "structure.infra.stream", name = "enabled", havingValue = "true", matchIfMissing = true)
+@EnableConfigurationProperties({StreamProperties.class, RouterProperties.class})
+public class StreamAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public StreamEventManager streamEventManager(StreamBridge streamBridge,
+ StreamProperties streamProperties) {
+ return new DefaultStreamEventManagerImpl(streamBridge, streamProperties);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public StreamEventRouter streamEventRouter() {
+ return new DefaultStreamEventRouterImpl();
+ }
+
+ @Bean
+ public static StreamBindingBeanFactoryPostProcessor streamBindingBeanFactoryPostProcessor() {
+ return new StreamBindingBeanFactoryPostProcessor();
+ }
+
+ @Bean
+ public EventListenerBeanPostProcessor eventListenerBeanPostProcessor(StreamEventManager streamEventManager,
+ StreamProperties streamProperties) {
+ return new EventListenerBeanPostProcessor(streamEventManager, streamProperties);
+ }
+
+}
\ No newline at end of file
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/event/StreamEvent.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/event/StreamEvent.java
new file mode 100644
index 0000000..d7dabf2
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/event/StreamEvent.java
@@ -0,0 +1,181 @@
+package cn.structure.infra.stream.event;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+public class StreamEvent {
+
+ private String eventId;
+ private String eventType;
+ private String businessType;
+ private String source;
+ private LocalDateTime timestamp;
+ private T payload;
+ private Map headers = new HashMap<>();
+ private String traceId;
+
+ public StreamEvent() {
+ }
+
+ public StreamEvent(String eventId, String eventType, String businessType, String source,
+ LocalDateTime timestamp, T payload, Map headers, String traceId) {
+ this.eventId = eventId;
+ this.eventType = eventType;
+ this.businessType = businessType;
+ this.source = source;
+ this.timestamp = timestamp;
+ this.payload = payload;
+ this.headers = headers != null ? headers : new HashMap<>();
+ this.traceId = traceId;
+ }
+
+ public String getEventId() {
+ return eventId;
+ }
+
+ public void setEventId(String eventId) {
+ this.eventId = eventId;
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ public void setEventType(String eventType) {
+ this.eventType = eventType;
+ }
+
+ public String getBusinessType() {
+ return businessType;
+ }
+
+ public void setBusinessType(String businessType) {
+ this.businessType = businessType;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public void setSource(String source) {
+ this.source = source;
+ }
+
+ public LocalDateTime getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(LocalDateTime timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public T getPayload() {
+ return payload;
+ }
+
+ public void setPayload(T payload) {
+ this.payload = payload;
+ }
+
+ public Map getHeaders() {
+ return headers;
+ }
+
+ public void setHeaders(Map headers) {
+ this.headers = headers != null ? headers : new HashMap<>();
+ }
+
+ public String getTraceId() {
+ return traceId;
+ }
+
+ public void setTraceId(String traceId) {
+ this.traceId = traceId;
+ }
+
+ public static StreamEvent of(String eventType, T payload) {
+ return new StreamEvent<>(
+ java.util.UUID.randomUUID().toString(),
+ eventType,
+ null,
+ null,
+ LocalDateTime.now(),
+ payload,
+ new HashMap<>(),
+ null
+ );
+ }
+
+ public static StreamEvent of(String eventType, String businessType, T payload) {
+ return new StreamEvent<>(
+ java.util.UUID.randomUUID().toString(),
+ eventType,
+ businessType,
+ null,
+ LocalDateTime.now(),
+ payload,
+ new HashMap<>(),
+ null
+ );
+ }
+
+ public static Builder builder() {
+ return new Builder<>();
+ }
+
+ public static class Builder {
+ private String eventId;
+ private String eventType;
+ private String businessType;
+ private String source;
+ private LocalDateTime timestamp;
+ private T payload;
+ private Map headers = new HashMap<>();
+ private String traceId;
+
+ public Builder eventId(String eventId) {
+ this.eventId = eventId;
+ return this;
+ }
+
+ public Builder eventType(String eventType) {
+ this.eventType = eventType;
+ return this;
+ }
+
+ public Builder businessType(String businessType) {
+ this.businessType = businessType;
+ return this;
+ }
+
+ public Builder source(String source) {
+ this.source = source;
+ return this;
+ }
+
+ public Builder timestamp(LocalDateTime timestamp) {
+ this.timestamp = timestamp;
+ return this;
+ }
+
+ public Builder payload(T payload) {
+ this.payload = payload;
+ return this;
+ }
+
+ public Builder headers(Map headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ public Builder traceId(String traceId) {
+ this.traceId = traceId;
+ return this;
+ }
+
+ public StreamEvent build() {
+ return new StreamEvent<>(eventId, eventType, businessType, source, timestamp, payload, headers, traceId);
+ }
+ }
+}
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/handler/StreamEventHandler.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/handler/StreamEventHandler.java
new file mode 100644
index 0000000..e64b4ce
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/handler/StreamEventHandler.java
@@ -0,0 +1,7 @@
+package cn.structure.infra.stream.handler;
+
+public interface StreamEventHandler {
+
+ void handle(T event);
+
+}
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
new file mode 100644
index 0000000..676a508
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/DefaultStreamEventManagerImpl.java
@@ -0,0 +1,231 @@
+package cn.structure.infra.stream.manager;
+
+import cn.structure.infra.stream.handler.StreamEventHandler;
+import cn.structure.infra.stream.properties.StreamProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.stream.function.StreamBridge;
+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 java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class DefaultStreamEventManagerImpl implements StreamEventManager {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultStreamEventManagerImpl.class);
+
+ private final StreamBridge streamBridge;
+ private final StreamProperties streamProperties;
+ private final Map>> registeredListeners = new ConcurrentHashMap<>();
+ private final SpelExpressionParser expressionParser = new SpelExpressionParser();
+
+ public DefaultStreamEventManagerImpl(StreamBridge streamBridge, StreamProperties streamProperties) {
+ this.streamBridge = streamBridge;
+ this.streamProperties = streamProperties;
+ }
+
+ @Override
+ public void publish(String bindingName, T event) {
+ StreamProperties.Binding binding = streamProperties.getBindings().get(bindingName);
+ if (binding == null) {
+ throw new IllegalArgumentException("Binding not found: " + bindingName);
+ }
+ publish(bindingName, binding.getDestination(), binding.getGroup(), event);
+ }
+
+ @Override
+ public void publish(String bindingName, String destination, T event) {
+ String group = streamProperties.getDefaultGroup();
+ publish(bindingName, destination, group, event);
+ }
+
+ @Override
+ public void publish(String bindingName, String destination, String group, T event) {
+ ensureBindingRegistered(bindingName, destination, group);
+
+ 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);
+ }
+
+ private synchronized void ensureBindingRegistered(String bindingName, String destination, String group) {
+ if (!streamProperties.getBindings().containsKey(bindingName)) {
+ registerBinding(bindingName, destination, group);
+ }
+ }
+
+ @Override
+ public void registerListener(String bindingName, Class eventType, StreamEventHandler handler) {
+ registerListener(bindingName, eventType, "", handler);
+ }
+
+ @Override
+ public void registerListener(String bindingName, Class eventType, String condition, StreamEventHandler handler) {
+ StreamProperties.Binding binding = streamProperties.getBindings().get(bindingName);
+ if (binding == null) {
+ throw new IllegalArgumentException("Binding not found: " + bindingName);
+ }
+ registerListener(bindingName, binding.getDestination(), binding.getGroup(), eventType, condition, handler);
+ }
+
+ @Override
+ public void registerListener(String bindingName, String destination, String group, Class eventType, StreamEventHandler handler) {
+ registerListener(bindingName, destination, group, eventType, "", handler);
+ }
+
+ @Override
+ public void registerListener(String bindingName, String destination, String group, Class eventType, String condition, StreamEventHandler handler) {
+ String listenerId = UUID.randomUUID().toString();
+ ListenerRegistration registration = ListenerRegistration.builder()
+ .listenerId(listenerId)
+ .eventType(eventType)
+ .handler(handler)
+ .condition(condition)
+ .destination(destination)
+ .group(group)
+ .build();
+
+ registeredListeners.computeIfAbsent(bindingName, k -> new ArrayList<>()).add(registration);
+
+ log.info("Registered listener for binding: {}, listenerId: {}, destination: {}, group: {}, eventType: {}, condition: {}",
+ bindingName, listenerId, destination, group, eventType.getName(), condition);
+ }
+
+ @Override
+ public void unregisterListener(String bindingName) {
+ registeredListeners.remove(bindingName);
+ log.info("Unregistered all listeners for binding: {}", bindingName);
+ }
+
+ @Override
+ public void unregisterListener(String bindingName, String listenerId) {
+ List> registrations = registeredListeners.get(bindingName);
+ if (registrations != null) {
+ boolean removed = registrations.removeIf(r -> r.getListenerId().equals(listenerId));
+ if (removed) {
+ log.info("Unregistered listener: {} for binding: {}", listenerId, bindingName);
+ }
+ if (registrations.isEmpty()) {
+ registeredListeners.remove(bindingName);
+ }
+ }
+ }
+
+ @Override
+ public boolean isListenerRegistered(String bindingName) {
+ return registeredListeners.containsKey(bindingName) && !registeredListeners.get(bindingName).isEmpty();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void dispatch(String bindingName, T event) {
+ List> registrations = registeredListeners.get(bindingName);
+ if (registrations == null || registrations.isEmpty()) {
+ log.debug("No listeners registered for binding: {}", bindingName);
+ return;
+ }
+
+ for (ListenerRegistration> registration : registrations) {
+ if (!registration.getEventType().isInstance(event)) {
+ continue;
+ }
+
+ if (matchesCondition(registration.getCondition(), event)) {
+ try {
+ ((StreamEventHandler) registration.getHandler()).handle(event);
+ log.debug("Dispatched event to listener: {} for binding: {}", registration.getListenerId(), bindingName);
+ } catch (Exception e) {
+ log.error("Error handling event in listener: {} for binding: {}", registration.getListenerId(), bindingName, e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public List> getListeners(String bindingName) {
+ return registeredListeners.getOrDefault(bindingName, new ArrayList<>());
+ }
+
+ @Override
+ public void registerBinding(String bindingName, String destination) {
+ registerBinding(bindingName, destination, streamProperties.getDefaultGroup());
+ }
+
+ @Override
+ public void registerBinding(String bindingName, String destination, String group) {
+ registerBinding(bindingName, destination, group, streamProperties.getDefaultContentType(), streamProperties.getDefaultConcurrency());
+ }
+
+ @Override
+ public void registerBinding(String bindingName, String destination, String group, String contentType, Integer concurrency) {
+ synchronized (this) {
+ if (streamProperties.getBindings().containsKey(bindingName)) {
+ log.warn("Binding already registered: {}", bindingName);
+ return;
+ }
+
+ StreamProperties.Binding binding = new StreamProperties.Binding();
+ binding.setDestination(destination);
+ binding.setGroup(group);
+ binding.setContentType(contentType != null ? contentType : streamProperties.getDefaultContentType());
+ binding.setConcurrency(concurrency != null ? concurrency : streamProperties.getDefaultConcurrency());
+
+ streamProperties.getBindings().put(bindingName, binding);
+
+ log.info("Dynamically registered binding: {}, destination: {}, group: {}, contentType: {}",
+ bindingName, destination, group, binding.getContentType());
+ }
+ }
+
+ @Override
+ public void unregisterBinding(String bindingName) {
+ synchronized (this) {
+ StreamProperties.Binding removed = streamProperties.getBindings().remove(bindingName);
+ if (removed != null) {
+ unregisterListener(bindingName);
+ log.info("Dynamically unregistered binding: {}", bindingName);
+ }
+ }
+ }
+
+ @Override
+ public boolean isBindingRegistered(String bindingName) {
+ return streamProperties.getBindings().containsKey(bindingName);
+ }
+
+ @Override
+ public StreamProperties.Binding getBinding(String bindingName) {
+ return streamProperties.getBindings().get(bindingName);
+ }
+
+ @Override
+ public Map getAllBindings() {
+ return streamProperties.getBindings();
+ }
+
+ private boolean matchesCondition(String condition, T event) {
+ if (condition == null || condition.isEmpty()) {
+ return true;
+ }
+
+ try {
+ Expression expression = expressionParser.parseExpression(condition);
+ EvaluationContext context = new StandardEvaluationContext();
+ context.setVariable("event", event);
+ Boolean result = expression.getValue(context, Boolean.class);
+ return Boolean.TRUE.equals(result);
+ } catch (Exception e) {
+ log.warn("Failed to evaluate condition: {} for event: {}", condition, event, e);
+ return false;
+ }
+ }
+}
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/ListenerRegistration.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/ListenerRegistration.java
new file mode 100644
index 0000000..b55a7c4
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/ListenerRegistration.java
@@ -0,0 +1,121 @@
+package cn.structure.infra.stream.manager;
+
+import cn.structure.infra.stream.handler.StreamEventHandler;
+
+public class ListenerRegistration {
+
+ private String listenerId;
+ private Class eventType;
+ private StreamEventHandler handler;
+ private String condition;
+ private String destination;
+ private String group;
+
+ public ListenerRegistration() {
+ }
+
+ public ListenerRegistration(String listenerId, Class eventType, StreamEventHandler handler,
+ String condition, String destination, String group) {
+ this.listenerId = listenerId;
+ this.eventType = eventType;
+ this.handler = handler;
+ this.condition = condition;
+ this.destination = destination;
+ this.group = group;
+ }
+
+ public String getListenerId() {
+ return listenerId;
+ }
+
+ public void setListenerId(String listenerId) {
+ this.listenerId = listenerId;
+ }
+
+ public Class getEventType() {
+ return eventType;
+ }
+
+ public void setEventType(Class eventType) {
+ this.eventType = eventType;
+ }
+
+ public StreamEventHandler getHandler() {
+ return handler;
+ }
+
+ public void setHandler(StreamEventHandler handler) {
+ this.handler = handler;
+ }
+
+ public String getCondition() {
+ return condition;
+ }
+
+ public void setCondition(String condition) {
+ this.condition = condition;
+ }
+
+ public String getDestination() {
+ return destination;
+ }
+
+ public void setDestination(String destination) {
+ this.destination = destination;
+ }
+
+ public String getGroup() {
+ return group;
+ }
+
+ public void setGroup(String group) {
+ this.group = group;
+ }
+
+ public static Builder builder() {
+ return new Builder<>();
+ }
+
+ public static class Builder {
+ private String listenerId;
+ private Class eventType;
+ private StreamEventHandler handler;
+ private String condition;
+ private String destination;
+ private String group;
+
+ public Builder listenerId(String listenerId) {
+ this.listenerId = listenerId;
+ return this;
+ }
+
+ public Builder eventType(Class eventType) {
+ this.eventType = eventType;
+ return this;
+ }
+
+ public Builder handler(StreamEventHandler handler) {
+ this.handler = handler;
+ return this;
+ }
+
+ public Builder condition(String condition) {
+ this.condition = condition;
+ return this;
+ }
+
+ public Builder destination(String destination) {
+ this.destination = destination;
+ return this;
+ }
+
+ public Builder group(String group) {
+ this.group = group;
+ return this;
+ }
+
+ public ListenerRegistration build() {
+ return new ListenerRegistration<>(listenerId, eventType, handler, condition, destination, group);
+ }
+ }
+}
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/StreamEventManager.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/StreamEventManager.java
new file mode 100644
index 0000000..d33a7ec
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/manager/StreamEventManager.java
@@ -0,0 +1,49 @@
+package cn.structure.infra.stream.manager;
+
+import cn.structure.infra.stream.handler.StreamEventHandler;
+import cn.structure.infra.stream.properties.StreamProperties;
+
+import java.util.List;
+import java.util.Map;
+
+public interface StreamEventManager {
+
+ void publish(String bindingName, T event);
+
+ void publish(String bindingName, String destination, T event);
+
+ void publish(String bindingName, String destination, String group, T event);
+
+ void registerListener(String bindingName, Class eventType, StreamEventHandler handler);
+
+ void registerListener(String bindingName, Class eventType, String condition, StreamEventHandler handler);
+
+ void registerListener(String bindingName, String destination, String group, Class eventType, StreamEventHandler handler);
+
+ void registerListener(String bindingName, String destination, String group, Class eventType, String condition, StreamEventHandler handler);
+
+ void unregisterListener(String bindingName);
+
+ void unregisterListener(String bindingName, String listenerId);
+
+ boolean isListenerRegistered(String bindingName);
+
+ void dispatch(String bindingName, T event);
+
+ List> getListeners(String bindingName);
+
+ void registerBinding(String bindingName, String destination);
+
+ void registerBinding(String bindingName, String destination, String group);
+
+ void registerBinding(String bindingName, String destination, String group, String contentType, Integer concurrency);
+
+ void unregisterBinding(String bindingName);
+
+ boolean isBindingRegistered(String bindingName);
+
+ StreamProperties.Binding getBinding(String bindingName);
+
+ Map getAllBindings();
+
+}
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/processor/EventListenerBeanPostProcessor.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/processor/EventListenerBeanPostProcessor.java
new file mode 100644
index 0000000..0692d5b
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/processor/EventListenerBeanPostProcessor.java
@@ -0,0 +1,160 @@
+package cn.structure.infra.stream.processor;
+
+import cn.structure.infra.stream.annotation.StreamEventListener;
+import cn.structure.infra.stream.manager.StreamEventManager;
+import cn.structure.infra.stream.properties.StreamProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.core.MethodIntrospector;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.messaging.Message;
+import org.springframework.util.StringUtils;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class EventListenerBeanPostProcessor implements BeanPostProcessor {
+
+ private static final Logger log = LoggerFactory.getLogger(EventListenerBeanPostProcessor.class);
+
+ private final StreamEventManager streamEventManager;
+ private final StreamProperties streamProperties;
+
+ private final Map listenerBeans = new ConcurrentHashMap<>();
+
+ public EventListenerBeanPostProcessor(StreamEventManager streamEventManager, StreamProperties streamProperties) {
+ this.streamEventManager = streamEventManager;
+ this.streamProperties = streamProperties;
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+ Class> targetClass = bean.getClass();
+ Map annotatedMethods = MethodIntrospector.selectMethods(targetClass,
+ (MethodIntrospector.MetadataLookup) method ->
+ AnnotatedElementUtils.findMergedAnnotation(method, StreamEventListener.class));
+
+ for (Map.Entry entry : annotatedMethods.entrySet()) {
+ Method method = entry.getKey();
+ StreamEventListener annotation = entry.getValue();
+ registerListener(bean, method, annotation);
+ }
+
+ if (targetClass.isAnnotationPresent(StreamEventListener.class)) {
+ StreamEventListener annotation = targetClass.getAnnotation(StreamEventListener.class);
+ registerClassListener(bean, targetClass, annotation);
+ }
+
+ return bean;
+ }
+
+ private void registerListener(Object bean, Method method, StreamEventListener annotation) {
+ String bindingName = resolveBindingName(annotation);
+ String destination = annotation.destination();
+ String group = annotation.group();
+ Class> eventType = annotation.eventType();
+
+ if (eventType == Object.class && method.getParameterTypes().length > 0) {
+ eventType = method.getParameterTypes()[0];
+ }
+
+ if (bindingName.isEmpty()) {
+ bindingName = method.getName();
+ }
+
+ // 确保绑定信息注册到 StreamProperties(供 publish 方法使用)
+ ensureBindingRegistered(bindingName, destination, group, annotation.contentType());
+
+ listenerBeans.put(bindingName, bean);
+
+ if (!destination.isEmpty()) {
+ streamEventManager.registerListener(bindingName, destination, group, eventType, event -> {
+ try {
+ method.invoke(bean, event);
+ } catch (Exception e) {
+ log.error("Failed to invoke listener method: {}", method.getName(), e);
+ }
+ });
+ } else {
+ streamEventManager.registerListener(bindingName, eventType, event -> {
+ try {
+ method.invoke(bean, event);
+ } catch (Exception e) {
+ log.error("Failed to invoke listener method: {}", method.getName(), e);
+ }
+ });
+ }
+
+ log.info("Registered listener method: {} for binding: {}", method.getName(), bindingName);
+ }
+
+ private void registerClassListener(Object bean, Class> targetClass, StreamEventListener annotation) {
+ String bindingName = resolveBindingName(annotation);
+ String destination = annotation.destination();
+ String group = annotation.group();
+ Class> eventType = annotation.eventType();
+
+ if (bindingName.isEmpty()) {
+ bindingName = targetClass.getSimpleName();
+ }
+
+ ensureBindingRegistered(bindingName, destination, group, annotation.contentType());
+
+ listenerBeans.put(bindingName, bean);
+
+ if (!destination.isEmpty()) {
+ streamEventManager.registerListener(bindingName, destination, group, eventType, event -> {
+ try {
+ Method handleMethod = targetClass.getMethod("handle", eventType);
+ handleMethod.invoke(bean, event);
+ } catch (Exception e) {
+ log.error("Failed to invoke listener handle method on class: {}", targetClass.getName(), e);
+ }
+ });
+ } else {
+ streamEventManager.registerListener(bindingName, eventType, event -> {
+ try {
+ Method handleMethod = targetClass.getMethod("handle", eventType);
+ handleMethod.invoke(bean, event);
+ } catch (Exception e) {
+ log.error("Failed to invoke listener handle method on class: {}", targetClass.getName(), e);
+ }
+ });
+ }
+
+ log.info("Registered listener class: {} for binding: {}", targetClass.getName(), bindingName);
+ }
+
+ /**
+ * 确保绑定信息注册到 StreamProperties,供 publish 方法使用
+ */
+ private void ensureBindingRegistered(String bindingName, String destination, String group, String contentType) {
+ if (!streamProperties.getBindings().containsKey(bindingName)) {
+ StreamProperties.Binding binding = new StreamProperties.Binding();
+ if (StringUtils.hasText(destination)) {
+ binding.setDestination(destination);
+ } else {
+ binding.setDestination(toDestination(bindingName));
+ }
+ binding.setGroup(StringUtils.hasText(group) ? group : streamProperties.getDefaultGroup());
+ binding.setContentType(StringUtils.hasText(contentType) ? contentType : streamProperties.getDefaultContentType());
+ streamProperties.getBindings().put(bindingName, binding);
+ }
+ }
+
+ private String toDestination(String name) {
+ return name.replace(".", "-").replace("_", "-").toLowerCase() + "-exchange";
+ }
+
+ private String resolveBindingName(StreamEventListener annotation) {
+ String bindingName = annotation.bindingName();
+ if (bindingName.isEmpty()) {
+ bindingName = annotation.value();
+ }
+ return bindingName;
+ }
+
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..aa38c06
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/processor/StreamBindingBeanFactoryPostProcessor.java
@@ -0,0 +1,182 @@
+package cn.structure.infra.stream.processor;
+
+import cn.structure.infra.stream.annotation.StreamEventListener;
+import cn.structure.infra.stream.annotation.StreamRouteHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+
+import java.lang.reflect.Method;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 在 Bean 实例化之前,扫描所有 BeanDefinition 中的 @StreamEventListener 和 @StreamRouteHandler 注解,
+ * 自动注册 Spring Cloud Stream 绑定配置和 spring.cloud.function.definition。
+ *
+ * 这样用户只需在方法上标注 @StreamEventListener,框架会自动完成绑定创建。
+ */
+public class StreamBindingBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
+
+ private static final Logger log = LoggerFactory.getLogger(StreamBindingBeanFactoryPostProcessor.class);
+
+ private static final String SPRING_BINDINGS_PREFIX = "spring.cloud.stream.bindings";
+ private static final String FUNCTION_DEFINITION = "spring.cloud.function.definition";
+
+ @Override
+ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+ ConfigurableEnvironment environment = beanFactory.getBean(ConfigurableEnvironment.class);
+
+ Boolean enabled = environment.getProperty("structure.infra.stream.enabled", Boolean.class, Boolean.TRUE);
+ if (!enabled) {
+ return;
+ }
+
+ String defaultGroup = environment.getProperty("structure.infra.stream.default-group", "default");
+ String defaultContentType = environment.getProperty("structure.infra.stream.default-content-type", "application/json");
+ String defaultBinder = environment.getProperty("structure.infra.stream.default-binder");
+ Integer defaultConcurrency = environment.getProperty("structure.infra.stream.default-concurrency", Integer.class, 1);
+
+ Map properties = new LinkedHashMap<>();
+ Set functionDefinitions = new LinkedHashSet<>();
+
+ // 扫描所有 BeanDefinition
+ String[] beanNames = beanFactory.getBeanDefinitionNames();
+ for (String beanName : beanNames) {
+ BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
+ String beanClassName = beanDefinition.getBeanClassName();
+ if (beanClassName == null) {
+ continue;
+ }
+
+ Class> beanClass;
+ try {
+ beanClass = ClassUtils.forName(beanClassName, beanFactory.getBeanClassLoader());
+ } catch (ClassNotFoundException e) {
+ continue;
+ }
+
+ // 扫描方法上的 @StreamEventListener 注解
+ for (Method method : beanClass.getDeclaredMethods()) {
+ StreamEventListener annotation = AnnotatedElementUtils.findMergedAnnotation(method, StreamEventListener.class);
+ if (annotation != null) {
+ processStreamEventListener(annotation, defaultGroup, defaultContentType, defaultBinder, defaultConcurrency, properties, functionDefinitions);
+ }
+
+ StreamRouteHandler routeAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, StreamRouteHandler.class);
+ if (routeAnnotation != null) {
+ processStreamRouteHandler(routeAnnotation, defaultGroup, defaultContentType, defaultBinder, defaultConcurrency, properties, functionDefinitions);
+ }
+ }
+
+ // 处理类级别的 @StreamEventListener 注解
+ StreamEventListener classAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanClass, StreamEventListener.class);
+ if (classAnnotation != null) {
+ processStreamEventListener(classAnnotation, defaultGroup, defaultContentType, defaultBinder, defaultConcurrency, properties, functionDefinitions);
+ }
+ }
+
+ // 自动设置 spring.cloud.function.definition(如果未配置)
+ String existingDefinition = environment.getProperty(FUNCTION_DEFINITION);
+ if (!StringUtils.hasText(existingDefinition) && !functionDefinitions.isEmpty()) {
+ properties.put(FUNCTION_DEFINITION, String.join(";", functionDefinitions));
+ log.info("Auto set spring.cloud.function.definition: {}", String.join(";", functionDefinitions));
+ }
+
+ if (!properties.isEmpty()) {
+ environment.getPropertySources().addFirst(
+ new MapPropertySource("stream-auto-binding", properties));
+ }
+ }
+
+ private void processStreamEventListener(StreamEventListener annotation, String defaultGroup,
+ String defaultContentType, String defaultBinder, Integer defaultConcurrency,
+ Map properties, Set functionDefinitions) {
+ String bindingName = annotation.bindingName();
+ if (!StringUtils.hasText(bindingName)) {
+ bindingName = annotation.value();
+ }
+ if (!StringUtils.hasText(bindingName)) {
+ return;
+ }
+
+ String destination = StringUtils.hasText(annotation.destination())
+ ? annotation.destination()
+ : toDestination(bindingName);
+ String group = StringUtils.hasText(annotation.group()) ? annotation.group() : defaultGroup;
+ String contentType = StringUtils.hasText(annotation.contentType()) ? annotation.contentType() : defaultContentType;
+ String binder = StringUtils.hasText(defaultBinder) ? defaultBinder : null;
+
+ registerBinding(bindingName, destination, group, contentType, binder, defaultConcurrency, properties);
+ functionDefinitions.add(bindingName);
+ }
+
+ private void processStreamRouteHandler(StreamRouteHandler annotation, String defaultGroup,
+ String defaultContentType, String defaultBinder, Integer defaultConcurrency,
+ Map properties, Set functionDefinitions) {
+ String eventType = annotation.eventType();
+ if (!StringUtils.hasText(eventType)) {
+ eventType = annotation.value();
+ }
+ if (!StringUtils.hasText(eventType)) {
+ return;
+ }
+
+ String bindingName = toBindingName(eventType);
+ String destination = toDestination(eventType);
+
+ registerBinding(bindingName, destination, defaultGroup, defaultContentType, defaultBinder, defaultConcurrency, properties);
+ functionDefinitions.add(bindingName);
+ }
+
+ private void registerBinding(String bindingName, String destination, String group,
+ String contentType, String binder, Integer concurrency,
+ Map properties) {
+ String inputBinding = bindingName + "-in-0";
+ String outputBinding = bindingName + "-out-0";
+
+ String inputDestKey = SPRING_BINDINGS_PREFIX + "." + inputBinding + ".destination";
+ // 避免重复注册
+ if (properties.containsKey(inputDestKey)) {
+ return;
+ }
+
+ properties.put(inputDestKey, destination);
+ properties.put(SPRING_BINDINGS_PREFIX + "." + outputBinding + ".destination", destination);
+ properties.put(SPRING_BINDINGS_PREFIX + "." + inputBinding + ".content-type", contentType);
+ properties.put(SPRING_BINDINGS_PREFIX + "." + outputBinding + ".content-type", contentType);
+
+ if (StringUtils.hasText(group)) {
+ properties.put(SPRING_BINDINGS_PREFIX + "." + inputBinding + ".group", group);
+ }
+ if (StringUtils.hasText(binder)) {
+ properties.put(SPRING_BINDINGS_PREFIX + "." + inputBinding + ".binder", binder);
+ properties.put(SPRING_BINDINGS_PREFIX + "." + outputBinding + ".binder", binder);
+ }
+ if (concurrency != null) {
+ properties.put(SPRING_BINDINGS_PREFIX + "." + inputBinding + ".consumer.concurrency", concurrency);
+ }
+
+ log.info("Auto registered binding: {}, destination: {}, group: {}, contentType: {}",
+ bindingName, destination, group, contentType);
+ }
+
+ private String toBindingName(String eventType) {
+ return eventType.replace(".", "-").replace("_", "-").toLowerCase();
+ }
+
+ private String toDestination(String name) {
+ return name.replace(".", "-").replace("_", "-").toLowerCase() + "-exchange";
+ }
+
+}
\ No newline at end of file
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/properties/StreamProperties.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/properties/StreamProperties.java
new file mode 100644
index 0000000..73916e5
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/properties/StreamProperties.java
@@ -0,0 +1,156 @@
+package cn.structure.infra.stream.properties;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@ConfigurationProperties(prefix = "structure.infra.stream")
+public class StreamProperties {
+
+ private boolean enabled = true;
+ private boolean autoBinding = true;
+ private String defaultGroup = "default";
+ private String defaultContentType = "application/json";
+ private String defaultBinder;
+ private Integer defaultConcurrency = 1;
+ private Map bindings = new HashMap<>();
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isAutoBinding() {
+ return autoBinding;
+ }
+
+ public void setAutoBinding(boolean autoBinding) {
+ this.autoBinding = autoBinding;
+ }
+
+ public String getDefaultGroup() {
+ return defaultGroup;
+ }
+
+ public void setDefaultGroup(String defaultGroup) {
+ this.defaultGroup = defaultGroup;
+ }
+
+ public String getDefaultContentType() {
+ return defaultContentType;
+ }
+
+ public void setDefaultContentType(String defaultContentType) {
+ this.defaultContentType = defaultContentType;
+ }
+
+ public String getDefaultBinder() {
+ return defaultBinder;
+ }
+
+ public void setDefaultBinder(String defaultBinder) {
+ this.defaultBinder = defaultBinder;
+ }
+
+ public Integer getDefaultConcurrency() {
+ return defaultConcurrency;
+ }
+
+ public void setDefaultConcurrency(Integer defaultConcurrency) {
+ this.defaultConcurrency = defaultConcurrency;
+ }
+
+ public Map getBindings() {
+ return bindings;
+ }
+
+ public void setBindings(Map bindings) {
+ this.bindings = bindings;
+ }
+
+ public Binding getBinding(String bindingName) {
+ return bindings.computeIfAbsent(bindingName, k -> new Binding());
+ }
+
+ public static class Binding {
+ private String destination;
+ private String contentType = "application/json";
+ private String group;
+ private String binder;
+ private Integer concurrency;
+ private String consumerPrefix = "consumer";
+ private String producerPrefix = "producer";
+
+ public Binding() {
+ }
+
+ public Binding(String destination) {
+ this.destination = destination;
+ }
+
+ public Binding(String destination, String group) {
+ this.destination = destination;
+ this.group = group;
+ }
+
+ public String getDestination() {
+ return destination;
+ }
+
+ public void setDestination(String destination) {
+ this.destination = destination;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+
+ public String getGroup() {
+ return group;
+ }
+
+ public void setGroup(String group) {
+ this.group = group;
+ }
+
+ public String getBinder() {
+ return binder;
+ }
+
+ public void setBinder(String binder) {
+ this.binder = binder;
+ }
+
+ public Integer getConcurrency() {
+ return concurrency;
+ }
+
+ public void setConcurrency(Integer concurrency) {
+ this.concurrency = concurrency;
+ }
+
+ public String getConsumerPrefix() {
+ return consumerPrefix;
+ }
+
+ public void setConsumerPrefix(String consumerPrefix) {
+ this.consumerPrefix = consumerPrefix;
+ }
+
+ public String getProducerPrefix() {
+ return producerPrefix;
+ }
+
+ public void setProducerPrefix(String producerPrefix) {
+ this.producerPrefix = producerPrefix;
+ }
+ }
+}
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/ConfigurableRouteInitializer.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/ConfigurableRouteInitializer.java
new file mode 100644
index 0000000..4639b7f
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/ConfigurableRouteInitializer.java
@@ -0,0 +1,82 @@
+package cn.structure.infra.stream.router;
+
+import cn.structure.infra.stream.event.StreamEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+
+@Component
+public class ConfigurableRouteInitializer implements CommandLineRunner {
+
+ private static final Logger log = LoggerFactory.getLogger(ConfigurableRouteInitializer.class);
+
+ private final RouterProperties routerProperties;
+ private final StreamEventRouter eventRouter;
+ private final ApplicationContext applicationContext;
+
+ public ConfigurableRouteInitializer(RouterProperties routerProperties,
+ StreamEventRouter eventRouter,
+ ApplicationContext applicationContext) {
+ this.routerProperties = routerProperties;
+ this.eventRouter = eventRouter;
+ this.applicationContext = applicationContext;
+ }
+
+ @Override
+ public void run(String... args) {
+ if (!routerProperties.isEnabled()) {
+ log.info("Configurable router is disabled");
+ return;
+ }
+
+ log.info("========== 配置驱动路由初始化 ==========");
+
+ for (RouterProperties.RouteDefinition route : routerProperties.getRoutes()) {
+ try {
+ registerRoute(route);
+ log.info("Registered route: id={}, eventType={}, handler={}.{}",
+ route.getId(), route.getEventType(), route.getHandlerBean(), route.getHandlerMethod());
+ } catch (Exception e) {
+ log.error("Failed to register route: id={}, eventType={}", route.getId(), route.getEventType(), e);
+ }
+ }
+
+ log.info("========== 配置驱动路由初始化完成,共 {} 条路由 ==========", routerProperties.getRoutes().size());
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void registerRoute(RouterProperties.RouteDefinition route) throws Exception {
+ Class> payloadType = Class.forName(route.getPayloadType());
+ Object handlerBean = applicationContext.getBean(route.getHandlerBean());
+ Method handlerMethod = handlerBean.getClass().getDeclaredMethod(route.getHandlerMethod(), payloadType);
+ handlerMethod.setAccessible(true);
+
+ StreamEventRouter.StreamRouteHandler handler = (payload, event) -> {
+ try {
+ handlerMethod.invoke(handlerBean, payload);
+ } catch (Exception e) {
+ log.error("Error invoking handler: {}.{}", route.getHandlerBean(), route.getHandlerMethod(), e);
+ throw new RuntimeException("Error invoking handler", e);
+ }
+ };
+
+ if (route.getBusinessType() != null && !route.getBusinessType().isEmpty()) {
+ if (route.getCondition() != null && !route.getCondition().isEmpty()) {
+ eventRouter.registerRoute(route.getEventType(), route.getBusinessType(), payloadType, route.getCondition(), handler);
+ } else {
+ eventRouter.registerRoute(route.getEventType(), route.getBusinessType(), payloadType, handler);
+ }
+ } else {
+ if (route.getCondition() != null && !route.getCondition().isEmpty()) {
+ eventRouter.registerRoute(route.getEventType(), payloadType, route.getCondition(), handler);
+ } else {
+ eventRouter.registerRoute(route.getEventType(), payloadType, handler);
+ }
+ }
+ }
+}
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/DefaultStreamEventRouterImpl.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/DefaultStreamEventRouterImpl.java
new file mode 100644
index 0000000..222626a
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/DefaultStreamEventRouterImpl.java
@@ -0,0 +1,142 @@
+package cn.structure.infra.stream.router;
+
+import cn.structure.infra.stream.event.StreamEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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 java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class DefaultStreamEventRouterImpl implements StreamEventRouter {
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultStreamEventRouterImpl.class);
+
+ private final Map>> routeRegistrations = new ConcurrentHashMap<>();
+ private final SpelExpressionParser expressionParser = new SpelExpressionParser();
+ private final java.util.concurrent.atomic.AtomicLong handlerCounter = new java.util.concurrent.atomic.AtomicLong(0);
+
+ @Override
+ public void registerRoute(String eventType, Class payloadType, StreamRouteHandler handler) {
+ registerRoute(eventType, "", payloadType, "", handler);
+ }
+
+ @Override
+ public void registerRoute(String eventType, Class payloadType, String condition, StreamRouteHandler handler) {
+ registerRoute(eventType, "", payloadType, condition, handler);
+ }
+
+ @Override
+ public void registerRoute(String eventType, String businessType, Class payloadType, StreamRouteHandler handler) {
+ registerRoute(eventType, businessType, payloadType, "", handler);
+ }
+
+ @Override
+ public void registerRoute(String eventType, String businessType, Class payloadType,
+ String condition, StreamRouteHandler handler) {
+ String handlerId = generateHandlerId(eventType, businessType, payloadType);
+ RouteRegistration registration = new RouteRegistration<>(handlerId, eventType, businessType,
+ payloadType, condition, handler);
+
+ routeRegistrations.computeIfAbsent(eventType, k -> new ArrayList<>()).add(registration);
+
+ log.info("Registered route: eventType={}, businessType={}, payloadType={}, condition={}",
+ eventType, businessType, payloadType.getName(), condition);
+ }
+
+ @Override
+ public void unregisterRoute(String eventType) {
+ routeRegistrations.remove(eventType);
+ log.info("Unregistered all routes for eventType: {}", eventType);
+ }
+
+ @Override
+ public void unregisterRoute(String eventType, String handlerId) {
+ List> registrations = routeRegistrations.get(eventType);
+ if (registrations != null) {
+ boolean removed = registrations.removeIf(r -> r.getHandlerId().equals(handlerId));
+ if (removed) {
+ log.info("Unregistered route: {} for eventType: {}", handlerId, eventType);
+ }
+ if (registrations.isEmpty()) {
+ routeRegistrations.remove(eventType);
+ }
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void route(StreamEvent event) {
+ if (event == null || event.getEventType() == null) {
+ log.warn("Cannot route null event or event with null eventType");
+ return;
+ }
+
+ String eventType = event.getEventType();
+ List> registrations = routeRegistrations.get(eventType);
+
+ if (registrations == null || registrations.isEmpty()) {
+ log.debug("No routes registered for eventType: {}", eventType);
+ return;
+ }
+
+ log.debug("Routing event: eventId={}, eventType={}, businessType={}",
+ event.getEventId(), eventType, event.getBusinessType());
+
+ for (RouteRegistration> registration : registrations) {
+ if (matchesBusinessType(registration.getBusinessType(), event.getBusinessType()) &&
+ registration.getPayloadType().isInstance(event.getPayload()) &&
+ matchesCondition(registration.getCondition(), event.getPayload())) {
+ try {
+ ((StreamRouteHandler) registration.getHandler()).handle(event.getPayload(), event);
+ log.debug("Dispatched event to handler: {} for eventType: {}", registration.getHandlerId(), eventType);
+ } catch (Exception e) {
+ log.error("Error handling event in handler: {} for eventType: {}", registration.getHandlerId(), eventType, e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean isRouteRegistered(String eventType) {
+ return routeRegistrations.containsKey(eventType) && !routeRegistrations.get(eventType).isEmpty();
+ }
+
+ @Override
+ public List> getRoutes(String eventType) {
+ return routeRegistrations.getOrDefault(eventType, new ArrayList<>());
+ }
+
+ private String generateHandlerId(String eventType, String businessType, Class> payloadType) {
+ return eventType + ":" + (businessType != null ? businessType : "default") + ":" + payloadType.getSimpleName() + ":" + handlerCounter.incrementAndGet();
+ }
+
+ private boolean matchesBusinessType(String pattern, String businessType) {
+ if (pattern == null || pattern.isEmpty() || "*".equals(pattern)) {
+ return true;
+ }
+ return pattern.equals(businessType);
+ }
+
+ private boolean matchesCondition(String condition, T payload) {
+ if (condition == null || condition.isEmpty()) {
+ return true;
+ }
+
+ try {
+ Expression expression = expressionParser.parseExpression(condition);
+ EvaluationContext context = new StandardEvaluationContext();
+ context.setVariable("payload", payload);
+ Boolean result = expression.getValue(context, Boolean.class);
+ return Boolean.TRUE.equals(result);
+ } catch (Exception e) {
+ log.warn("Failed to evaluate condition: {} for payload: {}", condition, payload, e);
+ return false;
+ }
+ }
+}
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouteHandlerBeanPostProcessor.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouteHandlerBeanPostProcessor.java
new file mode 100644
index 0000000..c4c2c21
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouteHandlerBeanPostProcessor.java
@@ -0,0 +1,81 @@
+package cn.structure.infra.stream.router;
+
+import cn.structure.infra.stream.annotation.StreamRouteHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+
+@Component
+public class RouteHandlerBeanPostProcessor implements BeanPostProcessor {
+
+ private static final Logger log = LoggerFactory.getLogger(RouteHandlerBeanPostProcessor.class);
+
+ private final StreamEventRouter eventRouter;
+
+ public RouteHandlerBeanPostProcessor(StreamEventRouter eventRouter) {
+ this.eventRouter = eventRouter;
+ }
+
+ @Override
+ public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+ return bean;
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+ Class> beanClass = bean.getClass();
+ for (Method method : beanClass.getDeclaredMethods()) {
+ if (method.isAnnotationPresent(StreamRouteHandler.class)) {
+ StreamRouteHandler annotation = method.getAnnotation(StreamRouteHandler.class);
+ String eventType = annotation.eventType();
+ if (eventType.isEmpty()) {
+ eventType = annotation.value();
+ }
+ String businessType = annotation.businessType();
+ String condition = annotation.condition();
+
+ if (eventType.isEmpty()) {
+ log.warn("Skipping method {} in bean {}: eventType is not specified", method.getName(), beanName);
+ continue;
+ }
+
+ Class>[] parameterTypes = method.getParameterTypes();
+ if (parameterTypes.length == 0) {
+ log.warn("Skipping method {} in bean {}: no parameters found", method.getName(), beanName);
+ continue;
+ }
+
+ Class> payloadType = parameterTypes[0];
+
+ registerRoute(eventType, businessType, payloadType, condition, bean, method);
+ }
+ }
+ return bean;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void registerRoute(String eventType, String businessType, Class> payloadType,
+ String condition, Object bean, Method method) {
+ try {
+ method.setAccessible(true);
+ StreamEventRouter.StreamRouteHandler handler = (payload, event) -> {
+ try {
+ method.invoke(bean, payload);
+ } catch (Exception e) {
+ log.error("Error invoking handler method: {}", method.getName(), e);
+ throw new RuntimeException("Error invoking handler method", e);
+ }
+ };
+
+ eventRouter.registerRoute(eventType, businessType, payloadType, condition, handler);
+ log.info("Registered route handler: eventType={}, businessType={}, payloadType={}, method={}",
+ eventType, businessType, payloadType.getName(), method.getName());
+ } catch (Exception e) {
+ log.error("Failed to register route handler for method: {}", method.getName(), e);
+ }
+ }
+}
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouteRegistration.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouteRegistration.java
new file mode 100644
index 0000000..599e5bc
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouteRegistration.java
@@ -0,0 +1,119 @@
+package cn.structure.infra.stream.router;
+
+public class RouteRegistration {
+
+ private String handlerId;
+ private String eventType;
+ private String businessType;
+ private Class payloadType;
+ private String condition;
+ private StreamEventRouter.StreamRouteHandler handler;
+
+ public RouteRegistration() {
+ }
+
+ public RouteRegistration(String handlerId, String eventType, String businessType,
+ Class payloadType, String condition, StreamEventRouter.StreamRouteHandler handler) {
+ this.handlerId = handlerId;
+ this.eventType = eventType;
+ this.businessType = businessType;
+ this.payloadType = payloadType;
+ this.condition = condition;
+ this.handler = handler;
+ }
+
+ public String getHandlerId() {
+ return handlerId;
+ }
+
+ public void setHandlerId(String handlerId) {
+ this.handlerId = handlerId;
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ public void setEventType(String eventType) {
+ this.eventType = eventType;
+ }
+
+ public String getBusinessType() {
+ return businessType;
+ }
+
+ public void setBusinessType(String businessType) {
+ this.businessType = businessType;
+ }
+
+ public Class getPayloadType() {
+ return payloadType;
+ }
+
+ public void setPayloadType(Class payloadType) {
+ this.payloadType = payloadType;
+ }
+
+ public String getCondition() {
+ return condition;
+ }
+
+ public void setCondition(String condition) {
+ this.condition = condition;
+ }
+
+ public StreamEventRouter.StreamRouteHandler getHandler() {
+ return handler;
+ }
+
+ public void setHandler(StreamEventRouter.StreamRouteHandler handler) {
+ this.handler = handler;
+ }
+
+ public static Builder builder() {
+ return new Builder<>();
+ }
+
+ public static class Builder {
+ private String handlerId;
+ private String eventType;
+ private String businessType;
+ private Class payloadType;
+ private String condition;
+ private StreamEventRouter.StreamRouteHandler handler;
+
+ public Builder handlerId(String handlerId) {
+ this.handlerId = handlerId;
+ return this;
+ }
+
+ public Builder eventType(String eventType) {
+ this.eventType = eventType;
+ return this;
+ }
+
+ public Builder businessType(String businessType) {
+ this.businessType = businessType;
+ return this;
+ }
+
+ public Builder payloadType(Class payloadType) {
+ this.payloadType = payloadType;
+ return this;
+ }
+
+ public Builder condition(String condition) {
+ this.condition = condition;
+ return this;
+ }
+
+ public Builder handler(StreamEventRouter.StreamRouteHandler handler) {
+ this.handler = handler;
+ return this;
+ }
+
+ public RouteRegistration build() {
+ return new RouteRegistration<>(handlerId, eventType, businessType, payloadType, condition, handler);
+ }
+ }
+}
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouterProperties.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouterProperties.java
new file mode 100644
index 0000000..7951775
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/RouterProperties.java
@@ -0,0 +1,104 @@
+package cn.structure.infra.stream.router;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@ConfigurationProperties(prefix = "structure.infra.stream.router")
+public class RouterProperties {
+
+ private boolean enabled = true;
+ private List routes = new ArrayList<>();
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public List getRoutes() {
+ return routes;
+ }
+
+ public void setRoutes(List routes) {
+ this.routes = routes;
+ }
+
+ public static class RouteDefinition {
+ private String id;
+ private String eventType;
+ private String businessType;
+ private String payloadType;
+ private String condition;
+ private String handlerBean;
+ private String handlerMethod;
+ private String description;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ public void setEventType(String eventType) {
+ this.eventType = eventType;
+ }
+
+ public String getBusinessType() {
+ return businessType;
+ }
+
+ public void setBusinessType(String businessType) {
+ this.businessType = businessType;
+ }
+
+ public String getPayloadType() {
+ return payloadType;
+ }
+
+ public void setPayloadType(String payloadType) {
+ this.payloadType = payloadType;
+ }
+
+ public String getCondition() {
+ return condition;
+ }
+
+ public void setCondition(String condition) {
+ this.condition = condition;
+ }
+
+ public String getHandlerBean() {
+ return handlerBean;
+ }
+
+ public void setHandlerBean(String handlerBean) {
+ this.handlerBean = handlerBean;
+ }
+
+ public String getHandlerMethod() {
+ return handlerMethod;
+ }
+
+ public void setHandlerMethod(String handlerMethod) {
+ this.handlerMethod = handlerMethod;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+ }
+}
diff --git a/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/StreamEventRouter.java b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/StreamEventRouter.java
new file mode 100644
index 0000000..6b2c9e0
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/java/cn/structure/infra/stream/router/StreamEventRouter.java
@@ -0,0 +1,31 @@
+package cn.structure.infra.stream.router;
+
+import cn.structure.infra.stream.event.StreamEvent;
+
+import java.util.List;
+
+public interface StreamEventRouter {
+
+ void registerRoute(String eventType, Class payloadType, StreamRouteHandler handler);
+
+ void registerRoute(String eventType, Class payloadType, String condition, StreamRouteHandler handler);
+
+ void registerRoute(String eventType, String businessType, Class payloadType, StreamRouteHandler handler);
+
+ void registerRoute(String eventType, String businessType, Class payloadType, String condition, StreamRouteHandler handler);
+
+ void unregisterRoute(String eventType);
+
+ void unregisterRoute(String eventType, String handlerId);
+
+ void route(StreamEvent event);
+
+ boolean isRouteRegistered(String eventType);
+
+ List> getRoutes(String eventType);
+
+ interface StreamRouteHandler {
+ void handle(T payload, StreamEvent event);
+ }
+
+}
diff --git a/structure-infra-stream-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/structure-infra-stream-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..6b03bd8
--- /dev/null
+++ b/structure-infra-stream-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+cn.structure.infra.stream.configuration.StreamAutoConfiguration
diff --git a/structure-infra-xxljob-starter/pom.xml b/structure-infra-xxljob-starter/pom.xml
new file mode 100644
index 0000000..c0a75c5
--- /dev/null
+++ b/structure-infra-xxljob-starter/pom.xml
@@ -0,0 +1,44 @@
+
+
+ 4.0.0
+
+ cn.structured
+ structure-pro-infra
+ ${revision}
+ ../pom.xml
+
+
+ structure-pro-xxljob-starter
+ structure-infra-xxljob-starter
+ structure-pro-xxljob-starter
+ jar
+
+
+
+ cn.structured
+ structure-common
+
+
+ cn.structured
+ structure-infra-schedule-starter
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+
+ cn.structured
+ structure-job-starter
+
+
+
+
\ No newline at end of file
diff --git a/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/configuration/AutoXxlJobConfiguration.java b/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/configuration/AutoXxlJobConfiguration.java
new file mode 100644
index 0000000..e898026
--- /dev/null
+++ b/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/configuration/AutoXxlJobConfiguration.java
@@ -0,0 +1,34 @@
+package cn.structure.infra.configuration;
+
+import cn.structure.infra.properties.XxlJobProperties;
+import cn.structure.infra.schedule.TaskScheduler;
+import cn.structure.infra.schedule.xxljob.XxlJobTaskScheduler;
+import cn.structure.infra.schedule.xxljob.XxlJobTemplate;
+import cn.structure.infra.schedule.xxljob.XxlJobTemplateImpl;
+import cn.structure.job.rpc.XxlJobClient;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+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;
+
+@Slf4j
+@Configuration
+@EnableConfigurationProperties(XxlJobProperties.class)
+@AutoConfigureBefore(cn.structure.infra.configuration.AutoScheduleConfiguration.class)
+public class AutoXxlJobConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(XxlJobTemplate.class)
+ public XxlJobTemplate xxlJobTemplate(XxlJobClient xxlJobClient, XxlJobProperties xxlJobProperties) {
+ log.info(">>>>>>>>>>> xxl-job template init.");
+ return new XxlJobTemplateImpl(xxlJobClient, xxlJobProperties);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(TaskScheduler.class)
+ public TaskScheduler taskScheduler(XxlJobTemplate xxlJobTemplate) {
+ return new XxlJobTaskScheduler(xxlJobTemplate);
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/properties/XxlJobProperties.java b/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/properties/XxlJobProperties.java
new file mode 100644
index 0000000..83546c5
--- /dev/null
+++ b/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/properties/XxlJobProperties.java
@@ -0,0 +1,14 @@
+package cn.structure.infra.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@Data
+@ConfigurationProperties(prefix = "structure.schedule.xxl-job")
+public class XxlJobProperties {
+
+ private boolean enabled = true;
+
+ private Integer jobGroup = 1;
+
+}
\ No newline at end of file
diff --git a/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTaskScheduler.java b/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTaskScheduler.java
new file mode 100644
index 0000000..50f4e4b
--- /dev/null
+++ b/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTaskScheduler.java
@@ -0,0 +1,163 @@
+package cn.structure.infra.schedule.xxljob;
+
+import cn.structure.infra.schedule.ScheduleTask;
+import cn.structure.infra.schedule.TaskScheduler;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+public class XxlJobTaskScheduler implements TaskScheduler {
+
+ private final XxlJobTemplate xxlJobTemplate;
+
+ private final Map taskIdToXxlJobIdMap = new ConcurrentHashMap<>();
+
+ private final Map taskMap = new ConcurrentHashMap<>();
+
+ public XxlJobTaskScheduler(XxlJobTemplate xxlJobTemplate) {
+ this.xxlJobTemplate = xxlJobTemplate;
+ log.info("XxlJobTaskScheduler initialized");
+ }
+
+ @Override
+ public void schedule(ScheduleTask task) {
+ validateTask(task);
+
+ remove(task.getTaskId());
+
+ String cronExpression = convertToCron(task);
+
+ String xxlJobId = xxlJobTemplate.add(
+ task.getTaskName(),
+ cronExpression,
+ task.getHandlerName(),
+ task.getHandlerParam()
+ );
+
+ taskIdToXxlJobIdMap.put(task.getTaskId(), xxlJobId);
+ task.setStatus(ScheduleTask.TaskStatus.RUNNING);
+ taskMap.put(task.getTaskId(), task);
+
+ log.info("Scheduled task via XXL-Job: taskId={}, xxlJobId={}, handler={}",
+ task.getTaskId(), xxlJobId, task.getHandlerName());
+ }
+
+ @Override
+ public void update(ScheduleTask task) {
+ validateTask(task);
+
+ String xxlJobId = taskIdToXxlJobIdMap.get(task.getTaskId());
+ if (xxlJobId == null) {
+ log.warn("XXL-Job task not found for update: {}", task.getTaskId());
+ schedule(task);
+ return;
+ }
+
+ String cronExpression = convertToCron(task);
+
+ xxlJobTemplate.update(
+ xxlJobId,
+ task.getTaskName(),
+ cronExpression,
+ task.getHandlerName(),
+ task.getHandlerParam()
+ );
+
+ task.setStatus(ScheduleTask.TaskStatus.RUNNING);
+ taskMap.put(task.getTaskId(), task);
+
+ log.info("Updated task via XXL-Job: taskId={}, xxlJobId={}", task.getTaskId(), xxlJobId);
+ }
+
+ @Override
+ public void remove(String taskId) {
+ String xxlJobId = taskIdToXxlJobIdMap.remove(taskId);
+ if (xxlJobId != null) {
+ xxlJobTemplate.remove(xxlJobId);
+ }
+
+ ScheduleTask task = taskMap.remove(taskId);
+ if (task != null) {
+ task.setStatus(ScheduleTask.TaskStatus.STOPPED);
+ }
+
+ log.info("Removed task via XXL-Job: taskId={}, xxlJobId={}", taskId, xxlJobId);
+ }
+
+ @Override
+ public void pause(String taskId) {
+ String xxlJobId = taskIdToXxlJobIdMap.get(taskId);
+ if (xxlJobId != null) {
+ xxlJobTemplate.pause(xxlJobId);
+
+ ScheduleTask task = taskMap.get(taskId);
+ if (task != null) {
+ task.setStatus(ScheduleTask.TaskStatus.PAUSED);
+ }
+ }
+
+ log.info("Paused task via XXL-Job: taskId={}, xxlJobId={}", taskId, xxlJobId);
+ }
+
+ @Override
+ public void resume(String taskId) {
+ String xxlJobId = taskIdToXxlJobIdMap.get(taskId);
+ if (xxlJobId != null) {
+ ScheduleTask task = taskMap.get(taskId);
+ if (task != null && task.getStatus() == ScheduleTask.TaskStatus.PAUSED) {
+ xxlJobTemplate.start(xxlJobId);
+ task.setStatus(ScheduleTask.TaskStatus.RUNNING);
+ }
+ }
+
+ log.info("Resumed task via XXL-Job: taskId={}, xxlJobId={}", taskId, xxlJobId);
+ }
+
+ @Override
+ public ScheduleTask getTaskInfo(String taskId) {
+ return taskMap.get(taskId);
+ }
+
+ @Override
+ public List getAllTasks() {
+ return List.copyOf(taskMap.values());
+ }
+
+ private void validateTask(ScheduleTask task) {
+ if (task == null || task.getTaskId() == null) {
+ throw new IllegalArgumentException("Task and taskId cannot be null");
+ }
+
+ if (task.getHandlerName() == null || task.getHandlerName().isEmpty()) {
+ throw new IllegalArgumentException("Handler name cannot be null or empty");
+ }
+
+ if (task.getScheduleType() == null) {
+ throw new IllegalArgumentException("ScheduleType cannot be null");
+ }
+ }
+
+ private String convertToCron(ScheduleTask task) {
+ if (task.getScheduleType() == ScheduleTask.ScheduleType.CRON) {
+ if (task.getCronExpression() == null || task.getCronExpression().isEmpty()) {
+ throw new IllegalArgumentException("Cron expression cannot be null for CRON schedule type");
+ }
+ return task.getCronExpression();
+ }
+
+ long milliseconds = task.getScheduleType() == ScheduleTask.ScheduleType.FIXED_RATE
+ ? (task.getPeriod() != null ? task.getPeriod() : 1000L)
+ : (task.getDelay() != null ? task.getDelay() : 1000L);
+
+ long seconds = milliseconds / 1000;
+
+ if (seconds < 1) {
+ seconds = 1;
+ }
+
+ return "0/" + seconds + " * * * * ?";
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTemplate.java b/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTemplate.java
new file mode 100644
index 0000000..8b64e5c
--- /dev/null
+++ b/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTemplate.java
@@ -0,0 +1,16 @@
+package cn.structure.infra.schedule.xxljob;
+
+public interface XxlJobTemplate {
+
+ String add(String jobName, String cronExpression, String handlerName, String handlerParam);
+
+ void update(String jobId, String jobName, String cronExpression, String handlerName, String handlerParam);
+
+ void remove(String jobId);
+
+ void pause(String jobId);
+
+ void start(String jobId);
+
+ String getJobId(String handlerName);
+}
\ No newline at end of file
diff --git a/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTemplateImpl.java b/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTemplateImpl.java
new file mode 100644
index 0000000..5c03f3c
--- /dev/null
+++ b/structure-infra-xxljob-starter/src/main/java/cn/structure/infra/schedule/xxljob/XxlJobTemplateImpl.java
@@ -0,0 +1,101 @@
+package cn.structure.infra.schedule.xxljob;
+
+import cn.structure.infra.properties.XxlJobProperties;
+import cn.structure.job.dto.XxlJobInfoDTO;
+import cn.structure.job.enums.ExecutorRouteStrategyEnum;
+import cn.structure.job.rpc.XxlJobClient;
+import com.xxl.job.core.constant.ExecutorBlockStrategyEnum;
+import com.xxl.job.core.context.XxlJobContext;
+import com.xxl.job.core.glue.GlueTypeEnum;
+import com.xxl.tool.response.Response;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@AllArgsConstructor
+public class XxlJobTemplateImpl implements XxlJobTemplate {
+
+ private final XxlJobClient xxlJobClient;
+
+ private final XxlJobProperties jobProperties;
+
+ @Override
+ public String add(String jobName, String cronExpression, String handlerName, String handlerParam) {
+ XxlJobInfoDTO jobInfo = buildJobInfo(null, jobName, cronExpression, handlerName, handlerParam);
+ Response returnT = xxlJobClient.add(jobInfo);
+ if (returnT.getCode() == XxlJobContext.HANDLE_CODE_SUCCESS) {
+ log.info("XXL-Job add success: jobName={}, handlerName={}, jobId={}", jobName, handlerName, returnT.getData());
+ return returnT.getData();
+ } else {
+ log.error("XXL-Job add failed: jobName={}, handlerName={}, message={}", jobName, handlerName, returnT.getMsg());
+ throw new RuntimeException("XXL-Job add failed: " + returnT.getMsg());
+ }
+ }
+
+ @Override
+ public void update(String jobId, String jobName, String cronExpression, String handlerName, String handlerParam) {
+ XxlJobInfoDTO jobInfo = buildJobInfo(Integer.parseInt(jobId), jobName, cronExpression, handlerName, handlerParam);
+ Response returnT = xxlJobClient.update(jobInfo);
+ if (returnT.getCode() == XxlJobContext.HANDLE_CODE_SUCCESS) {
+ log.info("XXL-Job update success: jobId={}, jobName={}, handlerName={}", jobId, jobName, handlerName);
+ } else {
+ log.error("XXL-Job update failed: jobId={}, jobName={}, message={}", jobId, jobName, returnT.getMsg());
+ throw new RuntimeException("XXL-Job update failed: " + returnT.getMsg());
+ }
+ }
+
+ @Override
+ public void remove(String jobId) {
+ Response returnT = xxlJobClient.remove(jobId);
+ if (returnT.getCode() == XxlJobContext.HANDLE_CODE_SUCCESS) {
+ log.info("XXL-Job remove success: jobId={}", jobId);
+ } else {
+ log.error("XXL-Job remove failed: jobId={}, message={}", jobId, returnT.getMsg());
+ throw new RuntimeException("XXL-Job remove failed: " + returnT.getMsg());
+ }
+ }
+
+ @Override
+ public void pause(String jobId) {
+ Response returnT = xxlJobClient.pause(jobId);
+ if (returnT.getCode() == XxlJobContext.HANDLE_CODE_SUCCESS) {
+ log.info("XXL-Job pause success: jobId={}", jobId);
+ } else {
+ log.error("XXL-Job pause failed: jobId={}, message={}", jobId, returnT.getMsg());
+ throw new RuntimeException("XXL-Job pause failed: " + returnT.getMsg());
+ }
+ }
+
+ @Override
+ public void start(String jobId) {
+ Response returnT = xxlJobClient.start(jobId);
+ if (returnT.getCode() == XxlJobContext.HANDLE_CODE_SUCCESS) {
+ log.info("XXL-Job start success: jobId={}", jobId);
+ } else {
+ log.error("XXL-Job start failed: jobId={}, message={}", jobId, returnT.getMsg());
+ throw new RuntimeException("XXL-Job start failed: " + returnT.getMsg());
+ }
+ }
+
+ @Override
+ public String getJobId(String handlerName) {
+ return null;
+ }
+
+ private XxlJobInfoDTO buildJobInfo(Integer id, String jobName, String cronExpression, String handlerName, String handlerParam) {
+ XxlJobInfoDTO jobInfo = new XxlJobInfoDTO();
+ jobInfo.setId(id);
+ jobInfo.setJobGroup(jobProperties.getJobGroup());
+ jobInfo.setJobCron(cronExpression);
+ jobInfo.setJobDesc(jobName);
+ jobInfo.setAuthor("system");
+ jobInfo.setExecutorHandler(handlerName);
+ jobInfo.setExecutorParam(handlerParam);
+ jobInfo.setExecutorRouteStrategy(ExecutorRouteStrategyEnum.FIRST.name());
+ jobInfo.setExecutorBlockStrategy(ExecutorBlockStrategyEnum.SERIAL_EXECUTION.name());
+ jobInfo.setExecutorTimeout(300);
+ jobInfo.setExecutorFailRetryCount(1);
+ jobInfo.setGlueType(GlueTypeEnum.BEAN.name());
+ return jobInfo;
+ }
+}
\ No newline at end of file
diff --git a/structure-infra-xxljob-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/structure-infra-xxljob-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 0000000..dd48a62
--- /dev/null
+++ b/structure-infra-xxljob-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+cn.structure.infra.configuration.AutoXxlJobConfiguration
\ No newline at end of file
From 2670b5b10cc2eeb3db7e989ab3e43d543807425f Mon Sep 17 00:00:00 2001
From: chuck <361648887@qq.com>
Date: Sat, 4 Jul 2026 01:33:44 +0800
Subject: [PATCH 2/8] =?UTF-8?q?refactor(infra):=20=E7=A7=BB=E9=99=A4?=
=?UTF-8?q?=E7=BC=93=E5=AD=98=E9=85=8D=E7=BD=AE=E5=B9=B6=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?Maven=E6=8F=92=E4=BB=B6=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 删除了CacheConfig类中的缓存配置
- 在pom.xml中添加了Spring Boot Maven插件的版本管理
- 统一了构建插件的版本控制机制
---
pom.xml | 9 +++++++++
.../infra/sample/infra/config/CacheConfig.java | 17 -----------------
2 files changed, 9 insertions(+), 17 deletions(-)
delete mode 100644 structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java
diff --git a/pom.xml b/pom.xml
index 0461c07..4bc2578 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,6 +41,15 @@
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring-boot.version}
+
+
+
org.apache.maven.plugins
diff --git a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java b/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java
deleted file mode 100644
index 56d423a..0000000
--- a/structure-infra-sample/structure-infra-sample-core/src/main/java/cn/structure/infra/sample/infra/config/CacheConfig.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package cn.structure.infra.sample.infra.config;
-
-import org.springframework.cache.CacheManager;
-import org.springframework.cache.annotation.EnableCaching;
-import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-@Configuration
-@EnableCaching
-public class CacheConfig {
-
- @Bean
- public CacheManager cacheManager() {
- return new ConcurrentMapCacheManager();
- }
-}
\ No newline at end of file
From 189fdbc02fb6460eb697d3479ffe11a525677aff Mon Sep 17 00:00:00 2001
From: chuck <361648887@qq.com>
Date: Sat, 4 Jul 2026 01:34:39 +0800
Subject: [PATCH 3/8] =?UTF-8?q?chore(build):=20=E6=9B=B4=E6=96=B0=E9=A1=B9?=
=?UTF-8?q?=E7=9B=AE=E7=89=88=E6=9C=AC=E5=8F=B7=E4=BB=8E=201.0.0-SNAPSHOT?=
=?UTF-8?q?=20=E5=88=B0=201.1.0-SNAPSHOT?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修改 pom.xml 中的 revision 属性值
- 将项目版本从 1.0-SNAPSHOT 升级到 1.1.0-SNAPSHOT
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 4bc2578..9367e80 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
pro项目父工程
- 1.0.0-SNAPSHOT
+ 1.1.0-SNAPSHOT
4.0.6
5.0.0
3.5.16
From 195d83981d589becf92cd038f1ed64d3fcc576a5 Mon Sep 17 00:00:00 2001
From: chuck <361648887@qq.com>
Date: Sat, 4 Jul 2026 01:38:05 +0800
Subject: [PATCH 4/8] =?UTF-8?q?fix(stream):=20=E4=BF=AE=E5=A4=8D=E6=94=AF?=
=?UTF-8?q?=E4=BB=98=E4=BA=8B=E4=BB=B6=E7=9B=91=E5=90=AC=E5=99=A8=E7=BB=91?=
=?UTF-8?q?=E5=AE=9A=E5=90=8D=E7=A7=B0=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修改 paymentEvent 监听器的 bindingName 为 paymentEvent1
- 确保支付成功事件能够正确路由和处理
---
.../infra/sample/stream/listener/PaymentEventListener.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java
index a706e3d..207e8f8 100644
--- a/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java
+++ b/structure-infra-sample/structure-infra-sample-stream/src/main/java/cn/structure/infra/sample/stream/listener/PaymentEventListener.java
@@ -17,7 +17,7 @@ public void handlePaymentEvent(PaymentEvent event) {
event.getPaymentId(), event.getOrderId(), event.getPaymentStatus(), event.getAmount());
}
- @StreamEventListener(bindingName = "paymentEvent", destination = "payment-exchange", group = "payment-group", condition = "#event.paymentStatus == 'SUCCESS'")
+ @StreamEventListener(bindingName = "paymentEvent1", destination = "payment-exchange", group = "payment-group", condition = "#event.paymentStatus == 'SUCCESS'")
public void handlePaymentSuccess(PaymentEvent event) {
log.info("[支付成功] paymentId={}, orderId={}, amount={}",
event.getPaymentId(), event.getOrderId(), event.getAmount());
From 1dd65eea98e68da256fb1260b884f82621601510 Mon Sep 17 00:00:00 2001
From: chuck <361648887@qq.com>
Date: Sat, 4 Jul 2026 02:05:50 +0800
Subject: [PATCH 5/8] =?UTF-8?q?docs(infra):=20=E6=B7=BB=E5=8A=A0=20Elastic?=
=?UTF-8?q?search=E3=80=81JPA=E3=80=81MongoDB=E3=80=81MyBatis=20Plus=20?=
=?UTF-8?q?=E5=9B=9B=E4=B8=AA=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=E5=90=AF?=
=?UTF-8?q?=E5=8A=A8=E5=99=A8=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 structure-infra-elasticsearch-starter 模块 README,介绍基于 Spring Data Elasticsearch
的仓储适配功能,包括自动配置、类型化仓储、低代码存储等特性
- 新增 structure-infra-jpa-starter 模块 README,介绍基于 JPA/Hibernate 的仓储适配功能,
包括自动配置、条件激活、委托工厂、CRUD 操作等核心功能
- 新增 structure-infra-mongodb-starter 模块 README,介绍基于 Spring Data MongoDB 的
仓储适配功能,涵盖自动配置、类型化仓储、低代码存储及索引自动创建特性
- 新增 structure-infra-mybatis-plus-starter 模块 README,介绍 MyBatis-Plus 仓储适配
与低代码 MySQL/H2 存储功能,支持多方言、自动 DDL、分页等特性
---
README.md | 419 ++++++++++----
.../README.md | 298 ++++++++++
structure-infra-jpa-starter/README.md | 298 ++++++++++
structure-infra-mongodb-starter/README.md | 274 +++++++++
.../README.md | 382 +++++++++++++
structure-infra-schedule-starter/README.md | 300 ++++++++++
structure-infra-starter/README.md | 531 ++++++++++++++++++
structure-infra-xxljob-starter/README.md | 335 +++++++++++
8 files changed, 2723 insertions(+), 114 deletions(-)
create mode 100644 structure-infra-elasticsearch-starter/README.md
create mode 100644 structure-infra-jpa-starter/README.md
create mode 100644 structure-infra-mongodb-starter/README.md
create mode 100644 structure-infra-mybatis-plus-starter/README.md
create mode 100644 structure-infra-schedule-starter/README.md
create mode 100644 structure-infra-starter/README.md
create mode 100644 structure-infra-xxljob-starter/README.md
diff --git a/README.md b/README.md
index 2a67648..9532e0f 100644
--- a/README.md
+++ b/README.md
@@ -1,62 +1,55 @@
# structure-pro-infra
-基于 DDD(领域驱动设计)理念的基础设施抽象层,提供统一的仓储接口和多种持久化技术的适配实现。
+基于 DDD(领域驱动设计)理念的基础设施抽象层,提供统一的仓储接口、多种持久化技术适配、事件管理、任务调度与流式事件路由能力。
## 项目简介
-该项目实现了一个基于 **Facade + Delegate** 模式的仓储抽象层,作为领域层与持久化层之间的防腐层(ACL),核心目标是:
+该项目实现了一个基于 **Facade + Delegate** 模式的仓储抽象层,作为领域层与持久化层之间的防腐层(ACL),同时集成了事件发布、任务调度与流式事件路由等基础设施能力。核心目标是:
- **解耦领域模型与持久化技术**:领域层只依赖统一的仓储接口,不关心底层使用哪种数据库
- **支持多持久化技术**:通过委托模式自动适配 MyBatis Plus、JPA、MongoDB、Elasticsearch 等
- **自动配置**:基于 Spring Boot AutoConfiguration 实现开箱即用
- **Entity-PO 自动转换**:RepositoryFacade 自动完成领域实体与持久化对象的转换
-- **CQRS 读写分离**:支持一个仓储配置多个代理,写操作走基础代理,读操作走读代理
+- **CQRS 读写分离**:支持一个仓储配置多个代理,写操作走基础代理,读操作走读代理,读失败自动回退
- **低代码仓储**:无需定义实体类,通过资源名称和 Map 动态操作数据,支持运行时动态注册
+- **事件管理**:统一的事件发布抽象,支持 Spring 事件与消息事件两种通道
+- **任务调度**:本地线程池调度与 XXL-Job 分布式调度两种实现,统一 TaskScheduler SPI
+- **流式事件路由**:基于 Spring Cloud Stream 的事件监听与统一路由能力
## 模块结构
```
structure-pro-infra/
-├── structure-infra-starter/ # 核心模块
-│ ├── annotations/ # 注解定义
-│ │ ├── Repository.java # @Repository 注解
-│ │ └── DelegateFor.java # @DelegateFor 注解
-│ ├── configuration/ # 自动配置
-│ ├── repository/ # 仓储核心接口
-│ │ ├── RepositoryFacade.java # 仓储门面(对外)
-│ │ ├── RepositoryDelegate.java # 仓储委托(对内)
-│ │ ├── RepositoryDelegateFactory.java # 委托工厂接口
-│ │ ├── RepositoryType.java # 仓储类型枚举
-│ │ ├── DelegateType.java # 委托类型枚举(BASE/READ)
-│ │ └── InMemoryRepositoryDelegate.java # 内存实现(开发/测试用)
-│ ├── lowcode/ # 低代码仓储
-│ │ ├── repository/ # 低代码仓储接口
-│ │ │ ├── LowCodeRepository.java # 低代码统一仓储接口(用户侧)
-│ │ │ ├── LowCodeStorage.java # 低代码存储接口(引擎侧)
-│ │ │ └── LowCodeRepoFactory.java # 低代码仓储工厂接口
-│ │ ├── router/ # 路由引擎
-│ │ │ └── LowCodeRepositoryRouter.java # 低代码仓储路由器
-│ │ ├── model/ # 模型定义
-│ │ │ ├── ResourceSchema.java # 资源 schema
-│ │ │ ├── FieldSchema.java # 字段 schema
-│ │ │ ├── StorageType.java # 存储类型枚举
-│ │ │ ├── FieldType.java # 字段类型枚举
-│ │ │ ├── AutoFillType.java # 自动填充类型枚举
-│ │ │ └── RepositoryConfig.java # 仓储配置
-│ │ └── registry/ # 注册与构建
-│ │ └── ResourceSchemaBuilder.java # 资源 schema 构建器
-│ └── event/ # 事件管理
-├── structure-infra-mybatis-plus-starter/ # MyBatis Plus 适配(含低代码实现)
-├── structure-infra-jpa-starter/ # JPA 适配
-├── structure-infra-mongodb-starter/ # MongoDB 适配(含低代码实现)
-├── structure-infra-elasticsearch-starter/ # Elasticsearch 适配(含低代码实现)
-└── structure-infra-sample/ # 示例模块
- ├── structure-infra-sample-core/ # 共享核心(Entity、PO、Repository接口)
- ├── structure-infra-sample-mybatis/ # MyBatis Plus 示例(含低代码测试)
- ├── structure-infra-sample-jpa/ # JPA 示例
- ├── structure-infra-sample-mongodb/ # MongoDB 示例(含低代码测试)
- ├── structure-infra-sample-elasticsearch/ # Elasticsearch 示例(含低代码测试)
- └── structure-infra-sample-cqrs/ # CQRS 读写分离示例
+├── structure-infra-starter/ # 核心模块(仓储抽象、低代码、事件管理、调度集成)
+│ ├── annotations/ # @Repository / @DelegateFor 注解
+│ ├── configuration/ # 自动配置(仓储、事件、调度)
+│ ├── event/ # 事件管理抽象
+│ ├── lowcode/ # 低代码仓储子系统
+│ │ ├── configuration/ # 低代码自动配置
+│ │ ├── model/ # ResourceSchema / FieldSchema 等模型
+│ │ ├── properties/ # LowCodeProperties 配置绑定
+│ │ ├── registry/ # ResourceSchemaBuilder
+│ │ ├── repository/ # LowCodeRepository / LowCodeStorage / LowCodeRepoFactory
+│ │ └── router/ # LowCodeRepositoryRouter
+│ ├── properties/ # InfraProperties 全局配置
+│ └── repository/ # 仓储核心接口(Facade / Delegate / Factory)
+├── structure-infra-mybatis-plus-starter/ # MyBatis Plus 适配(含低代码 MySQL 实现)
+├── structure-infra-jpa-starter/ # JPA 适配
+├── structure-infra-mongodb-starter/ # MongoDB 适配(含低代码实现)
+├── structure-infra-elasticsearch-starter/ # Elasticsearch 适配(含低代码实现)
+├── structure-infra-schedule-starter/ # 本地任务调度(基于 ScheduledExecutorService)
+├── structure-infra-xxljob-starter/ # XXL-Job 分布式任务调度适配
+├── structure-infra-stream-starter/ # Spring Cloud Stream 事件路由
+└── structure-infra-sample/ # 示例模块(不发布到中央仓库)
+ ├── structure-infra-sample-core/ # 共享核心(Entity、PO、Repository 接口)
+ ├── structure-infra-sample-mybatis/ # MyBatis Plus 示例(含低代码测试)
+ ├── structure-infra-sample-jpa/ # JPA 示例
+ ├── structure-infra-sample-mongodb/ # MongoDB 示例(含 REST API、低代码测试)
+ ├── structure-infra-sample-elasticsearch/ # Elasticsearch 示例(含 REST API、低代码测试)
+ ├── structure-infra-sample-cqrs/ # CQRS 读写分离示例
+ ├── structure-infra-sample-schedule/ # 本地调度示例(含 REST API)
+ ├── structure-infra-sample-xxljob/ # XXL-Job 调度示例(含 REST API)
+ └── structure-infra-sample-stream/ # 流式事件路由示例
```
## 核心概念
@@ -75,7 +68,7 @@ structure-pro-infra/
仓储委托,是持久化层的实现接口,负责:
- 直接操作 PO(持久化对象)
-- 与具体的持久化技术交互(MyBatis Plus、JPA、MongoDB 等)
+- 与具体的持久化技术交互(MyBatis Plus、JPA、MongoDB、Elasticsearch 等)
- 不同持久化技术提供各自的实现
### RepositoryDelegateFactory
@@ -143,6 +136,36 @@ structure-pro-infra/
| `REDIS` | - | 规划中 |
| `IN_MEMORY` | - | 规划中(测试用) |
+### 事件管理
+
+提供统一的事件发布抽象,支持三种事件通道:
+
+| 通道 | 说明 |
+|------|------|
+| `DEFAULT` | 默认通道,运行时通过 `structure.infra.default-event-channel` 决定路由 |
+| `SPRING_EVENT` | Spring 应用事件,通过 `ApplicationEventPublisher` 同步发布 |
+| `MESSAGE_EVENT` | 消息事件,通过 `DataScopeStreamBridge` 发送到消息中间件 |
+
+### 任务调度
+
+提供两种调度实现,通过 `TaskScheduler` SPI 统一抽象:
+
+| 实现 | 模块 | 适用场景 |
+|------|------|---------|
+| 本地线程池调度 | structure-infra-schedule-starter | 单机应用,无需外部依赖 |
+| XXL-Job 分布式调度 | structure-infra-xxljob-starter | 分布式应用,需要集中管理 |
+
+支持三种调度类型:`CRON`、`FIXED_DELAY`、`FIXED_RATE`,并提供统一的 `schedule / update / remove / pause / resume / getTaskInfo / getAllTasks` 生命周期 API。
+
+### 流式事件路由
+
+基于 Spring Cloud Stream 的事件监听与统一路由框架,提供:
+
+- 动态 Binding 配置(替代静态配置)
+- 基于 `eventType / businessType / condition` 的统一路由
+- 注解声明式、代码动态注册、配置文件驱动、运行时动态注册四种使用方式
+- SpEL 条件表达式过滤
+
## 快速开始
### 示例模块
@@ -159,7 +182,12 @@ mvn spring-boot:run -pl structure-infra-sample/structure-infra-sample-mongodb
mvn spring-boot:run -pl structure-infra-sample/structure-infra-sample-elasticsearch
```
-**REST API 接口**(两个示例模块接口一致):
+**调度示例**(端口 8086):
+```bash
+mvn spring-boot:run -pl structure-infra-sample/structure-infra-sample-schedule
+```
+
+**REST API 接口**(MongoDB / Elasticsearch 示例模块接口一致):
| 方法 | 路径 | 说明 |
|------|------|------|
@@ -172,6 +200,18 @@ mvn spring-boot:run -pl structure-infra-sample/structure-infra-sample-elasticsea
| POST | `/api/users/batch` | 批量创建 |
| GET | `/api/users/count` | 查询总数 |
+**调度示例 REST API**:
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| POST | `/job/add` | 添加任务(CRON) |
+| PUT | `/job/update/{taskId}` | 更新任务 |
+| DELETE | `/job/remove/{taskId}` | 删除任务 |
+| PUT | `/job/pause/{taskId}` | 暂停任务 |
+| PUT | `/job/resume/{taskId}` | 恢复任务 |
+| GET | `/job/info/{taskId}` | 查询任务详情 |
+| GET | `/job/list` | 查询所有任务 |
+
详细示例模块说明请参考 [SAMPLE_MODULES.md](./SAMPLE_MODULES.md)。
### 1. 添加依赖
@@ -213,6 +253,27 @@ mvn spring-boot:run -pl structure-infra-sample/structure-infra-sample-elasticsea
structure-infra-elasticsearch-starter
1.0.0-SNAPSHOT
+
+
+
+ cn.structured
+ structure-infra-schedule-starter
+ 1.0.0-SNAPSHOT
+
+
+
+
+ cn.structured
+ structure-infra-xxljob-starter
+ 1.0.0-SNAPSHOT
+
+
+
+
+ cn.structured
+ structure-infra-stream-starter
+ 1.0.0-SNAPSHOT
+
```
### 2. 定义领域实体和持久化对象
@@ -325,85 +386,151 @@ public class UserReadDelegate extends ElasticsearchRepositoryDelegate article = new HashMap<>();
article.put("title", "Hello World");
article.put("author", "zhangsan");
-Map saved = lowCodeRepositoryRouter.save("article", article);
+Map saved = lowCodeRepository.save("article", article);
// 查询数据
-Map found = lowCodeRepositoryRouter.findById("article", 1L);
+Map found = lowCodeRepository.findById("article", 1L);
// 条件查询
Map params = new HashMap<>();
params.put("author", "zhangsan");
-List