Activiti原理分析(一)从一个简单流程开始

简介: 本系列文章将基于 Activiti 6 源代码对 Activiti 的原理进行深入剖析,让读者快速知其然,同时知其所以然。 第一篇文章将分析一个简单流程在 Activiti 中的执行流程,借此把 Activiti 底层的一些概念快速梳理一遍,让读者对 Activiti 的底层实现有个大概了解,知晓每张表的作用。内容都是笔者从源码里分析出来或者核对过的,比一般的文档更加翔实靠谱。

Activiti 的历史


一般软件都是从 1.0 版本开始的,而Activiti 一出来的版本就是 5。可见它不是从零开始的,它是基于 jbpm 4 的技术体系启动的,jbpm 是在 Activiti 之前最出名的开源流程引擎,与 Drools 师出同门(JBoss),jbpm 5 决定完全放弃 jbpm 4 采用的架构,基于 Drools Flow 完全重写,但是 jpmn 的创建者 Tom Baeyens 不同意,它离开 JBoss,继续采用 jbpm 4 的继续体系开发了 Activiti 5,可见 Activiti 本质上是 jbpm 4 的延续。

同样的事情也发生在了 Activiti 6 上,团队与公司在 Activiti 7 的发展路线上产生了冲突,导致团队出走,在 Activiti 6 的基础上开发了新的引擎 Flowable,而 Activiti 7 完全由另外的团队维护了。

所以 jpmn, Activiti 和 Flowable 这三个项目的继承关系如下(相同颜色表示由同一团队维护):


image-20201114121926060.png


本系列文章将基于 Activiti 6 源代码对 Activiti 的原理进行深入剖析,让读者快速知其然,同时知其所以然。

第一篇文章将分析一个简单流程在 Activiti 中的执行流程,借此把 Activiti 底层的一些概念快速梳理一遍,让读者对 Activiti 的底层实现有个大概了解,知晓每张表的作用。内容都是笔者从源码里分析出来或者核对过的,比一般的文档更加翔实靠谱。

Activiti 的使用与调试


因为本系列文章更注重的是原理分析,所以对使用方面就不多讲了,推荐看下面的资料:

Activiti 的组成


到 Activiti 6 为止,Activiti 已经不仅仅是一个流程引擎了,它包含三个引擎:

  • 流程引擎:Activiti 一直以来的主打产品
  • 表单引擎:支持可配置的表单
  • 决策引擎:一个实现了 DMN 规范的自动化业务决策的引擎,至于什么是 DMN 规范,可以看另一篇文章 业务规则的通用模型 —— DMN简介

在 Activiti 6 中,表单引擎和决策引擎还处于刚刚起步的阶段,就不做重点分析了,我们主要看流程引擎。

源码结构


Activiti 自己实现了一套 Command 执行框架,任何操作对 Activiti 来说都是一个 Command,比如部署流程执行的就是一个 DeployCmd,发起流程就是一个 StartProcessInstanceCmd,他们都继承了 Command 接口:


image-20201114163154587.png


Activiti 任何操作的源代码基本都是用 commandExecutor 执行一下 Command 就结束了,比如流程部署的源代码:


publicDeploymentdeploy(DeploymentBuilderImpldeploymentBuilder) {
returncommandExecutor.execute(newDeployCmd<Deployment>(deploymentBuilder));
  }


为什么要把所有操作都封装成 Command ,而不是像通常的业务代码一样直接裸写呢?其实是为了对核心逻辑的执行上下文进行统一管理,比如 Command 级别的数据库查询缓存,统一开启事务环境,在 Command 执行结束时对落库进行统一优化。

比较有意思的一点是,在 Command 中任何一次 insert 方法调用都没有真的将数据存进数据库中,而是存进了一个 Command 级别的 cache,在 Command 结束时统一优化并落库,这里的优化可以先简单想象下,比如在 Command 分别调用了两次 insert,这里给优化成一条 insert 语句批量插入,当然其中的优化远不止这么简单,后面我再写专门的文章介绍 Activiti 的数据库访问优化

