Spring Event 玩转 DDD 领域事件

开发 架构
领域事件的落地,不仅需要强大的设计能力,还需要与之匹配的基础设施。Spring 作为最常用的框架,基于发布订阅实现了完整的一套 Event 管理机制。

1. 领域事件

领域事件是 DDD 中重要的模式之一,主要用于模型或系统间的解耦,提高系统的可扩展性和可维护性。

1.1. 什么是领域事件

领域事件是领域驱动设计(Domain-Driven Design,简称DDD)中的一个重要概念,特指在领域模型中发生的有意义的事件,是对领域模型中的重要业务动作执行结果的抽象,如订单创建、支付完成等。

在DDD中,领域事件是一种用于传递信息的机制,它使得不同领域模型之间的通信变得更加简单和灵活。通过将事件分发给相关的订阅者,可以让不同的领域模型之间实现松耦合,从而更容易扩展和维护应用程序。

领域事件通常由领域对象主动触发并发布,而事件处理器则负责订阅事件并对事件进行处理。通过事件发布和订阅机制,可以在应用程序中实现高效的事件驱动架构,从而更好地支持复杂的业务逻辑和业务流程。

说起来有点抽象,简单举个例子:
假设有一个电子商务系统,用户下单后需要生成订单并发送通知给相关人员。在领域模型中,可以定义一个 Order 领域对象,该对象可以包含多个属性,如订单号、下单时间、购买的商品信息、收货地址等等。当用户下单时,可以通过调用 Order 对象的方法来生成订单,同时也可以通过领域事件来发送通知。

具体来说,可以定义一个 OrderCreated 领域事件,用于表示订单创建完成的事件,该事件包含一些必要的属性,如订单号、下单时间、购买的商品信息、收货地址等等。当 Order 对象创建完成后,可以通过领域事件来触发发送通知的操作,比如发送邮件或短信通知相关人员。

1.2. 领域事件的应用场景

领域事件的应用创建众多,从图中可以看出:

image

领域事件可以:

  • 保证聚合间的数据一致性。当一个聚合根上的操作引发了其他聚合根的变更时,将这些变更作为领域事件发布出去,其他聚合根可以订阅这些事件并更新自己的状态,从而实现最终一致性。
  • 替换批量处理。可以作为任务的触发器,例如定时任务、异步任务,避免定时+扫描这类批量处理。
  • 实现事件源模式。将所有的领域事件全部存储下来,可以用于恢复聚合的状态,实现事件源模式;也可以用于后续的审计和调试。
  • 进行限界上下文集成。将事件从一个子域发布到另一个子域,使得这两个子域可以解耦,不用相互知道彼此的存在。

领域事件虽好,但仍需技术框架进行支持,其实 Spring 的 Event 机制就足以满足各类需求。

2. Spring 对 Event 的支持

在 Spring 中,事件的处理可以通过三种方式来实现:

  1. 基于接口的事件处理:通过实现 ApplicationListener 接口并重写 onApplicationEvent 方法来处理事件。
  2. 基于注解的事件处理:通过在方法上添加 @EventListener 或 @TransactionEventListener 注解来处理事件,可以指定事件的类型以及监听的条件等。
  3. 基于异步事件处理:通过使用 @Async 注解来异步处理事件,可以提高应用程序的响应速度。

2.1. 基于接口的事件处理

由于与 Spring 存在强耦合,现在已经很少使用,可以直接跳过。

下面是一个基于接口的事件处理的示例代码:

@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
// 处理事件
System.out.println("Received event: " + event.getMessage());
}
}
public class MyEvent {
private String message;
public MyEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
@Component
public class MyEventPublisher {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void publishEvent(String message) {
MyEvent event = new MyEvent(message);
eventPublisher.publishEvent(event);
}
}

在这个示例中,MyEvent 是一个自定义的事件类,MyEventListener 是一个实现了 ApplicationListener 接口的监听器,用于处理 MyEvent 事件,MyEventPublisher 是用于发布事件的类。

当应用程序调用 MyEventPublisher 的 publishEvent 方法时,会触发一个 MyEvent 事件,MyEventListener 中的 onApplicationEvent 方法将被自动调用,从而处理这个事件。

2.2. 基于注解的事件处理

Spring 提供 @EventListener 和 @TransactionListener 两个注解以简化对事件的处理。

2.2.1. @EventListener

Spring 的 EventListener 监听器是一种相对于传统的事件监听方式更为简洁和灵活的事件机制。与传统的事件机制不同,EventListener 不需要显示地继承特定的事件接口,而是使用注解标识需要监听的事件类型,然后通过一个单独的监听器类处理所有类型的事件。

相比之下 EventListener 的优势主要有以下几点:

