命令
1.介绍
Axon应用关联的第一种消息类型是命令消息(Command Message),简称命令。这类消息的核心意图是以某种方式修改应用的状态(例如创建聚合、更新聚合属性、执行领域操作等)
2.@CommandHandler
介绍
@CommandHandler是Axon中负责接收并处理Command的组件,核心职责是根据命令意图执行领域逻辑(如校验业务规则、发布领域事件)
类型
- 聚合内@CommandHandler
- 外部@CommandHandler
聚合内@CommandHandler
介绍
尽管@CommandHandler可放在普通组件中,但推荐将其直接定义在聚合上,因为聚合包含处理命令所需的状态,这种设计能更好地封装领域逻辑,符合DDD中聚合是领域逻辑的核心载体的原则
匹配规则
- 默认匹配规则:命令处理器处理的Command类型,由方法的第一个参数的全限定类名决定。例如,void handle(RedeemCardCommand cmd)会处理所有RedeemCardCommand类型的命令
- 自定义命令名:若Command是通过自定义命令名(非类名)分发的,可通过@CommandHandler(commandName = "自定义命令名") 显式指定命令名,确保正确匹配
单机路由
Axon需要知道哪个聚合实例应处理命令,因此命令对象中必须包含聚合标识符的引用,并通过@TargetAggregateIdentifier注解标记该引用字段。该注解可标注在Command的字段或访问器方法(如getter)上
分布式环境路由
在分布式环境中(如通过Axon Server部署多实例),命令路由依赖路由键(routing key)
@TargetAggregateIdentifier注解本质上就是指定路由键;若命令中无适合作为聚合ID的字段,需通过@RoutingKey注解显式指定路由键字段。若这两种注解都无法满足需求,可通过自定义RoutingStrategy调整路由逻辑
代码
java
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import static org.axonframework.modelling.command.AggregateLifecycle.apply;
public class GiftCard {
@AggregateIdentifier
private String id; // 聚合标识符
private int remainingValue; // 礼品卡剩余金额(聚合状态)
// 命令处理器 1:构造函数标注 @CommandHandler,处理"发行礼品卡"命令(创建聚合)
@CommandHandler
public GiftCard(IssueCardCommand cmd) {
// 发布"礼品卡已发行"事件(触发聚合状态初始化)
apply(new CardIssuedEvent(cmd.getCardId(), cmd.getAmount()));
}
// 命令处理器 2:普通方法标注 @CommandHandler,处理"消费礼品卡"命令(修改聚合状态)
@CommandHandler
public void handle(RedeemCardCommand cmd) {
// 业务规则校验:金额合法性、剩余金额充足性
if (cmd.getAmount() <= 0) {
throw new IllegalArgumentException("amount <= 0");
}
if (cmd.getAmount() > remainingValue) {
throw new IllegalStateException("amount > remaining value");
}
// 发布"礼品卡已消费"事件(触发聚合状态更新)
apply(new CardRedeemedEvent(id, cmd.getTransactionId(), cmd.getAmount()));
}
// 省略事件溯源处理器(Event Sourcing Handler):用于通过事件重建聚合状态
}
java
import org.axonframework.modelling.command.TargetAggregateIdentifier;
// 命令 1:发行礼品卡(创建聚合)
public class IssueCardCommand {
@TargetAggregateIdentifier
private final String cardId; // 聚合标识符(礼品卡 ID)
private final Integer amount; // 发行金额
public IssueCardCommand(String cardId, Integer amount) {
this.cardId = cardId;
this.amount = amount;
}
// 省略 getters、equals、hashCode、toString 方法
}
// 命令 2:消费礼品卡(修改聚合)
public class RedeemCardCommand {
@TargetAggregateIdentifier
private final String cardId; // 目标聚合标识符(礼品卡 ID)
private final String transactionId; // 交易 ID
private final Integer amount; // 消费金额
public RedeemCardCommand(String cardId, String transactionId, Integer amount) {
this.cardId = cardId;
this.transactionId = transactionId;
this.amount = amount;
}
// 省略 getters、equals、hashCode、toString 方法
}
注意
- 尽管创建聚合的命令(如 IssueCardCommand)无需定位 已存在的聚合实例,但为了一致性,仍建议标注@TargetAggregateIdentifier
- 自定义路由逻辑:若需覆盖默认的命令的聚合匹配逻辑,可通过自定义CommandTargetResolver实现,该类需根据命令返回聚合标识符和预期版本
构造函数与@CommandHandler
当@CommandHandler标注在聚合的构造函数上时,该命令会触发 "创建新聚合实例" 并将其添加到仓库。这类命令的特殊规则:
- 无需标注@TargetAggregateIdentifier或@TargetAggregateVersion
- 自定义CommandTargetResolver不会对这类命令生效
业务逻辑与状态变更的规范
聚合内@CommandHandler的核心职责是决策,而非直接修改状态,需遵守以下规范:
- 业务逻辑位置:命令处理器中应仅执行业务规则校验(如 消费金额不能为负),若校验通过则发布Event;若校验失败,可忽略命令或抛出异常(根据领域需求决定)
- 状态变更位置:聚合的状态变更必须放在@EventSourcingHandler中,而非@CommandHandler,若在@CommandHandler中直接修改状态,当聚合通过事件溯源重建时,会丢失这些状态变更
- 测试保障:Axon的Aggregate Test Fixture会检测@CommandHandler中的无意状态变更,建议为所有聚合编写完整的测试用例
事件处理的边界
- 仅处理必要事件:聚合只需处理 对未来命令校验有必要的事件,即仅保留影响业务决策的状态,避免冗余
- 聚合内事件的传递:在多实体聚合中,可在一个实体的@EventSourcingHandler中调用apply()发布新事件,以响应另一个实体的事件。Axon在回放历史事件时会自动忽略这类apply()调用,避免重复创建事件。若需基于内部事件后的状态发布更多事件,可使用apply(...).andThenApply(...) 链式调用
- 外部事件的响应:聚合不能处理来自其他聚合的事件,因为@EventSourcingHandler的作用是重建当前聚合的状态,仅需当前聚合的事件。若需响应外部事件(如其他聚合的事件),应通过Saga或事件处理组件实现
创建策略
默认情况下,聚合的命令处理器分为两类:
- 标注@CommandHandler的构造函数:仅用于创建新聚合
- 标注@CommandHandler的普通方法:仅用于处理现有聚合的命令
Axon允许通过@CreationPolicy注解自定义@CommandHandler的聚合创建策略,该注解需结合AggregateCreationPolicy枚举使用,支持以下三种策略:
- ALWAYS:强制创建新聚合,效果等同于标注@CommandHandler 的构造函数。支持返回除聚合标识符外的其他结果
- CREATE_IF_MISSING:Upsert逻辑。若聚合不存在则创建,若已存在则修改
- NEVER:仅处理现有聚合的命令,效果等同于普通的@CommandHandler方法(默认策略)
java
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.modelling.command.CreationPolicy;
import org.axonframework.modelling.command.AggregateCreationPolicy;
import org.axonframework.modelling.command.AggregateIdentifier;
import static org.axonframework.modelling.command.AggregateLifecycle.apply;
public class GiftCard {
@AggregateIdentifier
private String id;
private int remainingValue;
// 无参构造函数(Axon 强制要求)
public GiftCard() {
}
// 策略 1:ALWAYS - 处理命令时始终创建新聚合
@CommandHandler
@CreationPolicy(AggregateCreationPolicy.ALWAYS)
public void handle(IssueCardCommand cmd) {
this.id = cmd.getCardId();
apply(new CardIssuedEvent(cmd.getCardId(), cmd.getAmount()));
}
// 策略 2:CREATE_IF_MISSING - 不存在则创建,存在则更新(如"创建或充值")
@CommandHandler
@CreationPolicy(AggregateCreationPolicy.CREATE_IF_MISSING)
public void handle(CreateOrRechargeCardCommand cmd) {
if (this.id == null) {
// 聚合不存在:初始化并发布"创建"事件
this.id = cmd.getCardId();
apply(new CardIssuedEvent(cmd.getCardId(), cmd.getInitialAmount()));
} else {
// 聚合已存在:发布"充值"事件
apply(new CardRechargedEvent(cmd.getCardId(), cmd.getRechargeAmount()));
}
}
// 省略 EventSourcingHandler
@EventSourcingHandler
public void on(CardIssuedEvent evt) {
this.remainingValue = evt.getAmount();
}
@EventSourcingHandler
public void on(CardRechargedEvent evt) {
this.remainingValue += evt.getRechargeAmount();
}
}
外部@CommandHandler
介绍
外部@CommandHandler是独立于聚合的命令处理组件,适用于无法或不希望将命令直接路由到聚合的场景(如命令需要跨多个聚合协调、或需执行聚合外的预处理逻辑)
外部@CommandHandler是普通Java对象,通过@CommandHandler标注方法,并需手动通过聚合仓库加载聚合实例、调用聚合的业务方法
代码
java
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.modelling.command.Repository;
public class GiftCardCommandHandler {
// 聚合仓库:用于加载和存储 GiftCard 聚合
private final Repository<GiftCard> giftCardRepository;
// 构造函数注入聚合仓库(如通过 Spring 依赖注入)
public GiftCardCommandHandler(Repository<GiftCard> giftCardRepository) {
this.giftCardRepository = giftCardRepository;
}
// 外部命令处理器:处理 RedeemCardCommand
@CommandHandler
public void handle(RedeemCardCommand cmd) {
// 1. 通过仓库加载目标聚合实例(参数为聚合标识符)
giftCardRepository.load(cmd.getCardId())
// 2. 调用聚合的 execute 方法执行业务逻辑(确保聚合生命周期正确启动)
.execute(giftCard -> giftCard.handle(cmd));
}
}
注意
- 聚合仓库:负责聚合的加载与存储,是外部@CommandHandler与聚合交互的核心组件。若@CommandHandler直接定义在聚合上,Axon会自动调用仓库,无需手动处理
- Repository的load()方法:根据聚合标识符加载聚合实例,返回Aggregate<GiftCard>对象
- Aggregate的execute() 方法:接收Consumer<GiftCard>函数,用于调用聚合的业务方法。该方法会确保聚合的生命周期(如UnitOfWork)正确启动,避免状态管理问题
3.命令分发器
介绍
命令分发过程是命令消息流转的起点,Axon提供了两个核心接口用于将命令发送到@CommandHandler,分别是CommandBus和CommandGateway
CommandBus
介绍
CommandBus是将命令分发到对应@CommandHandler的核心机制,作为基础设施组件,它知晓 哪个组件能处理哪种命令
核心特性
- 一对一分发:每条命令始终只会发送给一个@CommandHandler。若没有找到能处理该命令的@CommandHandler,会抛出NoHandlerForCommandException
- 两种分发方法:CommandBus提供两个核心方法用于分发命令
- dispatch(CommandMessage):无回调的异步分发(不关心处理结果)
- dispatch(CommandMessage, CommandCallback):带回调的异步分发(需处理命令结果)
代码
java
private CommandBus commandBus;
public void dispatchCommands() {
// 生成聚合标识符(最佳实践:使用 UUID 确保唯一性;也支持自定义类型,需实现合理的 toString() 方法)
String cardId = UUID.randomUUID().toString();
// 1. 无回调分发:不关心命令处理结果
CommandMessage<IssueCardCommand> commandMsg1 = GenericCommandMessage.asCommandMessage(
new IssueCardCommand(cardId, 100, "shopId")
);
commandBus.dispatch(commandMsg1);
// 2. 带回调分发:需处理命令结果(成功/失败)
CommandMessage<IssueCardCommand> commandMsg2 = GenericCommandMessage.asCommandMessage(
new IssueCardCommand(cardId, 100, "shopId")
);
commandBus.dispatch(
commandMsg2,
// CommandCallback:命令处理完成后触发的回调逻辑
(CommandCallback<IssueCardCommand, String>) (cmdMsg, cmdResultMsg) -> {
if (cmdResultMsg.isExceptional()) {
// 处理失败:获取异常信息
Throwable throwable = cmdResultMsg.exceptionResult();
// 示例:记录异常日志
// logger.error("Command handling failed", throwable);
} else {
// 处理成功:获取返回结果(通常为 null,特殊场景为聚合标识符)
String commandResult = cmdResultMsg.getPayload();
// 示例:处理成功逻辑
// logger.info("Command handled successfully, result: {}", commandResult);
}
}
);
}
// 省略类定义、构造函数及结果后续处理逻辑
注意
使用带回调的分发方法时,不能假设回调在命令分发线程中执行(可能在处理线程中执行)。若需要等待结果后再继续执行,可使用FutureCallback(结合java.concurrent.Future和Axon的CommandCallback),或直接使用CommandGateway
CommandGateway
介绍
CommandGateway是对CommandBus的封装,提供更便捷的命令分发方式,它会自动处理CommandMessage包装、回调线程管理等细节,底层仍通过CommandBus完成实际分发
尽管不强制要求使用CommandGateway,但它通常是分发命令的最简选择,尤其适合需要同步等待结果或简化异步处理的场景
核心方法分类
CommandGateway的接口方法分为两类,覆盖异步和同步分发场景:
- send(Object):异步分发,返回CompletableFuture,支持链式处理结果
- sendAndWait(Object)/sendAndWait(Object, long, TimeUnit):同步分发,阻塞当前线程直到处理完成或超时
代码
java
private CommandGateway commandGateway;
public void sendCommand() {
// 生成聚合标识符(最佳实践:UUID)
String cardId = UUID.randomUUID().toString();
// 异步分发命令:返回 CompletableFuture,支持后续链式操作
CompletableFuture<String> futureResult = commandGateway.send(
new IssueCardCommand(cardId, 100, "shopId")
);
// 示例:链式处理结果(非阻塞)
futureResult.whenComplete((result, throwable) -> {
if (throwable != null) {
// 处理失败
// logger.error("Command handling failed", throwable);
} else {
// 处理成功
// logger.info("Command handled successfully, result: {}", result);
}
});
}
// 省略类定义、构造函数及结果后续处理逻辑
java
private CommandGateway commandGateway;
public void sendCommandAndWaitOnResult() {
// 创建命令对象
IssueCardCommand commandPayload = new IssueCardCommand(
UUID.randomUUID().toString(), 100, "shopId"
);
// 1. 无超时同步分发:阻塞当前线程,直到命令处理完成
String result1 = commandGateway.sendAndWait(commandPayload);
// 2. 带超时同步分发:阻塞当前线程,但最长等待 1000 毫秒
String result2 = commandGateway.sendAndWait(
commandPayload,
1000, // 超时时间
TimeUnit.MILLISECONDS // 时间单位
);
}
// 省略类定义、构造函数及结果后续处理逻辑
结果处理
介绍
命令分发的结果主要有两种:处理成功和处理异常,结果不仅取决于分发过程,更取决于@CommandHandler的实现(如业务逻辑是否抛出异常)
成功
- 按设计意图,命令处理器不建议返回值(命令的核心是修改状态,而非获取数据,获取数据应使用QueryMessage)
- 若CommandBus/CommandGateway返回成功结果,通常为null;仅在特殊场景下返回非null值,例如:
- 聚合创建命令(@CommandHandler标注构造函数):返回聚合的@AggregateIdentifier字段值
- 聚合内创建子实体的命令:返回子实体的标识符
异常
- 若@CommandHandler方法因业务逻辑抛出异常(如IllegalStateException),该异常会成为命令分发的结果,通过以下方式暴露:
- CommandBus:在CommandCallback的cmdResultMsg.exceptionResult()中获取
- CommandGateway:异步分发时通过CompletableFuture的throwable参数获取,同步分发时直接抛出该异常
4.深入CommandBus
总览

