Axon参考指南 - 3.命令处理 - Aggregate(聚合)

岁酱吖の 提交于 2020-03-02 17:21:59

简述

在本章中,我们将更详细地介绍Axon应用程序中处理和调度命令的过程。这里将涉及诸如聚合建模,外部命令处理程序,命令分派和测试之类的主题。

1. Aggregate 基本使用

聚合是一个常规对象,其中包含状态和更改该状态的方法。创建Aggregate对象时,您实际上是在创建“ Aggregate Root”,通常带有整个Aggregate的名称。出于此描述的目的,将使用“礼品卡”域,这使我们将GiftCard作为汇总(根)。默认情况下,Axon将您的聚合配置为“基于事件的”聚合(如此处所述)。此后,我们的基本GiftCard聚合结构将重点关注事件采购方法:

import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.eventsourcing.EventSourcingHandler;
import org.axonframework.modelling.command.AggregateIdentifier;

import static org.axonframework.modelling.command.AggregateLifecycle.apply;

public class GiftCard {

    @AggregateIdentifier // 1.
    private String id;

    @CommandHandler // 2.
    public GiftCard(IssueCardCommand cmd) {
        // 3.
       apply(new CardIssuedEvent(cmd.getCardId(), cmd.getAmount()));
    }

    @EventSourcingHandler // 4.
    public void on(CardIssuedEvent evt) {
        id = evt.getCardId();
    }

    // 5.
    protected GiftCard() {
    }
    // omitted command handlers and event sourcing handlers
}

给定的代码片段中有两个值得注意的概念,其中带有编号的Java注释,这些注释涉及以下项目符号:

  1. @AggregateIdentifier是GiftCard Aggregate中的外部参考点(主要用于命令,定位用)。这个字段是一个硬性要求,因为没有它,Axon将不知道给定Command的目标是哪个Aggregate。
  2. @CommandHandler批注的构造函数,或以其他方式放置“命令处理构造函数”。此注释告诉框架给定的构造函数能够处理IssueCardCommand。
  3. 静态AggregateLifecycle#apply(Object ...)是应发布事件消息时使用的对象。调用此函数后,提供的对象将在应用它们的聚合范围内以EventMessages的形式发布。
  4. 使用@EventSourcingHandler可以告诉框架,当“从其事件中获取”聚合时,应调用带注释的函数。
    由于所有事件源处理程序的合并将形成聚合,因此所有状态更改都将在此处发生。
    请注意,必须在聚合发布的第一个事件的@EventSourcingHandler中设置聚合标识符。(聚合对象里必须要有一个,它用于更改聚合的状态!
    这通常是创建事件。最后,使用特定规则解析@EventSourcingHandler注释的函数。
    这些规则与@EventHandler带注释的方法相同。
  5. Axon必需的无参数构造函数。Axon Framework使用此构造函数创建一个空的聚合实例,然后使用过去的事件对其进行初始化。如果不提供此构造函数,则会在加载Aggregate时导致异常。

消息处理功能的修饰符

只要JVM的安全性设置允许Axon
Framework更改方法的可访问性,事件处理程序方法就可以是私有的。这使您可以清楚地将Aggregate的公共API与处理事件的内部逻辑分开,后者公开了生成事件的方法。
 
大多数IDE都可以选择忽略带有特定批注的方法的“未使用的私有方法”警告。另外,您可以在方法中添加@SuppressWarnings(“
UnusedDeclaration”)批注,以确保您不会意外删除事件处理程序方法。

2. Aggregate 中命令的处理

尽管可以将命令处理程序放置在常规组件中(如将在此处讨论的那样,但建议直接在包含处理该命令状态的聚合上定义命令处理程序。

要在聚合中定义命令处理程序,只需使用@CommandHandler注释应处理命令的方法。带有@CommandHandler注释的方法将成为命令消息的命令处理程序,其中命令名称与该方法的第一个参数的完全限定的类名称匹配。因此,以@CommandHandler注释的void handle(RedeemCardCommand cmd)的方法签名将成为RedeemCardCommand命令消息的命令处理程序。

命令消息还可以使用不同的命令名称来分派。为了能够正确处理这些问题,可以在@CommandHandler批注中指定String commandName值。

为了使Axon知道应使用哪种Aggregate类型的实例处理命令消息,必须在命令对象中携带@Identity(@AggregateIdentifier,注解标识聚合的id,命令通过@TargetAggregateIdentifier与其匹配)的属性使用Aggregate Identifier进行注释。 注释可以放在Command对象中的字段或访问器方法(例如getter)上。

以GiftCard聚合为例,我们可以在聚合上标识两个命令处理程序:

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;

    @CommandHandler
    public GiftCard(IssueCardCommand cmd) {
        apply(new CardIssuedEvent(cmd.getCardId(), cmd.getAmount()));
    }

    @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()));
    }
    // omitted event sourcing handlers
}

GiftCard处理的Command对象IssueCardCommandRedeemCardCommand具有以下格式:

import org.axonframework.modelling.command.TargetAggregateIdentifier;