  1. 更加灵活:EventListener 不依赖于任何特定的事件接口,从而使得事件处理更加灵活,可以监听和处理任意类型的事件。
  2. 更加简洁:相比传统的事件监听方式,使用 EventListener 可以避免一系列繁琐的接口定义和实现,简化了代码结构,使得开发效率更高。
  3. 更加松耦合:EventListener 将事件发布方和事件处理方分离,遵循松耦合的设计原则,提高了代码的可维护性和扩展性。
  4. 更加可测试:由于 EventListener 可以监听和处理任意类型的事件,可以通过单元测试验证其功能是否正确,从而提高了测试的可靠性。

以下是一个简单的例子:

@Component
public class MyEventListener{
@EventListener
public void onApplicationEvent(MyEvent event) {
// 处理事件
System.out.println("Received event: " + event.getMessage());
}
}
public class MyEvent {
private String message;
public MyEvent(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
@Component
public class MyEventPublisher {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void publishEvent(String message) {
MyEvent event = new MyEvent(message);
eventPublisher.publishEvent(event);
}
}

相比基于接口的事件处理,EventListener 是一种更加简洁、灵活、松耦合、可测试的事件机制,能够有效地降低开发的复杂度,提高开发效率。

2.2.2. @TransactionEventListener

在 Spring 中,TransactionEventListner 和 EventListner 都是用于处理事件的接口。不同之处在于

  1. TransactionEventListner 是在事务提交后才会触发
  2. 而 EventListner 则是在事件发布后就会触发。

具体来说,在使用 Spring 的声明式事务时,可以在事务提交后触发某些事件。这就是 TransactionEventListner 的应用场景。而 EventListner 则不涉及事务,可以用于在事件发布后触发一些操作。

下面是一个简单的示例,演示了如何使用 TransactionEventListner 和 EventListner:

@Component
public class MyEventListener {
@EventListener
public void handleMyEvent(MyEvent event) {
// 处理 MyEvent
}
@TransactionalEventListener
public void handleMyTransactionalEvent(MyTransactionalEvent event) {
// 处理 MyTransactionalEvent
}
}
@Service
public class MyService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private MyRepository myRepository;
@Transactional
public void doSomething() {
// 做一些事情
MyEntity entity = myRepository.findById(1L);
// 发布事件
eventPublisher.publishEvent(new MyEvent(this, entity));
// 发布事务事件
eventPublisher.publishEvent(new MyTransactionalEvent(this, entity));
}
}

在这个例子中,MyEventListener 类定义了两个方法,handleMyEvent 和
handleMyTransactionalEvent,分别处理 MyEvent 和 MyTransactionalEvent 事件。其中,handleMyTransactionalEvent 方法用 @TransactionalEventListener 注解标记,表示它只会在事务提交后触发。

MyService 类中的 doSomething 方法使用 ApplicationEventPublisher 来发布事件。注意,它发布了两种不同类型的事件:MyEvent 和 MyTransactionalEvent。这两个事件会分别触发 MyEventListener 中的对应方法。

总的来说,Spring 的事件机制非常灵活,可以方便地扩展应用程序的功能。TransactionEventListner 和 EventListner 这两个接口的应用场景有所不同,可以根据实际需求选择使用。

2.3.基于异步事件处理

@Async是Spring框架中的一个注解,用于将一个方法标记为异步执行。使用该注解,Spring将自动为该方法创建一个新线程,使其在后台异步执行,不会阻塞主线程的执行。

在具体应用中,使用@Async可以大大提升应用的并发处理能力,使得系统能够更快地响应用户请求,提高系统的吞吐量。

@Async 和 @EventListener 或 @TransactionEventListener 注解在一起使用时,会产生异步的事件处理器。使用这种组合的方式,事件处理器会在单独的线程池中执行,以避免阻塞主线程。这种方式在需要处理大量事件或者事件处理器耗时较长的情况下非常有用,可以有效提高应用的性能和可伸缩性。同时,Spring 框架对这种方式也提供了完善的支持,可以方便地使用这种方式来实现异步事件处理。

下面是一个简单的示例代码,演示了如何在 Spring 中使用 @Async 和 @EventListener 一起实现异步事件处理:

@Component
public class ExampleEventListener {
@Async
@EventListener
public void handleExampleEvent(ExampleEvent event) {
// 在新的线程中执行异步逻辑
// ...
}
}

在这个示例中,ExampleEventListener 类中的 handleExampleEvent 方法使用了 @Async 和 @EventListener 注解,表示这个方法是一个异步事件监听器。当一个 ExampleEvent 事件被触发时,这个方法会被异步地执行。在这个方法中,可以执行任何异步的逻辑处理,比如向队列发送消息、调用其他服务等。

备注:在使用 @Async 时,需要根据业务场景对线程池进行自定义,以免出现资源不够的情况(Spring 默认使用单线程处理@Async异步任务)

4. 场景分析

综上所述,当领域事件发出来之后,不同的注解会产生不同的行为,简单汇总如下:


@EventListener

@TransactionEventListener

无 @Async

顺序、同步执行

事务提交后、同步执行

有 @Async

顺序、异步执行

事务提交后、异步执行

4.1. @EventListener

特点:

