19.Axon框架-命令

命令

1.介绍

Axon应用关联的第一种消息类型是命令消息(Command Message),简称命令。这类消息的核心意图是以某种方式修改应用的状态(例如创建聚合、更新聚合属性、执行领域操作等)

2.@CommandHandler

介绍

@CommandHandler是Axon中负责接收并处理Command的组件,核心职责是根据命令意图执行领域逻辑(如校验业务规则、发布领域事件)

类型

  1. 聚合内@CommandHandler
  2. 外部@CommandHandler

聚合内@CommandHandler

介绍

尽管@CommandHandler可放在普通组件中,但推荐将其直接定义在聚合上,因为聚合包含处理命令所需的状态,这种设计能更好地封装领域逻辑,符合DDD中聚合是领域逻辑的核心载体的原则

匹配规则
  1. 默认匹配规则:命令处理器处理的Command类型,由方法的第一个参数的全限定类名决定。例如,void handle(RedeemCardCommand cmd)会处理所有RedeemCardCommand类型的命令
  2. 自定义命令名:若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 方法
}
注意
  1. 尽管创建聚合的命令(如 IssueCardCommand)无需定位 已存在的聚合实例,但为了一致性,仍建议标注@TargetAggregateIdentifier
  2. 自定义路由逻辑:若需覆盖默认的命令的聚合匹配逻辑,可通过自定义CommandTargetResolver实现,该类需根据命令返回聚合标识符和预期版本
构造函数与@CommandHandler

当@CommandHandler标注在聚合的构造函数上时,该命令会触发 "创建新聚合实例" 并将其添加到仓库。这类命令的特殊规则:

  1. 无需标注@TargetAggregateIdentifier或@TargetAggregateVersion
  2. 自定义CommandTargetResolver不会对这类命令生效
业务逻辑与状态变更的规范

聚合内@CommandHandler的核心职责是决策,而非直接修改状态,需遵守以下规范:

  1. 业务逻辑位置:命令处理器中应仅执行业务规则校验(如 消费金额不能为负),若校验通过则发布Event;若校验失败,可忽略命令或抛出异常(根据领域需求决定)
  2. 状态变更位置:聚合的状态变更必须放在@EventSourcingHandler中,而非@CommandHandler,若在@CommandHandler中直接修改状态,当聚合通过事件溯源重建时,会丢失这些状态变更
  3. 测试保障:Axon的Aggregate Test Fixture会检测@CommandHandler中的无意状态变更,建议为所有聚合编写完整的测试用例
事件处理的边界
  1. 仅处理必要事件:聚合只需处理 对未来命令校验有必要的事件,即仅保留影响业务决策的状态,避免冗余
  2. 聚合内事件的传递:在多实体聚合中,可在一个实体的@EventSourcingHandler中调用apply()发布新事件,以响应另一个实体的事件。Axon在回放历史事件时会自动忽略这类apply()调用,避免重复创建事件。若需基于内部事件后的状态发布更多事件,可使用apply(...).andThenApply(...) 链式调用
  3. 外部事件的响应:聚合不能处理来自其他聚合的事件,因为@EventSourcingHandler的作用是重建当前聚合的状态,仅需当前聚合的事件。若需响应外部事件(如其他聚合的事件),应通过Saga或事件处理组件实现
创建策略

默认情况下,聚合的命令处理器分为两类:

  1. 标注@CommandHandler的构造函数:仅用于创建新聚合
  2. 标注@CommandHandler的普通方法:仅用于处理现有聚合的命令

Axon允许通过@CreationPolicy注解自定义@CommandHandler的聚合创建策略,该注解需结合AggregateCreationPolicy枚举使用,支持以下三种策略:

  1. ALWAYS:强制创建新聚合,效果等同于标注@CommandHandler 的构造函数。支持返回除聚合标识符外的其他结果
  2. CREATE_IF_MISSING:Upsert逻辑。若聚合不存在则创建,若已存在则修改
  3. 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));
    }
}
注意
  1. 聚合仓库:负责聚合的加载与存储,是外部@CommandHandler与聚合交互的核心组件。若@CommandHandler直接定义在聚合上,Axon会自动调用仓库,无需手动处理
  2. Repository的load()方法:根据聚合标识符加载聚合实例,返回Aggregate<GiftCard>对象
  3. Aggregate的execute() 方法:接收Consumer<GiftCard>函数,用于调用聚合的业务方法。该方法会确保聚合的生命周期(如UnitOfWork)正确启动,避免状态管理问题

