程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Spring Boot

    • Spring Boot - 自动配置
    • Spring Boot - 自定义starter
    • Spring Boot - 配置文件
    • Spring Boot - 自定义SpringApplication
    • Spring Boot - 生命周期与事件
    • Spring Boot - 事件驱动
      • 1. 事件驱动
      • 2. 事件驱动模型
      • 3. 解决的问题
      • 4. 观察者模型
      • 5. 发布、订阅模型
      • 6. Spring 事件驱动
        • 6.1 为什么需要事件驱动
        • 6.2 Spring 事件驱动
        • 6.3 实现Spring事件驱动的步骤
        • 1. 自定义事件类
        • 2. 发布事件
        • 3. 监听事件
        • 4. 异步事件处理
        • 6.4 使用事件驱动
        • 1. 定义事件类
        • 2. 发布事件
        • 3. 监听和处理事件
        • 方法1:实现ApplicationListener接口
        • 方法2:使用@EventListener注解
        • 4. 事件监听器的执行顺序(@Order)
        • 5. 自定义事件发布器
    • Spring Boot - Bean 加载方式
    • Spring Boot - 容器资源感知与获取
    • Spring Boot - 定时任务
    • Spring Boot - 异步任务
    • Spring Boot - 内置日志
    • Spring Boot - 函数式 Web
    • Spring Boot - 响应式远程调用
    • Spring Boot - 接口文档
    • Spring Boot - 单元测试
    • Spring Boot - 内容协商
    • Spring Boot - 参数校验
    • Spring Boot - HTTP客户端工具
    • Spring Boot - 控制器请求映射
    • Spring Boot - 请求参数接收
    • Spring Boot - 通用响应类
    • Spring Boot - 全局异常处理
    • Spring Boot - 整合Druid
    • Spring Boot - 整合Thymeleaf
    • Spring Boot - 国际化实现
    • Spring Boot - 自定义注解
  • Spring高级
  • Spring Boot
scholar
2023-10-29
目录

Spring Boot - 事件驱动

# 1. 事件驱动

事件驱动即跟随当前时间点上出现的时间,调动可用资源,执行相关任务,使不断出现的问题得以解决,防止事务堆积。

这些事件可以是用户的行为(如点击按钮、键入文字)、系统的信号(如定时器超时、硬件中断)或是其他程序的消息(如网络请求到达、数据库操作完成)。

在解决上述问题时,应用程序是由「时间」驱动运行的,这类程序在编写时往往可以采用相同的模型实现,我们可以将这种编程模型称为事件驱动模型。

# 2. 事件驱动模型

事件驱动模型有很多种体现形式,如简单的事件触发机制、单线程异步任务、多线程异步任务等,但是各种技术中实现事件驱动模型的思路基本相同。事件驱动模型包括四个(三个)基本要素:事件、事件消费方、事件生产方。

  • 事件:描述发生的事情。比如说浏览器页面点击事件,鼠标、键盘输入事件,spring 请求处理完成、spring 容器刷新完毕等。
  • 事件生产方(事件源):事件的生产方。任何一个事件都必须有一个事件源。比如 input、button、spring 中请求处理完毕的事件源就是 DispacherServlet、spring 容器刷新完毕的事件源 就是 ApplicationContext。
  • 事件管理器(时间广播器):派发事件。事件和事件监听的桥梁、负责把事件通知给事件监听器(可在事件源中实现)
  • 事件消费方(事件监听器):处理事件。监听事件的发生、可以再监听器中做一些处理。

# 3. 解决的问题

事件驱动模型通过对事件的生成、处理、监听和响应的方式,不仅实现了应用程序对于外部动作的实时响应能力,还有效地解决了复杂系统开发和维护中遇到的一系列问题。下面是事件驱动模型在实践中解决问题的一些关键领域:

实现组件之间的松耦合和解耦

  • 背景: 在复杂的应用或系统中,组件间高度耦合会导致维护难度增加,一个模块的变更可能引发连锁反应,影响到其他模块。
  • 事件驱动解决方案: 通过将组件间的直接交互替换为基于事件的通信,每个组件只需关注它感兴趣的事件即可,而无需知道这些事件的具体生产者。这样,即使某个组件发生变化,也不会直接影响到依赖于相同事件的其他组件,从而实现了组件间的松耦合和解耦。

实现异步任务处理

  • 背景: 在需要处理耗时任务的场景中,同步执行可能会阻塞整个应用的处理流程,导致用户体验下降。
  • 事件驱动解决方案: 通过将耗时任务封装成事件,并异步处理这些事件,可以让主处理流程继续进行,而不必等待耗时任务完成。这种方式提高了应用的响应性和吞吐量,同时也便于实现任务的并发处理。

跟踪状态变化

  • 背景: 在需要追踪对象状态变更历史的业务场景中,直接在业务逻辑中处理状态跟踪会增加复杂性和耦合度。
  • 事件驱动解决方案: 每次对象状态发生变化时,都生成一个事件,并将这个事件记录下来。这不仅可以为业务流程提供必要的历史数据,而且可以通过事件的处理逻辑来实现复杂的状态管理和恢复机制,无需在主业务逻辑中直接处理状态跟踪。

限流和消峰

  • 背景: 在面对高并发请求时,系统可能因为瞬时的负载超过处理能力而导致性能问题或服务不可用。
  • 事件驱动解决方案: 通过事件队列和事件处理机制,可以对进入系统的请求进行缓冲和调度,实现对请求流量的控制。在高负载情况下,可以通过调整事件处理速率或暂时缓存事件来避免系统过载,从而实现限流和消峰的目的。

总结

事件驱动模型通过对事件的有效管理和灵活应用,不仅提高了系统的可维护性和扩展性,还增强了应用的性能和稳定性。这种模型特别适合于需要快速响应外部事件、处理大量异步任务、追踪复杂状态变化或面对高并发场景的应用和服务。

# 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);
    }
}
1
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);
    }
}
1
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数据库、发送通知等
    }
}
1
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 事件驱动

image-20240330162712031

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);
    }
    // 可以添加额外的方法和属性来传递事件相关的信息
}
1
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));
    }
}
1
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());
    }
}
1
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());
    }
}
1
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) {
        // 异步处理事件
    }
}
1
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);
    }
}
1
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() + "登录成功!";
    }
}
1
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逻辑。");
        // 实际的业务逻辑
    }
}
1
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() + "登录成功,发送优惠券。");
        // 实际发送优惠券逻辑
    }
}
1
2
3
4
5
6
7
8
9
10

image-20240331035535099

image-20240331035500442

# 4. 事件监听器的执行顺序(@Order)

当你在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 + " 随机得到了一张优惠券");
    }
}
1
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);
    }
}
1
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() + "登录成功!";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

image-20240331040304505

在这个案例中,我们首先通过实现ApplicationEventPublisherAware接口来获取Spring的ApplicationEventPublisher实例。然后,我们在MyEventPublisher类中封装了publishEvent方法,使得在控制器中可以更方便地发布事件。这种方式提供了对事件发布过程的更大控制,同时保持了代码的清晰和可维护性。

编辑此页 (opens new window)
上次更新: 2025/03/21, 11:11:36
Spring Boot - 生命周期与事件
Spring Boot - Bean 加载方式

← Spring Boot - 生命周期与事件 Spring Boot - Bean 加载方式→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式