1. Overview
1.概述
In this tutorial, we’ll explain how to use @DomainEvents annotation and AbstractAggregateRoot class to conveniently publish and handle domain events produced by aggregate – one of the key tactical design patterns in Domain-driven design.
在本教程中,我们将解释如何使用@DomainEvents注解和AbstractAggregateRoot类来方便地发布和处理由聚合产生的领域事件–领域驱动设计中的关键战术设计模式之一。
Aggregates accept business commands, which usually results in producing an event related to the business domain – the Domain Event.
集合体接受业务指令,这通常会产生一个与业务领域相关的事件–领域事件。
If you’d like to learn more about DDD and aggregates, it’s best to start with Eric Evans’ original book. There’s also a great series about effective aggregate design written by Vaughn Vernon. Definitely worth reading.
如果您想进一步了解DDD和聚合,最好从Eric Evans的原书开始。还有Vaughn Vernon撰写的关于有效聚合设计的系列文章也很不错。绝对值得一读。
It can be cumbersome to manually work with domain events. Thankfully, Spring Framework allows us to easily publish and handle domain events when working with aggregate roots using data repositories.
手动处理域事件可能是很麻烦的。值得庆幸的是,Spring框架允许我们在使用数据存储库处理聚合根时轻松发布和处理域事件。
2. Maven Dependencies
2.Maven的依赖性
Spring Data introduced @DomainEvents in Ingalls release train. It’s available for any kind of repository.
Spring Data在Ingalls发布的培训中引入了@DomainEvents。它可用于任何种类的存储库。
Code samples provided for this article use Spring Data JPA. The simplest way to integrate Spring domain events with our project is to use the Spring Boot Data JPA Starter:
本文提供的代码样本使用Spring Data JPA。将Spring域事件与我们的项目集成的最简单方法是使用Spring Boot Data JPA Starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
3. Publish Events Manually
3.手动发布事件
First, let’s try to publish domain events manually. We’ll explain the @DomainEvents usage in the next section.
首先,让我们试着手动发布域事件。我们将在下一节解释@DomainEvents的用法。
For the needs of this article, we’ll use an empty marker class for domain events – the DomainEvent.
为了本文的需要,我们将使用一个空的标记类来处理领域事件 – DomainEvent。
We’re going to use standard ApplicationEventPublisher interface.
我们将使用标准的ApplicationEventPublisher接口。
There’re two good places where we can publish events: service layer or directly inside the aggregate.
我们有两个可以发布事件的好地方:服务层或直接在聚合内部。
3.1. Service Layer
3.1 服务层
We can simply publish events after calling the repository save method inside a service method.
我们可以在调用服务方法内的存储库save方法后简单地发布事件。
If a service method is part of a transaction and we handle the events inside the listener annotated with @TransactionalEventListener, then events will be handled only after the transaction commits successfully.
如果一个服务方法是事务的一部分,并且我们在用@TransactionalEventListener注解的监听器内处理事件,那么只有在事务提交成功后才会处理事件。
Therefore, there’s no risk of having “fake” events handled when the transaction is rolled back and the aggregate isn’t updated:
因此,当事务回滚时,不会有处理 “假 “事件的风险,而聚合体也不会被更新。
@Service
public class DomainService {
// ...
@Transactional
public void serviceDomainOperation(long entityId) {
repository.findById(entityId)
.ifPresent(entity -> {
entity.domainOperation();
repository.save(entity);
eventPublisher.publishEvent(new DomainEvent());
});
}
}
Here’s a test that proves events are indeed published by serviceDomainOperation:
这里有一个测试,证明事件确实是由服务DomainOperation发布的。
@DisplayName("given existing aggregate,"
+ " when do domain operation on service,"
+ " then domain event is published")
@Test
void serviceEventsTest() {
Aggregate existingDomainEntity = new Aggregate(1, eventPublisher);
repository.save(existingDomainEntity);
// when
domainService.serviceDomainOperation(existingDomainEntity.getId());
// then
verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}
3.2. Aggregate
3.2.集合体
We can also publish events directly from within the aggregate.
我们也可以直接从聚合体中发布事件。
This way we manage the creation of domain events inside the class which feels more natural for this:
这样,我们在类内管理领域事件的创建,感觉更自然。
@Entity
class Aggregate {
// ...
void domainOperation() {
// some business logic
if (eventPublisher != null) {
eventPublisher.publishEvent(new DomainEvent());
}
}
}
Unfortunately, this might not work as expected because of how Spring Data initializes entities from repositories.
不幸的是,由于Spring Data是如何从存储库初始化实体的,这可能不会像预期的那样工作。
Here’s the corresponding test that shows the real behavior:
下面是显示真实行为的相应测试。
@DisplayName("given existing aggregate,"
+ " when do domain operation directly on aggregate,"
+ " then domain event is NOT published")
@Test
void aggregateEventsTest() {
Aggregate existingDomainEntity = new Aggregate(0, eventPublisher);
repository.save(existingDomainEntity);
// when
repository.findById(existingDomainEntity.getId())
.get()
.domainOperation();
// then
verifyNoInteractions(eventHandler);
}
As we can see, the event isn’t published at all. Having dependencies inside the aggregate might not be a great idea. In this example, ApplicationEventPublisher is not initialized automatically by Spring Data.
我们可以看到,事件根本没有被发布。在聚合内部有依赖关系可能不是一个好主意。在这个例子中,ApplicationEventPublisher没有被Spring Data自动初始化。
The aggregate is constructed by invoking the default constructor. To make it behave as we would expect, we’d need to manually recreate entities (e.g. using custom factories or aspect programming).
聚合体是通过调用默认的构造函数来构建的。为了使它的行为符合我们的预期,我们需要手动重新创建实体(例如,使用自定义工厂或方面编程)。
Also, we should avoid publishing events immediately after the aggregate method finishes. At least, unless we are 100% sure this method is part of a transaction. Otherwise, we might have “spurious” events published when change is not yet persisted. This might lead to inconsistencies in the system.
另外,我们应该避免在聚合方法完成后立即发布事件。至少,除非我们100%确定这个方法是一个事务的一部分。否则,当变化尚未持久化时,我们可能会有 “虚假的 “事件被发布。这可能会导致系统中的不一致。
If we want to avoid this, we must remember to always call aggregate methods inside a transaction. Unfortunately, this way we couple our design heavily to the persistence technology. We need to remember that we don’t always work with transactional systems.
如果我们想避免这种情况,我们必须记住总是在一个事务中调用聚合方法。不幸的是,这种方式使我们的设计与持久化技术严重耦合。我们需要记住,我们并不总是与事务性系统一起工作。
Therefore, it’s generally a better idea to let our aggregate simply manage a collection of domain events and return them when it’s about to get persisted.
因此,一般来说,让我们的聚合体简单地管理一个领域事件的集合,并在即将被持久化时返回这些事件,是一个更好的主意。
In the next section, we’ll explain how we can make domain events publishing more manageable by using @DomainEvents and @AfterDomainEvents annotations.
在下一节中,我们将解释如何通过使用@DomainEvents和@AfterDomainEvents注解使域事件的发布更易于管理。
4. Publish Events Using @DomainEvents
4.使用@DomainEvents发布事件
Since Spring Data Ingalls release train we can use the @DomainEvents annotation to automatically publish domain events.
自Spring Data Ingalls发布培训以来,我们可以使用@DomainEvents注解来自动发布领域事件。
A method annotated with @DomainEvents is automatically invoked by Spring Data whenever an entity is saved using the right repository.
每当使用正确的存储库保存实体时,Spring Data就会自动调用一个用@DomainEvents注解的方法。
Then, events returned by this method are published using the ApplicationEventPublisher interface:
然后,使用ApplicationEventPublisher接口发布该方法返回的事件。
@Entity
public class Aggregate2 {
@Transient
private final Collection<DomainEvent> domainEvents;
// ...
public void domainOperation() {
// some domain operation
domainEvents.add(new DomainEvent());
}
@DomainEvents
public Collection<DomainEvent> events() {
return domainEvents;
}
}
Here’s the example explaining this behavior:
下面是解释这种行为的例子。
@DisplayName("given aggregate with @DomainEvents,"
+ " when do domain operation and save,"
+ " then event is published")
@Test
void domainEvents() {
// given
Aggregate2 aggregate = new Aggregate2();
// when
aggregate.domainOperation();
repository.save(aggregate);
// then
verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}
After domain events are published, the method annotated with @AfterDomainEventsPublication is called.
在域事件被发布后,用@AfterDomainEventsPublication注释的方法被调用。
The purpose of this method is usually to clear the list of all events, so they aren’t published again in the future:
这个方法的目的通常是清除列表中的所有事件,所以它们在未来不会被再次发布。
@AfterDomainEventPublication
public void clearEvents() {
domainEvents.clear();
}
Let’s add this method to the Aggregate2 class and see how it works:
让我们把这个方法添加到Aggregate2类中,看看它是如何工作的。
@DisplayName("given aggregate with @AfterDomainEventPublication,"
+ " when do domain operation and save twice,"
+ " then an event is published only for the first time")
@Test
void afterDomainEvents() {
// given
Aggregate2 aggregate = new Aggregate2();
// when
aggregate.domainOperation();
repository.save(aggregate);
repository.save(aggregate);
// then
verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}
We clearly see that event is published only for the first time. If we removed the @AfterDomainEventPublication annotation from the clearEvents method, then the same event would be published for the second time.
我们清楚地看到,事件只发布了第一次。如果我们从clearEvents方法中移除@AfterDomainEventPublication注解,那么同一事件将被第二次发布。
However, it’s up to the implementor what would actually happen. Spring only guarantees to call this method – nothing more.
然而,实际会发生什么,取决于实现者。Spring只保证调用这个方法–仅此而已。
5. Use AbstractAggregateRoot Template
5.使用AbstractAggregateRoot模板
It’s possible to further simplify publishing of domain events thanks to the AbstractAggregateRoot template class. All we have to do is to call register method when we want to add the new domain event to the collection of events:
由于有了AbstractAggregateRoot模板类,可以进一步简化域事件的发布。我们所要做的就是在我们想把新的域事件添加到事件集合中时调用register方法。
@Entity
public class Aggregate3 extends AbstractAggregateRoot<Aggregate3> {
// ...
public void domainOperation() {
// some domain operation
registerEvent(new DomainEvent());
}
}
This is a counterpart to the example shown in the previous section.
这是与上一节所示的例子相对应的。
Just to make sure everything works as expected – here are the tests:
只是为了确保一切按预期工作–这里是测试。
@DisplayName("given aggregate extending AbstractAggregateRoot,"
+ " when do domain operation and save twice,"
+ " then an event is published only for the first time")
@Test
void afterDomainEvents() {
// given
Aggregate3 aggregate = new Aggregate3();
// when
aggregate.domainOperation();
repository.save(aggregate);
repository.save(aggregate);
// then
verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}
@DisplayName("given aggregate extending AbstractAggregateRoot,"
+ " when do domain operation and save,"
+ " then an event is published")
@Test
void domainEvents() {
// given
Aggregate3 aggregate = new Aggregate3();
// when
aggregate.domainOperation();
repository.save(aggregate);
// then
verify(eventHandler, times(1)).handleEvent(any(DomainEvent.class));
}
As we can see, we can produce a lot less code and achieve exactly the same effect.
正如我们所看到的,我们可以产生更少的代码而达到完全相同的效果。
6. Implementation Caveats
6.实施注意事项
While it might look like a great idea to use the @DomainEvents feature at first, there are some pitfalls we need to be aware of.
虽然一开始使用@DomainEvents功能可能看起来是个好主意,但有一些陷阱我们需要注意。
6.1. Unpublished Events
6.1.未公布的事件
When working with JPA we don’t necessarily call save method when we want to persist the changes.
当我们使用JPA工作时,当我们想持久化变化时,不一定要调用save方法。
If our code is part of a transaction (e.g. annotated with @Transactional) and makes changes to the existing entity, then we usually simply let the transaction commit without explicitly calling the save method on a repository. So, even if our aggregate produced new domain events they will never get published.
如果我们的代码是事务的一部分(例如用@Transactional注解),并对现有实体进行修改,那么我们通常只是让事务提交,而不明确调用存储库的save方法。所以,即使我们的聚合产生了新的领域事件,它们也不会被发布。
We need also remember that @DomainEvents feature works only when using Spring Data repositories. This might be an important design factor.
我们还需要记住,@DomainEvents功能仅在使用Spring Data存储库时才会起作用。这可能是一个重要的设计因素。
6.2. Lost Events
6.2.丢失的事件
If an exception occurs during events publication, the listeners will simply never get notified.
如果在事件发布过程中发生异常,监听器将根本无法得到通知。
Even if we could somehow guarantee notification of event listeners, currently there’s no backpressure to let publishers know something went wrong. If event listener gets interrupted by an exception, the event will remain unconsumed and it will never be published again.
即使我们能以某种方式保证事件监听器的通知,目前也没有任何反压力来让发布者知道出了问题。如果事件监听器被异常打断,事件将保持未被吸收,它将永远不会被发布。
This design flaw is known to the Spring dev team. One of the lead developers even suggested a possible solution to this problem.
这个设计缺陷是Spring开发团队所知道的。其中一位主要开发人员甚至提出了一个可能的解决这个问题的方法。
6.3. Local Context
6.3.地方背景
Domain events are published using a simple ApplicationEventPublisher interface.
领域事件使用一个简单的ApplicationEventPublisher接口发布。
By default, when using ApplicationEventPublisher, events are published and consumed in the same thread. Everything happens in the same container.
默认情况下,当使用ApplicationEventPublisher时,事件在同一个线程中被发布和消费。一切都发生在同一个容器中。
Usually, we want to send events through some kind of message broker, so the other distributed clients/systems get notified. In such case, we’d need to manually forward events to the message broker.
通常情况下,我们希望通过某种消息代理来发送事件,这样其他分布式客户端/系统就会得到通知。在这种情况下,我们需要手动转发事件到消息代理。
It’s also possible to use Spring Integration or third-party solutions, such as Apache Camel.
也可以使用Spring Integration或第三方解决方案,例如Apache Camel。
7. Conclusion
7.结论
In this article, we’ve learned how to manage aggregate domain events using @DomainEvents annotation.
在这篇文章中,我们已经学习了如何使用@DomainEvents注解来管理聚合域事件。
This approach can greatly simplify events infrastructure so we can focus only on the domain logic. We just need to be aware that there’s no silver bullet and the way Spring handles domain events is not an exception.
这种方法可以大大简化事件基础架构,因此我们可以只关注领域逻辑。我们只需要意识到,没有银弹,Spring处理领域事件的方式也不例外。
The full source code of all the examples is available over on GitHub.
所有例子的完整源代码都可以在GitHub上找到,。