簡介
複雜的業務邏輯通常需要的內容超出了僅具有聚合根的聚合所提供的內容。在這種情況下,重要的是將複雜性分佈在聚合中的多個“實體”上。在本章中,我們將討論有關在聚合中創建實體的細節以及它們如何處理消息。
實體之間的狀態
對聚合不應公開狀態的規則的常見誤解是,任何實體都不應該包含任何屬性訪問器方法。不是這種情況。實際上,如果聚合中的實體將狀態暴露給同一聚合中的其他實體,則聚合可能會受益匪淺。但是,建議不要將狀態暴露在聚合之外。
在“禮品卡”域中,本節中定義了GiftCard
聚合根。讓我們利用此域來介紹實體:
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateMember;
import org.axonframework.modelling.command.EntityId;
public class GiftCard {
@AggregateIdentifier
private String id;
@AggregateMember // 1. 禮品卡交易列表
private List<GiftCardTransaction> transactions = new ArrayList<>();
private int remainingValue;
// omitted constructors, command and event sourcing handlers
}
public class GiftCardTransaction {
@EntityId // 2.
private String transactionId;
private int transactionValue;
private boolean reimbursed = false;
public GiftCardTransaction(String transactionId, int transactionValue) {
this.transactionId = transactionId;
this.transactionValue = transactionValue;
}
public String getTransactionId() {
return transactionId;
}
// omitted command handlers, event sourcing handlers and equals/hashCode
}
實體像聚合根一樣,是簡單的對象,如新的GiftCardTransaction
實體所示。上面的代碼段顯示了多實體聚合的兩個重要概念:
- 聲明子實體/實體的字段必須使用
@AggregateMember
註釋。此註釋告訴Axon,帶註釋的字段包含應檢查消息處理程序的類。
此示例顯示了Iterable實現的註釋,但也可以將其放置在單個Object或Map上。
在後一種情況下,預計Map的值將包含實體,而鍵包含的值將用作其引用。 @EntityId
批註指定實體的標識字段。
要求能夠將命令(或事件)消息路由到正確的實體實例。
有效負載上的屬性將用於查找消息應路由到的實體,默認爲@EntityId註釋字段的名稱。
例如,在註釋字段transactionId時,該命令必須定義具有相同名稱的屬性
,這意味着必須存在transactionId或getTransactionId() 方法。
如果字段名稱
和路由屬性
不同,則可以使用@EntityId(routingKey ="customRoutingProperty")
顯式提供一個值。
如果此註釋將成爲子實體的集合或映射的一部分,則在實體實現上是必需的。
定義實體類型
Collection或Map的字段聲明應包含適當的泛型,以允許Axon標識集合或Map中包含的Entity的類型。如果無法在聲明中添加泛型(例如,因爲您使用的是已經定義了泛型類型的自定義實現),則必須通過在@AggregateMember批註中指定類型字段來指定實體類型:
@AggregateMember(type = GiftCardTransaction.class).
1. 實體中的命令處理
@CommandHandler
註釋不限於聚合根。將所有命令處理程序放在根目錄中有時會導致在聚合根目錄上使用大量方法,而其中許多方法只是將調用轉發給基礎實體。在這種情況下,可以將@CommandHandler批註放置在基礎實體的方法上。
爲了使Axon能夠找到這些帶註釋的方法,在聚合根中聲明實體的字段必須用@AggregateMember標記:
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateMember;
import org.axonframework.modelling.command.EntityId;
import static org.axonframework.modelling.command.AggregateLifecycle.apply;
public class GiftCard {
@AggregateIdentifier
private String id;
@AggregateMember
private List<GiftCardTransaction> transactions = new ArrayList<>();
private int remainingValue;
// omitted constructors, command and event sourcing handlers
}
public class GiftCardTransaction {
@EntityId
private String transactionId;
private int transactionValue;
private boolean reimbursed = false;
public GiftCardTransaction(String transactionId, int transactionValue) {
this.transactionId = transactionId;
this.transactionValue = transactionValue;
}
@CommandHandler
public void handle(ReimburseCardCommand cmd) {
if (reimbursed) {
throw new IllegalStateException("Transaction already reimbursed");
}
apply(new CardReimbursedEvent(cmd.getCardId(), transactionId, transactionValue));
}
// omitted getter, event sourcing handler and equals/hashCode
}
請注意,僅檢查聲明字段的聲明類型的命令處理程序。如果在該實體的傳入命令到達時字段值是null,則將引發異常。如果存在子實體的Collection
或Map
,並且找不到與命令的路由鍵匹配的實體,則Axon會引發IllegalStateException,因爲顯然聚合在該時間點不能處理該命令。
命令處理程序注意事項
請注意,每個命令在聚合中必須只有一個處理程序。這意味着您不能使用處理相同命令類型的@CommandHandler註釋多個實體(無論是根節點還是不是根節點)。如果需要有條件地將命令路由到實體,則這些實體的父級應處理該命令,並根據適用條件轉發該命令。
字段的運行時類型不必完全是聲明的類型。但是,對於@CommandHandler
方法,僅檢查@AggregateMember
註釋字段的聲明類型。
2. 實體中的事件來源處理程序
當使用事件源作爲存儲聚合的機制時,不僅聚合根需要使用事件來觸發狀態轉換,而且聚合中的每個實體也需要使用事件來觸發狀態轉換。Axon開箱即用地爲事件來源的複雜聚合結構提供支持。
當實體(包括聚合根)應用事件時,將首先由聚合根處理該事件,然後通過每個@AggregateMember
註釋字段將其冒泡到其所有包含的子實體:
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateMember;
import org.axonframework.modelling.command.EntityId;
import static org.axonframework.modelling.command.AggregateLifecycle.apply;
public class GiftCard {
@AggregateIdentifier
private String id;
// 子類
@AggregateMember
private List<GiftCardTransaction> transactions = new ArrayList<>();
@CommandHandler
public void handle(RedeemCardCommand cmd) {
// Some decision making logic
apply(new CardRedeemedEvent(id, cmd.getTransactionId(), cmd.getAmount()));
}
@EventSourcingHandler
public void on(CardRedeemedEvent evt) {
// 1. 冒泡到子類
transactions.add(new GiftCardTransaction(evt.getTransactionId(), evt.getAmount()));
}
// omitted constructors, command and event sourcing handlers
}
public class GiftCardTransaction {
@EntityId
private String transactionId;
private int transactionValue;
private boolean reimbursed = false;
public GiftCardTransaction(String transactionId, int transactionValue) {
this.transactionId = transactionId;
this.transactionValue = transactionValue;
}
@CommandHandler
public void handle(ReimburseCardCommand cmd) {
if (reimbursed) {
throw new IllegalStateException("Transaction already reimbursed");
}
// 子類處理
apply(new CardReimbursedEvent(cmd.getCardId(), transactionId, transactionValue));
}
@EventSourcingHandler
public void on(CardReimbursedEvent event) {
// 2. 子類處理
if (transactionId.equals(event.getTransactionId())) {
reimbursed = true;
}
}
// omitted getter and equals/hashCode
}
上面的代碼片段中有兩個細節值得一提,並用帶編號的Java註釋指出:
- 實體的創建在其父級的事件源處理程序中進行。因此,不可能像聚合根一樣在實體類上具有“
命令處理構造函數
”。 - 實體中的事件源處理程序執行驗證檢查,以確認接收到的事件是否實際上屬於該實體。
這是必要的,因爲由一個實體實例應用的事件也將由相同類型的任何其他實體實例處理。
通過更改@AggregateMember
批註上的eventForwardingMode
,第二點中描述的情況是可自定義的:
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateMember;
import org.axonframework.modelling.command.ForwardMatchingInstances;
public class GiftCard {
@AggregateIdentifier
private String id;
@AggregateMember(eventForwardingMode = ForwardMatchingInstances.class)
private List<GiftCardTransaction> transactions = new ArrayList<>();
// omitted constructors, command and event sourcing handlers
}
通過將eventForwardingMode
設置爲ForwardMatchingInstances
,僅當事件消息包含與實體上@EntityId註釋字段名稱匹配的字段/獲取器時,纔會轉發事件消息(事件包含與實體一致的@EntityId註釋字段,或@EntityId批註上的routingKey字段
)。可以使用@EntityId
批註上的routingKey
字段進一步指定此路由行爲,以反映實體中路由命令的行爲。可以使用的其他轉發模式是ForwardAll(默認)
和ForwardNone
,它們分別將所有事件轉發到所有實體,或者根本不轉發任何事件。