不改内核也能挂业务:工作流引擎的三层挂接模型
不改内核也能挂业务:工作流引擎的三层挂接模型
一句话:评估流程引擎,别只看「能不能画流程」;更要看——业务逻辑能不能在不污染内核的前提下,挂到固定生命周期点上。
本文口径:把这种能力抽象为L1 交互层 / L2 服务层 / L3 配置编排层,再用同一把尺子对照 Java 与 .NET 两侧常见开源产品。
写作原则:名词可以不同,诉求相同;有优势写优势,有短板写短板;不以某一厂商叙事替代行业共性。
1. 先把「二开」说清楚
1.1 什么叫流程引擎的二开
流程引擎二开= 流程(模板)设计完成之后,在不修改引擎内核发送 / 流转代码的前提下,用脚本、类、配置或 Worker,与引擎在固定生命周期点交互,从而完成业务校验、集成、通知、台账等逻辑的过程。
| 对比项 | 改引擎源码 | 流程二开(本文范围) |
|---|---|---|
| 改动位置 | 发送 / 退回 / 调度内核 | 外挂、事件、自定义 Activity、配置执行体 |
| 升级成本 | 高,难合并 | 相对低,核心可独立升级 |
| 职责边界 | 引擎与业务缠在一起 | 引擎管流转,二开管业务 |
| 可交付性 | 难复用、难交接 | 可按流程模板 / 模块绑定交付 |
典型动作:发送前校验、发送后同步第三方、退回拦截、流程结束写台账、自定义活动节点、自定义办理页按钮等。
1.2 为什么产品名词各异,却是同一件事
各产品对「挂业务」的叫法并不统一:Listener、Delegate、Action、StepBody、Custom Activity、外挂、事件配置、ServiceTask……
评估时不必纠结名词,应看三件事是否成立:
- 有没有稳定的生命周期时钟(发送前 / 发送后 / 完成 / 退回 / 结束……)
- 业务代码是否落在内核之外(可独立升级、可按模板绑定)
- 前端、后端、配置是否都能接到同一套语义(或至少有清晰分层)
这三件事,正是下文「三层挂接模型」要回答的。
2. 三层挂接模型:L1 / L2 / L3
把二开能力按「谁写、写在哪、管什么边界」拆成三层:
| 层级 | 名称 | 一句话 | 谁写 | 典型载体 | 擅长 | 不适合单独承担 |
|---|---|---|---|---|---|---|
| L1 | 交互层 | 前端 / 页面侧写脚本交互 | 前端 / 全栈 | 办理页钩子、表单脚本、设计器 UI 插件 | 发送前校验、按钮定制、提示、字段联动 | 强事务写库、跨系统强一致 |
| L2 | 服务层 | 后端 / 进程内写代码 | 后端 | JavaDelegate、Listener、FlowEvent、StepBody、自定义 Activity | 强事务、改接收人 / 跳转、同步 ERP、审计 | 纯 UI 即时反馈 |
| L3 | 配置 / 编排层 | 设计器或模型配置 | 实施 / 低代码 | SQL / HTTP / 脚本 / 表达式 / CodeAction | 少写代码也能挂业务 | 复杂分支、深度引擎变量重构 |
运行时可以理解为同一条业务动作的分层叠加(示意):
用户点击「发送」 ├─ L1 交互层:页面校验 / 拦截 / 按钮与提示 └─ 请求进入引擎 ├─ L2 服务层:进程内代码(可改路由、写库、调系统) └─ L3 配置层:SQL / HTTP / 脚本 / 表达式(实施可配)关键认识:三层不是互斥三选一,而是允许叠加。常见顺序是 L1 先拦人机边界 → L2 再守系统真相 → L3 承接标准化连接。
2.1 这一能力的作用
- 把「流转」和「业务」拆开:引擎回答怎么走,二开回答走到某点时业务做什么。
- 把交付从「改核」变成「挂点」:项目差异落在外挂与配置,而不是 fork 一份引擎。
- 让不同角色都能参与:前端、后端、实施不必挤在同一条技术路径上。
- 让升级成为可能:内核独立演进,业务扩展可保留、可迁移、可按模板复用。
2.2 为什么它是重要评估指标
选型时如果只看「有没有设计器 / 是否 BPMN」,很容易漏掉真正决定项目成败的问题:
| 若二开能力弱 | 项目侧常见后果 |
|---|---|
| 只能改源码挂业务 | 升级噩梦、合并冲突、人员不敢动引擎 |
| 只有后端代码扩展 | 前端交互与实施配置缺位,交付慢、协作成本高 |
| 只有配置没有代码出口 | 复杂规则被迫塞进脚本地狱,难测难重构 |
| 事件时钟不产品化 | 团队靠猜「什么时候会触发」,集成不可控 |
因此:二开不是售后补丁,而是引擎产品能力的一等公民。
尤其在政企审批、ERP 集成、多系统待办同步等场景,L1/L2/L3 是否齐全,往往比「画布好不好看」更能决定能不能按期上线、能不能长期维护。
3. 三层分别解决什么问题
3.1 L1 交互层:人还在页面上时
问题:发送前要不要先提示?字段要不要联动?按钮要不要定制?成功后要不要局部刷新?
价值:反馈即时,减少无效请求;把「人机交互边界」拦在浏览器侧。
边界:页面可拦截,但业务真相以服务端为准——金额、库存、权限最终仍应在 L2/L3 再校验一遍。
常见形态:
- 办理页 / 表单脚本(发送前、打开后、字段变更)
- 前端外挂类 / Override 钩子
- 设计器 UI 扩展(工具栏、表单控件、画布元素)
3.2 L2 服务层:进入引擎事务边界之后
问题:要不要改下一节点接收人?要不要同步 ERP?要不要写审计台账?要不要在同一事务里失败回滚?
价值:强类型、可调试、可测试;能访问运行时上下文(当前节点、实例 ID、变量、发送结果等)。
边界:适合系统集成与规则编排;不适合用它替代页面即时交互。
常见形态:
- Java:
JavaDelegate、ExecutionListener、TaskListener - .NET:
StepBody、Custom Activity、Action Provider、流程事件基类 - 全局拦截 / 中间件(平台级策略)+ 流程级绑定(模板级策略)
3.3 L3 配置 / 编排层:少写代码也能挂
问题:没有专职开发时,实施能否在设计器里把「一条 SQL / 一个 HTTP / 一段表达式」挂上?
价值:把简单集成从程序员日程里解放出来;缩短交付周期。
边界:擅长标准化连接;复杂分支、深度重构仍应回到 L2。
常见形态:
- BPMN 扩展:Listener / ServiceTask 上挂 class / expression / script
- 设计器事件:SQL、WebApi、存储过程、业务单元
- 方案内 CodeAction、表达式语言(JUEL / FEEL / 自有 DSL)
4. 流行引擎怎么实现:Java 阵营
资料依据为各产品公开文档与社区常见做法。评级是「机制完整度」,不是业务场景总分。
强= 产品级、文档/样例完整;中= 能做但需自建较多;弱= 基本不覆盖或需从零实现。
4.1 总对照(Java)
| 产品 | 定位倾向 | L1 交互层 | L2 服务层 | L3 配置编排 | 二开主叙事 |
|---|---|---|---|---|---|
| Camunda(社区版 / 平台) | BPMN 标准向编排 + 运维 | 中(表单/办理页多自建或另接;Cockpit/Tasklist 可扩展但非审批门户一站式) | 强(JavaDelegate、Execution/Task Listener、外部任务) | 强(脚本、表达式、Listener 配置、Connector 生态) | Listener + Delegate + External Task |
| Flowable | BPMN / CMMN 引擎族 | 中(UI 多自建;企业版能力更完整) | 强(Delegate、Listener、解析期注入 Listener) | 强(expression / script / class 挂接) | 与 Camunda 同源思想,Delegate Code 体系 |
| Activiti | 经典 BPMN 引擎 | 中偏弱(产品 UI 依赖版本与生态) | 强(JavaDelegate、Listener) | 强偏中(脚本/表达式可用,实施配置体验因发行版而异) | 经典委托代码模型 |
| JFlow | 流程 + 表单一体化 BPM(Java) | 强(办理页前端外挂协议产品化) | 强(流程事件基类 / 后端外挂) | 强(模板事件配置:SQL/HTTP/脚本等) | 前端外挂 + 后端外挂 + 事件配置 |
Camunda / Flowable / Activiti:同一套「BPMN 扩展点」思维
三者同属 Activiti 谱系或其近亲,二开骨架高度相似:
| 层级 | 典型落点 |
|---|---|
| L2 | JavaDelegate(Service Task)、ExecutionListener(执行开始/结束)、TaskListener(人工任务 create/assignment/complete…) |
| L3 | BPMNextensionElements中配置 class / delegateExpression / expression / script;Script Task;条件表达式 |
| L1 | 引擎本身通常不强制提供与审批办理页对齐的「前端外挂协议」;表单与工作台多由业务系统自建,或依赖 Tasklist / 商业套件 / 自研前端 |
特点:
- L2/L3 极强:开发者友好,生态成熟,适合微服务与标准 BPMN。
- L1 往往外置:若你的项目强依赖「办理页发送前校验、按钮定制、字段联动」的产品级协议,需要额外评估表单/门户方案。
- External Task(Camunda 等)把重活外移到 Worker,适合解耦与多语言工人进程——这是 L2 的「进程外变体」,仍属服务侧挂接。
JFlow:把三层做成并列入口
JFlow 与下文 .NET 侧的 CCFlow同根同源、语言不同:事件名、分层思想、调度顺序一致,差异主要在 Java 包机制与 .NET 程序集机制。
产品概念上常称为:前端外挂(L1)、后端外挂(L2)、模板事件配置(L3)。详见第 6 节。
4.2 Java 阵营怎么读表
| 你更关心… | 相对更贴的路径 |
|---|---|
| 标准 BPMN + 强代码扩展 + 云原生 Worker | Camunda / Flowable |
| 轻量嵌入、经典委托模型 | Activiti 谱系引擎 |
| 审批办理页也能产品级挂脚本,且配置/代码并列 | JFlow 这类一体化 BPM |
5. 流行引擎怎么实现:.NET 阵营
5.1 总对照(.NET)
| 产品 | 定位倾向 | L1 交互层 | L2 服务层 | L3 配置编排 | 二开主叙事 |
|---|---|---|---|---|---|
| Elsa Workflows | 通用长流程编排 + Studio | 中偏强(Studio 元数据/UIHint;业务办理页多自建) | 强(Custom Activity、Middleware、Module/Feature) | 中(活动组合与表达式强;SQL/实施配置弱于 BPM) | 自定义 Activity 生态 |
| Workflow Core | 轻量嵌入式流程库 | 弱(几乎无产品级办理 UI) | 强(StepBody+ 中间件) | 中(JSON/YAML 引用 Step 类型) | 步骤即扩展单元 |
| WorkflowEngine.NET | 可嵌入引擎(生产多需商业许可) | 强(设计器模板 / 表单可定制) | 强(Action Provider、Plugin、Custom Activity) | 强(方案内 CodeActions 等) | Action + Plugin |
| Slickflow | BPMN 风格 .NET 引擎 | 中(设计师可嵌;业务页多自建) | 强(ExternalService、引擎 API) | 强(本地类 / WebApi / SQL / 过程) | 节点 Action 多执行体 |
| CCFlow | 流程 + 表单 + 组织一体化 BPM | 强(Vue 前端外挂) | 强(FlowEventBase后端外挂) | 强(设计器事件 + SQL/WebApi/过程等) | 前端外挂 + 后端外挂 + 事件配置 |
Elsa / Workflow Core:开发者编排优先
- Elsa:二开主路径是「自定义积木」(Activity)+ 中间件 + 可打包扩展;对人机审批语义(会签、组织待办)通常要自建。
- Workflow Core:
StepBody即业务单元,嵌入成本低;几乎没有产品级设计器/表单/待办门户——二开 ≈ 写代码 + 自建 UI。
WorkflowEngine.NET / Slickflow:设计器与节点执行体
- WorkflowEngine.NET:Action/Condition、CodeActions、Plugin、Custom Activity 文档完整;许可需单独评估。
- Slickflow:节点上挂本地服务 / WebApi / SQL / 过程,BPMN 语义清晰;前端「办理页外挂协议」完整度因产品线而异。
CCFlow:与 JFlow 同一套三层协议
见下一节——用同源实现对 L1/L2/L3 做「可核对」说明,而不是把某一品牌写成唯一正确答案。
5.2 .NET 阵营怎么读表
| 你更关心… | 相对更贴的路径 |
|---|---|
| 自定义流程积木、云原生编排 | Elsa、Workflow Core |
| 设计器内 Action / 商业嵌入 | WorkflowEngine.NET |
| BPMN 节点挂服务 / SQL / WebApi | Slickflow |
| 审批生命周期 + 办理页外挂 + 配置事件 | CCFlow / 同类一体化 BPM |
6. 同源实证:JFlow(Java)与 CCFlow(.NET)如何落在三层上
二者事件模型同源:同一套事件语义(如发送前 / 发送成功 / 流程结束后),三种写法并列。
本节只做机制对照与源码索引,便于读者用公开实现核对「三层模型」是否可落地;不作为唯一选型结论。
6.1 概念映射
| 三层模型 | 产品概念(JFlow / CCFlow) | 含义 |
|---|---|---|
| L1 交互层 | 前端外挂 | 浏览器侧挂流程脚本:校验、按钮、提示、联动 |
| L2 服务层 | 后端外挂 | 服务端强类型事件类:事务、改人、集成、审计 |
| L3 配置层 | 模板事件配置 | 设计器挂 SQL / HTTP / 脚本 / 业务单元等 |
服务端调度思想(示意):全局拦截 → 流程级后端外挂 → 配置事件 → 消息推送(与业务事件共用时钟、职责分离)。
用户点击发送 ├─ ① L1:前端外挂(可拦截) └─ ② HTTP → 引擎发送编排 └─ 统一事件调度 ├─ 全局后端拦截 ├─ L2:流程事件基类(后端外挂) ├─ L3:FrmEvent / 数据源执行体(模板事件配置) └─ 消息推送(同事件标记,独立配置) └─ ③ L1:发送成功后的前端副作用6.2 L1:前端外挂(以 CCFlow Vue3 为例)
约定:外挂类名以WGFlow_开头,并绑定流程号;与后端认同一套事件名(如SendWhen/SendSuccess)。
protected constructor(classID: string, flowNo: string) { if (classID.includes('WGFlow_') == false) { message.warning('外挂类名[' + classID + ']不符合规范,必须是以 WGFlow_ 开头.'); return; } super(classID); this.FlowNo = flowNo; }| 适合 | 说明 |
|---|---|
| 发送前弹窗校验、字段联动、按钮定制 | 反馈在页面完成 |
| 发送成功后的提示 / 局部刷新 | 不替代服务端写库 |
6.3 L2:后端外挂(.NET 与 Java 对照)
基类约定要点(两端一致):
- 子类重写事件方法与引擎交互
- 一个子类与一个(组)流程模板绑定(
FlowMark/getFlowMark) - 基类暴露运行时变量,降低重复查询
- 类进入约定程序集 / 包后由工厂发现
.NET Demo(CCFlow):
public class F065 : FlowEventBase { public override string FlowMark { get { return ",065,"; } } public override string SendWhen() { if (1 == 3) return "err@不符合流程发起条件,阻止流程发送。"; if (1 == 1) return "后端外挂 /App/Demo/F065 SendWhen 已经执行成功,节点ID:" + this.HisNode.NodeID + ",WorkID:" + this.WorkID;Java Demo(JFlow):bp.App.Demo.WaiGua.WaiGuaFlow继承bp.wf.FlowEventBase,通过getFlowMark()绑定流程,重写SendWhen()等——与 .NET 侧同一设计。
| 适合 | 说明 |
|---|---|
| 同步 ERP、改接收人、写台账 | 强一致、可调试 |
| 全公司统一审计 | 走全局拦截,而不是每个流程复制粘贴 |
6.4 L3:模板事件配置
在设计器为节点 / 流程 / 表单挂执行体,写入事件配置(如Sys_FrmEvent),执行体可为 SQL、WebApi、存储过程、事件类、业务单元等。
| 挂接点示例 | 配置做法(L3) | 代码做法(L2) |
|---|---|---|
| 发送前 | SQL 校验金额是否超限 | 后端外挂复杂规则 + 改接收人 |
| 发送成功 | WebApi 同步外部系统 | 后端外挂写第三方待办 |
| 流程结束 | 过程归档 | 后端关外部待办 + 前端提示 |
6.5 同源实现的启发(公平表述)
JFlow / CCFlow 证明了一件事:
三层挂接不必只存在于论文里——可以把 L1/L2/L3 做成同一事件时钟上的并列入口,让前端、后端、实施各走各的路,又在同一生命周期点汇合。
同时应看到边界:这类产品的扩展叙事偏「审批事件挂业务」,与 Elsa 那种「自定义画布 Activity 生态」不是同一赛道;选型时应按自己的主场景对齐,而不是按品牌热度对齐。
7. 用三层模型做选型:一张检查清单
评估任意引擎时,可用下面清单做「可核对」提问(建议对方给文档或样例,而不是口头承诺):
L1 交互层
- 办理页是否有稳定的发送前 / 发送后钩子?
- 能否定制按钮、提示、字段联动,且不改引擎前端内核?
- 前端事件名是否与后端生命周期对齐(或有明确映射表)?
L2 服务层
- 能否在发送前拦截并回滚?
- 能否读取并改写路由 / 接收人 / 流程变量?
- 扩展是否按流程模板绑定,并可独立部署(程序集 / 包 / NuGet / jar)?
- 是否区分「全局策略」与「单流程策略」?
L3 配置编排层
- 设计器能否挂 SQL / HTTP / 脚本 / 表达式,而无需每次发版?
- 配置执行体失败时,错误是否可观测、可阻断?
- 简单集成走配置、复杂逻辑走代码,路径是否清晰?
跨层原则
- 三层是否允许叠加,顺序是否文档化?
- 消息推送与业务脚本是否解耦(同事件点、分职责)?
- 升级引擎时,业务扩展目录是否默认不被覆盖?
8. 场景 × 层级:怎么选,而不是怎么站队
| 场景 | 更优先的层 | 原因 |
|---|---|---|
| 发送前弹窗、禁用按钮、字段联动 | L1 | 反馈即时 |
| 同步 ERP、改接收人、写业务台账 | L2 | 强一致、可调试 |
| 一条 SQL / 一个 HTTP 就能完成 | L3 | 实施可配,最快 |
| 全公司统一审计 / 组织策略 | L2 全局 | 一次拦截,全流程生效 |
| 前端团队强、后端紧 | L1 + L3 | 交互与简单集成分流 |
| 后端团队强、要长期演进 | L2 为主 | 可测试、可重构、可版本管理 |
| 标准 BPMN 微服务编排 | L2 Activity/Delegate + 可选 External Task | 积木与 Worker 更贴 |
| 政企审批 + 表单一体化交付 | L1+L2+L3 产品化是否齐全 | 三层缺一都会转嫁成本 |
9. 结语
流程引擎的竞争力,不只在「把图画出来」,更在:
业务能不能稳稳挂上去——挂在交互层、服务层、还是配置层——并且始终不污染内核。
三层挂接模型给出的是一把跨产品、跨语言的尺子:
- L1守人机边界
- L2守系统真相
- L3释放实施生产力
Java 阵营里,Camunda / Flowable / Activiti 把 L2/L3 做到了行业标杆,L1 多依赖外围表单与自建工作台;JFlow 则把三层做成办理页协议与设计器配置的并列能力。
.NET 阵营里,Elsa / Workflow Core 偏开发者编排,WorkflowEngine.NET / Slickflow 偏设计器与节点执行体,CCFlow 与 JFlow 同源,用前端外挂 / 后端外挂 / 事件配置覆盖三层。
没有绝对的第一名,只有与场景对齐的挂接方式。
选型时,把对方的名词翻译回 L1/L2/L3,再用第 7 节清单逐项核对——比比较口号更接近工程真相。
附录 A:术语速查
| 本文用语 | 常见等价叫法 |
|---|---|
| L1 交互层 | 前端外挂、表单脚本、UI Hook、设计器插件 |
| L2 服务层 | JavaDelegate、Listener、StepBody、Custom Activity、后端外挂、Action |
| L3 配置层 | 事件配置、CodeAction、Script Task、表达式、SQL/HTTP 执行体 |
| 生命周期时钟 | 事件列表、Listener event、发送前/后、任务 create/complete |
| 不改内核 | 业务在扩展点 / 外挂程序集 / 配置表,不在发送内核 |