3.命令分发器

介绍

命令分发过程是命令消息流转的起点,Axon提供了两个核心接口用于将命令发送到@CommandHandler,分别是CommandBus和CommandGateway

CommandBus

介绍

CommandBus是将命令分发到对应@CommandHandler的核心机制,作为基础设施组件,它知晓 哪个组件能处理哪种命令

核心特性
  1. 一对一分发:每条命令始终只会发送给一个@CommandHandler。若没有找到能处理该命令的@CommandHandler,会抛出NoHandlerForCommandException
  2. 两种分发方法: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的接口方法分为两类,覆盖异步和同步分发场景:

  1. send(Object):异步分发,返回CompletableFuture,支持链式处理结果
  2. 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的实现(如业务逻辑是否抛出异常)

成功
  1. 按设计意图,命令处理器不建议返回值(命令的核心是修改状态,而非获取数据,获取数据应使用QueryMessage)
  2. 若CommandBus/CommandGateway返回成功结果,通常为null;仅在特殊场景下返回非null值,例如:
    • 聚合创建命令(@CommandHandler标注构造函数):返回聚合的@AggregateIdentifier字段值
    • 聚合内创建子实体的命令:返回子实体的标识符
异常
  1. 若@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实现,通过优化多线程处理方式,能显著提升性能。具体来说,它会将任务交给两组线程处理:

  1. 第一组线程:执行@CommandHandler,修改聚合的状态
  2. 第二组线程:将生成的事件存储到事件存储并发布

DisruptorCommandBus的性能通常是SimpleCommandBus的4倍