本地
介绍
本地命令总线是Axon应用内部将命令分发给对应@CommandHandler的核心机制
SimpleCommandBus
介绍
顾名思义,SimpleCommandBus是最简单的CommandBus实现。它会在分发命令的线程中直接处理命令;命令处理完成后,被修改的聚合会被保存,生成的事件也会在同一个线程中发布。在Web应用等大多数场景下,这种实现足以满足需求
与大多数CommandBus实现类似,SimpleCommandBus支持配置拦截器
配置
原生API配置:
java
public class AxonConfig {
// 省略其他配置方法...
public void configureSimpleCommandBus(Configurer configurer) {
configurer.configureCommandBus(config -> {
CommandBus commandBus = SimpleCommandBus
.builder()
.transactionManager(config.getComponent(TransactionManager.class))
.spanFactory(config.spanFactory())
.messageMonitor(config.messageMonitor(SimpleCommandBus.class, "commandBus"))
// ...(可添加其他配置)
.build();
commandBus.registerHandlerInterceptor(
new CorrelationDataInterceptor<>(config.correlationDataProviders())
);
return commandBus;
});
}
}
SpringBoot配置:
java
@Configuration
public class AxonConfig {
// 省略其他配置方法...
@Bean
public CommandBus simpleCommandBus(TransactionManager transactionManager,
GlobalMetricRegistry metricRegistry,
SpanFactory spanFactory) {
return SimpleCommandBus.builder()
.transactionManager(transactionManager)
.messageMonitor(metricRegistry.registerCommandBus("commandBus"))
.spanFactory(spanFactory)
// ...(可添加其他配置)
.build();
}
@Bean
public ConfigurerModule commandBusCorrelationConfigurerModule() {
return configurer -> configurer.onInitialize(
config -> config.commandBus().registerHandlerInterceptor(
new CorrelationDataInterceptor<>(config.correlationDataProviders())
)
);
}
}
AsynchronousCommandBus
介绍
正如其名,AsynchronousCommandBus会在 与分发命令不同的线程中异步执行命令。它通过Executor在另一个线程中执行实际的处理逻辑
默认情况下,AsynchronousCommandBus使用无界缓存线程池,分发命令时会创建新线程,处理完命令的线程会被复用;若线程60秒内未处理任何命令,则会被终止
你也可以自行提供Executor实例,以配置不同的线程策略
注意
应用停止时,需调用AsynchronousCommandBus的shutdown()方法将其关闭,确保所有等待中的线程被正确终止。若传入的Executor实现了ExecutorService接口,shutdown()方法也会一并关闭该Executor
配置
原生API:
java
public class AxonConfig {
// 省略其他配置方法...
public void configureAsynchronousCommandBus(Configurer configurer) {
configurer.configureCommandBus(config -> {
CommandBus commandBus = AsynchronousCommandBus
.builder()
.transactionManager(config.getComponent(TransactionManager.class))
.spanFactory(config.spanFactory())
.messageMonitor(config.messageMonitor(
AsynchronousCommandBus.class, "commandBus"
))
// ...(可添加其他配置)
.build();
commandBus.registerHandlerInterceptor(
new CorrelationDataInterceptor<>(config.correlationDataProviders())
);
return commandBus;
});
}
}
SpringBoot配置:
java
@Configuration
public class AxonConfig {
// 省略其他配置方法...
@Bean
public CommandBus asynchronousCommandBus(TransactionManager transactionManager,
GlobalMetricRegistry metricRegistry,
SpanFactory spanFactory) {
return AsynchronousCommandBus
.builder()
.transactionManager(transactionManager)
.messageMonitor(metricRegistry.registerCommandBus("commandBus"))
.spanFactory(spanFactory)
// ...(可添加其他配置)
.build();
}
@Bean
public ConfigurerModule commandBusCorrelationConfigurerModule() {
return configurer -> configurer.onInitialize(
config -> config.commandBus().registerHandlerInterceptor(
new CorrelationDataInterceptor<>(config.correlationDataProviders())
)
);
}
}
DisruptorCommandBus
介绍
SimpleCommandBus的性能表现尚可,但它需要通过加锁防止多线程并发访问同一聚合,这会带来处理开销和锁竞争问题
DisruptorCommandBus采用了不同的多线程处理思路:它不让多个线程执行相同的完整流程,而是将流程拆分为多个阶段,由不同线程负责各自阶段的处理。DisruptorCommandBus基于Disruptor实现,通过优化多线程处理方式,能显著提升性能。具体来说,它会将任务交给两组线程处理:
- 第一组线程:执行@CommandHandler,修改聚合的状态
- 第二组线程:将生成的事件存储到事件存储并发布
DisruptorCommandBus的性能通常是SimpleCommandBus的4倍
构建DisruptorCommandBus实例时,需传入一个EventStore和DisruptorConfiguration(可选
限制
- 仅支持事件溯源聚合:该命令总线同时还会作为其处理聚合的Repository若需获取仓库实例,可调用 createRepository(AggregateFactory) 方法
- 一个命令只能导致单个聚合实例的状态变更
- 使用缓存时,同一标识符只能对应一个聚合,意味着不同类型的聚合不能拥有相同的标识符
- 命令执行应尽量避免触发工作单元回滚。回滚会导致DisruptorCommandBus无法保证命令按分发顺序处理,还可能需要重试其他无关命令,造成不必要的计算开销
- 创建新聚合实例后,更新该聚合的命令未必能完全按分发顺序执行;但聚合创建完成后,所有后续命令会严格按分发顺序执行。若需确保顺序,可在创建聚合的命令上添加回调(Callback),等待聚合创建完成后再发送后续命令
配置
你还可以可选地传入DisruptorConfiguration实例,通过调整配置优化其在特定环境下的性能,可配置项包括:
- Buffer size:环形缓冲区中用于注册入站命令的槽位数量。值越大可能提升吞吐量,但也会增加延迟;必须为2的幂,默认值为4096
- ProducerType:指定命令由单个线程还是多个线程生成,默认值为多线程
- WaitStrategy:指定处理线程(负责实际处理的三个线程)之间的等待策略,具体选择需结合机器核心数和其他运行进程
- 若需低延迟且允许DisruptorCommandBus占用核心资源,可使用BusySpinWaitStrategy
- 若需减少CPU占用并允许其他线程运行,可使用YieldingWaitStrategy
- 若需为其他进程让出CPU资源,可使用SleepingWaitStrategy或BlockingWaitStrategy(后者适用于CommandBus非满负荷运行的场景)
- 默认值为BlockingWaitStrategy
- Executor:为DisruptorCommandBus提供线程的执行器,至少需提供4个线程(3个用于处理逻辑,1个用于回调调用和 聚合状态损坏时的重试调度);默认使用名为DisruptorCommandBus的CachedThreadPool
- TransactionManager:确保事件的存储和发布在同一事务中执行
- InvokerInterceptors:用于调用命令处理器方法阶段的CommandHandlerInterceptor
- PublisherInterceptors:用于存储和发布事件阶段的 CommandHandlerInterceptor
- RollbackConfiguration:定义哪些异常会触发工作单元回滚,默认配置为 对未受检异常执行回滚
- RescheduleCommandsOnCorruptState:若聚合状态损坏(如工作单元回滚),是否重试已执行的命令false表示调用回调的onFailure()方法;true(默认)表示重试命令
- CoolingDownPeriod:等待所有命令处理完成的秒数。冷却期内不接受新命令,但会处理现有命令并在必要时重试;默认值为1000(即1秒)
- Cache:存储从事件存储中重建的聚合实例,用于缓存未被Disruptor活跃使用的聚合
- InvokerThreadCount:分配给@CommandHandler调用的线程数,建议初始值为机器核心数的一半
- PublisherThreadCount:用于事件发布的线程数,建议初始值为机器核心数的一半;若 IO操作耗时较长,可适当增加
- SerializerThreadCount:用于事件预序列化的线程数,默认值为 1;若未配置序列化器(Serializer),则此配置无效
- Serializer:用于执行预序列化的组件。配置后,DisruptorCommandBus会将所有生成的事件包装为SerializationAware消息,并在发布到事件存储前附加负载和元数据的序列化形式
配置
原生API:
java
public class AxonConfig {
// 省略其他配置方法...
public void configureDisruptorCommandBus(Configurer configurer) {
configurer.configureCommandBus(config -> {
CommandBus commandBus = DisruptorCommandBus
.builder()
.transactionManager(config.getComponent(TransactionManager.class))
.messageMonitor(config.messageMonitor(
DisruptorCommandBus.class, "commandBus"
))
.bufferSize(4096)
// ...(可添加其他配置)
.build();
commandBus.registerHandlerInterceptor(new CorrelationDataInterceptor<>(config.correlationDataProviders()));
return commandBus;
});
}
}
SpringBoot配置:
java
@Configuration
public class AxonConfig {
// 省略其他配置方法...
@Bean
public CommandBus disruptorCommandBus(TransactionManager transactionManager,
GlobalMetricRegistry metricRegistry) {
return DisruptorCommandBus
.builder()
.transactionManager(transactionManager)
.messageMonitor(metricRegistry.registerCommandBus("commandBus"))
.bufferSize(4096)
// ...(可添加其他配置)
.build();
}
@Bean
public ConfigurerModule commandBusCorrelationConfigurerModule() {
return configurer -> configurer.onInitialize(config -> config
.commandBus().registerHandlerInterceptor(
new CorrelationDataInterceptor<>(config.correlationDataProviders())
)
);
}
}
分布式
介绍
在实际场景中,你通常需要让不同JVM中的多个CommandBus实例协同工作,表现得像一个整体;在某个JVM的CommandBus上分发的命令,能无缝传输到另一个JVM的@CommandHandler中,并将结果返回,这就是分布式CommandBus的核心作用
核心概念
本地段(Local Segment)
与本地CommandBus实现不同,分布式CommandBus本身不执行任何命令处理,仅作为不同JVM间CommandBus的 桥梁,将接收到的命令委托给所谓的本地段(Local Segment)处理
默认情况下,本地段是SimpleCommandBus;你也可以将其配置为其他本地CommandBus
负载因子(Load Factor)
负载因子用于定义某个Axon应用实例相较于其他实例的负载承载能力。例如:
- 若两台机器的负载因子均为100,它们会承担等量的负载
- 若两台机器的负载因子均为200,它们仍会承担等量负载(负载因子的绝对值无关,仅相对比例影响负载分配)
在异构应用环境中(如部分机器性能较强、部分较弱),可通过调整负载因子让性能强的机器承担更多命令处理工作
分布式CommandBus实现的默认负载因子为100
路由策略(Routing Strategy)
命令需要被一致地路由到同一个应用实例(尤其是针对特定聚合的命令)这能确保单个实例负责该聚合的状态管理,解决并发访问问题,并让缓存等优化机制正常工作。在Axon应用中,负责一致路由的组件就是RoutingStrategy(路由策略)
RoutingStrategy接收CommandMessage,并基于该消息返回一个路由键。只要分布式环境的拓扑结构不变,具有相同路由键的命令始终会被路由到同一个段
目前,Axon Framework提供5种RoutingStrategy实现,其中3种为路由键无法解析时的降级方案:
- AnnotationRoutingStrategy:默认路由策略,要求命令类中存在被TargetAggregateIdentifier或RoutingKey注解标记的字段。框架会查找该注解标记的字段或其get方法,将其值作为命令的路由键
- MetaDataRoutingStrategy:基于创建该策略时指定的属性,从CommandMessage的MetaData中提取路由键
- ERROR UnresolvedRoutingKeyPolicy:默认降级策略,若无法从CommandMessage中解析路由键,会抛出异常
- RANDOM_KEY UnresolvedRoutingKeyPolicy:若无法解析路由键,返回随机值,此类命令会被路由到随机的CommandBus段
- STATIC_KEY UnresolvedRoutingKeyPolicy:若无法解析路由键,返回固定键,只要段配置不变,所有此类命令会被路由到同一个段
AnnotationRoutingStrategy和MetaDataRoutingStrategy是完整的可配置实现;ERROR、RANDOM_KEY和STATIC_KEY是降级策略,需配置在上述两种实现中使用。以下是配置示例:
原生Java配置:
java
// 自定义注解,用于驱动AnnotationRoutingStrategy
@interface CustomRoutingAnnotation {
}
public class AxonConfig {
// 省略其他配置方法...
public RoutingStrategy routingStrategy() {
return AnnotationRoutingStrategy
.builder()
.annotationType(CustomRoutingAnnotation.class)
.fallbackRoutingStrategy(UnresolvedRoutingKeyPolicy.STATIC_KEY)
.build();
}
}
SpringBoot配置:
java
public class AxonConfig {
// 省略其他配置方法...
public RoutingStrategy routingStrategy() {
return MetaDataRoutingStrategy
.builder()
.metaDataKey("my-routing-key")
.fallbackRoutingStrategy(UnresolvedRoutingKeyPolicy.RANDOM_KEY)
.build();
}
}
当然,若有需要,你也可以自定义RoutingStrategy实现。若需替换默认的AnnotationRoutingStrategy,可按以下方式配置:
java
@Configuration
public class AxonConfig {
// 省略其他配置方法...
@Bean
public RoutingStrategy routingStrategy() {
return /* 构建你的自定义路由策略 */;
}
}
AxonServerCommandBus
介绍
AxonServerCommandBus是Axon Framework默认的分布式CommandBus实现,它通过连接AxonServer实现命令的发送与接收
使用
原生可以这么用:
xml
<!-- POM文件中 -->
<dependencyManagement>
<!-- 依赖列表中 -->
<dependencies>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-bom</artifactId>
<version>${version.axon}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
<!-- ... -->
</dependencyManagement>
<!-- ... -->
<dependencies>
<!-- 依赖列表中 -->
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-server-connector</artifactId>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-configuration</artifactId>
</dependency>
<!-- ... -->
</dependencies>
java
public class AxonConfig {
public void configure() {
// 使用DefaultConfigurer构建配置时,AxonServerCommandBus会被默认配置为CommandBus
Configurer configurer = DefaultConfigurer.defaultConfiguration();
// ...(其他配置)
}
}
SpringBoot环境下:
Axon会自动配置AxonServerCommandBus:
xml
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
<version>${axon.version}</version>
</dependency>
java
public class AxonConfig {
public void configure() {
// 使用DefaultConfigurer构建配置时,AxonServerCommandBus会被默认配置为CommandBus
Configurer configurer = DefaultConfigurer.defaultConfiguration();
// ...(其他配置)
}
}
禁用
有两种方式可禁用Axon Framework默认的AxonServerCommandBus:
- 使用Spring Boot时,在配置中设置axon.server.enabled=false
- 排除axon-server-connector依赖
禁用后,Axon会回退到 SimpleCommandBus,除非你配置了其他CommandBus
本地段与负载因子配置
AxonServerCommandBus的负载因子通过CommandLoadFactorProvider定义。该接口允许你为不同命令设置不同的负载因子(例如,让某些命令更倾向于路由到特定实例)
以下是配置自定义本地段和/或负载因子的示例:
java
public class AxonConfig {
// 省略其他配置方法...
public CommandBus axonServerCommandBus(
CommandBus localSegment,
CommandLoadFactorProvider loadFactorProvider
) {
return AxonServerCommandBus
.builder()
.localSegment(localSegment)
.targetContextResolver(targetContextResolver)
// 所有必填配置组件可参考Builder的JavaDoc
.build();
}
}
java
@Configuration
public class AxonConfig {
// @Qualifier("localSegment") 注解会将此CommandBus指定为本地段
@Bean
@Qualifier("localSegment")
public CommandBus localSegment() {
return /* 构建你的本地段(如SimpleCommandBus、DisruptorCommandBus等) */;
}
@Bean
public CommandLoadFactorProvider loadFactorProvider() {
return /* 构建你的负载因子提供者 */;
}
}
DistributedCommandBus
介绍
DistributedCommandBus是AxonServerCommandBus的替代方案,每个JVM中的 DistributedCommandBus实例都被称为一个段
DistributedCommandBus依赖以下两个核心组件:
- CommandBusConnector:实现JVM间的通信协议,负责将命令通过网络发送到其他JVM,并接收响应
- CommandRouter:为每个入站命令选择目标节点,基于路由策略计算的路由键,决定将命令分发到DistributedCommandBus的哪个段
你可以选择不同的组件实现(作为扩展模块提供),目前Axon提供两种扩展:
- Spring Cloud扩展
- JGroups扩展
配置
DistributedCommandBus的配置(大部分情况下)无需修改配置文件,最简洁的方式是:
- 引入Spring Cloud扩展或JGroups扩展的Spring Boot Starter依赖
- 在应用上下文中添加以下配置项,启用 DistributedCommandBus
yaml
axon:
distributed:
enabled: true