Skip to content

设计原理 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
Javainterceptor/FlowInterceptor.javaevent/ProcessEventListener.javaServiceContext / Registry
Goengine/interceptor.goengine/interceptor.go(Listeners)engine/registry.go
Node.jssrc/extensions.tssrc/extensions.tssrc/registry.ts
Pythonjeeflow/extensions.pyjeeflow/extensions.pyjeeflow/extensions.py

命名差异:Go 的拦截器方法名 PascalCase(PreHandle),Node/Python 用 camelCase/snake_case,语义一致。


下一篇05 · SPI 设计——接口的边界在哪里

jeeflow · 轻量级多语言工作流引擎