构建企业数字化转型之某大厂审批流引擎核心技术揭秘
1. 定义
企业数字化转型的道路上,必不可少的一项就是审批流程的线上化,都需要搭建自己的审批系统,而审批系统的核心就是审批流引擎
什么是审批流引擎呢?
是一个工具包,用来驱动审批类业务的执行,通过使用这个引擎,可以方便快速的实现各类审批相关的功能,如:审批、同意、或签、并签、串签、拒绝、驳回、回退到任意节点、前加签、后加签、平行加签、平行减签、外部调用节点、抄送、填写、主子流程、分支、选择、流程预测、流程仿真、取回、撤销、传阅、以及各种加减签与或并签的任意嵌套。
2. 现有技术
业内的现有技术一般分为三大派系,一个是直接使用开源的工作流引擎作为审批流引擎,一个是在开源的工作流引擎的基础之上进行的二次开发,最后一个是完全的自研
2.1 开源的工作流引擎
这种技术的一个基本的逻辑就是把工作流引擎当做审批流引擎来使用,常见的就是activiti,也有flowable、camunda,但由于它们二者都是直接或间接来自于actitivi,虽然做了一些改进,但区别不是很大。
以activti为例,一个典型的技术架构如下图所示:
实践中,使用BPMN2.0协议作为审批流流程定义的载体,使用activiti工作流引擎解析并执行这个流程,引擎通过回调的方式来执行自定义的审批业务,审批业务通过API的方式来调用引擎,如此循环,直到结束。需要注意的是,Client可以调用其中的任何一层
这种技术优缺点如下
优点:
- 开源的多年成熟技术
- 文档资料比较多
缺点:
- 对绝大部分审批业务没有直接的API或回调支持,需要自己开发
- 一些复杂的场景,如:或并签与加减签的无限嵌套,支持的力度非常的弱,有时需要绕过工作流引擎,而去直接修改数据库本身,这样带来的危害是比较大的
- 开源的代码量巨大,50W行左右,遇到一些棘手的问题,又找不到资料,必需看源码找问题的时候,会非常非常的痛苦,对开发不友好,学习成本高
- 与BPMN协议依赖性较强,因审批流协议与BPMN协议的交集较小,需要BPMN协议的扩展功能来表达审批流语义,导致语义含糊,表达力不强
从根本上来说,直接把工作流引擎当做审批流引擎是混淆了工作流与审批流的概念,把一些审批流的逻辑范畴强行加到工作流上,把个性当共性,最终导致了复杂性的爆炸,审批业务做的不伦不类,无法做大做强。
2.2 二次开发
这种技术是在开源的工作流引擎的基础之上进行的二次开发,把activiti之流当做底层的技术底座,进行了一些封装,一般的技术架构如下图所示:
使用的时候,Client直接调用审批流API接口,然后进行相应的逻辑处理,最后调用工作流引擎,完成整个流程
这种技术优缺点如下
优点:
- 屏蔽了底层技术细节,接口对使用者友好
- 对大部分审批业务提供了直接的支持
缺点:
- 因其技术底座使用了activiti之流,故其带来的困扰一点没有减少,直接对引擎的维护者不友好,间接提高BUG率,进而影响上层业务,不稳定性增大
- 对一些特定的RDBMS的支持较差,如:达梦国产数据库、自研的内存数据库,需要做大量的二次开发,修改底层源代码
- 对数据库的异构性支持几乎没有,会直接影响本地事务的一致性
2.3 自研
每一家的解决方案各自不同,使用的技术底座不同,协议不同,持久化机制不同,接口参数不同,但一个相同的逻辑就是有针对性的对审批业务提供了直接的支持,不在混淆工作流与审批流的概念,对开发更加的友好,开发更加的简单,同时对现有的审批业务的范畴之内,提供了一些扩展能力,虽然有上述优点,缺点也不容易忽视,如下:
- 如果审批业务有了进化,提出了一些新的概念,而扩展性又覆盖不到的话,那么就需要开发引擎本身来进行支持,这对于软件的分发、升级、部署、数据一致性方面提出了挑战
- 与某一持久化机制强绑定,无论是RDBMS、KV还是文件,显得很笨重,集成的时候,会引起与现有基础设施不一致的问题,很是麻烦
3. 我们的技术
我们采用的是自研的方式,走了一条很不一样的技术路线,完全自研了工作流引擎框架,在这基础之上,构建了自己的审批流引擎,具有如下的特点:
- 与审批业务强耦合,开箱即用
- 使用简单,贫血模型,无状态依赖
- 本身代码量少,1W行左右,易于维护
- 只是一个jar包,可嵌入到任何业务系统中
- 不依赖具体的数据库,无论关系型的还是非关系型的,对持久化提供了扩展
- 不依赖某一个流程协议,如:BPMN,而是使用类似流程虚拟机的形式,对审批流定义本身提供了支持
- 可扩展自己业务特定的流程节点
- 可扩展自己业务特定的执行人指定逻辑
- 可扩展自己业务特定的流程流转事件
- 依赖少,只依赖slf4j、kryo等必要组件,不依赖任何工作流引擎,如:activiti、flowable等
- 可以方便的集成进Spring MVC,Spring Boot等
- 可以方便的控制本地事务、事务集成等
- 可运行时确定具体审批人,而不是在定义期
- 扩展性合适,既不是什么都可以扩展,也不是什么都不可以扩展,即:只扩展那些与审批相关的点
下面详细阐述我们是怎么实现的......
3.1 理论基础
工作流引擎的两大模块,一个是流程定义,一个是流程执行
现有的流程定义都是使用BPMN协议进行描述,这个协议实在是太庞大了,本着第一性原理,我们对工作流引擎的本质进行了深入的挖掘,重新对工作流进行了定义,提出了元模式的概念。
具体来讲,详细研究了43种工作流控制模式,这些工作流控制模式的组合基本上就能覆盖住绝绝绝大多数工作流场景,一般来讲,对43种直接进行支持就可以了,但43种未免也有些过多,加上以后也会出现有44、45、46种模式,所以我们更进一步的对这43种进行泛化与抽象,最终得出了6种工作流控制元模式,通过这6种元模式的组合与API调用,就能够直接或间接支持43种控制模式,同时,对未来出现的模式也有一定的支持,这6种为:
- 事件:事用来表示发送一个事件,也可以表示等待接收一个事件,事件是广播的方式传播的
- 活动:泛指各类活动任务,如:人工任务、脚本任务、服务任务、用户任务、审批节点任务等,是与各个业务相关联的
- 子流程:也是一个流程,用来重用或消除重复、或者减少主流程的复杂性
- 分裂:所有需要并行做的事情,都要用此元素,这里并不规定分裂逻辑,以及分裂后的分支的数目
- 汇聚:同步并行分支,在此之后,多条分支合并为一条,这里也并不规定同步逻辑,以及多次同步的方法
- 流转:以上五种元素的流转方向,一个源,一个目标,流程从源流转到目标
如下图所示:
对于流程执行来讲,业内一般采用以下两种技术:
- 基于FSM的技术:由于FSM天生的缺陷,对于状态的扩展能力较弱,应用面较窄,只能在特定业务领域有较好的落地应用
- 基于Token的技术:市面上最主流的技术,优点是理论模型较健全,缺点是落地较复杂,实现繁琐,理解起来较困难
我们使用的并不是二者之一,而是基于无限状态机模型的一种技术实现,如下图:
这种技术的本质是个循环,其设计灵感来自于CPU的执行逻辑,不断的读取状态,接下来对状态进行处理,处理的结果也是一个状态,然后继续,直到没有状态或状态暂时无法处理,这里面还有个类似与中断的唤醒状态的逻辑,鉴于保密原则,此处不在展开
有了这些理论基础,接下来就是落地实现了
3.2 工作流引擎框架
在落地实现的过程中,我们发现各种垂直业务领域的工作流都有着一些共性,同时也有着一些个性,而且,这些共性是框架维度的,并不是库维度的,因此,我们对此进行了抽象,提出了工作流引擎框架的概念。具体来讲,它是工作流引擎的模板,通过使用这个框架,可以快速的开发实现各种业务域的“工作流”引擎,包括审批领域的审批流引擎、任务领域的任务流引擎、采购领域的采购流引擎等,它提供了基本的流程定义与流程执行能力,及其强大的扩展能力
有别于传统的工作流引擎,它们对特定业务领域(如:审批流、任务流)的直接支持实在是太弱了,根本原因在于它们太通用了,通用到了为了支持各种工作流场景,把一块木板变成了木削,以至于当我要制造一个柜子的时候,需要从木削开始加工,这极大的增加了工作量,在加上学习成本,可以说,这不是在提高效率,而是在降低效率。所以,一般来讲,通用性越强,适用性越低,反之亦然。
具体到实现层面,框架一共分为两层
第一层为ISM,也就是无限状态机,是个微内核,其概念模型如下图所示
Func是一个状态转换器,其有两个方法,一个是transfer(),用来消费一个State,然后,生产一个State,另一个是eventHandle,用来消费一个消息,然后生产一个State;Funs维护着Func;Case是一个实例,其内部维护着所有的状态(State),记录着状态变化的历史,每一个状态包含:
- id:唯一标识本状态
- tobeStarted:需要对本状态进行处理的Func集合
- ing:进行中的Func集合
- toIngMsg:给进行中的Func发的消息
- alreadySend:已经处理完消息的Func集合
第二层为PVM,也就是流程虚拟机,通过这个虚拟机可以进行流程定义、流程执行、与中断唤醒等操作,如下图所示
流程定义(Definition)继承了Funcs,从而有了管理Func的能力,流程中的节点(Node)继承了Func,从而有了消费与产出State的基本能力;由Transition来表示两个节点之间的流转,指定了来源节点与目标节点;流程实例(Instance),使用了流程定义,从而可以据此来发起一个流程,同时,也使用了Case,进行流程实例的流转;对于节点来说,具象化了工作流领域内的常用的一些节点及其对状态的处理逻辑,包括:
- 活动节点Activity,以及以此为基础的子流程节点SubProcess
- 并发分裂网关Fork,以及与其配套使用的同步汇聚网关Join
- 事件节点Event,以及其两个重用分类,发送事件节点TxEvent,及其派生类EndEvent,接收事件节点RxEvent,及其派生类StartEvent
有了这个框架之后,就具备了基本的工作流流程定义与执行能力,能够通过此来开发上层的审批领域的审批流引擎了
3.3 审批流引擎
审批流引擎设计到的点非常多,由于篇幅所限,这里只分享最重要几点
第一个是在审批流流程定义层面,我们继承了Activity节点,派生出了审批节点、抄送节点、填写节点等
第二个是在API层面,我们直接提供了审批、同意、或签、并签、串签、拒绝、驳回、回退到任意节点、前加签、后加签、平行加签、平行减签、外部调用节点、抄送、填写、主子流程、分支、选择、流程预测、流程仿真、取回、撤销、传阅等方法,可以直接调用
第三个是在流程执行层面,整个流程是个PVM,每个审批节点内部也有个PVM,这么做的目的是因为审批业务的复杂性,同时也是一种现有能力的复用,由于PVM的足够轻量,使得这种模式成为了可能,如下图所示:
第四个是在流转层面,每一次流转的前后变化都存储在一个上下文中,通过对比前后变化,就能够得到一些信息,包括:
- 哪些节点已经开始执行了
- 哪些节点已经完成了,包括取消
- 哪些任务生成了
- 哪些任务完成了,包括取消
从而,可以据此信息来进行下一步的处理,如:持久化、回退等
第五个是通过访问者模式,提供了对流程定义合法性的发起前的检查能力,同时提供了一些默认实现,也可自行扩展
第六个是直接提供了流程预测的方法,该方法实现的内在逻辑为:在内存中模拟整个流程的流转,把其中涉及到的一些预测信息存储在一个Map中,这种方法有别于传统的使用数据库的方式,即快又不污染数据库中的数据,同时也可并行执行,在预测过程中,不会阻塞正常流程的流转
第七个是在节点扩展性方面,可以通过派生自审批节点的方式自行扩展自己特定审批逻辑的审批节点,也可通过派生自Activity、甚至是Func的方式,来扩展特定功能的节点
第八个是在分支条件表达式方面,对具体的表达式语法、语言进行了解耦,由其具体的表达式解析类进行表达式的解析,只要把Bool结果告知引擎即可
第九个是对流程实例变量的弱化,弱化为一个Map,这样可以表达更多的信息
4. 未来展望
- 在完善审批流引擎本身能力的同时,提供丰富的文档与示例,方便大家使用
- 基于审批流引擎,研发下一代审批流系统+,提供完善的集成能力,同时提供一些通用工作流能力
- 建设审批流前端页面组件能力,方便流程定义等前端复杂功能的实现