聚合
1.介绍
2.基本组成
3.类型
总览
事件溯源
事件溯源聚合(Event Sourced Aggregate)的存储方式是通过回放构成聚合变更的事件来重建状态
对于用户来说在Axon中主要通过@EventSourcingHandler来支持
使用方式和上面的基本组成一样
状态存储
介绍
聚合也可以直接按当前状态存储。这种情况下,用于保存和加载聚合的仓库(Repository)是GenericJpaRepository。状态存储聚合的结构与事件溯源聚合略有不同
示例
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.eventhandling.EventHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.modelling.command.AggregateMember;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
// 1.标注为 JPA 实体,因聚合通过 JPA 存储
@Entity
public class GiftCard {
// 2.@Id(JPA 要求)与 @AggregateIdentifier(Axon 要求)共用,标记聚合唯一标识
@Id
@AggregateIdentifier
private String id;
// 3.子实体列表:JPA 映射(@OneToMany)+ Axon 子实体声明(@AggregateMember)
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "giftCardId") // 关联子实体的外键字段
@AggregateMember
private List<GiftCardTransaction> transactions = new ArrayList<>();
// 聚合状态:礼品卡剩余金额
private int remainingValue;
// 4. 命令处理构造函数:处理“发行礼品卡”命令(IssueCardCommand)
@CommandHandler
public GiftCard(IssueCardCommand cmd) {
// 6. 业务规则校验:金额不能小于等于 0
if (cmd.getAmount() <= 0) {
throw new IllegalArgumentException("amount <= 0");
}
// 7. 直接修改聚合状态:初始化聚合标识和剩余金额
id = cmd.getCardId();
remainingValue = cmd.getAmount();
// 5. 发布“礼品卡已发行”事件(CardIssuedEvent)
apply(new CardIssuedEvent(cmd.getCardId(), cmd.getAmount()));
}
// 命令处理器:处理“消费礼品卡”命令(RedeemCardCommand)
@CommandHandler
public void handle(RedeemCardCommand cmd) {
// 6. 业务规则校验:金额合法性、剩余金额充足性、交易ID唯一性
if (cmd.getAmount() <= 0) {
throw new IllegalArgumentException("amount <= 0");
}
if (cmd.getAmount() > remainingValue) {
throw new IllegalStateException("amount > remaining value");
}
if (transactions.stream()
.map(GiftCardTransaction::getTransactionId)
.anyMatch(cmd.getTransactionId()::equals)) {
throw new IllegalStateException("TransactionId must be unique");
}
// 7. 直接修改聚合状态:扣减剩余金额、添加消费交易记录
remainingValue -= cmd.getAmount();
transactions.add(new GiftCardTransaction(id, cmd.getTransactionId(), cmd.getAmount()));
// 5. 发布“礼品卡已消费”事件(CardRedeemedEvent)
apply(new CardRedeemedEvent(id, cmd.getTransactionId(), cmd.getAmount()));
}
// 8. 事件处理器:监听“交易已退款”事件(CardReimbursedEvent),更新聚合状态
@EventHandler
protected void on(CardReimbursedEvent event) {
this.remainingValue += event.getAmount(); // 退款:恢复剩余金额
}
// 9. 无参构造函数:JPA 强制要求,用于加载聚合时实例化
protected GiftCard() { }
}
注意
- 直接修改状态:与事件溯源聚合不同,状态存储聚合的状态修改直接在CommandHandler中完成(无需通过 @EventSourcingHandler回放事件)。校验通过后,可直接更新聚合字段或子实体
- 无参构造函数:JPA强制要求实体类提供无参构造函数,Axon加载聚合时会通过该构造函数创建空实例,再从数据库加载状态填充。若未提供,加载聚合时会抛出异常,通常将其设为protected以避免外部误调用
- 领域模型被技术细节污染:领域模型理论上来说应该和技术分离的,这么搞导致领域模型和JPA耦合了,会导致一些问题,比如被数据库细节绑架等问题
4.生命周期函数
5.多实体聚合
介绍
组成
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; // 聚合根标识符
// 标注子实体列表,告知 Axon 需扫描该字段下的消息处理器
@AggregateMember
private List<GiftCardTransaction> transactions = new ArrayList<>();
private int remainingValue; // 礼品卡剩余金额(聚合根状态)
// 省略构造函数、命令处理器(command handlers)和事件溯源处理器(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;
}
// 向聚合内其他实体暴露状态(仅内部访问,不对外)
public String getTransactionId() {
return transactionId;
}
// 子实体中的命令处理器:处理“退款命令”
@CommandHandler
public void handle(ReimburseCardCommand cmd) {
// 业务规则校验:已退款的交易不允许再次退款
if (reimbursed) {
throw new IllegalStateException("Transaction already reimbursed");
}
// 发布“交易已退款”事件(事件会关联聚合根标识符)
apply(new CardReimbursedEvent(cmd.getCardId(), transactionId, transactionValue));
}
// 省略命令处理器、事件溯源处理器和 equals/hashCode 方法
}
消息在多实体聚合中的传播机制
介绍
命令路由
- 空值与不存在的子实体:Axon仅扫描@AggregateMember标注字段的声明类型以查找@CommandHandler。若命令到达时该字段值为null,会抛出异常;若子实体是Collection/Map的一部分,且无匹配路由键的子实体,Axon会抛出IllegalStateException(表示当前聚合无法处理该命令)
- @CommandHandler唯一性:一个聚合中,同一类型的命令必须仅有一个处理器,不能在多个实体(无论是否为聚合根)上为同一命令类型标注@CommandHandler。若需根据条件路由命令到不同子实体,应由父实体(如聚合根)处理命令,再根据条件转发到目标子实体
- 运行时类型不影响扫描:字段的运行时类型无需与声明类型完全一致,但Axon仅扫描@AggregateMember标注字段的声明类型以查找@CommandHandler方法
传播己知
介绍
当使用事件溯源存储聚合时,不仅聚合根需要通过事件触发状态变更,聚合内的所有子实体也需如此。Axon开箱即支持为这种复杂聚合结构提供事件溯源能力
流程
当某个实体(包括聚合根)发布事件时,事件会按以下顺序传播并被处理:
- 首先由聚合根处理该事件(调用聚合根的@EventSourcingHandler)
- 然后事件向下冒泡,通过所有@AggregateMember标注的字段,传播到聚合内的所有子实体(调用子实体的@EventSourcingHandler)
示例
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) { /* 省略实现 */ }
// 子实体的事件溯源处理器:处理“交易已退款”事件
@EventSourcingHandler
public void on(CardReimbursedEvent event) {
// 2. 校验事件是否属于当前子实体(避免其他子实体处理该事件)
if (transactionId.equals(event.getTransactionId())) {
reimbursed = true; // 更新子实体状态:标记为已退款
}
}
// 省略 getter 和 equals/hashCode 方法
}
注意
- 子实体的创建方式:子实体的创建必须在其父实体(如聚合根)的@EventSourcingHandler中完成,子实体类不能像聚合根那样拥有命令处理构造函数(因为子实体的创建依赖父实体的状态变更,需通过事件触发)
- 子实体的事件归属校验:由于一个实体发布的事件会传播到所有同类型的子实体,因此子实体的@EventSourcingHandler必须先校验事件是否属于自己(如通过transactionId匹配)
转发模式
Axon提供三种事件转发模式:
- ForwardAll(默认):将所有事件转发给所有子实体;
- ForwardMatchingInstances:仅当事件包含与子实体@EntityId字段同名的属性时,才转发给匹配的子实体(可通过 @EntityId(routingKey) 自定义路由键,与命令路由逻辑一致)
- ForwardNone:不将任何事件转发给子实体
可通过@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<>();
// 省略构造函数、命令和事件溯源处理器
}
7.多态
介绍
在某些场景下,为聚合结构设计成多态会带来便利。多态聚合层级中的子类型会继承父聚合的@CommandHandler、@EventSourcingHandler和@CommandHandlerInterceptor。Axon会基于@AggregateIdentifier加载正确的聚合类型,并在该类型上执行命令
案例
以下是一个示例:
// 抽象父聚合
public abstract class Card {}
// 具体聚合类型:继承自 Card
public class GiftCard extends Card {}
// 子聚合类型:继承自 GiftCard
public class ClosedLoopGiftCard extends GiftCard {}
public class OpenLoopGiftCard extends GiftCard {}
public class RechargeableGiftCard extends ClosedLoopGiftCard {}
我们可以将这种结构定义为以GiftCard为父类型的多态聚合,其子类型包括ClosedLoopGiftCard、OpenLoopGiftCard和RechargeableGiftCard。如果Card类中定义了处理器,所有子聚合都会继承这些处理器
约束
注册
原始方式
多态聚合层级可通过AggregateConfigurer注册,调用AggregateConfigurer#withSubtypes(Set<Class<? extends A>>) 方法即可
需要注意的是:未显式注册的父聚合直接子类会被框架自动注册为子类型,但间接子类不会。例如,若ClosedLoopGiftCard是GiftCard的直接子类,即使不显式注册,框架也会自动将其视为GiftCard的子类型;而如果存在LimitedRechargeableGiftCard extends RechargeableGiftCard(RechargeableGiftCard是GiftCard的间接子类),框架不会自动注册LimitedRechargeableGiftCard,需显式声明
public class AxonConfig {
// 省略其他配置方法...
// 配置 GiftCard 多态聚合及其子类型
public AggregateConfigurer<GiftCard> giftCardConfigurer() {
// 定义需要显式注册的子类型集合
Set<Class<? extends GiftCard>> subtypes = new HashSet<>();
subtypes.add(OpenLoopGiftCard.class); // 直接子类,显式注册
subtypes.add(RechargeableGiftCard.class); // 间接子类(继承自 ClosedLoopGiftCard),需显式注册
// 注册父聚合类型为 GiftCard,并关联子类型
return AggregateConfigurer.defaultConfiguration(GiftCard.class)
.withSubtypes(subtypes);
}
// ...
}
// 父聚合
class GiftCard {
// 为简洁起见,省略实现
}
// 直接子聚合(GiftCard 的直接子类)
class OpenLoopGiftCard extends GiftCard {
// 为简洁起见,省略实现
}
// 间接子聚合(继承自 ClosedLoopGiftCard,而 ClosedLoopGiftCard 继承自 GiftCard)
class RechargeableGiftCard extends GiftCard {
// 为简洁起见,省略实现
}
SpringBoot中
若使用Spring框架,Axon会基于@Aggregate注解和类层级自动检测多态聚合
注意:@Aggregate注解需标注在共享父类(包含聚合标识符的类)上,同时也需标注在每个可能作为该父类实例类型的子类上。这样Spring才能正确识别整个多态聚合层级
8.从一个聚合创建另一个聚合
介绍
通常情况下,实例化新聚合的方式是:发送一个创建命令,由标注了@CommandHandler的聚合构造函数处理该命令。这类命令可能由简单的REST接口发布,或由事件处理组件作为对特定事件的响应发布。但在某些领域场景中,业务规则明确要求从一个实体创建另一个实体,此时,从父聚合中实例化子聚合,更能贴合领域模型的设计意图
适用场景
从父聚合创建子聚合的最适合场景是:创建子聚合的决策逻辑属于父聚合的上下文范围。例如,父聚合中包含驱动子聚合创建所需的状态,只有基于父聚合的当前状态(如剩余配额、权限校验结果),才能判断是否允许创建子聚合。这种场景下,将子聚合的创建逻辑放在父聚合中,能更好地封装领域规则,确保决策的一致性
代码
假设我们有一个ParentAggregate,在处理某个命令时,会触发创建ChildAggregate的逻辑。ParentAggregate的实现示例如下:
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import static org.axonframework.modelling.command.AggregateLifecycle.createNew;
public class ParentAggregate {
@AggregateIdentifier
private String parentId; // 父聚合标识符
// 父聚合的命令处理器:处理 SomeParentCommand,触发子聚合创建
@CommandHandler
public void handle(SomeParentCommand command) {
// 1. (可选)基于父聚合状态做创建决策(如校验是否有创建子聚合的权限)
validateChildCreationPermission();
// 2. 调用 AggregateLifecycle.createNew() 创建子聚合
createNew(
ChildAggregate.class, // 第一个参数:待创建子聚合的类类型
// 第二个参数:工厂方法(Callable),用于实例化子聚合(需传入构造函数所需参数)
() -> new ChildAggregate(command.getChildAggregateId())
);
}
// 省略无参构造函数、事件溯源处理器(Event Sourcing Handler)和其他命令处理器
protected ParentAggregate() { }
// 示例:创建子聚合前的权限校验(父聚合状态驱动的决策逻辑)
private void validateChildCreationPermission() {
// 假设父聚合有“剩余子聚合配额”状态,需校验配额是否充足
// if (this.remainingChildQuota <= 0) {
// throw new IllegalStateException("No remaining quota to create child aggregate");
// }
}
}
import org.axonframework.modelling.command.AggregateIdentifier;
import static org.axonframework.modelling.command.AggregateLifecycle.apply;
public class ChildAggregate {
@AggregateIdentifier
private String childId; // 子聚合标识符
// 子聚合的构造函数(非命令处理构造函数,由父聚合的工厂方法调用)
public ChildAggregate(String aggregateId) {
// 发布“子聚合已创建”事件(ChildAggregateCreatedEvent)
apply(new ChildAggregateCreatedEvent(aggregateId));
}
// 3. 事件溯源处理器:通过创建事件初始化子聚合标识符
@EventSourcingHandler
public void on(ChildAggregateCreatedEvent event) {
this.childId = event.getChildAggregateId();
}
// 省略无参构造函数、其他命令处理器和事件溯源处理器
protected ChildAggregate() { }
}
注意
- 必须发布创建事件:子聚合的构造函数中需显式调用apply()发布子聚合已创建事件(如ChildAggregateCreatedEvent)。若不发布该事件,子聚合的创建逻辑仅封装在父聚合的命令处理器中,外部组件(如投影器、其他领域服务)无法感知子聚合的创建,导致状态不一致
- 子聚合的构造函数:子聚合的构造函数无需标注@CommandHandler(因为它由父聚合的createNew()调用,而非直接处理外部命令),但需确保构造函数能接收工厂方法传入的参数(如子聚合标识符)
禁止行为
9.变更冲突
介绍
更精确地检测冲突性变更。这类冲突性变更通常发生在两个用户(几乎)同时操作相同数据的场景中
案例理解
想象一下:两个用户都查看了数据的特定版本,并且都决定对该数据进行修改。他们都会发送类似在该聚合的版本X上执行某操作的命令(其中X是聚合的预期版本)。最终,只有一个用户的变更会实际应用到预期版本上,另一个用户的变更则不会生效
我们不必在聚合被其他进程修改时简单地拒绝所有传入命令,而是可以检查用户的操作意图是否与任何未察觉的变更存在冲突
使用
要检测冲突,可在聚合的@CommandHandler方法中传入ConflictResolver类型的参数。该接口提供了detectConflicts方法,允许你定义在执行特定类型命令时,哪些事件类型会被视为冲突
只有当聚合是基于预期版本加载时,ConflictResolver才会包含潜在的冲突事件。可在命令的某个字段上使用@TargetAggregateVersion注解,以指定聚合的预期版本
如果发现了与谓词匹配的事件,会抛出异常(detectConflicts的可选第二个参数允许你自定义要抛出的异常)。如果未发现匹配事件,则处理流程正常继续
如果未调用detectConflicts方法,但存在潜在的冲突事件,@CommandHandler会处理失败。这种情况可能发生在:命令提供了预期版本,但@CommandHandler方法的参数中没有ConflictResolver时