-
前言
从今天看工作流引擎可以分成2种:
- 用于流程管理领域的工作流引擎
- 用于服务或计算调度与编排领域的工作流引擎
1. 基本概念
工作流引擎是将控制与逻辑解耦的一种实践,将流程抽象成工作流控制,而将各个业务逻辑抽象成节点。
代码本身就是一种流程控制,由顺序、判断、循环组成
工作流简介
jBPM5、Activiti、flowable、camunda

工作流是有节点与连线组成的系统,与平时画流程图类似。
基本的流程图,节点包括:起止节点、顺序节点(处理节点)、判断节点等。
这里有2种形式的工作流:状态机工作流、人工工作流;状态机可以用在分布式计算领域,业务调度领域等等;人工工作流可以用在人工的审批上。
-
状态机工作流

状态机工作流增加了持久化功能,由此在持久化基础上的自动重试机制。
从数据上看,包括流程以及任务
流程包括:
- 节点node;
- 连线line;
任务包括:
- 实例id;
- 当前节点current_node;
- 当前状态status;
- 当前重试次数retry_times;
-
人工工作流

与状态机工作流相比,人工工作流多了外部触发功能,至少是存在一个或者多个节点是待外部确认、审核之后才能推进整体业务流程。
比如退款流程,需要负责人审批通过后才能进行打款;
bpm_workflow_instance : 工作流实例表,表示一个具体执行的业务流 ;
bpm_task_instance : 任务实例,将工作流的每个节点当做一个任务实例存储下来,描述一个工作流实例里 每个节点的具体状态;
bpm_param_instance : 参数实例,工作流或者任务实例的上下文入参快照
bpm_timer_task:处理定时任务表,比如人工节点未审批自动超期等
2. 场景
人工审批工作流
工作流迭代15关:
这里显然都是外部触发的工作流引擎,虽然分成了不同节点,但节点本身的业务都是外部的确认
-
工作流链表本质,任务相当于当前指针
-
会签节点
会签节点就是一个大节点,里面有很多审批人,当这个大节点里的所有人都审批通过后,才能进入下一个节点。
将节点增加了组合设计,会签节点各个子节点之间是与的关系
流程也可以看成一个节点组,各子节点之间是穿行的关系

节点状态:
- Ready: 可以进行审批操作的简单节点是Ready状态。
- Complete: 已经审批完成的节点状态。
- Future: 现在还没有走到的节点状态。
- Waiting: 只有复杂节点有该状态,表示在等待子节点审批。
就流程本身,只要流程、当前节点就能往下走,但如果为了能够画出图,就需要记录各节点的状态、状态转变的事件等等。
-
并行节点
并行节点是一个包含很多审批人的大节点,这个大节点里任何一个人审批通过,则该节点就完成。
并行节点也是组合节点,各个子节点之间是或的关系。
增加了状态:
- Skip:当一个并行节点的子节点状态为非(Ready, Waiting)时,其它兄弟节点及其子节点的状态被置为Skip。
-
条件节点
这里文章中把条件节点也看成组合节点,只有满足条件的子节点才能进入接下来的审批;
这样适合它给出的示例,但并不太适合所用情况;
条件节点就应该是判断,判断不同的条件,走向不同的后续节点。
-
函数节点
根据发起人不同选择不同的审批人,通过映射函数算出审批人。如get_主管("钱某") 得到钱某的主管 李某。

-
驳回功能
目前看,工作流已经是一个有向无环图了,而任务走过的路径依旧是链表,故而该工作流示例中,需要记录前向节点pre。
这样可以驳回到任意走过的节点
-
超时功能
给当前节点增加抵达时间、超时时间,可以通过轮询,也可以通过MQ的异步来检测超时
超时则next
-
代理功能
文章给出的是将当前节点变成并行节点,拉入其他人,如果其他人通过,则该节点通过

-
约束条件
文章中是前置条件与后置条件,我觉得都是约束条件,满足前置条件才能进入该节点,满足后置条件该节点才能审批完成。
后置条件增加一些约束,条件满足才可审批;前置条件在并行节点时能滤除一些审批。
前置与后置类似于网络的interceptor拦截,实现上在节点的父类中增加hook来实现。
-
审批进度
树节点的距离怎么算?树中两节点的最短路径
当前节点到根节点的距离 / (当前节点到根节点的距离 + 当前节点到结束节点的距离)
这里由于判断节点的不同实现,导致算法与文章中不同。文章中的判断节点相当于同深度,而这里可以不是。
业务调度工作流
工作流与分布式计算
工作流将流程与节点,也就是控制与逻辑进行了解耦,审批流程这种重人工接入的工作流并没有发挥出工作流引擎的全部优势,通过进一步将node抽象成计算节点,使工作流的调度更像是计算的编排。
-
任务池
不同的服务代表着不同的计算逻辑,他们从任务池就获取各自状态的任务进行计算,并将任务转变成下一个状态,重新放入任务池,直到计算完成。一个完整的流程实例的每个节点的计算和执行,会被打散到不同服务上去执行,从而实现了分布式计算。
这个重点是任务状态的转换,任务本身如果还有状态转换的逻辑,就可以将状态转换与节点解耦开,节点执行get_next_status(任务),然后重新放入任务池即可。
这个任务池也可以使MQ,与其等待遍历,不如发布订阅。
节点可以看做是不同服务。
整体看是一种通过任务池(MQ)去中心的一种方式
-
工作流