一个 Command 的执行流程如下:

image-20201114170540547.png


  • CommandInterceptor: 首先会经过拦截器,就是 Command 真正执行之前进行一些操作,其实这里一般情况下会有多个拦截器,虽然我只画了一个。用户可以在配置流程引擎的时候通过 ProcessEngineConfigurationImpl.getCustomPreCommandInterceptors.add(...) 添加自定义拦截器,默认情况下只有两个拦截器:
  • LogInterceptor
  • TransactionInterceptor

自定义拦截器必须通过 Impl 类才能加入,如无必要,并不推荐加自定义拦截器,毕竟这会触及底层实现,随着 Activiti 升级,这些可能会变动

  • Command:会执行其实现类的 execute 方法
  • Operation:在 execute 的时候可以动态添加一些 operation 到执行队列中,这些 operation 会在 execute 逻辑执行结束后继续在 Command 的上下文中执行,另外,operation 在执行同时也可以继续添加 operation,它给 Command 的执行带来了更大灵活性。当流程中有很多自动节点需要一次性执行完时,就是通过这种机制,不断地把下一个节点的 operation 加到执行队列中,实现流程的流转。

以上对于源码结构的解释也许过于抽象,下面我们将结合具体案例进行分析,同时在分析过程中去发现 Activiti 内建的数据库表的功能。

一个简单流程的部署,发起与执行


以下面的流程为例:


image-20201114205121000.png


这个流程可以简单理解为,我在开始节点下订单后,柜台负责收钱后把钱送到钱庄;仓库把原材料交给工厂后,工厂生产出订单需要的产品,这两个过程同时进行,所以一开始的时候引入了一个并行网关,因为只是简单的例子,所以每个节点都是采用的默认配置,UserTask 都设置了一个具体的办理人,xml 配置如下:

