事件、消息、数据驱动编程范式-R
事件驱动编程
角色:
- 事件:相当于一个行为,其中包含与事件源绑定的回调函数,即当该事件源完成后要执行的行为。
- 事件源:相当于生产者/被观察者,产生事件。
- 事件处理器:相当于消费者/观察者。
- 事件循环:负责监听事件源,当事件发生时,触发相应的事件处理器
监控方式:
-
使用一个线程轮询该组件
- 这种方式容易造成阻塞,CPU资源浪费和响应时间过长。
-
使用一个队列来存储发生的事件,有一个消费者不断从中读取事件并调用不同的处理程序,如前端中的onClick()。
- 事件(消息)一般各自保存自己的处理函数指针,每个消息都有独立的处理函数。
方式二相当于事件源是生产者,不断生成事件放入消息队列,监听器负责根据事件调用不同的处理程序。
比如书记让秘书过两个小时后通知他去开会,书记是事件源,它产生了一个事件:过两个小时去开会,而秘书则是一个事件队列,他会循环回忆书记的事件,以确保到点能通知书记去完成相应的行为。
单线程、多线程、事件驱动编程的对比,每个任务在执行I/O操作时阻塞自身,灰色表示阻塞。
- 单线程:按顺序执行3个任务,调用I/O阻塞当前线程,造成CPU资源浪费
- 多线程:每个线程执行一个任务,但在调用I/O时仍造成CPU资源浪费
- 事件驱动:当遇到代价昂贵的任务时,异步调用该任务,并将当前线程立即返回,继续执行后续的任务。当I/O异步执行完成后,注册一个事件到事件队列,事件循环持续轮训事件队列,检测到该事件则取出,调用其绑定的回调函数执行后续逻辑。
事件驱动编程步骤:
- 应用线程发起非阻塞 I/O 请求(如读取文件、发送网络包),同时将 “I/O 完成后的处理逻辑”(回调函数)封装成一个 “事件”,注册到事件队列;
- 发起 I/O 的线程不阻塞,立即返回,继续处理其他就绪任务(或被事件循环调度去处理其他事件);
- I/O 操作由操作系统内核异步处理(期间 CPU 可用于其他任务,无闲置);
- 当 I/O 完成后,操作系统向应用层发送 “I/O 完成通知”,触发对应的 “完成事件” 进入事件队列;
- 事件循环(Event Loop) (通常是单线程,如 Node.js 的事件循环)持续轮询事件队列,一旦检测到待处理的 “I/O 完成事件”,就从队列中取出,调用其绑定的回调函数执行后续逻辑;
- 回调执行完毕后,事件循环继续轮询下一个事件,循环往复。
实现方式
回调函数: 这是更轻量级的方式,特别是在异步编程中。
Reactive Streams: 如RxJS、Project Reactor,它们提供了更强大的事件流处理能力,背压控制等,其思想根基也来自于观察者模式,但功能远超传统观察者模式。
消息队列/发布-订阅模式: 这是观察者模式的升级版,在解耦方面更彻底。
与观察者模式、发布订阅模式的关系
观察者模式是事件驱动编程的一个实现。编程范式是一个目标,而设计模式则是其中的一种实现方式。
| 概念 | 角色 | 类比 |
|---|---|---|
| 事件驱动编程 | 架构范式/目标 | 想要建立一个 “应急响应系统” 。核心思想是:有事发生,立刻响应。 |
| 观察者模式 | 设计模式/实现手段 | 实现这个系统的一种具体组织方式:设立一个报警中心(Subject),让各个应急单位(Observers)在中心注册。一旦有警报(事件),中心就通知所有相关单位。 |
| 其他模式(如Pub/Sub) | 另一种实现手段 | 实现该系统的另一种更解耦的方式:通过一个公共的无线电频道(Message Broker),任何单位可以发布消息,任何单位可以订阅感兴趣的消息。 |
情况 1:事件驱动可以基于发布订阅模式实现(解耦场景)
情况 2:事件驱动也可以不依赖发布订阅模式(简单场景),使用观察者模式。
消息驱动编程
发布订阅1模式是该编程范式的实现方式之一,与事件驱动编程的区别是,事件源产生事件/消息后,事件/消息由第三方系统放入队列,事件源仅负责消息的生成,而事件处理器责负责从第三方系统中获取消息并处理。
比如作家编写文章之后,由报社来负责收集、发表、销售等,报社会将文章售卖给订阅相关主题的读者。作家通过文章来传达他的想法,而读者通过文章去了解作家要表达的内容。两者通过文章(消息) 完成跨时空的交流。
组件间不直接调用,而是通过发送 / 接收消息协作。
角色:生产者、消费者、消息中间件(比如Kafka、RabbitMQ)。
目标:解耦组件间的通信。
数据驱动编程
传统应用: 使用if、for、Switch语句进行硬编码,无法在运行后修改。
数据驱动:由数据驱动行为,数据和行为独立于系统,系统根据不同的数据执行不同的行为。
比如在企业级应用开发中,在数据库中配置菜单和功能,不同的角色显示不同的菜单和功能。
将业务规则和行为控制从硬编码中解放出来,转而由外部数据源(数据库)来驱动程序的运行时的行为和表现。
这种架构使得系统更加灵活、可配置和易于维护,是企业级应用追求的目标之一。
| 特性 | 传统应用 | 数据驱动编程 |
|---|---|---|
| 控制源 | 代码逻辑 | 外部数据 |
| 灵活性 | 编译时/运行时确定 | 运行时可变更 |
| 修改方式 | 修改代码重新编译 | 修改数据文件 |
| 核心思想 | "我知道要做什么" | "根据数据决定做什么" |
根据用户鉴权动态生成菜单和功能
工作流程
graph TD
A[用户登录] --> B[查询用户角色和权限数据]
B --> C[根据权限数据过滤可用菜单]
C --> D[动态生成用户界面]
D --> E[用户交互]
E --> F[根据权限数据控制功能访问]
数据库表结构
-- 菜单表:定义所有可用菜单项
CREATE TABLE sys_menu (
id BIGINT PRIMARY KEY,
name VARCHAR(50), -- 菜单名称
path VARCHAR(100), -- 路由路径
component VARCHAR(100), -- 前端组件
parent_id BIGINT, -- 父级菜单
sort INT, -- 排序
visible BOOLEAN -- 是否可见
);
-- 角色表:定义角色类型
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY,
role_key VARCHAR(50), -- 角色标识符
role_name VARCHAR(50) -- 角色名称
);
-- 角色-菜单关联表:配置数据的核心
CREATE TABLE sys_role_menu (
role_id BIGINT,
menu_id BIGINT,
PRIMARY KEY (role_id, menu_id)
);
-- 用户-角色关联表
CREATE TABLE sys_user_role (
user_id BIGINT,
role_id BIGINT
);
业务层接口
// 数据驱动的菜单生成示例
@Service
public class MenuService {
public List<MenuDTO> generateUserMenu(Long userId) {
// 1. 从数据库查询用户的菜单权限数据
List<Menu> userMenus = menuMapper.selectMenusByUserId(userId);
// 2. 由数据驱动生成菜单结构
return buildMenuTree(userMenus); // 菜单结构完全由数据决定
}
}
动态菜单生成
// 数据驱动的菜单生成示例
@Service
public class MenuService {
public List<MenuDTO> generateUserMenu(Long userId) {
// 1. 从数据库查询用户的菜单权限数据
List<Menu> userMenus = menuMapper.selectMenusByUserId(userId);
// 2. 由数据驱动生成菜单结构
return buildMenuTree(userMenus); // 菜单结构完全由数据决定
}
}
数据驱动的权限控制
// 基于注解的权限控制 - 由数据库数据驱动
@PreAuthorize("@ss.hasPermission('system:user:list')")
@GetMapping("/user/list")
public Result<List<User>> userList() {
// 权限判断逻辑由数据库中的权限数据驱动
List<User> users = userService.listUsers();
return Result.success(users);
}
// 自定义权限校验服务
@Service("ss")
public class PermissionService {
public boolean hasPermission(String permission) {
// 从数据库查询当前用户的权限数据
Set<String> permissions = getCurrentUserPermissions();
// 由权限数据决定是否允许访问
return permissions.contains(permission);
}
}
功能开关的数据驱动
// 数据库中的功能配置表
@Entity
@Table(name = "sys_feature_toggle")
public class FeatureToggle {
private String featureKey; // 功能标识
private boolean enabled; // 是否启用
private String allowedRoles; // 允许的角色
}
// 功能访问控制
@Service
public class FeatureAccessService {
public boolean isFeatureEnabled(String featureKey, User user) {
// 从数据库读取功能配置数据
FeatureToggle toggle = featureToggleRepository.findByFeatureKey(featureKey);
if (toggle == null || !toggle.isEnabled()) {
return false; // 由数据驱动功能开关
}
// 由数据驱动角色权限
return user.getRoles().stream()
.anyMatch(role -> toggle.getAllowedRoles().contains(role.getKey()));
}
}
消息驱动、事件驱动、发布订阅模式的关联
- 消息驱动编程 → 必然关联发布订阅模式:消息驱动以 “消息解耦” 为核心,发布订阅模式是实现这一目标的核心机制,二者几乎是 “绑定关系”;
- 事件驱动编程 → 可选关联发布订阅模式:事件驱动以 “响应事件” 为核心,简单场景用 “回调 / 观察者模式”,复杂解耦场景才用发布订阅模式,二者是 “灵活关联关系”。
观察者模式通过 “接口依赖” 解耦被观察者和观察者(被观察者只需知道观察者的接口,无需知道具体实现) ;事件驱动编程通过 “事件注册” 解耦事件源和处理器(事件源无需知道谁会处理事件,只需触发事件即可)。
参考资料
事件驱动编程、消息驱动编程、数据驱动编程_和事件驱动编程对应的-CSDN博客
deepseek:物理层与数据传输层知识解析 - DeepSeek
(25 封私信 / 64 条消息) 数据驱动编程:让你的嵌入式代码更优雅更灵活! - 知乎
事件驱动编程:基础知识与实践1.背景介绍 事件驱动编程(Event-Driven Programming,EDP)是一种 - 掘金
Footnotes
-
发布订阅
是一个软件架构中的一种消息范式,也是经典的设计模式。
发布者不直接与订阅者联系,发布者将要发布的消息交由中心管理,订阅者也是根据自己的情况,按需订阅中心中的消息。
比如作家(生产者) 写好文章交给报社(发布订阅中心) ,由报社负责销售 。你(订阅者) 订阅了某个专题(主题)报纸,你订阅的报纸由报社安排人送到你家。
好处:生产者与订阅者解耦,生产者发送消息后不需要关注消息的状态,仅需要专注于自己的工作。订阅者订阅消息后,只需要等消息到达后进行处理,不需要关注消息如何到达。由发布订阅中心负责消息的存储、转发、传输。
角色:
- 发布者:
- 发布订阅中心:该范式的重点,
- 订阅者:
应用:消息队列,jdk9的响应式流
背压:减缓发布者发布消息的速度 ↩