这里工作流引擎控制整个流程,每个计算函数只负责计算,并不负责状态的流转。工作流引擎将某个节点的任务,提交给指定的计算函数执行,完成后按流程交给下一个节点,同样下一个节点也是将任务提交给计算函数执行,依次类推。
工作流引擎相当于调度器,哪个任务ready了,它就获取任务的下一个节点,并交给相应的计算函数(服务)进行计算。
这里函数计算编排是明显的主控方式,可以对流程进行编排,也可以对结构进行探寻,如果失败了,更方便控制重试。
这里可以去看看分布式计算的算子,图计算的过程等等
这种通过工作流引擎可以配置业务,如果把各个业务服务当成节点,那节点越多,可配置的空间越大,越适合使用工作流引擎;比较适合微服务比较多,或者上图的函数编程的场合。
业务调度工作流引擎是总管,各个节点负责自己的逻辑,逻辑之前的调度交给它来完成;
这里要对各个服务的输入、输出的接口进行标准化,否在在编排的时候会出现不匹配问题。
示例如下:

与任务池相比,通过工作流引擎将流程与计算解耦开,有更强的编排能力。
与人工审批相比,节点之间的逻辑关系会有很大不同:
- 组合节点中的与、或的关系不存在的,进一步代理的功能也不会存在
- 函数节点预计也不会存在
- 节点携带的数据差异,审批节点携带的是表单,而计算节点携带的格式各异
相同点:
- 顺序与条件是节点是相同的;
- 超时控制是相同的
难点:怎么设计接口,怎么处理数据,使编排能力更强。
从流程的复杂度上看,业务调度工作流复杂度上会降低;但从数据的复杂度上看,业务工作流复杂度会上升。
我感觉,这两种工作流虽然有相同之处,可以相互借鉴,但却并不是一个东西,节点差异与数据差异太大,而流程依赖于节点。
3. 小结
工作流引擎是把流程与节点,控制与逻辑分离的一种实践方式。
下边主要来对人工审批这种节点进行思考
关系
流程是节点之间的关系,一般来说有2种
- 顺序流程
- 判断流程
这其实像极了编程语言:顺序、判断、循环(一种特殊的顺序)
大约可以表达为:
class Edge {
private Node fromNode;
private String condition; // 可以为空
private Node toNode;
}
关系在数据结构中,以指针的形式存在;在数据库中,以中间表的方式存在;
关系依赖于节点
节点
节点包括着逻辑关系,也包含着:
顺序节点、判断节点;
组合节点、函数节点;
在节点上,还有会重试的机制;
节点还有功能的属性,不同场景,他们的功能空间差别很大;
人工审批功能单一,就是通过;
class Node {
private Long id;
private String name;
private int type;
private long expire;
private Node next;
}
class ComposeNode extends Node{
private List<Node> childrenList;
}
class JudgeNode extends Node {
// 相当于一个表达式
private String rules;
private Node falseNext;
}
class FunctionNode extends Node {
// 相当于一个getter函数,取对象的某个值,这个值一般与发起者相关。
private String strfunction;
}
流程
流程包括着节点与关系,目前节点中已经包含了关系
class Process {
private Long id;
private String name;
private Node root;
}
指明头节点即可,链表形式
流程实例与节点实例
具体流程
class ProcessInstance {
private Long id;
private Long userId;
private Date createTime;
private int status;
// 对应的流程
private Flow flow;
private NodeInstance currentNode;
// 记录该实例对应节点示例的状态
private List<NodeInstance> nodeList;
}
class NodeInstance {
private Long id;
private Long taskId;
private Node node;
private int status;
private Date startTime;
private Date operateTime;
}
这里具体的流程实例与流程本身的概念做了区分,流程可以通过工具来进行设置,是固定的,与发起者无关的,比如物资审批的流程要怎么走;流程实例就是由具体人发起的申请,申请办公物资。
可以在发起具体流程的时候,将各个节点实例生成,这样在往下怎么走,直接查询NodeInstance即可了。
另外还可以将具体的函数节点变成普通节点。
4. 规则引擎
基本概念
规则引擎
规则引擎本质是编译器,简化版的编译器。规则本质上就是代码,可以是表达式、函数调用、接口调用,规则是代码的一个子集,故而规则引擎也就是简化版的编译器。识别变量、函数、运算符等等。
对于简单的表达式,给出了Spring自带的解析器:
// 1. 构建解析器
org.springframework.expression.ExpressionParser parser = new SpelExpressionParser();
// 2. 解析表达式
// 数字类型
Expression integerExpression = parser.parseExpression("100 * 2 + 400 * 1 + 66");
// boolean类型
Expression booleanExpression = parser.parseExpression("1>0 && 1<2 || (4%2=0)");
// 3. 获取结果
int integerResult = (Integer) integerExpression.getValue();
System.out.println(integerResult); // 结果:666
boolean booleanResult = (Boolean) booleanExpression.getValue();
System.out.println(booleanResult); // 结果:true
另外给出了开源的Drools,以及Luaj的脚本。
最后是自定义规则,这里与Elasticsearch的查询有些类似
{
"conditions": [
{"args":["flightNo","CZ8123"],"method": "equals"}
],
"operations": [
{"args":["flightNo","CZ81","OQ23"]," method ":"replace"}
]
}
逆波兰算法解析表达式
来看看“逆波兰算法在规则引擎中的运用”
这里用到了表达式解析:
$合同类型$ = ‘商务合同’ || ( $合同总金额$ > 1000000 && $合同总金额$ < 2000000 )
,解析为后缀方式:
合同类型、商务合同、=、合同总金额、1000000、>、合同总金额、2000000、<、&&、||
可以利用栈来输出结果:当为数值时压栈,当遇到表达式,从栈里面拿出两个数进行比较(运算)。
过程就分成2部分:表达式解析成后缀方式、通过该后缀方式进行逻辑计算。
对于第一部分,文中给出了基本思路:
转换为逆波兰表达式的基本思路是:
1、需要两个栈来存放“操作数”(如:合同类型)、运算符(如:||)
2、每个操作符都有自己的优先级
3、如果是操作数,则放入操作数栈
4、如果是运算符(不包括括号),则与运算符栈顶元素进行比较,如果优先级大于栈顶的元素,则直接入栈,如果小于,则将栈顶的元素取出,入操作数栈
5、如果运算符是“(”,则直接放入运算符栈顶,如果运算符是“)”,则从运算符依次取出元素放入操作数栈,直到遇到“(”
对于逻辑运算部分思路是:
1、遍历队列,如果是操作数,则入栈
2、如果是操作符,则从栈顶取出两个操作数进行运算操作
3、将最终结果返回
变量与函数思考
工作流除了节点与关系,还有包含的负载payload,一般是一个表单;
表单可以看做一个json,在规则中,需要识别这些json的字段,当成变量来计算;
这里给出的范式是 特殊字符:$$
显示的字段与json中的字段可以做个映射;
函数首先也需要识别符:
可以是确定的,如: GET_USER_MANAGER( userId ),识别到之后,就调用相应的函数,这种往往有入参;
可以是泛化的,如: GET_ORDER_NAME(),取json表单中字段的值;【这种也可以同变量来表达】
这样看,流程与表单是分不开的,条件的判断依赖表单的数据
表单的难点是SELECT等形式的链表,需要将另外一个表单的数据当做该字段的可选值;被选可能是配置类型的静态数据,也有可能就是常用的人员、组织等表
几种规则引擎
EasyRules,对条件的判断
Drools,更像脚本一些
Aviator,各种表达式的动态求值
5. 开源实现Activiti
简介
Activiti 的发展线路:
- Tom Baeyens 是 JBPM 的创始人,因为理念不合 Tom Baeyens 加入 Alfresco 公司后又发布了 Activit 5
- JBPM 因此放弃 JBPM 4 架构,完全基于 Drools Flow 重新开发 JBMP 5,但他们同样都支持 BPMN 2.0 规范
- 从正统的延续来看,Activit 5 更像是对 JBPM 3、JBPM 4 的延续,所以国内大多企业都选择 Activit
JBPM、Activiti 区别
- JBPM 推翻历史架构,重新使用了 Drools Flow 作为工作流架构
- JBPM 采用的是 LGPL 开源协议,对源码修改需要商业授权
- Activiti 采用了更为宽松的 Apache License 2.0 协议
BPMN: Business Process Model and Notation,构造业务流程的规范
BPMN与工作流
概念
参考
重要的几个Service
RuntimeService:运行时的Service
RepositoryService:存储Service
TaskService:任务Servcie
HistoryService:历史数据Service
基本流程:
定义流程 -> deploy process -> start instance process -> task query/complete
-
定义流程:
process -> startTask -> userTask -> endTask
-> sequenceFlow (sourceRef -> targetRef,可以定义condition),
它的实现应该是关系也存在的方式,抽象成了sequenceFlow
-
repositoryService deploy
-
runtimeService startProcessInstacneByKey
-
taskService具体执行
-
businessKey,业务的id,如订单id
挂起状态
RepositoryService通过processDefinitionId去挂起流程,相应的intance的task 不能再执行了,
runtimeService也可以挂起某个processInstance,只有该install的task不能执行,
variable:流程变量
流程变量起到表单作用,通过map来传递数据,好对该map数据进行关联、判断等
条件的判断直接通过${}
来取数据,或者进行表达式计算即可
流程变量有2种:Global类型与Local类型
${xxx}
UEL表达式
通过流程变量map,将流程与数据分割开来
通过taskService可以在task中修改流程变量
gateway:网关
网关是组合节点的意思
排他网关,多条连线只走一条 => 判断节点
并行网关 => 会签功能,组合节点 => 要用节点的话,相当于前处理忽略Condition,后处理等待前节点全部完成
包含网关 => 结合排他与并行,会计算连线上的条件
它的实现不是组合的方式,因为它是前后两个节点的方式表达组合。
Candidate:候选(组任务)
=> 并行节点的意思
CandidateUsers,候选者需要认领(claim)任务,然后才能处理,它解决不了一致性问题
还可以通过setAssignee来退换
=> 通过角色进行任务审批的时候
数据分析
节点(Activity):

节点包括:Event、Task、Gateway,
流程定义:
<process id="myLeave" name="员工请假审批流程" isClosed="false" isExecutable="true" processType="None">
<startEvent id="_2" name="StartEvent"/>
<userTask activiti:exclusive="true" id="_3" name="提交请假申请" activiti:assignee="worker"/>
<userTask activiti:exclusive="true" id="_4" name="部门经理审批" activiti:assignee="manager"/>
<userTask activiti:exclusive="true" id="_5" name="财务审批" activiti:assignee="financer"/>
<endEvent id="_6" name="EndEvent"/>
<sequenceFlow id="_7" sourceRef="_2" targetRef="_3"/>
<sequenceFlow id="_8" sourceRef="_3" targetRef="_4"/>
<sequenceFlow id="_9" sourceRef="_4" targetRef="_5"/>
<sequenceFlow id="_10" sourceRef="_5" targetRef="_6"/>
</process>
发布相关表
- ACT_RE_DEPLOYMENT:发布,可以对其进行管理
- ACT_RE_PROCDEF:流程定义,与发布是1:n 关系,一次发布可以定义多个流程
- ACT_GE_BYTEARRAY:具体的节点并没有使用表来存储,而直接存储的xml
运行过程相关表
- ACT_HI_PROCINST:流程示例,一次执行的流程
- ACT_HI_ACTINST: 这个表定义将历史节点都保存在其中,历史节点包括着StartEvent、以及userTask
- ACT_HI_TASKINST: 定义的历史的任务,
- ACT_RU_TASK: 正在执行的任务
- ACT_RU_EXECUTION: 执行,对执行的记录,这个表具体作用不太清晰
- ACT_RU_VARIABLE、ACT_HI_VARIABLE:存放流程变量的
- ACT_RU_TIMER_JOB:定时器事件相关
对比
-
数据结构
连线的抽象并不是用的指针,这种方式,而是具体的模型SequenceFlow
-
存储
流程定义与流程实例不同存储方式,流程定义采用xml存储方式,流程实例通过HI、RU等进行存储
HI存储已经走过的数据,包括是processInstance、节点Instance(activity、task)等
RU存储当前的数据,通过当前task查询到流程进行到了哪里
-
判断
在SequenceFlow中可以定义Condition,在某个节点complete后,选择与其关联的SequenceFlow,如果包含Condition,则执行相关UEL,对过程变量进行判断。由此判断放在连线上,而不再需要条件节点。
-
会签
通过gateway来实现,并行网关会等待各个节点完成,完成后再网下走;
排他节点是种控制,确定2者选一
-
并签
通过组任务,设置各个候选人来完成,各候选人要认领
-
流程与数据解构
通过map,将节点用到的数据进行了抽象,从而将流程与数据进行解耦,引擎只关注流程
-
Task与Event分开
起止节点只是标志,并且通过Task整合UserTask与ServiceTask
定时器也是通过Event来实现,将timerEvent (boundaryEvent)附加到UserTask来实现
6. 结尾
从对比来看,关系通过真实的对象来表达更好一些;
判断功能不再通过节点,而是通过关系来定义它们;
组合的概念通过插入成对的gateway进行了,有的是在前gateway进行排他判断,有的是在后gateway等待条件满足,这样看gateway相当于interceptor的概念,可以在逻辑前后进行处理。
组合模式的思维习惯影响到了对工作流的认识
开始觉得流程需要依赖节点提供的数据,而关系实例化以及通过map结构数据依赖之后,流程不再依赖节点