从 Spring 的角度来看,域事件只是另一个应用程序事件,可以使用内置的ApplicationEventPublisher. 换句话说,我们不需要担心构建事件总线或其他一些基础设施来发布域事件:您将事件发布者注入到您的域服务中并发布事件。但是,在大多数情况下,您希望直接从聚合中发布域事件,而不必为此目的通过域服务。幸运的是,我们可以做到这一点。

Spring Data 提供了一种机制,可以直接从聚合中发布域事件,而无需获取事件发布者。我们在研究时已经触及了这种机制BaseAggregateRoot,现在我们将仔细研究它。

在后台,Spring Boot 将为所有名称以save开头的存储库方法注册一个方法拦截器,例如saveand saveAndFlush。此拦截器将在您的聚合中查找两种方法:一种带有注释的方法@DomainEvents,另一种带有注释的方法@AfterDomainEventPublication

带有注释的方法@DomainEvents预计会返回要发布的事件列表。拦截器将使用 Spring 应用程序事件发布者发布这些事件。@AfterDomainEventPublication发布事件后,将调用带有注释的方法。此方法有望清除事件列表,以防止在下次保存聚合时再次发布它们。

以这种方式设计和发布域事件时要记住一个警告,它与包含对聚合根本身的引用的事件有关,例如:

public class PotentiallyProblematicDomainEvent {
    private final MyAggregate myAggregate;

    public PotentiallyProblematicDomainEvent(@NotNull MyAggregate myAggregate) {
        this.myAggregate = myAggregate;
    }

    public @NotNull MyAggregate getMyAggregate() {
        return myAggregate;
    }
}

每当您设计这样的事件时,您必须了解 Spring Data 和 JPA 如何在后台工作。

当您保存现有实体时(这里我说的是 JPA 实体概念,而不是 DDD 实体概念),Spring Data 最终会调用EntityManager.merge. 如果实体被分离,JPA 将检索托管实体,将所有属性从分离实体复制到托管实体,保存并返回。托管实体将增加其乐观锁定版本,而分离实体保持不变。

但是,由于域事件是在分离实体上注册的,因此域事件侦听器将获得对分离实体的引用。如果侦听器尝试直接在实体上执行任何操作然后保存它,这可能会导致乐观锁定错误。

以下是一些侦听器最终会收到带有不正确乐观锁定版本的陈旧实体的示例:

public class PotentiallyProblematicApplicationService {

    @Transactional
    public void firstProblematicMethod(@NotNull MyAggregate aggregate) { // <1>
        aggregate.performAnOperationThatRegistersAProblematicDomainEvent();
        myAggregateRepository.saveAndFlush(aggregate);
    }

    public void secondProblematicMethod(@NotNull MyAggregateId aggregateId) { // <2>
        var aggregate = myAggregateRepository.getById(aggregateId);
        aggregate.performAnOperationThatRegistersAProblematicDomainEvent();
        myAggregateRepository.saveAndFlush(aggregate);
    }
}
  1. 此方法接受聚合作为参数并直接对其执行操作。这意味着聚合是分离的,一旦保存就会变得陈旧。
  2. 此方法接受聚合 ID 作为参数,并在对其执行操作之前查找聚合。但是,该方法并不@Transactional意味着对存储库的两个调用都将在它们自己的事务中运行,从而在它们之间分离聚合。

现在我们如何解决这个问题?第二种方法很容易修复:只需制作整个方法@Transactional。这样,在保存聚合时仍将对其进行管理,并且域事件侦听器将获得正确的实例。

但是第一种方法呢?一个明显的解决方案是在事件中使用聚合 ID 而不是聚合本身。但是,这有其自身的问题:如果事件在聚合被持久化之前注册,聚合没有 ID。如果您从未从非持久聚合中发布任何事件,这不是问题。但是,如果你这样做了,你可以像这样修复它:

public class SaferDomainEvent { 
    private final MyAggregate myAggregate;

    public SaferDomainEvent(@NotNull MyAggregate myAggregate) { // <1>
        this.myAggregate = myAggregate;
    }

    public @NotNull MyAggregateId getMyAggregateId() { // <2>
        return myAggregate.getIdentifier();
    }
}
  1. 我们仍然在事件中存储对聚合的引用……
  2. …但我们只将其 ID 暴露给外界,如果他们想对它做任何事情,则强制任何侦听器从存储库中获取聚合的新副本。

这又是一个底层技术悄悄潜入你的领域模型的例子——你选择的持久性技术甚至会影响你的领域事件的设计。幸运的是,在这种情况下,这没什么大不了的,但是如果您将早期设计建立在后来被证明是不正确的假设上,它仍然可能会在以后回来并咬您一口(我一直在那里做过,尤其是当涉及到 JPA 时)。底线是您需要充分了解您的工具——不仅要了解如何使用它们,还要了解它们是如何工作的。