Appearance
设计原理 03 · 执行引擎——一次审批的完整旅程
系列:jeeflow 工作流引擎设计原理
1. 核心抽象
执行引擎的本质是一个图遍历器:从 start 节点出发,沿着边推进,在任务节点停下生成待办,在决策节点选路,在 fork/join 处分合。
FlowModel(流程定义)
├── nodes[]:start / task / decision / fork / join / end / custom
└── edges[]:sourceNodeId → targetNodeId(决策边带 expr + text.value)
执行状态 = ProcessInstance + 当前节点指针(通过 task.taskName 反查节点)关键设计:任务完成后,引擎通过 task.taskName 在 FlowModel 里反查节点,再 followEdges 找到下一个节点——不需要在实例里维护"当前节点"指针,任务本身就是指针。这简化了持久化模型(5 张表,无游标表)。
2. 启动流程
2.1 startProcessInstanceById(引擎职责)
1. findDefineById → JSON → FlowModel
2. ProcessInstance.create(工厂:state=10,注入用户变量)
3. saveInstance
4. 发布 PROCESS_START 事件
5. 找到 start 节点 → followEdges → 逐个 executeNode启动后生成的第一个任务:发起申请节点(assignee="applicant" → 解析为发起人)。
2.2 startAndExecute(调用方契约,引擎不内置)
1. 调 startProcessInstanceById
2. 取所有进行中任务
3. 逐个 executeProcessTask(submitType=0 APPLY)为什么引擎不自动做第 2、3 步?因为"第一个任务是否自动完成"是业务决策——有些流程第一个节点就是申请人填单(需要手动),有些是纯审批流(自动跳过)。引擎保持中立,由调用方选择。详见 06-契约约定。
3. 完成任务(核心路径)
executeProcessTask(taskId, operator, args):
┌─ loadAndCheck
│ task = findTaskById
│ 校验:task.taskState == DOING
│ 校验:task.isAllowed(operator) # actorIds 包含 operator
│ inst = findInstanceById
│
├─ 聚合根:inst.completeTask(task, operator, vars)
│ task.finish(...) # 10→20
│ inst.variables 合并
│
├─ 持久化 + 发布 TASK_COMPLETE 事件
│
└─ 推进(关键路径):
curNode = findNode(flow, task.taskName)
for next in followEdges(curNode):
case next.type:
END → inst.finish() + 发布 PROCESS_FINISH
TASK → executeNode → createTask(解析参与者)
DECISION → evaluateDecision(选一条出边)
FORK → 递归 executeNode 每条出边
JOIN → 若无进行中任务才放行4. 决策节点:三种求值优先级
evaluateDecision(node):
1. Registry 按名解析(decisionHandler/assignmentHandler)
→ 返回目标边 ID → 直接跳转
2. 扩展注入的 DecisionHandler
3. 出边 expr 表达式求值(ExpressionEvaluator SPI)
→ 遍历出边,第一个 expr 为真的边获胜
→ 若出边无 expr,作为默认分支优先级设计理由:Registry(注册的处理器)是编译期可确定的强约定;表达式是运行期自由求值。有处理器走处理器,没有才回退表达式——业务方按需选择。
5. Fork / Join 语义
FORK:无条件并行——递归执行每条出边(生成多个任务)
JOIN:等待所有分支完成——只有 findDoingTasks 为空时才放行Join 的实现不统计分支数,而是问聚合根"还有没有进行中任务":
JOIN 放行条件 = inst 无任何 DOING 任务为什么可以这么简化:因为 fork 之后只可能产生任务节点(决策/嵌套 fork 最终都落到任务),任务完成顺序不定,但只要还有任务在做,join 就等着;全部完成则放行。这是 snaker 风格引擎的经典做法,牺牲了"分支数精确计数",换来了模型简单(不需要在实例上记录分支计数)。
6. 会签:三种模式
会签节点的判定:performType=1 且有 countersignType。
6.1 并行会签(PARALLEL)
createTask: 为每个 actor 创建一个独立任务(同 taskName)
完成一个:检查是否还有同节点 DOING 任务 → 有则等待
全部完成:走下一节点6.2 串行会签(SEQUENTIAL)
createTask: 只创建第一个 actor 的任务
任务变量写入 operatorList / loopCounter / nrOfInstances
完成任务:loopCounter+1 < nrOfInstances → 创建下一个 actor 的任务
否则 → 走下一节点串行会签的关键:进度存在任务变量里(operatorList_${nodeId} 等),不落表——因为串行是"同一节点的多次实例化",复用同一 taskName。
6.3 按比例会签(RATIO)
按并行方式创建任务,放行条件由 countersignCompletionCondition 表达式决定
(如 #nrOfCompletedInstances>=2,已支持,见 07-countersign-ratio.json)按比例的阈值判断(如"2/4 同意即通过")通过会签节点
countersignCompletionCondition表达式实现(如#nrOfCompletedInstances>=2),已支持。
7. 驳回与跳转
executeAndJumpToEnd(taskId, operator, args): # 拒绝(REJECT=2)→ 跳结束
聚合根 abandonAllDoing → 完成任务 → inst.reject()(10→45)
executeAndJumpTask(taskId, operator, args, target): # 跳转(JUMP=4)/ 退回上一步(ROLLBACK=3)
聚合根 abandonAllDoing → 完成任务
target 节点 → executeNode(重新生成目标任务)
executeAndJumpToFirstTaskNode(taskId, operator, args): # 退回发起人(ROLLBACK_TO_OPERATOR=6)
聚合根 abandonAllDoing → 完成任务
第一个任务节点重执行,参与者强制为发起人 → 发起人收到新待办(实例保持 10)submitType 行为(与 mldong 框架一致):submitType=2(REJECT) 调 executeAndJumpToEnd(实例→45,无新待办);退回发起人用 submitType=6 调 executeAndJumpToFirstTaskNode。详见 06-契约约定。
8. 拦截器与事件挂载点
executeNode(node):
preHandle(node, instance) # 按 order 升序;返回 false 中断该节点执行
├── 节点分派(task/decision/fork/join/end)
postHandle(node, instance) # 无论成功失败都执行
事件挂载点:
PROCESS_START 启动流程
PROCESS_FINISH 到达 end
PROCESS_REJECT 拒绝(跳结束)
TASK_CREATE 创建任务(扩展预留)
TASK_COMPLETE 完成任务9. 一次完整旅程(时序)
发起人: startAndExecute
start → apply(自动完成) → 组长审批
组长: executeProcessTask(同意, submitType=1)
→ 经理审批
经理: executeAndJumpToFirstTaskNode(退回发起人, submitType=6)
→ apply 重执行(参与者=发起人)→ 发起人收到新待办
发起人: executeProcessTask(重新提交, submitType=0)
→ 组长审批 → 经理审批 → 组长审批... 直到 end
→ inst.finish() → PROCESS_FINISH10. 关键源码位置
| 主题 | Java | Go | Node.js | Python |
|---|---|---|---|---|
| 启动/执行/跳转 | core/JeeflowEngineImpl.java | engine/engine_impl.go | src/engine.ts | jeeflow/engine.py |
| 节点遍历 executeNode | core/JeeflowEngineImpl.java | engine/engine_impl.go | src/engine.ts | jeeflow/engine.py |
| 决策求值 | core/... | engine/engine_impl.go | src/engine.ts | jeeflow/engine.py |
| 会签创建 | handler/impl/CreateTaskHandler.java | engine/engine_impl.go | src/engine.ts | jeeflow/engine.py |
| 聚合根状态转换 | domain/ProcessInstance.java | model/instance.go | src/model.ts | jeeflow/model.py |