簡述
在本章中,我們將更詳細地介紹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註釋,這些註釋涉及以下項目符號:
@AggregateIdentifier
是GiftCard Aggregate中的外部參考點(主要用於命令,定位用)。這個字段是一個硬性要求,因爲沒有它,Axon將不知道給定Command的目標是哪個Aggregate。@CommandHandler
批註的構造函數,或以其他方式放置“命令處理構造函數”。此註釋告訴框架給定的構造函數能夠處理IssueCardCommand。- 靜態
AggregateLifecycle#apply(Object ...)
是應發佈事件消息時使用的對象。調用此函數後,提供的對象將在應用它們的聚合範圍內以EventMessages的形式發佈。 - 使用
@EventSourcingHandler
可以告訴框架,當“從其事件中獲取”聚合時,應調用帶註釋的函數。
由於所有事件源處理程序的合併將形成聚合,因此所有狀態更改都將在此處發生。
請注意,必須在聚合發佈的第一個事件的@EventSourcingHandler
中設置聚合標識符。(聚合對象裏必須要有一個,它用於更改聚合的狀態!
)
這通常是創建事件。最後,使用特定規則解析@EventSourcingHandler
註釋的函數。
這些規則與@EventHandler
帶註釋的方法相同。 - 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對象IssueCardCommand和RedeemCardCommand具有以下格式:
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類提供了兩個靜態函數:
apply(Object) 和apply(Object, MetaData): AggregateLifecycle#apply
將在EventBus上發佈事件消息,這樣就知道它源自執行該操作的聚合。可以僅提供事件對象,也可以提供事件和某些特定的元數據(MetaData)。createNew(Class, Callable)
:通過處理命令實例化新的聚合。閱讀此內容以獲得更多詳細信息。isLive()
:檢查以確認聚合是否處於“活動”狀態。
如果聚合完成重播歷史事件以重新創建其狀態,則視爲“實時”。如果因此正在將Aggregate用作事件源,則AggregateLifecycle.isLive() 調用將返回false。
使用此isLive()方法,您可以執行僅應在處理新生成的事件時執行的活動。- markDeleted():將標記該函數的Aggregate實例標記爲“已刪除”。如果域指定可以刪除/刪除/關閉給定的聚合,則很有用,此後不應再允許它處理任何命令。 應該從
@EventSourcingHandler
帶註釋的函數中調用其函數,以確保被標記爲已刪除是該Aggregate狀態的一部分。