<?xmlversion="1.0" encoding="UTF-8"?><definitionsxmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:xsd="http://www.w3.org/2001/XMLSchema"xmlns:activiti="http://activiti.org/bpmn"xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"typeLanguage="http://www.w3.org/2001/XMLSchema"expressionLanguage="http://www.w3.org/1999/XPath"targetNamespace="http://www.activiti.org/processdef"><processid="deleteExecution"name="Execution删除测试"isExecutable="true"><startEventid="startEvent1"></startEvent><parallelGatewayid="sid-BB3C6DE7-5046-4B58-BE62-A37DCE4E4571"name="并行分叉"></parallelGateway><userTaskid="sid-27CF3F4A-82E1-4CD6-AF03-7272164D68CF"name="柜台"activiti:assignee="xiaohong"><extensionElements><modeler:activiti-idm-assigneexmlns:modeler="http://activiti.com/modeler"><![CDATA[true]]></modeler:activiti-idm-assignee><modeler:assignee-info-emailxmlns:modeler="http://activiti.com/modeler"><![CDATA[xiaohong]]></modeler:assignee-info-email><modeler:assignee-info-firstnamexmlns:modeler="http://activiti.com/modeler"><![CDATA[小]]></modeler:assignee-info-firstname><modeler:assignee-info-lastnamexmlns:modeler="http://activiti.com/modeler"><![CDATA[红]]></modeler:assignee-info-lastname><modeler:initiator-can-completexmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete></extensionElements></userTask><userTaskid="sid-9CBF1546-49A3-4668-A9EB-24AA1E275969"name="仓库"activiti:assignee="xiaoming"><extensionElements><modeler:activiti-idm-assigneexmlns:modeler="http://activiti.com/modeler"><![CDATA[true]]></modeler:activiti-idm-assignee><modeler:assignee-info-emailxmlns:modeler="http://activiti.com/modeler"><![CDATA[xiaoming]]></modeler:assignee-info-email><modeler:assignee-info-firstnamexmlns:modeler="http://activiti.com/modeler"><![CDATA[小]]></modeler:assignee-info-firstname><modeler:assignee-info-lastnamexmlns:modeler="http://activiti.com/modeler"><![CDATA[明]]></modeler:assignee-info-lastname><modeler:initiator-can-completexmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete></extensionElements></userTask><userTaskid="sid-D0BBE125-A502-4C60-8D72-EB614E64E3F3"name="钱庄"activiti:assignee="xiaoming"><extensionElements><modeler:activiti-idm-assigneexmlns:modeler="http://activiti.com/modeler"><![CDATA[true]]></modeler:activiti-idm-assignee><modeler:assignee-info-emailxmlns:modeler="http://activiti.com/modeler"><![CDATA[xiaoming]]></modeler:assignee-info-email><modeler:assignee-info-firstnamexmlns:modeler="http://activiti.com/modeler"><![CDATA[小]]></modeler:assignee-info-firstname><modeler:assignee-info-lastnamexmlns:modeler="http://activiti.com/modeler"><![CDATA[明]]></modeler:assignee-info-lastname><modeler:initiator-can-completexmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete></extensionElements></userTask><sequenceFlowid="sid-2564BED7-A49C-4C6D-81F8-AD0FBA163734"sourceRef="sid-27CF3F4A-82E1-4CD6-AF03-7272164D68CF"targetRef="sid-D0BBE125-A502-4C60-8D72-EB614E64E3F3"></sequenceFlow><userTaskid="sid-8929F32A-3068-41D0-9466-147F6094B447"name="工厂"activiti:assignee="xiaohong"><extensionElements><modeler:activiti-idm-assigneexmlns:modeler="http://activiti.com/modeler"><![CDATA[true]]></modeler:activiti-idm-assignee><modeler:assignee-info-emailxmlns:modeler="http://activiti.com/modeler"><![CDATA[xiaohong]]></modeler:assignee-info-email><modeler:assignee-info-firstnamexmlns:modeler="http://activiti.com/modeler"><![CDATA[小]]></modeler:assignee-info-firstname><modeler:assignee-info-lastnamexmlns:modeler="http://activiti.com/modeler"><![CDATA[红]]></modeler:assignee-info-lastname><modeler:initiator-can-completexmlns:modeler="http://activiti.com/modeler"><![CDATA[false]]></modeler:initiator-can-complete></extensionElements></userTask><sequenceFlowid="sid-91633F3C-001D-4BC7-94A4-05577634758A"sourceRef="sid-9CBF1546-49A3-4668-A9EB-24AA1E275969"targetRef="sid-8929F32A-3068-41D0-9466-147F6094B447"></sequenceFlow><endEventid="sid-371798EE-D065-4F0B-9214-E9E34B87EF80"></endEvent><endEventid="sid-B7B083AE-9DC3-4EB3-A3BE-62A9EE714B40"></endEvent><sequenceFlowid="sid-74468023-2185-40D2-A0C5-D1AA16F29AA4"sourceRef="startEvent1"targetRef="sid-BB3C6DE7-5046-4B58-BE62-A37DCE4E4571"></sequenceFlow><sequenceFlowid="sid-2B592017-47EA-4836-AE15-C19E07BE7FF1"sourceRef="sid-BB3C6DE7-5046-4B58-BE62-A37DCE4E4571"targetRef="sid-27CF3F4A-82E1-4CD6-AF03-7272164D68CF"></sequenceFlow><sequenceFlowid="sid-C96DACEE-69E3-4900-84B2-93E7B6CA3947"sourceRef="sid-BB3C6DE7-5046-4B58-BE62-A37DCE4E4571"targetRef="sid-9CBF1546-49A3-4668-A9EB-24AA1E275969"></sequenceFlow><sequenceFlowid="sid-17988F0A-8A1D-4223-A42D-DD1A4C573A53"sourceRef="sid-D0BBE125-A502-4C60-8D72-EB614E64E3F3"targetRef="sid-371798EE-D065-4F0B-9214-E9E34B87EF80"></sequenceFlow><sequenceFlowid="sid-6EA2CEDA-3833-4386-973F-2F13B2DFACC8"sourceRef="sid-8929F32A-3068-41D0-9466-147F6094B447"targetRef="sid-B7B083AE-9DC3-4EB3-A3BE-62A9EE714B40"></sequenceFlow></process></definitions>


