Axon參考指南 - 3.命令處理 - Aggregate(聚合)

簡述

在本章中,我們將更詳細地介紹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狀態的一部分。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章