public class IssueCardCommand {

    @TargetAggregateIdentifier
    private final String cardId;
    private final Integer amount;

    public IssueCardCommand(String cardId, Integer amount) {
        this.cardId = cardId;
        this.amount = amount;
    }
    // omitted getters, equals/hashCode, toString functions
}


public class RedeemCardCommand {

    @TargetAggregateIdentifier
    private final String cardId;
    private final String transactionId;
    private final Integer amount;

    public RedeemCardCommand(String cardId, String transactionId, Integer amount) {
        this.cardId = cardId;
        this.transactionId = transactionId;
        this.amount = amount;
    }
    // omitted getters, equals/hashCode, toString functions
}

两个命令中都存在的cardId是对GiftCard实例的引用,因此使用@TargetAggregateIdentifier注释进行注释。创建聚合实例的命令不需要标识目标聚合标识符,因为还不存在聚合。但是,为了保持一致性,建议在其上也注释聚合标识符。

如果您喜欢使用其他机制来路由命令,则可以通过提供自定义CommandTargetResolver来覆盖行为。此类应基于给定命令返回聚合标识符和预期版本(如果有)。

聚合创建命令处理程序

将@CommandHandler批注放置在聚合的构造函数上时,相应的命令将创建该聚合的新实例并将其添加到存储库中。这些命令不需要针对特定​​的聚合实例。因此,这些命令不需要任何@TargetAggregateIdentifier或@TargetAggregateVersion批注,也不会为这些命令调用自定义CommandTargetResolver。
 
但是,无论使用哪种命令,都强烈建议您通过Axon Server 等分发应用程序时,在给定的消息上指定一个路由密钥。这样,@TargetAggregateIdentifier会加倍,但是在缺少值得注释的字段的情况下,应添加@RoutingKey注释以确保可以路由命令。此外,可以配置不同的RoutingStrategy,如“命令分派”部分中进一步指定。

3. 业务逻辑和状态变更

在聚合中,有一个特定的位置可以执行业务逻辑验证和聚合状态更改。命令处理程序应确定聚合是否处于正确状态(先检查当前状态是否可操作,如:送货必须要等下单了才可以送吧。如果是,则发布一个事件。否则,根据域的需要,可能会忽略该命令或引发异常。

**在任何命令处理功能中都不应发生状态更改。事件源处理程序应该是更新聚合状态的唯一方法。**如果不这样做,则意味着从事件中获取聚合时,聚合将丢失状态更改。

综合测试装置将防止命令处理功能中的意外状态更改。因此,建议为任何汇总实施提供全面的测试案例。

何时处理事件

Aggregate 所需的唯一状态是决策所需的状态。因此,仅在聚合的状态需要的状态验证相同时才去要处理由聚合发布的事件。

4. 从事件来源处理程序发布事件消息

在某些情况下,尤其是当汇总结构增长到不只是几个实体时,对在同一汇总的其他实体中发布的事件做出反应比较干净(此处将详细解释多实体汇总)。但是,由于在重建聚合状态时也会调用事件处理方法,因此必须采取特殊的预防措施。

可以在Event Sourcing Handler方法内apply()新事件。这使得实体“ B”可以应用事件来响应实体“ A”做某事。在采购给定的Aggregate时重播历史事件时,Axon将忽略apply()调用。请注意,在从事件来源处理程序发布事件消息的情况下,内部apply()调用的事件仅在所有实体都收到第一个事件之后才发布给实体。如果需要发布更多事件,请在应用内部事件后基于实体的状态,使用apply(...).andThenApply(...)

对其他事件做出反应

聚合无法处理来自其自身之外其他来源的事件。这是故意的,因为事件源处理程序用于重新创建聚合的状态。为此,它只需要它自己的事件即可,因为那些事件代表着状态改变。
 
为了使聚合对来自其他聚合实例的事件做出反应,应该利用Sagas或事件处理组件。(分布式)

5. Aggregate 生命周期

在聚合的生命周期中,需要执行几项操作。为此,Axon中的AggregateLifecycle类提供了两个静态函数:

  1. apply(Object) 和apply(Object, MetaData): AggregateLifecycle#apply将在EventBus上发布事件消息,这样就知道它源自执行该操作的聚合。可以仅提供事件对象,也可以提供事件和某些特定的元数据(MetaData
  2. createNew(Class, Callable):通过处理命令实例化新的聚合。阅读此内容以获得更多详细信息
  3. isLive():检查以确认聚合是否处于“活动”状态。
    如果聚合完成重播历史事件以重新创建其状态,则视为“实时”。如果因此正在将Aggregate用作事件源,则AggregateLifecycle.isLive() 调用将返回false。
    使用此isLive()方法,您可以执行仅应在处理新生成的事件时执行的活动。
  4. markDeleted():将标记该函数的Aggregate实例标记为“已删除”。如果域指定可以删除/删除/关闭给定的聚合,则很有用,此后不应再允许它处理任何命令。 应该从@EventSourcingHandler带注释的函数中调用其函数,以确保被标记为已删除是该Aggregate状态的一部分。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!