CQRS(Command Query Responsibility Segregation)**:命令查詢職責分離,將寫操作與讀操作模型分離,配合**事件溯源**實現狀態重建與審計追蹤。Axon Framework 是 Java 生態成熟的 CQRS/ES 框架,提供命令總線、事件總線、聚合根、事件存儲等核心組件。
demo.axon ├── command # 命令對象 ├── event # 事件對象 ├── handler # 事件處理器(投影/通知) ├── model # 聚合根(領域模型) └── ToDoItemRunner.java # 啟動入口
dependencies {
implementation "org.axonframework:axon-core:2.4.3"
implementation "org.axonframework:axon-test:2.4.3"
implementation "org.springframework:spring-context:4.2.3.RELEASE"
implementation "org.apache.logging.log4j:log4j-slf4j-impl:2.5"
implementation "javax.persistence:persistence-api:2.1"
testImplementation "junit:junit:4.12"
}
// 创建待辦命令
public class CreateToDoItemCommand {
@TargetAggregateIdentifier private final String todoId;
private final String description;
// 構造器、Getter 省略
}
// 完成待辦命令
public class MarkCompletedCommand {
@TargetAggregateIdentifier private final String todoId;
// 構造器、Getter 省略
}
@TargetAggregateIdentifier 標識路由鍵,命令總線據此路由到對應聚合實例。
public class ToDoItemCreatedEvent {
private final String todoId;
private final String description;
// 構造器、Getter
}
public class ToDoItemCompletedEvent {
private final String todoId;
// 構造器、Getter
}
事件不可變,僅攜帶業務事實數據,用於狀態變更與異步投影。
@Aggregate // Axon 2.x 使用 AbstractAnnotatedAggregateRoot
public class ToDoItem extends AbstractAnnotatedAggregateRoot {
@AggregateIdentifier private String id;
protected ToDoItem() {} // 反射重建用
@CommandHandler
public ToDoItem(CreateToDoItemCommand command) {
// 發布事件,而非直接修改狀態
apply(new ToDoItemCreatedEvent(command.getTodoId(), command.getDescription()));
}
@EventHandler
public void on(ToDoItemCreatedEvent event) {
this.id = event.getTodoId(); // 事件溯源:從事件重建狀態
}
@CommandHandler
public void markCompleted(MarkCompletedCommand command) {
apply(new ToDoItemCompletedEvent(id));
}
// ToDoItemCompletedEvent 的 @EventHandler 可選,若無狀態變更可省略
}
關鍵點:聚合根**不直接修改字段**,而是通過 apply(event) 發布事件,事件處理器(@EventHandler)更新狀態。這保證了事件溯源的一致性。
@Component
public class ToDoEventHandler {
private static final Logger logger = LoggerFactory.getLogger(ToDoEventHandler.class);
@EventHandler
public void handle(ToDoItemCreatedEvent event) {
logger.info("新待辦創建: {} ({})", event.getDescription(), event.getTodoId());
// 實際項目:更新查詢數據庫、發送通知、觸發下游流程
}
@EventHandler
public void handle(ToDoItemCompletedEvent event) {
logger.info("待辦完成: {}", event.getTodoId());
}
}
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:axon="https://www.axonframework.org/schema/core"
xsi:schemaLocation="... axon-core-2.0.xsd">
<axon:command-bus id="commandBus"/>
<axon:event-bus id="eventBus"/>
<axon:event-sourcing-repository id="toDoRepository"
aggregate-type="demo.axon.model.ToDoItem"/>
<axon:aggregate-command-handler id="toDoItemHandler"
aggregate-type="demo.axon.model.ToDoItem"
repository="toDoRepository"
command-bus="commandBus"/>
<axon:filesystem-event-store id="eventStore" base-dir="events"/> <!-- 文件系统事件存储 -->
<axon:annotation-config /> <!-- 掃描 @CommandHandler/@EventHandler -->
<bean class="demo.axon.handler.ToDoEventHandler"/>
<bean class="org.axonframework.commandhandling.gateway.CommandGatewayFactoryBean">
<property name="commandBus" ref="commandBus"/>
</bean>
</beans>
public class ToDoItemRunner {
private final CommandGateway commandGateway;
public ToDoItemRunner(CommandGateway commandGateway) {
this.commandGateway = commandGateway;
}
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("sampleContext.xml");
new ToDoItemRunner(ctx.getBean(CommandGateway.class)).run();
}
private void run() {
String itemId = UUID.randomUUID().toString();
commandGateway.send(new CreateToDoItemCommand(itemId, "Need to do this"));
commandGateway.send(new MarkCompletedCommand(itemId));
}
}
12:01:36,113 INFO [main] ToDoEventHandler - 新待辦創建: Need to do this (3126f293-...) 12:01:36,114 INFO [main] LoggingCallback - Command executed: CreateToDoItemCommand 12:01:36,205 INFO [main] ToDoEventHandler - 待辦完成: 3126f293-... 12:01:36,206 INFO [main] LoggingCallback - Command executed: MarkCompletedCommand
public class ToDoItemTest {
private FixtureConfiguration fixture;
@Before public void setUp() {
fixture = Fixtures.newGivenWhenThenFixture(ToDoItem.class);
}
@Test public void testCreate() {
fixture.givenNoPriorActivity()
.when(new CreateToDoItemCommand("todo1", "implement aggregate"))
.expectEvents(new ToDoItemCreatedEvent("todo1", "implement aggregate"));
}
@Test public void testComplete() {
fixture.given(new ToDoItemCreatedEvent("todo1", "implement aggregate"))
.when(new MarkCompletedCommand("todo1"))
.expectEvents(new ToDoItemCompletedEvent("todo1"));
}
}
given/when/expectEvents 流式 API 直觀表達:「給定歷史事件 → 當執行命令 → 預期產生事件」。
https://docs.axoniq.io/https://github.com/AxonFramework/Axon-trader(含讀模型投影、Saga、分布式命令總線等)org.axonframework.modelling.command)、簡化配置、Kotlin DSL 支持等。