流程部署

流程部署对应的实现是  DeployCmd,会首先产生一个 DeploymentEntity 代表这一次部署,然后存入 ACT_RE_DEPLOYMENT 表中,部署的流程则会变成 ProcessDefinitionEntity 存入 ACT_RE_PROCDEF 表中,并且在该表中通过 DEPLOYMENT_ID_ 表示该流程定义是在哪一次部署中部署的,在 Activiti 中,一次部署是可以部署多个流程的,所以在 ACT_RE_PROCDEF 可能有多条记录对应一个 Deployment。

上一节给出的那堆 xml 配置是存在哪里的呢?这个配置没有和 ProcessDefinitionEntity 存储在一起,而是单独作为 ResourceEntity 存储在了 ACT_GE_BYTEARRAY 表中。ResourceEntity 表示部署对应的”资源“,主要是一些有可能很大的配置,比如 bpmn xml,表单配置或者DMN配置等,通过 DeploymentEntitygetResources 方法可以惰性获得该部署所有的资源。Activiti 这么设计也是非常合理的,因为关系型数据库中单条记录太长会影响查询性能,所以这种由用户配置,有可能非常长的字段应该拆开到另一张表中去,需要的时候再根据 RESOURCE_NAME_ + DEPLOYMENT_ID_ACT_GE_BYTEARRAY 中惰性加载出来。


image-20201114221546879.png


从上面三张部署相关的表名中,可以看出 Activiti 是通过前缀区分不同的表的,上面我们遇到的两种前缀的含义分别是:

  • ACT_RE_*:这里的 RE 是 repository 的意思,表示是用来存储配置信息的表
  • ACT_GE_*:GE 是指 general 的意思,可以理解为通用的意思,其实就是大杂烩,各种资源都可以往里面存

还有其他前缀,这里暂时省略,等后面遇到的环节在解释。

发起流程

对应的实现是 StartProcessInstanceCmd,发起流程可不止“发起”这么简单,他会执行流程,直到进入“等待状态”(wait state)时,才返回。

所谓流程进入“等待状态”,也很好理解,就是流程所有并行的分支都遇上了无法立即执行的节点,无法立即执行的节点包括 userTask,以及 async 属性设置为 true 的节点(待会再说这个 async 属性的含义),对于这个简单流程,遇上的第一个等待状态其实就是:


image-20201115000040672.png


流程实例的信息存储在哪里呢?很遗憾,Activiti 并没有专门的流程实例表,流程实例本质上只是一个特殊的 Execution ,存放在 ACT_RU_EXECUTION 表中。

Execution 代表流程中一串节点的执行,ACT_ID_ 代表该 Execution 当前执行到的节点,如果刚好是 UserTask 节点的话,还会在 ACT_RU_TASK 表中生成 TaskTask 代表的就是一次用户任务,后续可以通过这张表查询某个用户有哪些任务。


image-20201115010644877.png


Execution 还有以下特性:

  • 存在父子关系,Execution 的父Execution 的 ID 存储在 PARENT_ID_ 字段中,只有代表流程实例的那个 Execution 是没有父亲的,其他都是有父亲的
  • 兄弟 Execution 是并行执行的,互相没有关系
  • 当兄弟 Execution 全部执行完后,便开始执行父 Execution

下图中彩色的箭头表示执行的依赖关系

image-20201115104248957.png

这个概念听起来很复杂,可以把它理解成“流程虚拟机”中的一个线程,或者是一次子调用,具体到案例里的流程,总共有三个 Execution


image-20201115011520742.png


从图中可以看出代表整个流程的 Execution0 只起到了一个占位,它唯一的作用就是等待所有子 Execution 结束后,将自己终结掉,从而标志整个流程的结束。而 Execution2 就可以理解为经过并行网关而产生的一个新的“线程”。

