Appearance
设计原理 04 · 扩展机制——拦截器、事件与 HandlerRegistry
系列:jeeflow 工作流引擎设计原理
1. 要解决什么问题
引擎核心必须零框架依赖,但业务方总需要"在流程跑起来的时候干点自己的事":
- 审批通过后发通知(企业微信/钉钉/短信)
- 特定节点完成时改业务单据状态
- 参与者不是固定的人,而是按业务规则算出来的
- 决策分支不是表达式能表达的,要写代码判断
这些问题不解决,引擎就只能 demo 不能上线。解决方式就是三类扩展点:拦截器、事件、处理器注册表。
2. 三类扩展点总览
| 扩展点 | 时机 | 用途 |
|---|---|---|
| FlowInterceptor | 节点执行前后 | 横切逻辑(日志/统计/熔断) |
| ProcessEventListener | 流程生命周期 | 异步通知/集成(钉钉、MQ) |
| HandlerRegistry | 运行时按名解析 | 动态参与者、动态决策 |
3. 拦截器(FlowInterceptor)
3.1 设计
interface FlowInterceptor:
preHandle(node, instance): boolean # 节点执行前;false 中断该节点
postHandle(node, instance): void # 节点执行后(无论成败)
order(): int # 排序,越小越先3.2 为什么 pre 返回 boolean?
拦截器需要能阻止节点执行——典型场景:审批节点要求前置条件满足才创建任务(如"该客户已实名认证才进入人工审核")。preHandle 返回 false,引擎跳过该节点的任务创建,流程停在原地。
3.3 执行顺序
所有拦截器按 order 升序执行 preHandle(任一 false 即中断)
节点执行
所有拦截器按 order 降序执行 postHandle(对称的洋葱模型)对称顺序保证"先注册的后收尾",与中间件惯例一致。
3.4 与事件的区别
| 拦截器 | 事件 | |
|---|---|---|
| 能阻止执行 | ✅(pre 返回 false) | ❌ |
| 关注点 | 节点级横切 | 流程级通知 |
| 顺序敏感 | ✅(order) | ❌(广播) |
经验法则:要"拦"用拦截器;要"通知"用事件;两者不要混。
4. 事件监听(ProcessEventListener)
4.1 事件清单
| 事件 | 触发点 | 携带信息 |
|---|---|---|
PROCESS_START | 启动流程 | instanceId, operator |
PROCESS_FINISH | 到达 end 节点 | instanceId, operator |
PROCESS_REJECT | 拒绝(jumpToEnd) | instanceId, taskId, operator |
TASK_COMPLETE | 完成任务 | instanceId, taskId, taskName, operator |
4.2 监听器怎么写
java
// 伪代码(四版同构)
listener = (event) -> {
switch (event.type) {
case TASK_COMPLETE:
dingtalk.send("任务完成通知", event.taskName);
case PROCESS_FINISH:
orderService.markDone(event.instanceId);
}
}4.3 为什么事件不带"业务上下文"?
事件只带 instanceId/taskId,业务方需要更多数据时自己查仓储。刻意不带完整快照:事件是"通知发生了",不是"传输数据"——避免事件体膨胀、避免监听器拿到过期数据。
5. HandlerRegistry(自写 IoC)
5.1 为什么需要它
流程定义里写的是字符串(assignmentHandler: "deptLeaderHandler"),运行时引擎需要把这个字符串变成可调用的处理器。这就是注册表:按名字找到实例。
流程定义 JSON Registry
┌──────────────────┐ ┌──────────────────┐
│ assignmentHandler:│ ──名字──▶│ deptLeaderHandler│──▶ 返回 [userId]
│ "deptLeaderHandler"│ │ (已注册实例) │
└──────────────────┘ └──────────────────┘5.2 设计
interface IAssignmentHandler:
assign(node, instance): string[] # 返回参与者列表
interface IDecisionHandler:
decide(node, instance, vars): string # 返回目标边 ID
class HandlerRegistry:
register(name, handler) # 启动时注册
resolve(name): handler # 运行时按名解析5.3 解析优先级(参与者)
resolveActors(node):
1. assignee 非空 → 固定参与者(含 "applicant" → 发起人)
2. Registry 按名解析 → 动态参与者(推荐)
3. 扩展注入的 AssignmentHandler(兼容旧 API)设计要点:assignee 优先于 assignmentHandler——静态配置优先于动态逻辑,减少误解析。
5.4 决策解析优先级
evaluateDecision(node):
1. Registry 的 decisionHandler
2. 扩展注入的 DecisionHandler
3. 出边 expr 表达式求值5.5 为什么不用真正的 Spring?
- 引擎核心零依赖:不能引入 Spring/Guice
- 引擎不需要"对象生命周期管理"——处理器是无状态的,一个 Map 就够了
- 集成方若用 Spring,只需在启动时把 Bean 注册进 Registry(适配 3 行代码)
java
// Spring 集成示例(demo 之外的使用方)
@Configuration
class JeeflowConfig {
@Bean
void register(JeeflowEngine engine, DeptLeaderHandler handler) {
engine.getRegistry().register("deptLeaderHandler", handler);
}
}6. 扩展点挂载位置(引擎内部)
executeNode(node):
preHandle(拦截器) ←── 拦截器
├── createTask → resolveActors ←── Registry / 扩展
├── evaluateDecision ←── Registry / 扩展 / 表达式
postHandle(拦截器)
完成/启动/驳回 时 → fireEvent ←── 事件监听器7. 四版实现对照
| 语言 | 拦截器 | 事件 | Registry |
|---|---|---|---|
| Java | interceptor/FlowInterceptor.java | event/ProcessEventListener.java | ServiceContext / Registry |
| Go | engine/interceptor.go | engine/interceptor.go(Listeners) | engine/registry.go |
| Node.js | src/extensions.ts | src/extensions.ts | src/registry.ts |
| Python | jeeflow/extensions.py | jeeflow/extensions.py | jeeflow/extensions.py |
命名差异:Go 的拦截器方法名 PascalCase(PreHandle),Node/Python 用 camelCase/snake_case,语义一致。