单个聚合根通常无法支持复杂业务逻辑,这时候就需要引入多个聚合。业务的复杂性可以分散在多个实体(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 {
@AggregateIdentifier
private 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 {
@AggregateIdentifier
private String id;
@AggregateMember
private List<GiftCardTransaction> transactions = new ArrayList<>();
private int remainingValue;
// 省略其它
}
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));
}
// 省略其它
}
只有被注解字段所声明的类型会被检查,如果字段为值空则会抛出异常,如果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 {
@AggregateIdentifier
private String id;
@AggregateMember
private List<GiftCardTransaction> transactions = new ArrayList<>();
@CommandHandler
public void handle(RedeemCardCommand cmd) {
// 业务逻辑
apply(new CardRedeemedEvent(id, cmd.getTransactionId(), cmd.getAmount()));
}
@EventSourcingHandler
public void on(CardRedeemedEvent evt) {
// 1.
transactions.add(new GiftCardTransaction(evt.getTransactionId(), evt.getAmount()));
}
// 省略其它
}
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;
}
}
// 省略其它
}
上述有两个需要注意:
- 父类中的事件溯源处理器进行实体创建,因此实体中不可能像聚合根一样包含命令处理构造器。
- 实体中的事件溯源处理器验证接受到的事件是否属于当前实体。这是非常有必要的且可以避免事件被同类型实体实例处理的情况。
第二点可以通过@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
,分别表示转发所有和都不转发。