单个聚合根通常无法支持复杂业务逻辑,这时候就需要引入多个聚合。业务的复杂性可以分散在多个实体(Entity)中。本部分将讨论聚合中创建实体的注意事项,并阐述实体是如何处理消息的。
实体中的状态 我们对聚合不应暴露任何状态存在一定的误解,并不是任何实体都不应该包含属性访问方法(如getter、setter方法)相反,聚合内的实体将其状态共享给其他实体是可以给聚合带来好处的。当然,实体不应该将状态暴露至聚合外部。
以Gift Card领域为例,列举如何使用实体:
import org.axonframework.modelling.command.AggregateIdentifier;import org.axonframework.modelling.command.AggregateMember;import org.axonframework.modelling.command.EntityId;public class GiftCard {@AggregateIdentifierprivate String id;@AggregateMember // 1.private List<GiftCardTransaction> transactions = new ArrayList<>();private int remainingValue;// 省略其它}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;}// 省略其它}
GiftCardTransaction实体和聚合根类似。上述代码展示了多实体聚合的重要概念:
- 声明为子实体的字段必须使用
@AggregateMember注解。此注解使Axon检查字段以便处理消息。该例被注解的对象是Iterable的实现类,也可以是简单对象或者Map。在后续的例子中将使用Map。注意此注解可以使用在字段和方法上。 @EntityId注解指定实体的id字段,字段的值用来路由命令或者事件消息至对应的实体实例。默认情况下命令消息必须定义和被注解字段相同名称的属性字段,如当transactionId被注解时,命令消息必须含有transactionId字段或getTransactionId()方法。如果两者不一致,则必须通过@EntityId(routingKey = "customRoutingProperty")显式指定。如果该注解作为子实体的Map或Collection的一部分,则其必须添加。注意此注解可以使用在字段和方法上。定义实体类型
Map或Collection类型的字段应该显式指定泛型以便Axon识别被包含实体的类型。如果无法通过泛型指定,则必须使用@AggregateMember注解的type字段指定:@AggregateMember(type = GiftCardTransaction.class).
实体中的命令处理
@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 {@AggregateIdentifierprivate String id;@AggregateMemberprivate List<GiftCardTransaction> transactions = new ArrayList<>();private int remainingValue;// 省略其它}public class GiftCardTransaction {@EntityIdprivate String transactionId;private int transactionValue;private boolean reimbursed = false;public GiftCardTransaction(String transactionId, int transactionValue) {this.transactionId = transactionId;this.transactionValue = transactionValue;}@CommandHandlerpublic void handle(ReimburseCardCommand cmd) {if (reimbursed) {throw new IllegalStateException("Transaction already reimbursed");}apply(new CardReimbursedEvent(cmd.getCardId(), transactionId, transactionValue));}// 省略其它}
只有被注解字段所声明的类型会被检查,如果字段为值空则会抛出异常,如果Map或Collection中找不到匹配的实体,Axon则会抛出IllegalStateException异常进行显式提醒。
命令处理器注意事项 每个命令必须对应一个聚合内的处理器,也就是说,同一聚合内(包括聚合内实体)不能声明多个
@CommandHandler来处理同一命令。这种情况下,可由实体的双亲即聚合进行预处理,再根据情况路由。 虽然字段运行时的类型不需要显式指定,但只有被@AggregateMember显式指定的类型才会被检查并交由@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 {@AggregateIdentifierprivate String id;@AggregateMemberprivate List<GiftCardTransaction> transactions = new ArrayList<>();@CommandHandlerpublic void handle(RedeemCardCommand cmd) {// 业务逻辑apply(new CardRedeemedEvent(id, cmd.getTransactionId(), cmd.getAmount()));}@EventSourcingHandlerpublic void on(CardRedeemedEvent evt) {// 1.transactions.add(new GiftCardTransaction(evt.getTransactionId(), evt.getAmount()));}// 省略其它}public class GiftCardTransaction {@EntityIdprivate String transactionId;private int transactionValue;private boolean reimbursed = false;public GiftCardTransaction(String transactionId, int transactionValue) {this.transactionId = transactionId;this.transactionValue = transactionValue;}@CommandHandlerpublic void handle(ReimburseCardCommand cmd) {if (reimbursed) {throw new IllegalStateException("Transaction already reimbursed");}apply(new CardReimbursedEvent(cmd.getCardId(), transactionId, transactionValue));}@EventSourcingHandlerpublic void on(CardReimbursedEvent event) {// 2.if (transactionId.equals(event.getTransactionId())) {reimbursed = true;}}// 省略其它}
上述有两个需要注意:
- 父类中的事件溯源处理器进行实体创建,因此实体中不可能像聚合根一样包含命令处理构造器。
- 实体中的事件溯源处理器验证接受到的事件是否属于当前实体。这是非常有必要的且可以避免事件被同类型实体实例处理的情况。
第二点可以通过@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<>();
// 省略其它
}
通过将eventForwardingMode设置为ForwardMatchingInstances,事件消息将只会匹配实体内被@EntityId注解的字段/方法,且事件中的字段必须和该字段/方法名称匹配。这种情况还可以通过@EntityId注解的routingKey来指定(与实体命令的路由相似)。其它转发方式有ForwardAll(默认)和ForwardNone,分别表示转发所有和都不转发。
