单个聚合根通常无法支持复杂业务逻辑,这时候就需要引入多个聚合。业务的复杂性可以分散在多个实体(Entity)中。本部分将讨论聚合中创建实体的注意事项,并阐述实体是如何处理消息的。

实体中的状态 我们对聚合不应暴露任何状态存在一定的误解,并不是任何实体都不应该包含属性访问方法(如getter、setter方法)相反,聚合内的实体将其状态共享给其他实体是可以给聚合带来好处的。当然,实体不应该将状态暴露至聚合外部。

Gift Card领域为例,列举如何使用实体:

  1. import org.axonframework.modelling.command.AggregateIdentifier;
  2. import org.axonframework.modelling.command.AggregateMember;
  3. import org.axonframework.modelling.command.EntityId;
  4. public class GiftCard {
  5. @AggregateIdentifier
  6. private String id;
  7. @AggregateMember // 1.
  8. private List<GiftCardTransaction> transactions = new ArrayList<>();
  9. private int remainingValue;
  10. // 省略其它
  11. }
  12. public class GiftCardTransaction {
  13. @EntityId // 2.
  14. private String transactionId;
  15. private int transactionValue;
  16. private boolean reimbursed = false;
  17. public GiftCardTransaction(String transactionId, int transactionValue) {
  18. this.transactionId = transactionId;
  19. this.transactionValue = transactionValue;
  20. }
  21. public String getTransactionId() {
  22. return transactionId;
  23. }
  24. // 省略其它
  25. }

GiftCardTransaction实体和聚合根类似。上述代码展示了多实体聚合的重要概念:

  1. 声明为子实体的字段必须使用@AggregateMember注解。此注解使Axon检查字段以便处理消息。该例被注解的对象是Iterable的实现类,也可以是简单对象或者Map。在后续的例子中将使用Map注意此注解可以使用在字段和方法上。
  2. @EntityId注解指定实体的id字段,字段的值用来路由命令或者事件消息至对应的实体实例。默认情况下命令消息必须定义和被注解字段相同名称的属性字段,如当transactionId被注解时,命令消息必须含有transactionId字段或getTransactionId()方法。如果两者不一致,则必须通过@EntityId(routingKey = "customRoutingProperty")显式指定。如果该注解作为子实体的MapCollection的一部分,则其必须添加。注意此注解可以使用在字段和方法上。

    定义实体类型 MapCollection类型的字段应该显式指定泛型以便Axon识别被包含实体的类型。如果无法通过泛型指定,则必须使用@AggregateMember注解的type字段指定: @AggregateMember(type = GiftCardTransaction.class).

实体中的命令处理

@CommandHandler不仅可以在聚合根中使用,也可以使用在实体中。命令处理器过多将导致聚合根十分庞大,如果大多数命令处理器只是简单调用内部实体,则可以将@CommandHandler注解移动至实体中对应的方法。Axon借助@AggregateMember寻找实体中对应的命令处理器:

  1. import org.axonframework.commandhandling.CommandHandler;
  2. import org.axonframework.modelling.command.AggregateIdentifier;
  3. import org.axonframework.modelling.command.AggregateMember;
  4. import org.axonframework.modelling.command.EntityId;
  5. import static org.axonframework.modelling.command.AggregateLifecycle.apply;
  6. public class GiftCard {
  7. @AggregateIdentifier
  8. private String id;
  9. @AggregateMember
  10. private List<GiftCardTransaction> transactions = new ArrayList<>();
  11. private int remainingValue;
  12. // 省略其它
  13. }
  14. public class GiftCardTransaction {
  15. @EntityId
  16. private String transactionId;
  17. private int transactionValue;
  18. private boolean reimbursed = false;
  19. public GiftCardTransaction(String transactionId, int transactionValue) {
  20. this.transactionId = transactionId;
  21. this.transactionValue = transactionValue;
  22. }
  23. @CommandHandler
  24. public void handle(ReimburseCardCommand cmd) {
  25. if (reimbursed) {
  26. throw new IllegalStateException("Transaction already reimbursed");
  27. }
  28. apply(new CardReimbursedEvent(cmd.getCardId(), transactionId, transactionValue));
  29. }
  30. // 省略其它
  31. }

只有被注解字段所声明的类型会被检查,如果字段为值空则会抛出异常,如果MapCollection中找不到匹配的实体,Axon则会抛出IllegalStateException异常进行显式提醒。

命令处理器注意事项 每个命令必须对应一个聚合内的处理器,也就是说,同一聚合内(包括聚合内实体)不能声明多个@CommandHandler来处理同一命令。这种情况下,可由实体的双亲即聚合进行预处理,再根据情况路由。 虽然字段运行时的类型不需要显式指定,但只有被@AggregateMember显式指定的类型才会被检查并交由@CommandHandler方法处理。

实体中的事件溯源处理器

使用事件溯源机制保存聚合时,不仅聚合根需要使用事件触发状态事务,而且聚合内的实体也需要。Axon提供了复杂根据结构事件溯源的支持。
当一个实体(包括聚合根)触发了一个事件,它首先交由聚合根处理,随后向下冒泡至每个@AggregateMember(包含其所有孩子):

  1. import org.axonframework.commandhandling.CommandHandler;
  2. import org.axonframework.modelling.command.AggregateIdentifier;
  3. import org.axonframework.modelling.command.AggregateMember;
  4. import org.axonframework.modelling.command.EntityId;
  5. import static org.axonframework.modelling.command.AggregateLifecycle.apply;
  6. public class GiftCard {
  7. @AggregateIdentifier
  8. private String id;
  9. @AggregateMember
  10. private List<GiftCardTransaction> transactions = new ArrayList<>();
  11. @CommandHandler
  12. public void handle(RedeemCardCommand cmd) {
  13. // 业务逻辑
  14. apply(new CardRedeemedEvent(id, cmd.getTransactionId(), cmd.getAmount()));
  15. }
  16. @EventSourcingHandler
  17. public void on(CardRedeemedEvent evt) {
  18. // 1.
  19. transactions.add(new GiftCardTransaction(evt.getTransactionId(), evt.getAmount()));
  20. }
  21. // 省略其它
  22. }
  23. public class GiftCardTransaction {
  24. @EntityId
  25. private String transactionId;
  26. private int transactionValue;
  27. private boolean reimbursed = false;
  28. public GiftCardTransaction(String transactionId, int transactionValue) {
  29. this.transactionId = transactionId;
  30. this.transactionValue = transactionValue;
  31. }
  32. @CommandHandler
  33. public void handle(ReimburseCardCommand cmd) {
  34. if (reimbursed) {
  35. throw new IllegalStateException("Transaction already reimbursed");
  36. }
  37. apply(new CardReimbursedEvent(cmd.getCardId(), transactionId, transactionValue));
  38. }
  39. @EventSourcingHandler
  40. public void on(CardReimbursedEvent event) {
  41. // 2.
  42. if (transactionId.equals(event.getTransactionId())) {
  43. reimbursed = true;
  44. }
  45. }
  46. // 省略其它
  47. }

上述有两个需要注意:

  1. 父类中的事件溯源处理器进行实体创建,因此实体中不可能像聚合根一样包含命令处理构造器。
  2. 实体中的事件溯源处理器验证接受到的事件是否属于当前实体。这是非常有必要的且可以避免事件被同类型实体实例处理的情况。

第二点可以通过@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,分别表示转发所有和都不转发。