简介
复杂的业务逻辑通常需要的内容超出了仅具有聚合根的聚合所提供的内容。在这种情况下,重要的是将复杂性分布在聚合中的多个“实体”上。在本章中,我们将讨论有关在聚合中创建实体的细节以及它们如何处理消息。
实体之间的状态
对聚合不应公开状态的规则的常见误解是,任何实体都不应该包含任何属性访问器方法。不是这种情况。实际上,如果聚合中的实体将状态暴露给同一聚合中的其他实体,则聚合可能会受益匪浅。但是,建议不要将状态暴露在聚合之外。
在“礼品卡”域中,本节中定义了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
,它们分别将所有事件转发到所有实体,或者根本不转发任何事件。