什么情况下 Execution 可以理解成一个“子调用”呢?那就是在有子流程的情况下,当子流程结束时使用父 Execution 恢复原流程上下文(图中我就省略掉代表整个流程的那个占位的 Execution0 了):


image-20201115120907980.png


这里我们又遇到了一个新的前缀:

  • ACT_RU_*:表是流程的运行时信息,Execution 以及 Task 显然都是这个层面的东西

流程恢复

当流程进入一个 wait state,便要等待用户操作才能继续,当用户完成任务后,我们一般会在上层调用 taskService.complete(taskId) 来让流程继续流转,底层执行的是 CompleteTaskCmd

它会将 TaskACT_RU_TASK 中删除,然后使用和 “流程发起” 差不多的套路执行流程,直到遇到下一个等待状态,注意这里的“等待状态”不是指整个流程的,而是指这个 Task 所属的 Execution 的下一个等待状态,就是图中的“钱庄”节点:


image-20201115112951351.png


流程结束

这个流程比较奇怪的地方是有两个结束节点。Acitiviti 支持多种结束节点,对于图中这种普通的结束节点,即 "End event",是要等到所有的分支都结束,才算结束的。还有一种特殊的结束节点叫做 "End terminate Event",它会直接结束掉整个流程。


image-20201115113853301.png


image-20201115113912363.png


Activiti 是怎么判断流程结束的呢?依据以下两个条件:

  • 没有任何出边可以走了
  • 没有还在执行的 Execution 了

按照比较过程式的方法描述就是,每当 Activiti 发现没有出边可以走时,除了将当前 Execution 置为 “失活” 外,还会去检查一遍还有没有“活跃”的 Execution 了,如果没有了,就结束整个流程,否则什么事也不干。

从这个底层逻辑可以看出,普通的结束节点就算是不加也是可以的。

其他

从之前分析中可以看出,这个简单流程是完全没有走异步调度的,整个流程都是靠“流程发起”和“流程恢复”同步执行的,不光简单流程是这样,复杂流程也是,这个就是 Activiti 的默认行为,除非你把某个节点的 async 属性设置为 true,运行到这个节点时才会走异步调度,这么做能够大大降低调度系统的开销,对于调度系统,本篇文章就不多赘述了,后面的文章再做进一步分析。

此外,由于简单案例中没有使用流程变量,故本篇不做重点分析,但是流程变量也是流程引擎中非常重要的一部分,它决定着流程的运行时行为,流程变量都是存储在 ACT_RU_VARIABLE 中的。

总结

这里总结一下这篇文章提到的 Activiti 在实现层面的一些概念:

  • Deployment:一次部署,可能会部署多个流程
  • Resource:部署中可能比较大的用户配置,比如 bpmn xml
  • Execution:一串执行,可以理解成“流程虚拟机”中的一个“线程”,或者一次子调用
  • Task:对应一次 UserTask,表示一个需要人完成的任务

参考文章



相关文章
|
8月前
|
SQL Java 关系型数据库
Activiti工作流框架学习笔记(二)之springboot2.0整合工作流Activiti6.0
Activiti工作流框架学习笔记(二)之springboot2.0整合工作流Activiti6.0
131 0
|
存储 数据库 UED
万字解析Activiti7流程框架(三)
万字解析Activiti7流程框架(三)
万字解析Activiti7流程框架(三)
|
存储 XML SQL
|
测试技术 数据库
|
Java 数据库连接 API
Activiti-流程操作2
Activiti-流程操作2
Activiti-流程操作2
|
Java API 数据库
Activiti-流程操作
Activiti-流程操作
Activiti-流程操作
|
数据库
Activiti进阶篇-流程实例
Activiti进阶篇-流程实例
Activiti进阶篇-流程实例
|
存储 API
Activiti进阶篇-流程变量
Activiti进阶篇-流程变量
Activiti进阶篇-流程变量