构建DisruptorCommandBus实例时,需传入一个EventStore和DisruptorConfiguration(可选

限制
  1. 仅支持事件溯源聚合:该命令总线同时还会作为其处理聚合的Repository若需获取仓库实例,可调用 createRepository(AggregateFactory) 方法
  2. 一个命令只能导致单个聚合实例的状态变更
  3. 使用缓存时,同一标识符只能对应一个聚合,意味着不同类型的聚合不能拥有相同的标识符
  4. 命令执行应尽量避免触发工作单元回滚。回滚会导致DisruptorCommandBus无法保证命令按分发顺序处理,还可能需要重试其他无关命令,造成不必要的计算开销
  5. 创建新聚合实例后,更新该聚合的命令未必能完全按分发顺序执行;但聚合创建完成后,所有后续命令会严格按分发顺序执行。若需确保顺序,可在创建聚合的命令上添加回调(Callback),等待聚合创建完成后再发送后续命令
配置

你还可以可选地传入DisruptorConfiguration实例,通过调整配置优化其在特定环境下的性能,可配置项包括:

  1. Buffer size:环形缓冲区中用于注册入站命令的槽位数量。值越大可能提升吞吐量,但也会增加延迟;必须为2的幂,默认值为4096
  2. ProducerType:指定命令由单个线程还是多个线程生成,默认值为多线程
  3. WaitStrategy:指定处理线程(负责实际处理的三个线程)之间的等待策略,具体选择需结合机器核心数和其他运行进程
    • 若需低延迟且允许DisruptorCommandBus占用核心资源,可使用BusySpinWaitStrategy
    • 若需减少CPU占用并允许其他线程运行,可使用YieldingWaitStrategy
    • 若需为其他进程让出CPU资源,可使用SleepingWaitStrategy或BlockingWaitStrategy(后者适用于CommandBus非满负荷运行的场景)
    • 默认值为BlockingWaitStrategy
  4. Executor:为DisruptorCommandBus提供线程的执行器,至少需提供4个线程(3个用于处理逻辑,1个用于回调调用和 聚合状态损坏时的重试调度);默认使用名为DisruptorCommandBus的CachedThreadPool
  5. TransactionManager:确保事件的存储和发布在同一事务中执行
  6. InvokerInterceptors:用于调用命令处理器方法阶段的CommandHandlerInterceptor
  7. PublisherInterceptors:用于存储和发布事件阶段的 CommandHandlerInterceptor
  8. RollbackConfiguration:定义哪些异常会触发工作单元回滚,默认配置为 对未受检异常执行回滚
  9. RescheduleCommandsOnCorruptState:若聚合状态损坏(如工作单元回滚),是否重试已执行的命令false表示调用回调的onFailure()方法;true(默认)表示重试命令
  10. CoolingDownPeriod:等待所有命令处理完成的秒数。冷却期内不接受新命令,但会处理现有命令并在必要时重试;默认值为1000(即1秒)
  11. Cache:存储从事件存储中重建的聚合实例,用于缓存未被Disruptor活跃使用的聚合
  12. InvokerThreadCount:分配给@CommandHandler调用的线程数,建议初始值为机器核心数的一半
  13. PublisherThreadCount:用于事件发布的线程数,建议初始值为机器核心数的一半;若 IO操作耗时较长,可适当增加
  14. SerializerThreadCount:用于事件预序列化的线程数,默认值为 1;若未配置序列化器(Serializer),则此配置无效
  15. 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应用实例相较于其他实例的负载承载能力。例如:

  1. 若两台机器的负载因子均为100,它们会承担等量的负载
  2. 若两台机器的负载因子均为200,它们仍会承担等量负载(负载因子的绝对值无关,仅相对比例影响负载分配)

在异构应用环境中(如部分机器性能较强、部分较弱),可通过调整负载因子让性能强的机器承担更多命令处理工作

分布式CommandBus实现的默认负载因子为100

路由策略(Routing Strategy)

命令需要被一致地路由到同一个应用实例(尤其是针对特定聚合的命令)这能确保单个实例负责该聚合的状态管理,解决并发访问问题,并让缓存等优化机制正常工作。在Axon应用中,负责一致路由的组件就是RoutingStrategy(路由策略)

RoutingStrategy接收CommandMessage,并基于该消息返回一个路由键。只要分布式环境的拓扑结构不变,具有相同路由键的命令始终会被路由到同一个段

目前,Axon Framework提供5种RoutingStrategy实现,其中3种为路由键无法解析时的降级方案:

  1. AnnotationRoutingStrategy:默认路由策略,要求命令类中存在被TargetAggregateIdentifier或RoutingKey注解标记的字段。框架会查找该注解标记的字段或其get方法,将其值作为命令的路由键
  2. MetaDataRoutingStrategy:基于创建该策略时指定的属性,从CommandMessage的MetaData中提取路由键
  3. ERROR UnresolvedRoutingKeyPolicy:默认降级策略,若无法从CommandMessage中解析路由键,会抛出异常
  4. RANDOM_KEY UnresolvedRoutingKeyPolicy:若无法解析路由键,返回随机值,此类命令会被路由到随机的CommandBus段
  5. 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:

  1. 使用Spring Boot时,在配置中设置axon.server.enabled=false
  2. 排除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依赖以下两个核心组件:

  1. CommandBusConnector:实现JVM间的通信协议,负责将命令通过网络发送到其他JVM,并接收响应
  2. CommandRouter:为每个入站命令选择目标节点,基于路由策略计算的路由键,决定将命令分发到DistributedCommandBus的哪个段

你可以选择不同的组件实现(作为扩展模块提供),目前Axon提供两种扩展:

  1. Spring Cloud扩展
  2. JGroups扩展
配置

DistributedCommandBus的配置(大部分情况下)无需修改配置文件,最简洁的方式是:

  1. 引入Spring Cloud扩展或JGroups扩展的Spring Boot Starter依赖
  2. 在应用上下文中添加以下配置项,启用 DistributedCommandBus
yaml 复制代码
axon:
  distributed:
    enabled: true
相关推荐
David爱编程16 分钟前
多核 CPU 下的缓存一致性问题:隐藏的性能陷阱与解决方案
java·后端
追逐时光者37 分钟前
一款基于 .NET 开源、功能全面的微信小程序商城系统
后端·.net
绝无仅有2 小时前
Go 并发同步原语:sync.Mutex、sync.RWMutex 和 sync.Once
后端·面试·github
绝无仅有2 小时前
Go Vendor 和 Go Modules:管理和扩展依赖的最佳实践
后端·面试·github
自由的疯2 小时前
Java 实现TXT文件导入功能
java·后端·架构
现在没有牛仔了2 小时前
SpringBoot实现操作日志记录完整指南
java·spring boot·后端
小蒜学长2 小时前
基于django的梧桐山水智慧旅游平台设计与开发(代码+数据库+LW)
java·spring boot·后端·python·django·旅游
文心快码BaiduComate2 小时前
七夕,画个动态星空送给Ta
前端·后端·程序员
文心快码BaiduComate3 小时前
早期人类奴役AI实录:用Comate Zulu 10min做一款Chrome插件
前端·后端·程序员
大象席地抽烟3 小时前
Java异步编程的方式
后端