Spring Boot - 事件驱动
# 1. 事件驱动
事件驱动即跟随当前时间点上出现的时间,调动可用资源,执行相关任务,使不断出现的问题得以解决,防止事务堆积。
这些事件可以是用户的行为(如点击按钮、键入文字)、系统的信号(如定时器超时、硬件中断)或是其他程序的消息(如网络请求到达、数据库操作完成)。
在解决上述问题时,应用程序是由「时间」驱动运行的,这类程序在编写时往往可以采用相同的模型实现,我们可以将这种编程模型称为事件驱动模型。
# 2. 事件驱动模型
事件驱动模型有很多种体现形式,如简单的事件触发机制、单线程异步任务、多线程异步任务等,但是各种技术中实现事件驱动模型的思路基本相同。事件驱动模型包括四个(三个)基本要素:事件、事件消费方、事件生产方。
- 事件:描述发生的事情。比如说浏览器页面点击事件,鼠标、键盘输入事件,spring 请求处理完成、spring 容器刷新完毕等。
- 事件生产方(事件源):事件的生产方。任何一个事件都必须有一个事件源。比如 input、button、spring 中请求处理完毕的事件源就是 DispacherServlet、spring 容器刷新完毕的事件源 就是 ApplicationContext。
- 事件管理器(时间广播器):派发事件。事件和事件监听的桥梁、负责把事件通知给事件监听器(可在事件源中实现)
- 事件消费方(事件监听器):处理事件。监听事件的发生、可以再监听器中做一些处理。
# 3. 解决的问题
事件驱动模型通过对事件的生成、处理、监听和响应的方式,不仅实现了应用程序对于外部动作的实时响应能力,还有效地解决了复杂系统开发和维护中遇到的一系列问题。下面是事件驱动模型在实践中解决问题的一些关键领域:
# 3.1 实现组件之间的松耦合和解耦
- 背景: 在复杂的应用或系统中,组件间高度耦合会导致维护难度增加,一个模块的变更可能引发连锁反应,影响到其他模块。
- 事件驱动解决方案: 通过将组件间的直接交互替换为基于事件的通信,每个组件只需关注它感兴趣的事件即可,而无需知道这些事件的具体生产者。这样,即使某个组件发生变化,也不会直接影响到依赖于相同事件的其他组件,从而实现了组件间的松耦合和解耦。
# 3.2 实现异步任务处理
- 背景: 在需要处理耗时任务的场景中,同步执行可能会阻塞整个应用的处理流程,导致用户体验下降。
- 事件驱动解决方案: 通过将耗时任务封装成事件,并异步处理这些事件,可以让主处理流程继续进行,而不必等待耗时任务完成。这种方式提高了应用的响应性和吞吐量,同时也便于实现任务的并发处理。
# 3.3 跟踪状态变化
- 背景: 在需要追踪对象状态变更历史的业务场景中,直接在业务逻辑中处理状态跟踪会增加复杂性和耦合度。
- 事件驱动解决方案: 每次对象状态发生变化时,都生成一个事件,并将这个事件记录下来。这不仅可以为业务流程提供必要的历史数据,而且可以通过事件的处理逻辑来实现复杂的状态管理和恢复机制,无需在主业务逻辑中直接处理状态跟踪。
# 3.4 限流和消峰
- 背景: 在面对高并发请求时,系统可能因为瞬时的负载超过处理能力而导致性能问题或服务不可用。
- 事件驱动解决方案: 通过事件队列和事件处理机制,可以对进入系统的请求进行缓冲和调度,实现对请求流量的控制。在高负载情况下,可以通过调整事件处理速率或暂时缓存事件来避免系统过载,从而实现限流和消峰的目的。
总结
事件驱动模型通过对事件的有效管理和灵活应用,不仅提高了系统的可维护性和扩展性,还增强了应用的性能和稳定性。这种模型特别适合于需要快速响应外部事件、处理大量异步任务、追踪复杂状态变化或面对高并发场景的应用和服务。
# 4. 观察者模型
观察者模式是一种对象行为模式。它定义对象间的一种一对多的依赖关系(被观察者维护观察者列表),当一个对象的状态发生改变时,列表中所有观察者都会接收到状态改变的通知,观察者把自己注册到被观察者持有的列表中,当被观察者发布通知,也就是有事件触发时,由被观察者轮询调用观察者的处理代码。
# 5. 发布、订阅模型
发布订阅模式其实是对象间的一对多的依赖关系(利用消息管理器),当一个对象的状态发生改变时,所有依赖于它的对象都得到状态改变的通知,订阅者通过调度中心订阅自己关注的事件,当发布者发布事件到调度中心,也就是该事件触发时,由调度中心统一调度订阅者的处理代码。
# 6. Spring 事件驱动
经过上面的介绍,实际我们可以看事件驱动看作 MQ,而在 Spring Boot 的事件驱动,它类似于单机版 MQ。
如果想了解 Spring Boot 更多的内置事件,请看:Spring Boot - 生命周期与事件。
# 6.1 为什么需要事件驱动
简介: 在项目实际开发过程中,我们有很多这样的业务场景:一个事务中处理完一个业务逻辑后需要跟着处理另外一个业务逻辑,伪码大致如下:
@Service
public class ProductServiceImpl {
public void saveProduct(Product product) {
// 1. 保存订单
productMapper.saveOrder(product);
// 2. 发送通知
notifyService.notify(product);
}
}
2
3
4
5
6
7
8
9
很简单并且很常见的一段业务逻辑:首先将产品先保存数据库,然后发送通知。
某一天你们可能需要把新增的产品存到 ES 数据库中,这时候也需要代码可能变成这样:
@Service
public class ProductServiceImpl {
public void saveProduct(Product product) {
// 1. 保存订单
productMapper.saveProduct(product);
// 保存到 ES 数据库
esService.saveProduct(product)
// 2. 发送通知
notifyService.notify(product);
}
}
2
3
4
5
6
7
8
9
10
11
随着业务需求的变化,代码也需要跟着一遍遍的修改。而且还会存在另外一个问题,如果通知系统挂了,那就不能再新增产品了,这也不符合设计模式的开闭原则。
对于上面这种情况非常适合引入消息中间件(消息队列)来对业务进行解耦,但并非所有的业务系统都会引入消息中间件(引入会第三方架构组件会带来很大的运维成本)。
Spring 提供了事件驱动机制可以帮助我们实现这一需求,引入Spring事件驱动机制后,代码可以重构为:
@Service
public class ProductServiceImpl {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void saveProduct(Product product) {
// 1. 保存产品
productMapper.saveProduct(product);
// 发布事件,异步处理其他逻辑
eventPublisher.publishEvent(new ProductCreatedEvent(product));
}
}
// 定义一个事件
public class ProductCreatedEvent extends ApplicationEvent {
public ProductCreatedEvent(Product product) {
super(product);
}
}
// 定义事件的监听器
@Component
public class ProductEventListener {
@EventListener
public void onProductCreated(ProductCreatedEvent event) {
// 处理事件,如保存到ES数据库、发送通知等
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
通过这种方式,当产品保存逻辑发生变化时,例如需要添加新的业务处理流程,只需添加相应的事件监听器即可,无需修改原有的saveProduct
方法,极大地提高了代码的扩展性和维护性。
# 6.2 Spring 事件驱动
Spring框架通过提供一套事件驱动模型,支持在应用程序中进行基于事件的编程。这种模型使得应用程序各个部分之间可以通过事件来进行通信,而不是直接调用对方的方法,从而达到解耦合的目的。在Spring事件驱动模型中,主要涉及到三个核心组成部分:ApplicationEvent
、ApplicationEventPublisher
和ApplicationListener
。
- ApplicationEvent:表示事件本身,自定义事件需要继承该类,用来定义事件
- ApplicationEventPublisher:事件发送器,主要用来发布事件
- ApplicationListener:事件监听器接口,监听类实现 ApplicationListener 里 onApplicationEvent 方法即可,也可以在方法上增加@EventListener以实现事件监听
# 6.3 实现Spring事件驱动的步骤
# 1. 自定义事件类
首先,需要定义一个事件类来表示你想要通知的事件。这个类需要继承自ApplicationEvent
类。通过扩展ApplicationEvent
,你可以将任何需要的信息作为事件的一部分进行传递。
public class CustomEvent extends ApplicationEvent {
public CustomEvent(Object source) {
super(source);
}
// 可以添加额外的方法和属性来传递事件相关的信息
}
2
3
4
5
6
# 2. 发布事件
事件的发布是通过ApplicationEventPublisher
接口实现的。在Spring管理的Bean中,你可以注入这个接口的实例,并使用它来发布事件。
@Component
public class CustomEventPublisher {
@Autowired
private ApplicationEventPublisher publisher;
public void publishEvent(final String message) {
publisher.publishEvent(new CustomEvent(message));
}
}
2
3
4
5
6
7
8
9
# 3. 监听事件
对事件的监听可以通过实现ApplicationListener
接口,或者更简便地使用@EventListener
注解来完成。监听器会接收到事件并进行处理。
@Component
public class CustomEventListener implements ApplicationListener<CustomEvent> {
@Override
public void onApplicationEvent(CustomEvent event) {
System.out.println("Received custom event - " + event.getSource());
}
}
2
3
4
5
6
7
或者使用@EventListener
注解:
@Component
public class CustomEventListener {
@EventListener
public void handleCustomEvent(CustomEvent event) {
System.out.println("Received custom event - " + event.getSource());
}
}
2
3
4
5
6
7
# 4. 异步事件处理
默认情况下,Spring中的事件是同步发布的,意味着事件发布者会在所有监听器处理完事件之前阻塞。如果你想要异步处理事件,可以在@EventListener
注解的方法上添加@Async
注解。但是,请注意,为了使用@Async
,你需要在配置中启用异步处理。
@EnableAsync
@Configuration
public class AsyncConfig {
}
@Component
public class CustomEventListener {
@Async
@EventListener
public void handleCustomEvent(CustomEvent event) {
// 异步处理事件
}
}
2
3
4
5
6
7
8
9
10
11
12
13
使用@Async
注解可以让事件的处理在不同的线程中进行,这样事件的发布者就不会被阻塞,可以继续执行其他的任务。这对于提高应用程序的响应性和吞吐量非常有帮助。
通过这种方式,Spring应用可以灵活地实现事件驱动的架构,以支持更加松耦合的组件间通信和异步处理能力。
# 6.4 使用事件驱动
Spring框架的事件驱动模型为应用内的组件间通信提供了一种灵活而强大的机制。以下是实现Spring事件驱动的详细步骤,以用户登录成功为例:
# 1. 定义事件类
首先,我们需要定义一个事件类来表示特定的业务动作——在这个案例中,是用户登录成功。这个类需要继承自ApplicationEvent
类,并传递用户信息。
// User类
public class User {
private String username;
private String password;
// 构造函数、getter和setter省略
}
// 登录成功事件类
public class LoginSuccessEvent extends ApplicationEvent {
// 将用户信息传递到事件中
public LoginSuccessEvent(User user) {
super(user);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 2. 发布事件
在用户登录逻辑中,当用户成功登录后,我们使用ApplicationEventPublisher
发布登录成功事件。这个过程模拟了消息队列的发布机制,但完全在Spring应用内部处理。
@RestController
public class UserController {
@Autowired
private ApplicationEventPublisher eventPublisher;
@RequestMapping ("/login")
public String login(User user) {
// 假设用户验证逻辑并登录成功
LoginSuccessEvent event = new LoginSuccessEvent(user);
eventPublisher.publishEvent(event); // 发布登录成功事件
return user.getUsername() + "登录成功!";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 3. 监听和处理事件
# 方法1:实现ApplicationListener接口
创建一个服务(例如AccountService
),实现ApplicationListener
接口,并指定要监听的事件类型。在onApplicationEvent
方法中实现业务逻辑。
@Service
public class AccountService implements ApplicationListener<LoginSuccessEvent> {
@Override
public void onApplicationEvent(LoginSuccessEvent event) {
User user = (User) event.getSource();
System.out.println(user.getUsername() + "登录成功,执行AccountService逻辑。");
// 实际的业务逻辑
}
}
2
3
4
5
6
7
8
9
10
# 方法2:使用@EventListener注解
使用@EventListener
注解,可以直接在方法上标注,监听特定的事件。这种方式更简洁,易于阅读。
@Service
public class CouponService {
@EventListener
public void handleLoginSuccess(LoginSuccessEvent event) {
User user = (User) event.getSource();
System.out.println(user.getUsername() + "登录成功,发送优惠券。");
// 实际发送优惠券逻辑
}
}
2
3
4
5
6
7
8
9
10
# 4. 控制事件监听器的执行顺序
当你在Spring应用中有多个监听同一事件的监听器,并且这些监听器之间存在执行顺序的依赖时,你可以使用@Order
注解来指定监听器的执行顺序。@Order
注解接受一个整数值,值越小,优先级越高,监听器越早执行。
以下是使用@Order
注解来控制两个服务处理用户登录成功事件的执行顺序的案例。我们希望CouponService
在AccountService
之后执行:
@Service
public class AccountService implements ApplicationListener<LoginSuccessEvent> {
// 使用@Order注解指定执行顺序
@Order(1)
@Override
public void onApplicationEvent(LoginSuccessEvent event) {
User user = (User) event.getSource();
login(user);
}
private void login(User user) {
System.out.println(user.getUsername() + " 登录,密码为:" + user.getPassword());
}
}
@Service
public class CouponService {
// 指定CouponService的执行顺序在AccountService之后
@Order(2)
@EventListener
public void onEvent(LoginSuccessEvent event) {
User user = (User) event.getSource();
sendCoupon(user.getUsername());
}
private void sendCoupon(String username){
System.out.println(username + " 随机得到了一张优惠券");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 5. 自定义事件发布器
虽然Spring自动注入的ApplicationEventPublisher
已足够用于大多数场景,但在某些情况下,你可能需要自定义事件发布逻辑。通过实现ApplicationEventPublisherAware
接口,你可以获取到Spring的事件发布器(ApplicationEventPublisher
),然后封装它以便在你的应用中重复使用。
以下是创建自定义事件发布器MyEventPublisher
的案例,它实现了ApplicationEventPublisherAware
接口,并提供了一个publishEvent
方法来发布事件:
@Component
public class MyEventPublisher implements ApplicationEventPublisherAware {
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
// 封装publishEvent方法,方便在应用中使用
public void publishEvent(ApplicationEvent event) {
publisher.publishEvent(event);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
使用自定义的MyEventPublisher
来发布事件的示例:
@RestController
@Slf4j
public class UserController {
@Autowired
private MyEventPublisher myEventPublisher;
@GetMapping("/login")
public String login(User user) {
// 创建登录成功事件
LoginSuccessEvent event = new LoginSuccessEvent(user);
// 使用自定义的事件发布器发布事件
myEventPublisher.publishEvent(event);
return user.getUsername() + "登录成功!";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在这个案例中,我们首先通过实现ApplicationEventPublisherAware
接口来获取Spring的ApplicationEventPublisher
实例。然后,我们在MyEventPublisher
类中封装了publishEvent
方法,使得在控制器中可以更方便地发布事件。这种方式提供了对事件发布过程的更大控制,同时保持了代码的清晰和可维护性。