  1. 顺序执行。调用 publish(Event) 后,自动触发对 @EventListner 注释方法的调用
  2. 同步执行。使用主线程执行,方法抛出异常会中断调用链路,会触发事务的回归

应用场景:

  1. 事务消息表。在同一事务中完成对业务数据和消息表的修改
  2. 业务验证。对业务对象进行最后一次验证,如果验证不通过直接抛出异常中断数据库事务
  3. 业务插件。在当前线程和事务中执行插件完成业务扩展

4.2. @TransactionEventListener

image

特点:

  1. 事务提交后执行。调用 publish(Event) 时,只是向上下文中注册了一个回调器,并不会立即执行;只有在事务提交后,才会触发对 @TransactionEventListner 注释方法的调用
  2. 同步执行。使用主线程执行,方法抛出异常会中断调用链路,当不会回归事务(事务已提交,没有办法进行回归)

应用场景:

  1. 数据同步。事务提交后,将变更同步到 ES 或 Cache
  2. 记录审计日志。只有在业务变更成功更新到数据库时才进行记录

备注:
@TransactionEventLisnter 必须在事务上下文中,脱离上下文,调用不会生效

4.3. @EventListener + @Async

特点:

  1. 顺序执行。调用 publish(Event) 后,自动触发对 @EventListner 注释方法的调用
  2. 异步执行。使用独立的线程池执行任务,方法抛出异常对主流程没有任何影响

应用场景:

  1. 记日志明细日志,辅助排查问题

4.4. @TransactionEventListener + @Async

特点:

  • 事务提交后执行。调用 publish(Event) 时,只是向上下文中注册了一个回调器,并不会立即执行;只有在事务提交后,才会触发对 @TransactionEventListner 注释方法的调用
  • 异步执行。使用独立的线程池执行任务,方法抛出异常对主流程没有任何影响

应用场景:
异步处理。记录操作日志,异步保存数据等
备注:
@TransactionEventLisnter 必须在事务上下文中,脱离上下文,调用不会生效

5. 小结

领域事件的落地,不仅需要强大的设计能力,还需要与之匹配的基础设施。Spring 作为最常用的框架,基于发布订阅实现了完整的一套 Event 管理机制。工具在手是否能根据业务场景选择合适的解决方案就成了研发的职责,简单思考以下组合适用场景是什么:

  • @EventListener
  • @TransactionEventListener
  • @EventListener + @Async
  • @TransactionEventListener + @Async
责任编辑:武晓燕 来源: 今日头条
相关推荐

2023-02-19 12:44:07

领域事件DDD

2021-09-08 09:22:23

领域驱动设计

2023-09-26 01:18:55

解密系统业务

2011-07-04 14:50:49

QT Event 事件

2017-11-17 05:39:27

DDD建模模型

2023-02-20 14:44:22

DDD领域模型

2023-02-15 13:50:58

DDD战略设计

2021-10-09 11:54:46

DDD微服务业务

2012-10-12 14:28:32

BYOD安全网络

2017-07-14 10:55:05

2020-09-02 08:12:05

CodeDDD代码

2016-09-06 21:23:25

JavaScriptnode异步

2023-01-09 09:00:00

树服务架构驱动决策

2014-09-26 10:00:25

驱动设计DDD领域

2024-11-08 08:37:25

2024-11-27 15:33:17

软件架构DDD

2022-04-19 08:15:53

DDD领域建模实战

2011-08-29 14:59:26

QtEvent事件

2020-03-18 13:28:29

SpringDDDWeb

2023-04-28 15:20:37

JavaScript事件循环
点赞
收藏

51CTO技术栈公众号