阅读完需:约 193 分钟
工作流概述
工作流是什么?
工作流是将一组任务组织起来以完成某个经营过程:定义了任务的触发顺序和触发条件,每个任务可以由一个或多个 软件系统完成,也可以由一个或一组人完成,还可以由一个或多个人与软件系统协作完成。
为什么要用工作流?
工作流技术通过将工作分解成定义良好的任务、角色,按照一定的规则和过程来执行这些任务井对它们进行监控.可以提高办事效率、降低生产成本.提高企业生产经营管理水乎和企业的竞争力。实现现代企业经营过程自动化。
众多工作流
JBPM(Java Business Process Management)
由JBoss公司开发,目前最高版本JPBM7,不过从JBPM5开始已经跟之前不是同一个产品了,JBPM5的代码基础不是JBPM4,而是从Drools Flow重新开始。下面要涉及的很多产品都是以JBPM4的代码为起点进行开发的。
Activiti
Alfresco软件开发,基于JBPM4,后并入OMG,目前最高版本activiti 7。Activiti5版本的时候,核心团队发生了比较大的变动(离职),activiti6的开发团队在新版本中去除了PVM,纳入了DMN,重构XML解析,BUG较多,目前主要团队致力于activiti7,5&6已经官宣不维护。
Osworkflow
完全用java语言编写的开放源代码的工作流引擎,具有显著的灵活性及完全面向有技术背景的用户的特点。由opensymphony组织维护,其不遵守XPDL等业务规范,完全使用XML编排业务。面向开发人员。
Shark
靠山是Enhydra。是一个可扩展的工作流引擎框架,它包括一个完全基于 WFMC 规范的标准实现,它使用XPDL(没有任何自己新的扩展)作为自身的工作流流程定义格式。其持久层和设计器都是自己公司研发的,持久层实现采用的标准是轻量级的Enhydra DODS O/R mapping 工具,设计器可以用Enhydra JaWE 图形XPDL编辑器。
Apache ODE
轻型的、可嵌入的组件,利用组件组装成一个完整的BPM系统。关键模块包括ODE BPEL编译器、ODE BPEL运行时、ODE数据访问对象(DAOs)、ODE集成层(ILs)和用户工具。虽然挂在Apache下面,但已经年久失修。
Flowable
基于activiti6,最新的开源版本是flowable6,开发团队是从activiti中分裂出来的,修复了一众activiti6的bug,并在其基础上研发了DMN支持,BPEL支持等等。相对开源版,其商业版的功能会更强大。
Camunda
基于activiti5,所以其保留了PVM,最新版本Camunda7,开发团队也是从activiti中分裂出来的,发展轨迹与flowable相似,同时也提供了商业版。
JFlow
前身ccFlow,国产的工作流引擎,由济南驰骋公司开发维护,主打中国式的业务流程,由于是国产的软件,中文化程度比较深,业务开发也对用户比较友好。国产的开源工作流引擎还是挺多的,JFlow是其中功能比较完善的一个,同时对比activiti,流程上更加中国化,支持自定义流程跳转,加签等。其他国产工作流就不列举了。
还有很多工作流,比如ProcessMaker,SWF,oracle,Bonita,openwebflow,snaker等,不过做BPM的话,相对于上面列举的产品还是有些缺陷,比如流程过于简单,资料过少等。
在众多开源工作流中最常用的还是Activiti,Flowable,Camunda
开源Flowable/Activity/Camunda的发展史
基于BPM有各种开源软件,以Activiti为首的Java开源是主要流派,基于Activity有各种分支,比较著名有Camunda和Flowable
- 2002年 ,Activiti的创始人Tom Baeyens,创建了基于状态机原理的jBPM流程引擎,同类产品有osworkflow。
- 2002年至2004年底,在JBoss和Redhat公司的支持下,发展到了jBPM4.0, 创始人Tom Baeyens离开后,老东家干脆放弃了原来的BPMN4.0架构,据于Drools Flow进行彻底重构,推出了JBPM5(JBoss的亲生儿子),所以这个做法,逼的大家转向Activiti,无法升级大家受不了啊。没办法“话说天下大势,分久必合,合久必分”。
- 2005年第一次分家: 创始人Tom Baeyens加入到了Alfresco公司(专门提供企业内容和流程服务的一站式解决方案)。分家的原因是jBPM是据于GPL开源协议,导致做的功能和JBoss和Redhat公司的产品线太过紧密,限制了开源的发展。
- 2010年3月启动到2010年12月正式发布ativity第一个版本:activit5.0。
- 2011年10月:发布 Activiti 5.8
- 2012年12月:发布Activiti 5.11 (这时候开始出现分家趋向,Tom Baeyens不再领导Activiti工程,并决定三年后离开Alfresco公司,camunda这个机构是Activit最大的贡献者之一(另一家是Alfresco)不满意以Alfresco的以文档为中心的工作流的产品设计理念,独立出来一个叫camunda BPM的开源产品,Tom Baeyens获得了不错的许可费用,同时Tom Baeyens获得了Signavio的启动资金(应该是用来开发activiti)。这时候相当于分裂为两大阵营  Activiti / Camunda(activiti的团队来自多家组织机构:Alfresco、camunda、SpringSource、MuleSoft、FuseSoft。)
- 2013年10月:发布了Activiti 5.14 (在Aciviti开发期间,Tom Baeyens已经离职, tijsrademakers开始担任领导并全面负责Activiti5的发展,Joram Barrez担任架构师的职位)
- 2014年12月: 发布了Activiti 5.17
- 2016年7月第二次分家:Activiti发展太慢,不支持CMMN/DMN新的两个规范,只支持BPMN规范,这时候分支出去的Camunda框架发展的更牛逼,创始人一看不服啊,儿子比老子厉害了,不干,但想法与背后的大公司发生技术和项目走向分歧时,看来主创人员只能分家了,另立山头。
- 2017年flowable5.22发布(这时候Activity也是5.22),2周后发布了6.0
- 2022.7.21截止这个日期,flowable6.7.2版本
Activiti和Flowable的主创人员
- Tijs Rademakers,算是activiti5以及6比较核心的leader了。现在是flowable框架的leader。
- Joram Barrez 算是activiti5以及6比较核心的leader了。目前从事flowable框架开发。
- Salaboy Activiti Cloud BPM leader(Activiti Cloud BPM 也就是目前的activiti7框架)(大部分已经离职)
(1)activiti5以及activiti6、flowable是Tijs Rademakers团队开发的,activiti6的很多框架bug在flowable框架中已经修复的差不多了。
(2)Activiti7是 Salaboy团队开发的,对于activiti6以及activiti5的代码官方已经宣称暂停维护了。activiti7就是噱头 内核使用的还是activiti6。并没有为引擎注入更多的新特性,只是在activiti之外的上层封装了一些应用。
名词解释
在工作流中会有各种名词的出现,需要我们一开始有一个概念
BPM
Business Process Management,业务流程管理,“通过建模、自动化、管理和优化流程,打破跨部门跨系统业务过程依赖,提高业务效率和效果”。
BPMN
Business Process Modeling Notation,业务流程建模与标注,包括这些图元如何组合成一个业务流程图(Business Process Diagram);讨论BPMN的各种的用途,包括以何种精度来影响一个流程图中的模型;BPMN作为一个标准的价值,以及BPMN未来发展的远景。
BPEL
Business Process Execution Language,意为业务过程执行语言,是一种基于XML的,用来描写业务过程的编程语言,被描写的业务过程的每个单一步骤则由Web服务来实现。
XPDL
XML Process Definition Language,是由Workflow Management Coalition(简写为:WfMC)所提出的一个标准化规格,使用XML文件让不同的工作流程软件能够交换商业流程定义。XPDL被设计为图形上和语义上都满足交换用的商业流程定义,是描述BPMN图的最佳文件格式。BPEL也可以描述商业流程。但是XPDL不仅包含流程执行的描述,还包括了元素的图形信息,更适于商业流程建模。
JPDL
JBoss jBPM Process Definition Language,是构建于jBPM框架上的流程语言之一。在jPDL中提供了任务(tasks)、待处理状态 (wait states)、计时器(timers)、自动处理(automated actions)等术语,并通过图型化的流程定义,很直观地描述业务流程。
PVM
Process Virtual Machine,流程虚拟机,他的设计初衷是通过实现接口和定制插件等方式兼容多种流程定义语言和流程活动场景,为所有的业务流程定义提供一套通用API平台。那么,无论是需要对jBPM 原有流程定义语言进行扩展,或者重新实现一套专用的流程定义语言,都可以通过实现 PVM 指定的接口规范完成。
DMN
Decision Model and Notation,DMN的目的是提供一个模型决策结构,从而使组织的策略可以用图形清晰的地描绘出来,通过业务分析准确的定义,使其自动化(可选地)。
CMMN
Case Management Model and Notation,CMMN是一种图形化的符号,用于捕获工作方法,这些工作方法基于处理需要各种活动的情况,这些活动可能以不可预测的顺序执行,以响应不断变化的情况。通过使用以事件为中心的方法和案例文件的概念,CMMN扩展了可以用BPMN建模的边界,包括结构化程度较低的工作和由知识工人驱动的工作。结合使用BPMN和CMMN,用户可以涵盖更广泛的工作方法。
关于工作流标准
这里对名词解释里的内容做一些扩充
BPMN是听得比较多的工作流标准,但工作流的规范其实不止一种,还有XPDL,BPML等。甚至他们的出现时间比BPMN更早,只是因为一些技术和非技术原因,BPMN2.0被普遍使用了,而非BMPN2.0规范的厂商也逐渐转移了。
以下的内容是关于规范标准之争中,BPMN2.0如何从众多规范中战胜并被普遍使用的。
BPMN 1.X
在BPMN1.X里,BPMN是Business Process Modeling Notation的缩写,即业务流程建模符号,而在BPMN2.0里,BPMN变成了Business Process Model And Notation的缩写,即业务流程模型和符号,一个单词的增加却标示着BPMN本身发生了巨大的变化。
其中BPMN1.0在2004年5月由BPMI组织正式发布。这个阶段WSFL和BPEL-WS都已经被发布。这三种规范中,BPMN1.0仅仅作为业务流程建模的一系列符号标准,对业务比较友好。厂商们认为统一的建模标准能够使他们围绕核心建模工具提供其他更多的价值,更加愿意接受BPMN。
但BPMN1.x只是一些建模符号,不支持元模型,不支持存储和交换,也不支持执行。2008年,BPMN1.1发布,但仍然存在这些对开发人员并不友好的缺点,XPDL、BPEL和BPDM围绕着BPMN1.x的存储、交换和执行,产生了新的竞争。
XPDL作为WfMC提出的流程定义语言规范,本身就是一个元模型,可以存储,并且具备执行语义,因此理论上来讲,将BPMN转换为XPDL就可以解决存储、交换和执行的问题。XPDL2.0于2005年10月发布,在规范里,WfMC直接将XPDL的目标定义为BPMN的XML序列化格式。2008年4月23日发布的XPDL2.1规范,直接支持BPMN1.1到XPDL2.1的转换。XPDL是面向图的,BPMN也是面向图的,因此BPMN到XPDL的转换有着天然的优势。如今有超过80个的不同公司的产品使用XPDL来交换流程定义,同时也有一些厂商在自己提供的BPMN工具中使用了XPDL作为交换和存储格式。
BPEL-WS规范在2003年4月提交给了OASIS(Organizationfor the Advancement of Structured Information Standards,结构化信息标准促进组织)并更名为WSBPEL(Web Services Business Process Execution Language)规范, 2007年4月发布WSBPEL2.0版本,除了Microsoft、 BEA、 IBM、 SAP 和Siebel,Sun Microsystems和甲骨文公司也相继加入了OASIS组织。除去政治因素,BPEL的流行还在于Web正成为分布式系统架构的平台以及SOA的雄起,SOA强调服务的分解和解耦,而BPEL则对这些WEB服务进行编制,两者密不可分。但BPMN到BPEL的转换存在着先天上的缺陷,原因是BPMN是基于图的,而BPEL是基于块的,BPEL是一个结构化(块[Block])和非结构化(控制链和事件)的混合体。这个缺陷导致有些BPMN建模的流程无法映射到BPEL,两者的双向工程更是存在问题。这个缺陷成为人们反复诟病的对象。许多支持BPEL的产品为了解决这一问题,不得不在用户建模时做出种种限制,让用户绘制不出无法转换的模型。
而BPDM(业务流程定义元模型,Business Process Definition Metamodel)则是OMG组织自己提出来解决BPMN存储和交换问题的规范。于2007年7月形成初稿,2008年7月被OMG最终采用。BPDM是一个标准的概念定义,用来表达业务流程模型。元模型定义了用来交换的概念,关系和场景,可以使得不同的建模工具所建模出来的流程模型进行交换。BPDM超越了BPMN和BPEL所定义的业务流程建模的要素,它定义了编排和编制。
BPMN 2.0
随后,BPMN2.0发布了。
BPMN2.0 beta1版本于2009年8月发布,BPMN2.0 beta2版本于2010年5月发布,BPMN2.0正式版本于2011年1月3日发布。BPMN2.0正式将自己更名为Business Process Model And Notation(业务流程模型和符号),相比BPMN1.x,最重要的变化在于其定义了流程的元模型和执行语义,即它自己解决了存储、交换和执行的问题,BPMN由单纯的业务建模重新回归了它的本源,即作为一个对业务人员友好的标准流程执行语言的图形化前端。BPMN2.0一出手,竞争就结束了,XPDL、BPEL和BPDM各自准备回家钓鱼。
BPMN2.0是最被广泛使用的标准,也是当前热门产品使用的标准:
| 工作流框架 | 遵循规范 | 备注 | 
| Bonita BPM | XPDL | 流程过于简单 | 
| Shark | XPDL | 不维护-2017 | 
| Osworkflow | 自定义XML规范 | 不维护 | 
| JBPM | BPMN2.0 | JBPM4.3后添加了对BPMN的支持,持续开源 | 
| Apache ODE | WS-BPEL、BPEL4WS | 不维护 | 
| Activiti | BPMN2.0,XPDL,JPDL | Activiti7维护 | 
| Flowable | BPMN2.0,XPDL,JPDL | 持续开源 | 
| JFlow | BPMN2.0,Ccbpm | 2015年后为了与国际接轨,开发支持BPMN | 
| Camunda | BPMN2.0,XPDL,JPDL | 持续开源 | 
这个表格可以对一些工作流产品有一个初步印象。
PVM
PVM是在JBPM4的时候被纳入的,activiti5沿用,activiti团队在activiti6就已经移除了,ActivitiImpl, ProcessDefinitionImpl, ExecutionImpl, TransitionImpl 都不可用了。所有的流程定义有关的信息都可以通过BpmnModel来获得,通过org.activiti.engine.impl.util.ProcessDefinitionUtil来拿到BpmnModel。
工作流中,由于flowable是基于activiti6开发的,所以代码中也没有PVM,Camunda基于activiti5开发的,所以PVM还在,更改这个核心引擎没有绝对的好坏之分。
DMN
BPMN是OMG公司发布的工作流规范,而DMN同样是OMG公司发布规范,该规范主要用于定义业务决策的模型和图形,1.0版本发布于2015年,目前最新的是1.1版本,发布于2016年。
BPMN主要用于规范业务流程,业务决策的逻辑由PMML等规范来定义,例如在某些业务流程中,需要由多个决策来决定流程走向,而每个决策都要根据自身的规则来决定,并且每个决策之间可能存在关联,此时在BPMN与PMML之间出现了空白,DMN规范出现前,决策者无法参与到业务中。为了填补模型上的空白,新增了DMN规范,定义决策的规范以及图形,DMN规范相当于业务流程模型与决策逻辑模型之间的桥梁。
虽然DMN只作为工作流与决策逻辑的桥梁,但实际上,规范中也包含决策逻辑部分,同时也兼容PMML规范所定义的表达式语言。换言之,实现DMN规范的框架,同时也会具有业务规则的处理能力。
CMMN
CMMN具有与BPMN不同的基本范例。 CMMN没有顺序的流程。相反,它以某种状态对案例建模。根据状态,视情况而定,有些事情可能会处理,而有些事情可能不会。控制主要由人来执行。 CMMN是声明性的,该模型说明了要应用的内容,但没有说明如何实现它。相反,BPMN强制性地规定了流程中某些步骤必须进行的工作。对于大多数人而言,声明性建模更为复杂且较不直观。结果,CMMN模型更加难以理解。您不能简单地用手指追踪路径!
CMMN对可能的活动和活动限制进行建模。它对活动何时发生,何时必须发生以及何时不应该发生进行建模。 CMMN同样限制了流程中人员可以使用的操作范围。事例模型必须事先经过仔细考虑。重要的是要提出这一点,以应对人们经常误解的事实,即人们在案件管理方面可以做他们想做的任何事情。
CMMN和BPMN都描述了业务流程中的活动。这些标准之间的主要区别是:
- BPMN采用绑定方法。 它提供了活动的确切顺序。提供自由度比较困难。比如加个节点、任意跳转就很麻烦。
- CMMN采用非约束性方法,然后增加了限制。建立排序比较困难。
换句话说,原则上您可以用任何一种表示法表达大多数问题。但是,根据问题的类型,建模将在BPMN或CMMN中更好地工作,并且这些标准之一更可能产生整洁有效的模型。
使用CMMN的指标包括:
- 无需序列:如果序列无关紧要,并且可以按任何顺序执行任务,则这将在BPMN中产生过多的连接-临时建模。也许使用临时子流程可以避免混乱。
- 活动取决于条件:定义条件是CMMN的强项。可以定义许多任务,但是它们只能在某些情况下起作用。例如,这种情况可能是订单超过一定数量或客户具有VIP身份;其他已完成的任务也会影响条件。可选因素和数据相关因素的这种组合不能在BPMN中反映出来。
- 专用计划阶段:由于能够处理任意任务,CMMN可以适应一个计划阶段,在该阶段中,一个工人计划一个案例并启用任务。 其他工人将不得不遵守计划。 BPMN不能做任何事情。
包括BPMN标准,这三个标准都是由OMG提出的。多数机构认为DMN和CMMN是工作流发展趋势。
工作流对比
从支持的标准和社区活跃度表现比较好的工作流中筛选出几个选项进行进一步对比
| Activiti 7 | Flowable 6 | Camunda bpm | JBPM 7 | JFLOW(国产的) | |
| 会签 | √ | √ | √ | √ | √ | 
| 回退 | × | √ | √ | – | √ | 
| 驳回 | × | √ | √ | √ | √ | 
| 自定义流转 | × | × | √ | – | √ | 
| 加签、减签 | × | √ | √ | – | √ | 
| 多实例 | √ | √ | √ | √ | √ | 
| 事务子流程 | √ | √ | √ | √ | √ | 
| 版本迁移 | × | × | √ | × | × | 
| 支持的流程格式 | BPMN2.0、XPDL、PDL | BPMN2.0、XPDL、XPDL | BPMN2.0、XPDL、XPDL | BPMN2.0 | BPMN2.0 | 
| 开源情况 | 开源 | 提供商业和开源版 | 提供商业和开源版 | 开源 | 开源 | 
| 开发基础 | jBPM4 | Activiti 5 & 6 | Activiti 5 | 版本5之后Drools Flow | 自开发 | 
| Spring结合 | √ | √ | √ | √ | √ | 
| CMMN支持 | × | √ | √ | × | × | 
| DMN支持 | √ | √ | √ | √ | × | 
| 支持数据库 | Oracle、SQL Server、MySQL | Oracle、SQL Server、MySQL、postgre | Oracle、SQL Server、MySQL、postgre | Mysql,postgre | oracle,sqlserver,mysql | 
| 集成接口 | SOAP、Mule、RESTful | SOAP、Mule、RESTful | SOAP、Mule、RESTful | 消息通讯 | SOAP、Mule、RESTful | 
| 内部服务通讯 | Service间通过API调用 | Service间通过API调用 | Service间通过API调用 | 基于Apache Mina异步通讯 | – | 
大致总结以下调研的总体感受。Activiti7相对于5和6没有太多功能上的变化,主要致力于一些辅助功能,对接一些基础技术。比如云原生,ELK,spring cloud。分布式的应用或许会对性能有一定的提升。
Flowable的NoSql方案和消息队列比较特别,同时对DMN和CMMN的研究也比较多,是个不错的选择。
JBPM近年来新的文档少一些,应用和二次开发可能会比较吃力。JFlow功能比较齐全,而且中文化的设计器对开发人也和业务人也都比较友好,但是他的材料基本限于官网,后期不会保障。
Camunda BPM支持的功能比较多,对DMN和CMMN的支持也是推出最早的,性能上看起来也做了比较多的应对,商业版的推出减少了开源版的维护,PVM的保留也会使得迁移比较顺滑。
这里选择 Flowable 作为后续工作流引擎的开发。
Flowable概述
Flowable是一个使用Java编写的轻量级业务流程引擎。Flowable流程引擎可用于部署BPMN 2.0流程定义(用于定义流程的行业XML标准), 创建这些流程定义的流程实例,进行查询,访问运行中或历史的流程实例与相关数据,等等。
Flowable可以十分灵活地加入你的应用/服务/构架。可以将JAR形式发布的Flowable库加入应用或服务,来嵌入引擎。 以JAR形式发布使Flowable可以轻易加入任何Java环境:Java SE;Tomcat、Jetty或Spring之类的servlet容器;JBoss或WebSphere之类的Java EE服务器,等等。 另外,也可以使用Flowable REST API进行HTTP调用。也有许多Flowable应用(Flowable Modeler, Flowable Admin, Flowable IDM 与 Flowable Task),提供了直接可用的UI示例,可以使用流程与任务。
初识Flowable引擎
官方文档:
https://tkjohn.github.io/flowable-userguide/#_introduction
https://www.flowable.com/open-source/docs/oss-introduction
Flowable引擎,每个之间都是相互独立互不影响。
ProcessEngine是里面最核心也是最重要的一个引擎,如果失去它那Flowable也就意义了。
引擎是组成flowable框架的服务单元,每个引擎由相对应的 EngineConfiguration进行创建配置

| 名称 | 含义 | 说明 | 
| ProcessEngine | 流程引擎 | 流程设计、发布、任务的查询和操作等 | 
| DmnEngine | 决策引擎 | 各决策表的配置和使用等 | 
| FormEngine | 表单引擎 | 动态表单的设计和使用 | 
| IdmEngine | 身份引擎 | 提供用户和用户组还有权限的创建、修改、删除等 | 
| ContentEngine | 内容引擎 | 提供对Mybatis的封装,还提供了文件读取、文件保存的功能 | 
IdmEngine身份引擎
Flowable 自带身份管理模块,但是从 Flowable V6 起身份管理(IDM IDentity Management)组件从 Flowable 引擎模块中抽出,并将其逻辑移至几个不同的模块。默认情况下,IDM 引擎在 Flowable 引擎启动时初始化并启动。Flowable 提供的几个 web 应用中就包括 Flowable IDM(身份管理应用),为所有 Flowable UI 应用提供单点登录认证功能,并且为拥有 IDM 管理员权限的用户提供了管理用户、组与权限的功能。(不过多介绍,因为权限管理需要结合业务来整合)
Flowable在运行时并不做任何用户检查。例如任务可以分派给任何用户,而引擎并不会验证系统中是否存在该用户。这给我们留下了很大自定义用户和组的空间,这样当我们将Flowable嵌入应用时,可以与应用已有的用户和组结合使用,也可以结合LDAP、Active Directory等服务使用。
Flowable DMN 决策引擎
作为以 BPMN 为核心的工作流引擎,Flowable 原本与规则引擎的关联并不强,但实际业务流程中,有时需要由多个决策来决定流程走向,而每个决策都要根据自身的规则来决定,每个决策之间也可能存在关联。此时就需要规则引擎来提供决策支撑。在规则引擎开源产品中,Drools 是最知名的一款,它实现了 PMML(Predictive Model Markup Language)规范,同时支持 DMN (Decision Model and Notation)标准。Flowable 目前实现了 DMN V1.1 规范的框架,由于 DMN 规范中要求对 PMML 提供兼容性,这意味着 Flowable 具有相对强大的业务规则的处理能力。
在 Flowable Modeler 应用中 DMN 引擎体现为「决策表」菜单,可以通过界面进行 Input 与 Output 的配置,可导入 .dmn 扩展名格式的 DMN 定义。在 OMG (Object Management Group)制定的 DMN 规范中也有相应的 XML 格式约束。如果 DMN 引擎已经插入流程引擎,就可以与其他流程相关资源一起,将 DMN 定义打包进业务存档(BAR)文件中。流程引擎部署服务会将 DMN 资源部署至 DMN 引擎。
DMN 定义由决策(decision)和其他东西组成,决策由表达式描述。DMN 标准描述了几种表达式的类型,目前在 Flowable DMN 中仅支持决策表(decision table)类型的表达式。决策表分为输入表达式与输出表达式两个主要区域。在输入表达式中,可以定义变量,用于规则输入项(input entries)的表达式。可以通过选择 Add Input(添加输入),定义多个输入表达式。在输出表达式中,可以定义选择表执行结果要创建的变量(变量的值将用于输出项表达式,在下面解释)。可以通过选择 Add Output(添加输出),定义多个输出表达式。

在决策表编辑界面,可以选择命中策略,共有两大类(单命中、多命中)七种命中策略可选:
- (1)单命中、第一命中(single hit & FIRST):多个规则允许交叉,执行从上到下的第一条命中项。
- (2)单命中、唯一命中(single hit & UNIQUE):多个规则不允许交叉,执行从上到下的第一条唯一命中项。
- (3)单命中、任一命中(single hit & ANY):规则允许交叉,但是所有输出的优先级相同,随机执行一条命中项。
- (4)单命中、优先级(single hit & PRIORITY):多个命中规则的优先级不同,执行优先级最高的那条。
- (5)多命中、输出优先级排序(multiple hit & OUTPUT ORDER):按照输出优先级递减的顺序返回所有命中。
- (6)多命中、规则顺序排序(multiple hit & RULE ORDER):按照规则顺序返回所有命中。
- (7)多命中、聚合(multiple hit & COLLECT):按照随机顺序返回所有命中。
DMN 可以被 BPMN 定义的流程调用:在流程中引入一个决策任务(Decision task),并选中引用决策表(Decision table reference),来使用新创建的选择表。
Flowable CMMN 案例模型引擎
CMMN 是 Case Management Model 的缩写,在 Flowable Modeler 应用中体现为「案例模型」菜单,使用时可以类似于流程引擎可视化配置流程,也可通过 XML 格式文件。CMMN(Case Management Model and Notation)行业标准 V1.1 版本于 2016 年发布,目前 Flowable 的 V6.4.1 已支持此标准。
与 BPMN 引擎相比,CMMN 引擎适用于如下几种场景:
- (1)重复与并行的工作分发。BPMN 引擎在处理顺序执行、职责分工明确的工作流程时有优势,但面对动态、自由、并行的情况时,BPMN 显得灵活性不足,此时 CMMN 则更适合应对。
- (2)处理带有生命周期特征的场景,如客户、产品、项目、雇员。以项目为例,项目的立项、中止、收尾、交付等阶段(phases),可以在 CMMN 中通过阶段(Stages)概念在更高层次进行描述。

CMMN 中一个案例模型呈现为一个公文夹的样式。每个案例模型都包含一个用于安置计划元素的「计划模型」,每个计划元素包含一个明确其类型和可能配置选项的计划元素定义,常见计划元素如用户任务(human task)、里程碑(milestone)、流程任务(process task)、案例任务(case task)和阶段(stage)。例如下图中的计划模型包含三个用户任务计划项和一个里程碑。

Flowable CMMN 引擎支持如下类型的案例元素:
- 1. 阶段(Stage):阶段用于把一组元素聚合在一起,可以有进入和退出的条件。阶段可以嵌套,一个阶段中的计划元素只有其父阶段激活时才生效。
- 2. 任务(Task):任务是发生于引擎外部的事件,包含名称、阻塞(决定任务是否阻塞的布尔值)、阻塞表达式(表达式的布尔值决定任务是否阻塞)等属性。
- 3. 用户任务(Human task):通常指需要用户通过表单执行的手动任务,包含一系列属性。
- 4. 里程碑(Milestone):里程碑标识某一具体案例到达特定点。
- 5. 案例任务(Case task):案例可以嵌套,案例中的子案例就是案例任务。
- 6. 流程任务(Process task):当流程任务阻塞时,实例化的计划要素会处于激活状态,直至流程任务完成。
- 7. 条件(Criteria):分为进入条件和退出条件。
- 8. 决策任务(Decision task):调用 DMN 引擎中的决策表。
- 9. HTTP 任务、脚本任务、Java 服务任务、时间监听器等:与 BPMN 中的相应元素含义相近,不再赘述。
Flowable BPMN 业务流程引擎
Flowable引擎在使用前需要先通过配置来初始化ProcessEngine。
初始化ProcessEngineConfiguration一般有两种方式:
- 通过Spinrg配置文件进行依赖注入,通过flowable.cfg.xml文件来初始化ProcessEngineConfiguration(这里的文件名必须为flowable.cfg.xml,否则Flowable识别不到)
 (这个配置XML文件实际上是一个Spring配置文件)
- 通过编写程序的方式来构造ProcessEngineConfiguration对象

有多个类可以用于定义processEngineConfiguration。这些类用于不同的环境,并各自设置一些默认值。最佳实践是选择最匹配你环境的类,以便减少配置引擎需要的参数。
- 
org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration:流程引擎独立运行。Flowable自行处理事务。在默认情况下,数据库检查只在引擎启动时进行(如果Flowable表结构不存在或表结构版本不对,会抛出异常)。
- 
org.flowable.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration:这是一个便于使用单元测试的类。Flowable自行处理事务。默认使用H2内存数据库。数据库会在引擎启动时创建,并在引擎关闭时删除。使用这个类时,很可能不需要更多的配置(除了使用任务执行器或邮件等功能时)。
- 
org.flowable.spring.SpringProcessEngineConfiguration:在流程引擎处于Spring环境时使用。
- 
org.flowable.engine.impl.cfg.JtaProcessEngineConfiguration:用于引擎独立运行,并使用JTA事务的情况。
ProcessEngineConfiguration在初始化过程中会同时初始化数据库,如果数据库已经存在,则不会做创建更新操作,如果数据库不存在,则会默认执行数据库创建脚本。
创建ProcessEngine
硬编码的方式
创建一个基本的maven工程,可以是Eclipse也可以是其他IDEA
注意这里并没有和Spring或SpringBoot结合,只是一个单纯的Maven工程
- Flowable流程引擎。使我们可以创建一个ProcessEngine流程引擎对象,并访问Flowable API。
- 一个是MySQL的数据库驱动
在pom.xml文件中添加下列行:
<dependency>
    <groupId>org.flowable</groupId>
    <artifactId>flowable-engine</artifactId>
    <version>6.7.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.21</version>
</dependency>
<!-- 日志框架 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.21</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.21</version>
</dependency>Log4j需要一个配置文件。在src/main/resources文件夹下添加log4j.properties文件,并写入下列内容:
log4j.rootLogger=DEBUG, CA
log4j.appender.CA=org.apache.log4j.ConsoleAppender
log4j.appender.CA.layout=org.apache.log4j.PatternLayout
log4j.appender.CA.layout.ConversionPattern= %d{hh:mm:ss,SSS} [%t] %-5p %c %x - %m%n然后创建一个普通的Java类,添加对应的main方法,首先要做的是初始化ProcessEngine流程引擎实例。这是一个线程安全的对象,因此通常只需要在一个应用中初始化一次。 ProcessEngine由ProcessEngineConfiguration实例创建。该实例可以配置与调整流程引擎的设置。 通常使用一个配置XML文件创建ProcessEngineConfiguration,但是(像在这里做的一样)也可以编程方式创建它。 ProcessEngineConfiguration所需的最小配置,是数据库JDBC连接:
public static void main(String[] args) {
        ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
                .setJdbcUrl("jdbc:mysql://localhost:3306/flowable-learn1?serverTimezone=UTC&nullCatalogMeansCurrent=true")
                .setJdbcUsername("root")
                .setJdbcPassword("123456")
                .setJdbcDriver("com.mysql.cj.jdbc.Driver")
                .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
        ProcessEngine processEngine = cfg.buildProcessEngine();
    }这种方式会调用buildProcessEngine()方法,里面的核心代码为:


最后可以看到关于引擎启动与创建数据库表结构:

这样就得到了一个启动可用的流程引擎(大概79张表)。接下来为它提供一个流程!
配置文件方式
除了上面的硬编码的方式外,我们还可以在resources目录下创建一个flowable.cfg.xml文件,注意这个名称是固定的哦。内容如下:
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="processEngineConfiguration"
          class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/flow1?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&nullCatalogMeansCurrent=true" /><property name="jdbcDriver" value="com.mysql.cj.jdbc.Driver" />
        <property name="jdbcUsername" value="root" />
        <property name="jdbcPassword" value="123456" />
        <property name="databaseSchemaUpdate" value="true" />
        <property name="asyncExecutorActivate" value="false" />
    </bean>
</beans>在上面的配置文件中配置相关的信息。我们在Java代码中就可以简化为:
 @Test
    public void test01(){
        // 获取流程引擎对象
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        System.out.println("processEngine = " + processEngine);
    }可以看下getDefaultProcessEngine的源码,在里面最终还是执行了和硬编码一样的代码
public static ProcessEngine getProcessEngine(String processEngineName) {
        if (!isInitialized()) {
            init(); // 完成初始化操作
        }
        return processEngines.get(processEngineName);
    }进入init方法
public static synchronized void init() {
        if (!isInitialized()) {
            if (processEngines == null) {
                // Create new map to store process-engines if current map is null
                processEngines = new HashMap<>();
            }
            ClassLoader classLoader = ReflectUtil.getClassLoader();
            Enumeration<URL> resources = null;
            try {
                resources = classLoader.getResources("flowable.cfg.xml"); // 加载flowable.cfg.xml配置文件
            } catch (IOException e) {
                throw new FlowableIllegalArgumentException("problem retrieving flowable.cfg.xml resources on the classpath: " + System.getProperty("java.class.path"), e);
            }
            // Remove duplicated configuration URL's using set. Some
            // classloaders may return identical URL's twice, causing duplicate
            // startups
            Set<URL> configUrls = new HashSet<>();
            while (resources.hasMoreElements()) {
                configUrls.add(resources.nextElement());
            }
            for (Iterator<URL> iterator = configUrls.iterator(); iterator.hasNext();) {
                URL resource = iterator.next();
                LOGGER.info("Initializing process engine using configuration '{}'", resource.toString());
                initProcessEngineFromResource(resource); // 初始化ProcessEngine
            }
            try {
                resources = classLoader.getResources("flowable-context.xml"); // 在整合Spring的情况下加载该文件
            } catch (IOException e) {
                throw new FlowableIllegalArgumentException("problem retrieving flowable-context.xml resources on the classpath: " + System.getProperty("java.class.path"), e);
            }
            while (resources.hasMoreElements()) {
                URL resource = resources.nextElement();
                LOGGER.info("Initializing process engine using Spring configuration '{}'", resource.toString());
                initProcessEngineFromSpringResource(resource); // 从Spring的资源文件中完成ProcessEngine的初始化
            }
            setInitialized(true);
        } else {
            LOGGER.info("Process engines already initialized");
        }
    }在源码中提供了单独使用好整合Spring的配置加载方式。再进入到initProcessEngineFromResource(resource)方法中:



而且我们也可以看到ProcessEngine最终的实现是 ProcessEngineImpl对象。
自定义配置文件
最后我们如果要加载自定义名称的配置文件可以通过ProcessEngineConfiguration中的对应构造方法来实现
@Test
    public void test2() throws Exception{
        ProcessEngineConfiguration configuration = ProcessEngineConfiguration
                .createProcessEngineConfigurationFromResource("flowable.cfg.xml");
        ProcessEngine processEngine = configuration.buildProcessEngine();
        System.out.println("processEngine = " + processEngine);
    }部署流程定义
接下来我们构建一个非常简单的请假流程,Flowable引擎需要流程定义为BPMN 2.0格式,这是一个业界广泛接受的XML标准。 在Flowable术语中,我们将其称为一个流程定义(process definition)。一个流程定义可以启动多个流程实例(process instance)。流程定义可以看做是重复执行流程的蓝图。 在这个例子中,流程定义定义了请假的各个步骤,而一个流程实例对应某个雇员提出的一个请假申请。
BPMN 2.0存储为XML,并包含可视化的部分:使用标准方式定义了每个步骤类型(人工任务,自动服务调用,等等)如何呈现,以及如何互相连接。这样BPMN 2.0标准使技术人员与业务人员能用双方都能理解的方式交流业务流程。
我们要使用的流程定义为:

流程定义说明:
- 我们假定启动流程需要提供一些信息,例如雇员名字、请假时长以及说明。当然,这些可以单独建模为流程中的第一步。 但是如果将它们作为流程的“输入信息”,就能保证只有在实际请求时才会建立一个流程实例。否则(将提交作为流程的第一步),用户可能在提交之前改变主意并取消,但流程实例已经创建了。 在某些场景中,就可能影响重要的指标(例如启动了多少申请,但还未完成),取决于业务目标。
- 左侧的圆圈叫做启动事件(start event)。这是一个流程实例的起点。
- 第一个矩形是一个用户任务(user task)。这是流程中用户操作的步骤。在这个例子中,经理需要批准或驳回申请
- 取决于经理的决定,排他网关(exclusive gateway) (带叉的菱形)会将流程实例路由至批准或驳回路径
- 如果批准,则需要将申请注册至某个外部系统,并跟着另一个用户任务,将经理的决定通知给申请人。当然也可以改为发送邮件。
- 如果驳回,则为雇员发送一封邮件通知他。
一般来说,这样的流程定义使用可视化建模工具建立,如Flowable Designer(Eclipse)或Flowable Web Modeler(Web应用)。但在这里我们直接撰写XML,以熟悉BPMN 2.0及其概念。
与上面展示的流程图对应的BPMN 2.0 XML在下面显示。请注意这只包含了“流程部分”。如果使用图形化建模工具,实际的XML文件还将包含“可视化部分”,用于描述图形信息,如流程定义中各个元素的坐标(所有的图形化信息包含在XML的BPMNDiagram标签中,作为definitions标签的子元素)。
将下面的XML保存在src/main/resources文件夹下名为holiday-request.bpmn20.xml的文件中。
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="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: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"
             xmlns:flowable="http://flowable.org/bpmn"
             typeLanguage="http://www.w3.org/2001/XMLSchema"
             expressionLanguage="http://www.w3.org/1999/XPath"
             targetNamespace="http://www.flowable.org/processdef">
    <process id="holidayRequest" name="Holiday Request" isExecutable="true">
        <startEvent id="startEvent"/>
        <sequenceFlow sourceRef="startEvent" targetRef="approveTask"/>
        <userTask id="approveTask" name="Approve or reject request"/>
        <sequenceFlow sourceRef="approveTask" targetRef="decision"/>
        <exclusiveGateway id="decision"/>
        <sequenceFlow sourceRef="decision" targetRef="externalSystemCall">
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[
          ${approved}
        ]]>
            </conditionExpression>
        </sequenceFlow>
        <sequenceFlow  sourceRef="decision" targetRef="sendRejectionMail">
            <conditionExpression xsi:type="tFormalExpression">
                <![CDATA[
          ${!approved}
        ]]>
            </conditionExpression>
        </sequenceFlow>
        <serviceTask id="externalSystemCall" name="Enter holidays in external system"
                     flowable:class="org.flowable.CallExternalSystemDelegate"/>
        <sequenceFlow sourceRef="externalSystemCall" targetRef="holidayApprovedTask"/>
        <userTask id="holidayApprovedTask" name="Holiday approved"/>
        <sequenceFlow sourceRef="holidayApprovedTask" targetRef="approveEnd"/>
        <serviceTask id="sendRejectionMail" name="Send out rejection email"
                     flowable:class="org.flowable.SendRejectionMail"/>
        <sequenceFlow sourceRef="sendRejectionMail" targetRef="rejectEnd"/>
        <endEvent id="approveEnd"/>
        <endEvent id="rejectEnd"/>
    </process>
</definitions>活动之间通过顺序流(sequence flow)连接,在流程图中是一个有向箭头。在执行流程实例时,执行(execution)会从启动事件沿着顺序流流向下一个活动。
离开排他网关(带有X的菱形)的顺序流很特别:都以表达式(expression)的形式定义了条件(condition) 。当流程实例的执行到达这个网关时,会计算条件,并使用第一个计算为true的顺序流。这就是排他的含义:只选择一个。当然如果需要不同的路由策略,可以使用其他类型的网关。
这里用作条件的表达式为${approved},这是${approved == true}的简写。变量’approved’被称作流程变量(process variable)。流程变量是持久化的数据,与流程实例存储在一起,并可以在流程实例的生命周期中使用。在这个例子里,我们需要在特定的地方(当经理用户任务提交时,或者以Flowable的术语来说,完成(complete)时)设置这个流程变量,因为这不是流程实例启动时就能获取的数据。
现在我们已经有了流程BPMN 2.0 XML文件,下来需要将它部署(deploy)到引擎中。部署一个流程定义意味着:
- 流程引擎会将XML文件存储在数据库中,这样可以在需要的时候获取它。
- 流程定义转换为内部的、可执行的对象模型,这样使用它就可以启动流程实例。

将流程定义部署至Flowable引擎,需要使用RepositoryService,其可以从ProcessEngine对象获取。使用RepositoryService,可以通过XML文件的路径创建一个新的部署(Deployment),并调用deploy()方法实际执行:
/**
     * 部署流程
     */
    @Test
    public void testDeploy(){
        // 配置数据库相关信息 获取 ProcessEngineConfiguration
        ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
                .setJdbcUrl("jdbc:mysql://localhost:3306/flowable-learn2?serverTimezone=UTC&nullCatalogMeansCurrent=true")
                .setJdbcUsername("root")
                .setJdbcPassword("123456")
                .setJdbcDriver("com.mysql.cj.jdbc.Driver")
                .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
        // 获取流程引擎对象
        ProcessEngine processEngine = cfg.buildProcessEngine();
        // 部署流程 获取RepositoryService对象
        RepositoryService repositoryService = processEngine.getRepositoryService();
        Deployment deployment = repositoryService.createDeployment()// 创建Deployment对象
                .addClasspathResource("holiday-request.bpmn20.xml") // 添加流程部署文件
                .name("请求流程") // 设置部署流程的名称
                .deploy(); // 执行部署操作
        System.out.println("deployment.getId() = " + deployment.getId());
        System.out.println("deployment.getName() = " + deployment.getName());
    }然后执行该方法日志操作成功:

在后台表结构也可以看到相关的信息
act_re_deployment: 流程定义部署表,每部署一次就增加一条记录

act_re_procdef :流程定义表,部署每个新的流程定义都会在这张表中增加一条记录

act_ge_bytearray :流程资源表,流程部署的 bpmn文件和png图片会保存在该表中

我们现在可以通过API查询验证流程定义已经部署在引擎中(并学习一些API)。通过RepositoryService创建的ProcessDefinitionQuery对象实现。
/**
 * 查看流程定义
 */
@Test
public void testDeployQuery(){
	// 配置数据库相关信息 获取 ProcessEngineConfiguration
	ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
			.setJdbcUrl("jdbc:mysql://localhost:3306/flowable-learn2?serverTimezone=UTC&nullCatalogMeansCurrent=true")
			.setJdbcUsername("root")
			.setJdbcPassword("123456")
			.setJdbcDriver("com.mysql.cj.jdbc.Driver")
			.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
	// 获取流程引擎对象
	ProcessEngine processEngine = cfg.buildProcessEngine();
	// 部署流程 获取RepositoryService对象
	RepositoryService repositoryService = processEngine.getRepositoryService();
	// 获取流程定义对象
	ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
			.deploymentId("2501")
			.singleResult();
	System.out.println("processDefinition.getId() = " + processDefinition.getId());
	System.out.println("processDefinition.getName() = " + processDefinition.getName());
	System.out.println("processDefinition.getDeploymentId() = " + processDefinition.getDeploymentId());
	System.out.println("processDefinition.getDescription() = " + processDefinition.getDescription());
}输出结果为:
processDefinition.getId() = holidayRequest:2:2503
processDefinition.getName() = Holiday Request
processDefinition.getDeploymentId() = 2501
processDefinition.getDescription() = null启动流程实例
现在已经在流程引擎中部署了流程定义,因此可以使用这个流程定义作为“模板”启动流程实例。

要启动流程实例,需要提供一些初始化流程变量。一般来说,可以通过呈现给用户的表单,或者在流程由其他系统自动触发时通过REST API,来获取这些变量。在这个例子里,我们简化直接在代码中定义了,我们使用RuntimeService启动一个流程实例。
/**
     * 启动流程实例
     */
    @Test
    public void testRunProcess(){
        // 配置数据库相关信息 获取 ProcessEngineConfiguration
        ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
                .setJdbcUrl("jdbc:mysql://localhost:3306/flowable-learn2?serverTimezone=UTC&nullCatalogMeansCurrent=true")
                .setJdbcUsername("root")
                .setJdbcPassword("123456")
                .setJdbcDriver("com.mysql.cj.jdbc.Driver")
                .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
        // 获取流程引擎对象
        ProcessEngine processEngine = cfg.buildProcessEngine();
        // 启动流程实例通过 RuntimeService 对象
        RuntimeService runtimeService = processEngine.getRuntimeService();
        // 构建流程变量
        Map<String,Object> variables = new HashMap<>();
        variables.put("employee","张三") ;// 谁申请请假
        variables.put("nrOfHolidays",3); // 请几天假
        variables.put("description","工作累了,想出去玩玩"); // 请假的原因
        // 启动流程实例,第一个参数是流程定义的id
        ProcessInstance processInstance = runtimeService
                .startProcessInstanceByKey("holidayRequest", variables);// 启动流程实例
        // 输出相关的流程实例信息
        System.out.println("流程定义的ID:" + processInstance.getProcessDefinitionId());
        System.out.println("流程实例的ID:" + processInstance.getId());
        System.out.println("当前活动的ID:" + processInstance.getActivityId());
    }启动成功,输出结果如下:
流程定义的ID:holidayRequest:2:2503
流程实例的ID:5001
当前活动的ID:null对应的流程实例ID为:5001
启动流程实例涉及到的表结构:
- act_hi_actinst 流程实例执行历史
- act_hi_identitylink 流程的参与用户的历史信息
- act_hi_procinst 流程实例历史信息
- act_hi_taskinst 流程任务历史信息
- act_ru_execution 流程执行信息
- act_ru_identitylink 流程的参与用户信息
- act_ru_task 任务信息
查看任务
上面员工发起了一个请假流程,接下来就会流转到总经理这儿来处理,之前我们没有指定经理这的处理人,我们可以加一个

这里的处理人目前是写死的,我们还可以利用表达式代替后期用变量填充,或者是用taskService.setAssignee(taskId,userId)来指定处理人
然后我们来查看下lisi的任务
 /**
     * 查看任务
     */
    @Test
    public void testQueryTask(){
        // 配置数据库相关信息 获取 ProcessEngineConfiguration
        ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
                .setJdbcUrl("jdbc:mysql://localhost:3306/flowable-learn2?serverTimezone=UTC&nullCatalogMeansCurrent=true")
                .setJdbcUsername("root")
                .setJdbcPassword("123456")
                .setJdbcDriver("com.mysql.cj.jdbc.Driver")
                .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
        // 获取流程引擎对象
        ProcessEngine processEngine = cfg.buildProcessEngine();
        TaskService taskService = processEngine.getTaskService();
        List<Task> list = taskService.createTaskQuery()
                .processDefinitionKey("holidayRequestNew")
                .taskAssignee("lisi")
                .list();
        for (Task task : list) {
            System.out.println("task.getProcessDefinitionId() = " + task.getProcessDefinitionId());
            System.out.println("task.getId() = " + task.getId());
            System.out.println("task.getAssignee() = " + task.getAssignee());
            System.out.println("task.getName() = " + task.getName());
        }
    }输出结果为:
task.getProcessDefinitionId() = holidayRequestNew:1:10003
task.getId() = 12508
task.getAssignee() = lisi
task.getName() = Approve or reject request这里值得注意的是查询任务时用的是taskService.createTaskQuery().processDefinitionKey("holidayRequestNew").taskAssignee("lisi").list();
方式,是从act_ru_task这张运行时表里获取的数据,其实我们还可以从act_hi_taskinst这张历史任务表里获取数据,因为历史任务表和运行任务表的数据时同步的,使用这个方法historyService.createHistoricTaskInstanceQuery().processInstanceId(pi.getProcessInstanceId()).unfinished().singleResult();因为unfinished()获取的是未完成的任务列表。
完成任务
现在李四这个角色可以来完成当前的任务了

在此处我们直接解决掉这个请假,然后会走发送拒绝邮件的流程,这块我们需要用到JavaDelegate来触发。

我们定义这样一个Java类
public class SendRejectionMail implements JavaDelegate {
    /**
     * 触发发送邮件的操作
     * @param delegateExecution
     */
    @Override
    public void execute(DelegateExecution delegateExecution) {
        System.out.println("请假被拒绝,,,安心工作吧");
    }
}然后来完成任务
/**
     * 完成任务
     */
    @Test
    public void testCompleteTask(){
        // 配置数据库相关信息 获取 ProcessEngineConfiguration
        ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
                .setJdbcUrl("jdbc:mysql://localhost:3306/flowable-learn2?serverTimezone=UTC&nullCatalogMeansCurrent=true")
                .setJdbcUsername("root")
                .setJdbcPassword("123456")
                .setJdbcDriver("com.mysql.cj.jdbc.Driver")
                .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
        // 获取流程引擎对象
        ProcessEngine processEngine = cfg.buildProcessEngine();
        TaskService taskService = processEngine.getTaskService();
        Task task = taskService.createTaskQuery()
                .processDefinitionKey("holidayRequestNew")
                .taskAssignee("lisi")
                .singleResult();
        // 添加流程变量
        Map<String,Object> variables = new HashMap<>();
        variables.put("approved",false); // 拒绝请假
        // 完成任务
        taskService.complete(task.getId(),variables);
    }然后可以看到JavaDelegate触发了

这里值得注意的是这里的JavaDelegate是java服务任务,在Flowable的众多活动任务组件中是服务组件的监听类,常常用于发送消息。
Flowable监听包括:任务监听器,执行监听器,服务任务监听,事件监听器
- 服务任务监听就是继承JavaDelegate类来实现,实现方式已经赘述过了。
- 任务监听器:实现TaskListener接口- 任务监听器是处理业务逻辑的重要的地方,当任务创建、设定负责人、完成任务时都可以监听的到从而来处理自己的业务。
- 常用于监听Assignment事件,设置完负责人给负责人发一个消息来通知提示。注意:任务监听器只能用在UserTask上使用。
- 监听的事件类型:- Create:任务创建后触发。常用于任务创建后设置任务负责人等。
- Assignment:任务分配后触发。常用于设置完负责人后向负责人发邮件、短信等通知一下。
- Delete:任务完成后触发。
- All:所有事件发生都触发。
 
 
- 任务监听器是处理业务逻辑的重要的地方,当
// bpmn.xml
<userTask id="USERTASK" name="USERTASK" >
  <extensionElements>
    <activiti:taskListener event="complete" class="com.github.flowable.delegate.MyListener"/>
  </extensionElements>
</userTask>
// java 代码
public class MyListener implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        System.out.println("===========执行监听器============");
    }
}- 执行监听器:实现ExecutionListener接口- 执行监听器(execution listener)可以在流程执行中发生特定的事件时,执行外部Java代码或计算表达式
 - 任务监听器只能监听UserTask,执行监听器用在流程的不同的阶段上:- 流程实例的启动和结束。
- 流程执行转移。
- 活动的启动和结束。
- 网关的启动和结束。
- 中间事件的启动和结束。
- 启动事件的结束,和结束事件的启动。
 
- 监听事件类型:- start:开始
- end:结束
- take:执行
 
 
import org.activiti.engine.delegate.ExecutionListener;
public class MyExecutionListener implements ExecutionListener {
    @Override
    public void notify(DelegateExecution execution) {
        // Id=_2
        System.out.println("Id=" + execution.getCurrentFlowElement().getId());
        // Name=StartEvent
        System.out.println("Name=" + execution.getCurrentFlowElement().getName());
        // EventName=start
        System.out.println("EventName=" + execution.getEventName());
        // ProcessDefinitionId=helloworld:1:3
        System.out.println("ProcessDefinitionId=" + execution.getProcessDefinitionId());
        // ProcessInstanceId=2501
        System.out.println("ProcessInstanceId=" + execution.getProcessInstanceId());
    }
}
任务监听器和执行监听器的区别,主要是使用位置的区别,只有用户任务节点才能用任务监听器,当你发现你要做监听器的节点不能用任务监听器实现时,就可以用执行监听器
- 事件监听器:实现FlowableEventListener接口(全局监听)- 事件监听器基类,可用来监听实体(entity)相关事件,特定或所有实体的事件都可以。
- 可以只为特定种类的事件注册监听器,而不是在任何类型的事件发送时都被通知。可以通过配置添加引擎全局的事件监听器,在运行时通过API添加引擎全局的事件监听器,也可以在BPMN XML文件为个别流程定义添加事件监听器。
- 除了这个FlowableEventListener接口还可以继承AbstractFlowableEngineEventListener类来实现,而这个AbstractFlowableEngineEventListener的底层就是FlowableEventListener
 

通常都是用来做全局监听事件
// 注册监听器到Spring容器中
@Configuration
@RequiredArgsConstructor
public class FlowableGlobalListenerConfig implements ApplicationListener<ContextRefreshedEvent> {
    @Autowired
    private final SpringProcessEngineConfiguration configuration;
    @Autowired
    private final MyEventListener myEventListener;
    @Autowired
    private final MyEventListener2 myEventListener2;
    @Autowired
    private RuntimeService runtimeService;
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        FlowableEventDispatcher dispatcher = configuration.getEventDispatcher();
        // 任务创建全局监听-待办消息发送
        dispatcher.addEventListener(myEventListener, FlowableEngineEventType.TASK_CREATED);
//        runtimeService.addEventListener(myEventListener2, FlowableEngineEventType.PROCESS_COMPLETED);
    }
}
// 全局监听
@Slf4j
@Component
public class MyEventListener implements FlowableEventListener {
    @Override
    public void onEvent(FlowableEvent event) {
        log.info("MyEventListener====》onEvent:{}", event);
    }
    @Override
    public boolean isFailOnException() {
        return false;
    }
    @Override
    public boolean isFireOnTransactionLifecycleEvent() {
        return false;
    }
    @Override
    public String getOnTransaction() {
        return null;
    }
}
@Slf4j
@Component
public class MyEventListener2 extends AbstractFlowableEngineEventListener {
    @Autowired
    private TaskService taskService;
    @Autowired
    private RuntimeService runtimeService;
    @Override
    protected void processCompleted(FlowableEngineEntityEvent event) {
        System.out.println("进入流程结束监听器……");
        String processInstanceId = event.getProcessInstanceId();
        Map<String, Object> variables = runtimeService.getVariables(processInstanceId);
        Map<String, Object> startForm = (Map<String, Object>) variables.get("startForm");
        System.out.println("variables: "+variables);
        System.out.println("startForm: "+startForm);
        super.processCompleted(event);
    }
    @Override
    protected void taskCompleted(FlowableEngineEntityEvent event) {
        System.out.println("进入taskCompleted监听器……");
        super.taskCompleted(event);
    }
    @Override
    public void onEvent(FlowableEvent flowableEvent) {
        System.out.println("进入taskCompleted监听器--onEvent……");
        super.onEvent(flowableEvent);
    }
}
通过配置添加引擎全局的事件监听器
在流程引擎中配置的事件监听器会在流程引擎启动时生效,引擎重启后也会保持有效。
eventListeners参数为org.flowable.engine.delegate.event.FlowableEventListener类实例的列表(list)。与其他地方一样,你可以声明内联bean定义,也可以用ref指向已有的bean。下面的代码片段在配置中添加了一个事件监听器,无论任何类型的事件分发时,都会得到通知:
<bean id="processEngineConfiguration"
    class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
    ...
    <property name="eventListeners">
      <list>
         <bean class="org.flowable.engine.example.MyEventListener" />
      </list>
    </property>
</bean>要在特定类型的事件分发时得到通知,使用typedEventListeners参数,值为map。map的key为逗号分隔的事件名字列表(或者一个事件的名字),取值为org.flowable.engine.delegate.event.FlowableEventListener实例的列表。下面的代码片段在配置中添加了一个事件监听器,它会在作业执行成功或失败时得到通知:
<bean id="processEngineConfiguration"
    class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
    ...
    <property name="typedEventListeners">
      <map>
        <entry key="JOB_EXECUTION_SUCCESS,JOB_EXECUTION_FAILURE" >
          <list>
            <bean class="org.flowable.engine.example.MyJobEventListener" />
          </list>
        </entry>
      </map>
    </property>
</bean>事件分发的顺序由加入监听器的顺序决定。首先,所有普通(eventListeners参数定义的)事件监听器按照在list里的顺序被调用;之后,如果分发的是某类型的事件,则(typedEventListeners 参数定义的)该类型监听器被调用。
在运行时添加监听器
可以使用API(RuntimeService)为引擎添加或删除事件监听器:
/**
 * 新增一个监听器,会在所有事件发生时被通知。
 * @param listenerToAdd 要新增的监听器
 */
void addEventListener(FlowableEventListener listenerToAdd);
/**
 * 新增一个监听器,在给定类型的事件发生时被通知。
 * @param listenerToAdd 要新增的监听器
 * @param types 监听器需要监听的事件类型
 */
void addEventListener(FlowableEventListener listenerToAdd, FlowableEventType... types);
/**
 * 从分发器中移除指定监听器。该监听器将不再被通知,无论该监听器注册为监听何种类型。
 * @param listenerToRemove 要移除的监听器
 */
 void removeEventListener(FlowableEventListener listenerToRemove);请注意,运行时新增的监听器在引擎重启后不会保持。
为个别流程定义增加监听器
可以为某一流程定义增加监听器。只有与该流程定义相关,或使用该流程定义启动的流程实例相关的事件,才会调用这个监听器。监听器实现可以用完全限定类名(fully qualified classname)定义;也可以定义为表达式,该表达式需要能被解析为实现监听器接口的bean;也可以配置为抛出消息(message)/信号(signal)/错误(error)的BPMN事件。
执行用户定义逻辑的监听器
下面的代码片段为流程定义增加了2个监听器。第一个监听器接收任何类型的事件,使用完全限定类名定义。第二个监听器只在作业成功执行或失败时被通知,使用流程引擎配置中beans参数定义的bean作为监听器。
<process id="testEventListeners">
  <extensionElements>
    <flowable:eventListener class="org.flowable.engine.test.MyEventListener" />
    <flowable:eventListener delegateExpression="${testEventListener}" events="JOB_EXECUTION_SUCCESS,JOB_EXECUTION_FAILURE" />
  </extensionElements>
  ...
</process>实体相关的事件也可以在流程定义中增加监听器,只有在特定实体类型的事件发生时得到通知。下面的代码片段展示了如何设置。可以响应实体的所有事件(第一个例子),或只响应实体的特定类型事件(第二个例子)。
<process id="testEventListeners">
  <extensionElements>
    <flowable:eventListener class="org.flowable.engine.test.MyEventListener" entityType="task" />
    <flowable:eventListener delegateExpression="${testEventListener}" events="ENTITY_CREATED" entityType="task" />
  </extensionElements>
  ...
</process>entityType可用的值有:attachment(附件), comment(备注), execution(执行), identity-link(身份关联), job(作业), process-instance(流程实例), process-definition(流程定义), task(任务)。
监听器的内容是一个补充,回到正题
流程的删除
有些流程已经没有用了,我们需要删除掉,其实也非常简单
/**
     * 删除流程
     */
    @Test
    public void testDeleteProcess(){
        // 配置数据库相关信息 获取 ProcessEngineConfiguration
        ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
                .setJdbcUrl("jdbc:mysql://localhost:3306/flowable-learn2?serverTimezone=UTC&nullCatalogMeansCurrent=true")
                .setJdbcUsername("root")
                .setJdbcPassword("123456")
                .setJdbcDriver("com.mysql.cj.jdbc.Driver")
                .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
        // 获取流程引擎对象
        ProcessEngine processEngine = cfg.buildProcessEngine();
        RepositoryService repositoryService = processEngine.getRepositoryService();
        // 删除流程定义,如果该流程定义已经有了流程实例启动则删除时报错
        // repositoryService.deleteDeployment("1");
        // 设置为TRUE 级联删除流程定义,及时流程有实例启动,也可以删除,设置为false 非级联删除操作。
        repositoryService.deleteDeployment("2501",true);
    }查看历史信息
选择使用Flowable这样的流程引擎的原因之一,是它可以自动存储所有流程实例的审计数据或历史数据。这些数据可以用于创建报告,深入展现组织运行的情况,瓶颈在哪里,等等。
例如,如果希望显示流程实例已经执行的时间,就可以从ProcessEngine获取HistoryService,并创建历史活动(historical activities)的查询。在下面的代码片段中,可以看到我们添加了一些额外的过滤条件:
- 只选择一个特定流程实例的活动
- 只选择已完成的活动
结果按照结束时间排序,代表其执行顺序。
/**
 * 查看历史
 */
@Test
public void testQueryHistory(){
	// 配置数据库相关信息 获取 ProcessEngineConfiguration
	ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
			.setJdbcUrl("jdbc:mysql://localhost:3306/flowable-learn2?serverTimezone=UTC&nullCatalogMeansCurrent=true")
			.setJdbcUsername("root")
			.setJdbcPassword("123456")
			.setJdbcDriver("com.mysql.cj.jdbc.Driver")
			.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);
	// 获取流程引擎对象
	ProcessEngine processEngine = cfg.buildProcessEngine();
	HistoryService historyService = processEngine.getHistoryService();
	List<HistoricActivityInstance> list = historyService.createHistoricActivityInstanceQuery()
			.processDefinitionId("holidayRequestNew:1:10003")
			.finished()
			.orderByHistoricActivityInstanceEndTime().asc()
			.list();
	for (HistoricActivityInstance historicActivityInstance : list) {
		System.out.println(historicActivityInstance.getActivityId() + " took "
				+ historicActivityInstance.getDurationInMillis() + " milliseconds");
	}
}输出结果
startEvent took 1 milliseconds
approveTask took 837735 milliseconds
decision took 13 milliseconds
sendRejectionMail took 2 milliseconds
rejectEnd took 1 milliseconds
基本流程就结束了。
Flowable UI应用
安装UI应用最快捷的方式就是Docker安装
docker run -d -p 8080:8080 flowable/flowable-uiFlowable提供了几个web应用,用于演示及介绍Flowable项目提供的功能:
- Flowable IDM: 身份管理应用。为所有Flowable UI应用提供单点登录认证功能,并且为拥有IDM管理员权限的用户提供了管理用户、组与权限的功能。
- Flowable Modeler: 让具有建模权限的用户可以创建流程模型、表单、选择表与应用定义。
- Flowable Task: 运行时任务应用。提供了启动流程实例、编辑任务表单、完成任务,以及查询流程实例与任务的功能。
- Flowable Admin: 管理应用。让具有管理员权限的用户可以查询BPMN、DMN、Form及Content引擎,并提供了许多选项用于修改流程实例、任务、作业等。管理应用通过REST API连接至引擎,并与Flowable Task应用及Flowable REST应用一同部署。
所有其他的应用都需要Flowable IDM提供认证。每个应用的WAR文件可以部署在相同的servlet容器(如Apache Tomcat)中,也可以部署在不同的容器中。由于每个应用使用相同的cookie进行认证,因此应用需要运行在相同的域名下。
默认账号密码admin/test
登录界面

在这4个功能上最常用的就是建模器这是核心,其余的可以按需求使用
点击建模器程序界面,进入后可以看到可以进行导入和创建流程,创建好的流程会被保存在上面。

创建一个流程,流程中的模型名称与模型Key可以相同。

随后就可以画BPMN图

当我们创建好流程图后就可以下载下来

下载下来的是BPMN的xml信息
Flowable基础表结构
- 
ACT_APP_*(5)
- 
ACT_CMMN_*(12)
- 
ACT_CO_*(3)
- 
ACT_DMN_*(6)
- 
ACT_EVT_*(1)
- 
ACT_FO_*(6)
- 
ACT_GE_*(2)
- 
ACT_HI_*(10)
- 
ACT_ID_*(9)
- 
ACT_PROCDEF_*(1)
- 
ACT_RE_*(3)
- 
ACT_RU_*(13)
- 
FLW_CHANNEL_*(1)
- 
FLW_EV_*(2)
- 
FLW_EVENT_*(3)
- 
FLW_RU_*(2)
* 是通配符,后面的数字表示这种类型的表有多少个。
这些表最明显的规律就表名通过两个 _ 隔开成了三部分
- ACT_
- FLW_
ACT_ 就是 Activiti,Flowable 是基于 Activiti 开发出来的,而特定于 Flowable Work 或 Engage 的数据库表以 FLW_ 前缀开头。
紧跟着 ACT_ 或者 FLW_ 后面的内容,基本上也都是简称
- 
APP表示这都是跟应用程序相关的表。
- 
CMMN表示这都是跟 CMMN 协议相关的表。
- 
CO(CONTENT)表示这都是跟内容引擎相关的表。
- 
DMN表示这都是跟 DMN 协议相关的表。
- 
EVT(EVENT)表示这都是跟事件相关的表。
- 
FO(FORM)表示这都是跟表单相关的表。
- 
GE(GENERAL)表示这都是通用表,适用于各种用例的。
- 
HI(HISTORY)这些是包含历史数据的表。当从运行时表中删除数据时,历史表仍然包含这些已完成实例的所有信息。
- 
ID(IDENTITY)表示这都是跟用户身份认证相关的表。
- 
PROCDEF(PROCESSDEFINE) 表示这都是跟记录流程定义相关的表。
- 
RE(REPOSITORY)表示这都是跟流程的定义、流程的资源等等包含了静态信息相关的表。
- 
RU(RUNTIME)代表运行时,这些是包含尚未完成的流程、案例等的运行时数据的运行时表。Flowable 仅在执行期间存储运行时数据,并在实例结束后删除记录,这使运行时表保持小而快。
- 
CHANNEL表示这都是跟泳道相关的表。
- 
EV表示这个是跟 FLW_ 搭配的,在这里似乎并没有一个明确的含义,相关的表也都是跟 Liquibase 相关的表。
- 
EVENT表示这都是跟事件相关的表。
表名的后缀,有一些是通用的后缀名词
- 
DATABASECHANGELOG:表名中包含这个单词的,表示这个表是 Liquibase 执行的记录,Liquibase 是一个数据库脚本管理的工具,有点像 flyway,包含 DATABASECHANGELOG 后缀的表一共是 6 张。
- 
DATABASECHANGELOGLOCK:表名中包含这个单词的,表示这个表记录 Liquibase 执行锁的,用以确保一次只运行一个 Liquibase 实例,包含 DATABASECHANGELOGLOCK 后缀的表也是 6 张。
凡是涉及到 DATABASECHANGELOG 和 DATABASECHANGELOGLOCK 的表,可以直接省略了。
ACT_APP_*
以 ACT_APP_* 开头的表负责应用引擎存储和应用部署定义。相关的表一共是五张。
| ACT_APP_APPDEF | 应用程序模型产生应用程序定义。此定义,如流程/案例/等。是成功部署到应用引擎的应用模型的表示。 | 
| ACT_APP_DEPLOYMENT | 当通过应用引擎部署应用模型时,会存储一条记录以保存此部署。部署的实际内容被存储在 ACT_APP_DEPLOYMENT_RESOURCE表中,并从该表中引用。 | 
| ACT_APP_DEPLOYMENT_RESOURCE | 此表包含构成应用程序部署的实际资源(存储为字节)。当引擎需要实际模型时,将从该表中获取资源。 | 
ACT_CMMN_*
Flowable CMMN Engine 的数据库名称都以 ACT_CMMN_ 开头。这里涉及到一个东西就是 CMMN,CMMN 与 BPMN 协议一致,也是一种流程内容的规范,CMMN 这类表一般用于存储处理 BPMN 所不能适用的业务场景数据,CMMN 通常与 BPMN 搭配使用,不过只有符合 CMMN 规范的模型数据才会使用这类表。
| ACT_CMMN_CASEDEF ACT_CMMN_DEPLOYMENT ACT_CMMN_DEPLOYMENT_RESOURCE | 这三个都是没有附加前缀的表,主要定义了静态信息,例如 case 的定义和部署和以及相关的资源等。 | 
| 接下来这些以 ACT_CMMN_HI_开头的表代表历史数据,例如过去的案例实例、计划项目等。 | |
| ACT_CMMN_HI_CASE_INST | 此表记录由 CMMN 引擎启动的每个案例实例的数据。 | 
| ACT_CMMN_HI_MIL_INST | 此表记录了在案例实例中达到的每个里程碑的数据。 | 
| ACT_CMMN_HI_PLAN_ITEM_INST | 此表记录了作为案例实例执行的一部分创建的每个计划项实例的数据。 | 
| 接下来以 ACT_CMMN_RU_开始的表代表运行时的数据,这些数据包含案例实例、计划项等的运行时数据。Flowable 仅在案例实例执行期间存储运行时数据,并在案例实例结束时删除记录,这使运行时表保持小且查询速度快。 | |
| ACT_CMMN_RU_CASE_INST | 此表包含每个已启动但尚未完成的案例实例的条目。 | 
| ACT_CMMN_RU_MIL_INST | 此表包含作为运行案例实例的一部分达到的每个里程碑的条目。 | 
| ACT_CMMN_RU_PLAN_ITEM_INST | 案例实例执行由案例定义中定义的计划项的多个实例组成,此表包含在案例实例执行期间创建的每个实例的条目。 | 
| ACT_CMMN_RU_SENTRY_PART_INST | 计划项目实例可以有守卫状态转换的哨兵,这样的哨兵在状态改变之前可以包含多个部分,这个表就是专门用来存储这种哨兵。 | 
ACT_DMN_*
Flowable DMN 的数据库名称都以 ACT_DMN_ 开头
| ACT_DMN_DEPLOYMENT ACT_DMN_DEPLOYMENT_RESOURCE | 跟前面的都一样,主要定义了静态信息,只不过这里部署的是 DMN。 | 
| ACT_DMN_DECISION | 此表包含已部署决策表的元数据,并与来自其他引擎的定义相对应。 | 
| ACT_DMN_HI_DECISION_EXECUTION | 此表包含有关 DMN 决策表执行的审计信息。 | 
ACT_FO_FORM_*
以 ACT_FO_FORM_ 开头的表存储表单引擎和围绕表单模型和这些表单的实例数据。
| ACT_FO_FORM_DEFINITION | 表单定义表。 | 
| ACT_FO_FORM_DEPLOYMENT | 表单部署表。 | 
| ACT_FO_FORM_INSTANCE | 表单实例表。 | 
| ACT_FO_FORM_RESOURCE | 表单源数据表。 | 
FLW_EVENT_*
| FLW_EVENT_DEFINITION | 已部署事件定义的元数据。 | 
| FLW_EVENT_DEPLOYMENT | 已部署事件部署元数据。 | 
| FLW_EVENT_RESOURCE | 事件所需资源。 | 
BPMN常用表的大致分类
| 前缀 | 含义 | 说明 | 
| ACT_RE_* | RE表示repository | RepositoryService接口操作的表。如,流程定义,流程的资 源(图片,规则等) | 
| ACT_RU_* | RU表示runtime | 运行时流程变量,用户任务,定时任务等,流程实例结束时将被删除 | 
| ACT_ID_* | ID表示identity | 存储如用户,用户组,权限等,flowable画图时,选择受理人或受理组就会查询这些表 | 
| ACT_HI_* | HI表示history | 历史的相关数据,如结束的流程实例,变量,任务等 | 
| ACT_GE_* | GE表示general | 普通数据,各种情况都使用的数据 | 
| FLW_* | — | — | 
表的详细信息
| 表分类 | 表名 | 解释 | 
|---|---|---|
| 一般数据 | ||
| [ACT_GE_BYTEARRAY] | 通用的流程定义和流程资源 | |
| [ACT_GE_PROPERTY] | 系统相关属性 | |
| 流程历史记录 | ||
| [ACT_HI_ACTINST] | 历史的流转节点信息 | |
| [ACT_HI_ATTACHMENT] | 历史的流程附件 | |
| [ACT_HI_COMMENT] | 历史的说明性信息 | |
| [ACT_HI_DETAIL] | 历史的流程运行中的细节信息 | |
| [ACT_HI_IDENTITYLINK] | 历史的流程运行过程中用户关系 | |
| [ACT_HI_PROCINST] | 历史的流程实例 | |
| [ACT_HI_TASKINST] | 历史的任务实例 | |
| [ACT_HI_VARINST] | 历史的流程运行中的变量信息 | |
| [ACT_HI_TSK_LOG] | 每一次执行可能会带上数据,存在这里。 | |
| [ACT_HI_ENTITYLINK] | 历史参与的人员表。 | |
| 流程定义表 | ||
| [ACT_RE_DEPLOYMENT] | 部署单元信息 | |
| [ACT_RE_MODEL] | 模型信息,通过flowable-modler画图程序时才会产生该记录 | |
| [ACT_RE_PROCDEF] | 已部署的流程定义 | |
| [ACT_PROCDEF_INFO] | 流程定义动态改变信息表 | |
| 运行实例表 | ||
| [ACT_RU_EVENT_SUBSCR] | 运行时事件 | |
| [ACT_RU_EXECUTION] | 运行时流程执行实例 | |
| [ACT_RU_IDENTITYLINK] | 运行时用户关系信息,候选用户、候选组 | |
| [ACT_RU_JOB] | 运行时作业 | |
| [ACT_RU_TASK] | 运行时任务表 | |
| [ACT_RU_VARIABLE] | 运行时变量表 | |
| [ACT_RU_ACTINST] | 运行中实例的各流转节点信息 | |
| [ACT_RU_TIMER_JOB] | 定时作业表 | |
| [ACT_RU_DEADLETTER_JOB] | 正在运行的任务表 | |
| [ACT_RU_HISTORY_JOB] | 历史作业表 | |
| [ACT_RU_SUSPENDED_JOB] | 暂停作业表 | |
| [ACT_RU_ENTITYLINK] | 此表存储有关实例的父子关系的信息。例如,如果流程实例启动子案例实例,则此关系存储在此表中。这样可以轻松查询关系。 | |
| [ACT_RU_EXTERNAL_JOB] | Flowable 引擎使用作业表来实现异步逻辑、计时器或历史处理。这些表存储每个作业所需的数据。 | |
| 用户用户组表 | ||
| [ACT_ID_BYTEARRAY] | 二进制数据表 | |
| [ACT_ID_GROUP] | 用户组信息表 | |
| [ACT_ID_INFO] | 用户信息详情表 | |
| [ACT_ID_MEMBERSHIP] | 人与组关系表 | |
| [ACT_ID_PRIV] | 权限表 | |
| [ACT_ID_PRIV_MAPPING] | 用户或组权限关系表 | |
| [ACT_ID_PROPERTY] | 属性表 | |
| [ACT_ID_TOKEN] | 记录用户的token信息 | |
| [ACT_ID_USER] | 用户表 | |
| 其他表 | ||
| [ACT_EVT_LOG] | 事件日志表 | |
| [FLW_RU_BATCH] [FLW_RU_BATCH_PART] | 这两个是批量迁移流程时使用。 | |
| [ACT_CO_CONTENT_ITEM] | 每项内容在此表中都有个条目。 | |
| [FLW_CHANNEL_DEFINITION] | 泳池管道定义表。 | 
部署涉及表结构
涉及到的三张表:
部署资源表:act_ge_bytearray
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| REV_ | 版本号 | |
| NAME_ | 名称 | 部署的文件名称,如:holiday-request-new.bpmn20.xml、holiday-request-new.bpmn20.png | 
| DEPLOYMENT_ID_ | 部署ID | |
| BYTES_ | 字节(二进制数据) | |
| GENERATED_ | 是否系统生成 | 0为用户上传, 1为系统自动生成, 比如系统会 自动根据xml生 成png | 
部署ID表:act_re_deployment
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| NAME_ | 名称 | |
| CATEGORY_ | 分类 | |
| TENANT_ID_ | 租户ID | |
| DEPLOY_TIME_ | 部署时间 | |
| DERIVED_FROM_ | 来源于 | |
| DERIVED_FROM_ROOT_ | 来源于 | |
| ENGINE_VERSION_ | 流程引擎的版本 | 
流程表:act_re_procdef
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| REV_ | 版本号 | |
| CATEGORY_ | 分类 | 流程定义的Namespace就是类别 | 
| NAME_ | 名称 | |
| KEY_ | 标识 | |
| VERSION_ | 版本 | |
| DEPLOYMENT_ID_ | 部署ID | |
| RESOURCE_NAME_ | 资源名称 | 流程bpmn文件名称 | 
| DGRM_RESOURCE_NAME_ | 图片资源名称 | |
| DESCRIPTION_ | 描述 | |
| HAS_START_FORM_KEY_ | 拥有开始表单标识 | start节点是否存在formKey 0否 1是 | 
| HAS_GRAPHICAL_NOTATION_ | 拥有图形信息 | |
| SUSPENSION_STATE_ | 挂起状态 | 暂停状态 1激活 2暂停 | 
| TENANT_ID_ | 租户ID | 
注意:
业务流程定义数据表。此表和ACT_RE_DEPLOYMENT是多对一的关系,即,一个部署的bar包里可能包含多个流程定义文件,每个流程定义文件都会有一条记录在ACT_REPROCDEF表内,每个流程定义的数据,都会对于ACT_GE_BYTEARRAY表内的一个资源文件和PNG图片文件。和ACT_GE_BYTEARRAY的关联是通过程序用ACT_GE_BYTEARRAY.NAME与ACT_RE_PROCDEF.NAME_完成的
挂起和激活
部署的流程默认的状态为激活,如果我们暂时不想使用该定义的流程,那么可以挂起该流程。当然该流程定义下边所有的流程实例全部暂停。
流程定义为挂起状态,该流程定义将不允许启动新的流程实例,同时该流程定义下的所有的流程实例都将全部挂起暂停执行。
/**
 * 挂起流程
 */
@Test
public void test05(){
	// 获取流程引擎对象
	ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
	RepositoryService repositoryService = processEngine.getRepositoryService();
	ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
			.processDefinitionId("holiday:1:4")
			.singleResult();
	// 获取流程定义的状态
	boolean suspended = processDefinition.isSuspended();
	System.out.println("suspended = " + suspended);
	if(suspended){
		// 表示被挂起
		System.out.println("激活流程定义");
		repositoryService.activateProcessDefinitionById("holiday:1:4",true,null);
	}else{
		// 表示激活状态
		System.out.println("挂起流程");
		repositoryService.suspendProcessDefinitionById("holiday:1:4",true,null);
	}
}具体的实现其实就是更新了流程定义表中的字段

而且通过REV_字段来控制数据安全,也是一种乐观锁的体现了,如果要启动一个已经挂起的流程就会出现如下的错误

启动流程实例
然后我们来看看启动流程实例的过程。实现代码如下:
/**
 * 启动流程实例
 */
@Test
public void testRunProcess(){
	// 获取流程引擎对象
	ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
	// 启动流程实例通过 RuntimeService 对象
	RuntimeService runtimeService = processEngine.getRuntimeService();
	// 构建流程变量
	Map<String,Object> variables = new HashMap<>();
	variables.put("employee","张三") ;// 谁申请请假
	variables.put("nrOfHolidays",3); // 请几天假
	variables.put("description","工作累了,想出去玩玩"); // 请假的原因
	// 启动流程实例,第一个参数是流程定义的id
	ProcessInstance processInstance = runtimeService
			.startProcessInstanceById("holiday:1:4", variables);// 启动流程实例
	// 输出相关的流程实例信息
	System.out.println("流程定义的ID:" + processInstance.getProcessDefinitionId());
	System.out.println("流程实例的ID:" + processInstance.getId());
	System.out.println("当前活动的ID:" + processInstance.getActivityId());
}当我们启动了一个流程实例后,会在ACT_RU_*对应的表结构中操作,运行时实例涉及的表结构共10张:
- ACT_RU_DEADLETTER_JOB 正在运行的任务表
- ACT_RU_EVENT_SUBSCR 运行时事件
- ACT_RU_EXECUTION 运行时流程执行实例
- ACT_RU_HISTORY_JOB 历史作业表
- ACT_RU_IDENTITYLINK 运行时用户关系信息
- ACT_RU_JOB 运行时作业表
- ACT_RU_SUSPENDED_JOB 暂停作业表
- ACT_RU_TASK 运行时任务表
- ACT_RU_TIMER_JOB 定时作业表
- ACT_RU_VARIABLE 运行时变量表
启动一个流程实例的时候涉及到的表有
- ACT_RU_EXECUTION 运行时流程执行实例
- ACT_RU_IDENTITYLINK 运行时用户关系信息
- ACT_RU_TASK 运行时任务表
- ACT_RU_VARIABLE 运行时变量表
ACT_RU_EXECUTION表结构
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| REV_ | 版本号 | |
| PROC_INST_ID_ | 流程实例ID | |
| BUSINESS_KEY_ | 业务主键ID | |
| PARENT_ID_ | 父执行流的ID | |
| PROC_DEF_ID_ | 流程定义的数据ID | |
| SUPER_EXEC_ | ||
| ROOT_PROC_INST_ID_ | 流程实例的root流程id | |
| ACT_ID_ | 节点实例ID | |
| IS_ACTIVE_ | 是否存活 | |
| IS_CONCURRENT_ | 执行流是否正在并行 | |
| IS_SCOPE_ | ||
| IS_EVENT_SCOPE_ | ||
| IS_MI_ROOT_ | ||
| SUSPENSION_STATE_ | 流程终端状态 | |
| CACHED_ENT_STATE_ | ||
| TENANT_ID_ | 租户编号 | |
| NAME_ | ||
| START_TIME_ | 开始时间 | |
| START_USER_ID_ | 开始的用户编号 | |
| LOCK_TIME_ | 锁定时间 | |
| IS_COUNT_ENABLED_ | ||
| EVT_SUBSCR_COUNT_ | ||
| TASK_COUNT_ | ||
| JOB_COUNT_ | ||
| TIMER_JOB_COUNT_ | ||
| SUSP_JOB_COUNT_ | ||
| DEADLETTER_JOB_COUNT_ | ||
| VAR_COUNT_ | ||
| ID_LINK_COUNT_ | 
创建流程实例后对应的表结构的数据

ACT_RU_TASK 运行时任务表
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| REV_ | 版本号 | |
| EXECUTION_ID_ | 任务所在的执行流ID | |
| PROC_INST_ID_ | 流程实例ID | |
| PROC_DEF_ID_ | 流程定义数据ID | |
| NAME_ | 任务名称 | |
| PARENT_TASK_ID_ | 父任务ID | |
| DESCRIPTION_ | 说明 | |
| TASK_DEF_KEY_ | 任务定义的ID值 | |
| OWNER_ | 任务拥有人 | |
| ASSIGNEE_ | 被指派执行该任务的人 | |
| DELEGATION_ | 委托人 | |
| PRIORITY_ | 优先级 | |
| CREATE_TIME_ | 创建时间 | |
| DUE_DATE_ | 耗时 | |
| CATEGORY_ | 类别 | |
| SUSPENSION_STATE_ | 是否挂起 | 1代表激活 2代表挂起 | 
| TENANT_ID_ | 租户编号 | |
| FORM_KEY_ | ||
| CLAIM_TIME_ | 拾取时间 | 
创建流程实例后对应的表结构的数据

ACT_RU_VARIABLE 运行时变量表
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| REV_ | 版本号 | |
| TYPE_ | 参数类型 | 可以是基本的类型,也可以用户自行扩展 | 
| NAME_ | 参数名称 | |
| EXECUTION_ID_ | 参数执行ID | |
| PROC_INST_ID_ | 流程实例ID | |
| TASK_ID_ | 任务ID | |
| BYTEARRAY_ID_ | 资源ID | |
| DOUBLE_ | 参数为double,则保存在该字段中 | |
| LONG_ | 参数为long,则保存在该字段中 | |
| TEXT_ | 用户保存文本类型的参数值 | |
| TEXT2_ | 用户保存文本类型的参数值 | 
创建流程实例后对应的表结构的数据

ACT_RU_IDENTITYLINK 运行时用户关系信息
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| REV_ | 版本号 | |
| GROUP_ID_ | 用户组ID | |
| TYPE_ | 关系数据类型 | assignee支配人(组)、candidate候选人(组)、owner拥有人,participant参与者 | 
| USER_ID_ | 用户ID | |
| TASK_ID_ | 任务ID | |
| PROC_INST_ID_ | 流程定义ID | |
| PROC_DEF_ID_ | 属性ID | 
创建流程实例后对应的表结构的数据:

处理流程
上面的流程已经流转到了zhangsan这个用户这里,然后可以开始审批了
// 获取流程引擎对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
Task task = taskService.createTaskQuery()
		.processDefinitionId("holiday:1:4")
		.taskAssignee("zhangsan")
		.singleResult();
// 添加流程变量
Map<String,Object> variables = new HashMap<>();
	variables.put("approved",false); // 拒绝请假
// 完成任务
	taskService.complete(task.getId(),variables);在正常处理流程中涉及到的表结构
- ACT_RU_EXECUTION 运行时流程执行实例
- ACT_RU_IDENTITYLINK 运行时用户关系信息
- ACT_RU_TASK 运行时任务表
- ACT_RU_VARIABLE 运行时变量表
ACT_RU_TASK 运行时任务表 :会新生成一条记录

ACT_RU_VARIABLE 运行时变量表:会记录新的流程变量

当然流程实例也可以挂起
// 1.获取ProcessEngine对象
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
// 2.获取RuntimeService
RuntimeService runtimeService = engine.getRuntimeService();
// 3.获取流程实例对象
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
    .processInstanceId("25001")
    .singleResult();
// 4.获取相关的状态操作
boolean suspended = processInstance.isSuspended();
String id = processInstance.getId();
if(suspended){
    // 挂起--》激活
    runtimeService.activateProcessInstanceById(id);
    System.out.println("流程定义:" + id + ",已激活");
}else{
    // 激活--》挂起
    runtimeService.suspendProcessInstanceById(id);
    System.out.println("流程定义:" + id + ",已挂起");
}
启动第二个流程实例后再查看相关的表结构时,对他们的关系理解会更加的清楚一些
启动一个新的流程实例对应的就会产生两条记录

IDENTITYLINK中会记录每次流程操作的信息

流程变量数据,key 相同,但是属于不同的流程实例相互间也是隔离的

完成一个流程
然后我们把第一个流程处理完成
ProcessEngine processEngine = cfg.buildProcessEngine();
TaskService taskService = processEngine.getTaskService();
Task task = taskService.createTaskQuery()
    .processDefinitionId("holiday:1:4")
    .taskAssignee("lisi")
    .singleResult();
// 添加流程变量
Map<String,Object> variables = new HashMap<>();
variables.put("approved",false); // 拒绝请假
// 完成任务
taskService.complete(task.getId(),variables);处理完了一个工作流程后,我们来看看相关的表结构信息
首先我们会发现
- ACT_RU_EXECUTION 运行时流程执行实例
- ACT_RU_IDENTITYLINK 运行时用户关系信息
- ACT_RU_TASK 运行时任务表
- ACT_RU_VARIABLE 运行时变量表
这四张表中对应的数据都没有了,也就是这个流程已经不是运行中的流程了。然后在对应的历史表中我们可以看到相关的信息
- ACT_HI_ACTINST 历史的流程实例
- ACT_HI_ATTACHMENT 历史的流程附件
- ACT_HI_COMMENT 历史的说明性信息
- ACT_HI_DETAIL 历史的流程运行中的细节信息
- ACT_HI_IDENTITYLINK 历史的流程运行过程中用户关系
- ACT_HI_PROCINST 历史的流程实例
- ACT_HI_TASKINST 历史的任务实例
- ACT_HI_VARINST 历史的流程运行中的变量信息
在我们上面的处理流程的过程中设计到的历史表有
ACT_HI_ACTINST 历史的流程实例
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| PROC_DEF_ID_ | 流程定义ID | |
| PROC_INST_ID_ | 流程实例ID | |
| EXECUTION_ID_ | 执行ID | |
| ACT_ID_ | 节点实例ID | |
| TASK_ID_ | 任务ID | |
| CALL_PROC_INST_ID_ | 调用外部的流程实例ID | |
| ACT_NAME_ | 节点名称 | |
| ACT_TYPE_ | 节点类型 | |
| ASSIGNEE_ | 处理人 | |
| START_TIME_ | 开始时间 | |
| END_TIME_ | 结束时间 | |
| DURATION_ | 耗时 | |
| DELETE_REASON_ | 删除原因 | |
| TENANT_ID_ | 租户编号 | 

ACT_HI_IDENTITYLINK 历史的流程运行过程中用户关系
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| GROUP_ID_ | 组编号 | |
| TYPE_ | 类型 | |
| USER_ID_ | 用户编号 | |
| TASK_ID_ | 任务编号 | |
| CREATE_TIME_ | 创建时间 | |
| PROC_INST_ID_ | 流程实例编号 | |
| SCOPE_ID_ | ||
| SCOPE_TYPE_ | ||
| SCOPE_DEFINITION_ID_ | 

ACT_HI_PROCINST 历史的流程实例
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| PROC_INST_ID_ | 流程实例ID | |
| BUSINESS_KEY_ | 业务主键 | |
| PROC_DEF_ID_ | 属性ID | |
| START_TIME_ | 开始时间 | |
| END_TIME_ | 结束时间 | |
| DURATION_ | 耗时 | |
| START_USER_ID_ | 起始人 | |
| START_ACT_ID_ | 起始节点 | |
| END_ACT_ID_ | 结束节点 | |
| SUPER_PROCESS_INSTANCE_ID_ | 父流程实例ID | |
| DELETE_REASON_ | 删除原因 | |
| TENANT_ID_ | 租户编号 | |
| NAME_ | 名称 | 

ACT_HI_TASKINST 历史的任务实例
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| PROC_DEF_ID_ | 流程定义ID | |
| TASK_DEF_KEY_ | 任务定义的ID值 | |
| PROC_INST_ID_ | 流程实例ID | |
| EXECUTION_ID_ | 执行ID | |
| PARENT_TASK_ID_ | 父任务ID | |
| NAME_ | 名称 | |
| DESCRIPTION_ | 说明 | |
| OWNER_ | 实际签收人 任务的拥有者 | 签收人(默认为空,只有在委托时才有值) | 
| ASSIGNEE_ | 被指派执行该任务的人 | |
| START_TIME_ | 开始时间 | |
| CLAIM_TIME_ | 任务拾取时间 | |
| END_TIME_ | 结束时间 | |
| DURATION_ | 耗时 | |
| DELETE_REASON_ | 删除原因 | |
| PRIORITY_ | 优先级别 | |
| DUE_DATE_ | 过期时间 | |
| FORM_KEY_ | 节点定义的formkey | |
| CATEGORY_ | 类别 | |
| TENANT_ID_ | 租户 | 

ACT_HI_VARINST 历史的流程运行中的变量信息:流程变量虽然在任务完成后在流程实例表中会删除,但是在历史表中还是会记录的
| 字段 | 名称 | 备注 | 
|---|---|---|
| ID_ | 主键 | |
| PROC_INST_ID_ | 流程实例ID | |
| EXECUTION_ID_ | 指定ID | |
| TASK_ID_ | 任务ID | |
| NAME_ | 名称 | |
| VAR_TYPE_ | 参数类型 | |
| REV_ | 数据版本 | |
| BYTEARRAY_ID_ | 字节表ID | |
| DOUBLE_ | 存储double类型数据 | |
| LONG_ | 存储long类型数据 | |
| ….. | 

Servcie服务接口
引擎API是与Flowable交互的最常用手段。总入口点是ProcessEngine。ProcessEngine可以使用多种方式创建。使用ProcessEngine,可以获得各种提供工作流/BPM方法的服务。ProcessEngine与服务对象都是线程安全的,因此可以在服务器中保存并共用同一个引用。

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RuntimeService runtimeService = processEngine.getRuntimeService();
RepositoryService repositoryService = processEngine.getRepositoryService();
TaskService taskService = processEngine.getTaskService();
ManagementService managementService = processEngine.getManagementService();
IdentityService identityService = processEngine.getIdentityService();
HistoryService historyService = processEngine.getHistoryService();
FormService formService = processEngine.getFormService();
DynamicBpmnService dynamicBpmnService = processEngine.getDynamicBpmnService();在ProcessEngines.getDefaultProcessEngine()第一次被调用时,将初始化并构建流程引擎,之后的重复调用都会返回同一个流程引擎。可以通过ProcessEngines.init()创建流程引擎,并由ProcessEngines.destroy()关闭流程引擎。
ProcessEngines会扫描flowable.cfg.xml与flowable-context.xml文件。对于flowable.cfg.xml文件,流程引擎会以标准Flowable方式构建引擎:ProcessEngineConfiguration.createProcessEngineConfigurationFromInputStream(inputStream).buildProcessEngine()。对于flowable-context.xml文件,流程引擎会以Spring的方式构建:首先构建Spring应用上下文,然后从该上下文中获取流程引擎。
所有的服务都是无状态的。这意味着你可以很容易的在集群环境的多个节点上运行Flowable,使用同一个数据库,而不用担心上一次调用实际在哪台机器上执行。不论在哪个节点执行,对任何服务的任何调用都是幂等(idempotent)的。
常用API
| service名称 | service作用 | 
|---|---|
| RepositoryService | Flowable的资源管理类 | 
| RuntimeService | Flowable的流程运行管理类 | 
| TaskService | Flowable的任务管理类 | 
| HistoryService | Flowable的历史管理类 | 
| ManagerService | Flowable的引擎管理类 | 
| IdentityService | Flowable的用户管理引擎 | 
| FormService | Flowable的表单服务引擎 | 
| DynamicBpmnService | Flowable的动态修改流程引擎 | 
简单介绍:
RepositoryService
资源管理类,提供了管理和控制流程发布包和流程定义的操作。使用工作流建模工具设计的业务流程图需要使用此service将流程定义文件的内容部署到计算机。
除了部署流程定义以外还可以:查询引擎中的发布包和流程定义。
暂停或激活发布包,对应全部和特定流程定义。 暂停意味着它们不能再执行任何操作了,激活是对应的反向操作。获得多种资源,像是包含在发布包里的文件, 或引擎自动生成的流程图。
获得流程定义的pojo版本, 可以用来通过java解析流程,而不必通过xml。
1.提供了带条件的查询模型流程定义的api
repositoryService.createXXXQuery()
例如:
repositoryService.createModelQuery().list() 模型查询 
repositoryService.createProcessDefinitionQuery().list() 流程定义查询
repositoryService.createDeploymentQuery().list() 查询所有部署	
repositoryService.createXXXXQuery().XXXKey(XXX) (查询该key是否存在)
2.提供一大波模型与流程定义的通用方法
repositoryService.getModel()  (获取模型)
repositoryService.saveModel()  (保存模型)
repositoryService.deleteModel() (删除模型)
repositoryService.createDeployment().deploy(); (部署模型)
repositoryService.getModelEditorSource()  (获得模型JSON数据的UTF8字符串)
repositoryService.getModelEditorSourceExtra()  (获取PNG格式图像)
3.流程定义相关
repositoryService.getProcessDefinition(ProcessDefinitionId);  获取流程定义具体信息
repositoryService.activateProcessDefinitionById()或ByKey 激活流程定义
repositoryService.suspendProcessDefinitionById()或ByKey  挂起流程定义
repositoryService.deleteDeployment()  删除流程定义
repositoryService.getProcessDiagram()获取流程定义图片流
repositoryService.getResourceAsStream()获取流程定义xml流
repositoryService.getBpmnModel(pde.getId()) 获取bpmn对象(当前进行到的那个节点的流程图使用)
4.流程定义授权相关
repositoryService.getIdentityLinksForProcessDefinition() 流程定义授权列表
repositoryService.addCandidateStarterGroup()新增组流程授权
repositoryService.addCandidateStarterUser()新增用户流程授权
repositoryService.deleteCandidateStarterGroup() 删除组流程授权
repositoryService.deleteCandidateStarterUser()  删除用户流程授权
RuntimeService
流程运行管理类。可以从这个服务类中获取很多关于流程执行相关的信息。
runtimeService.createProcessInstanceBuilder().start() 发起流程
runtimeService.deleteProcessInstance() 删除正在运行的流程
runtimeService.suspendProcessInstanceById() 挂起流程定义
runtimeService.activateProcessInstanceById() 激活流程实例
runtimeService.getVariables(processInstanceId); 获取表单中填写的值
runtimeService.setVariable(processInstance.getId(),"key3","value3") 新增或修改参数
runtimeService.getActiveActivityIds(processInstanceId)获取以进行的流程图节点 (当前进行到的那个节点的流程图使用)
runtimeService.createChangeActivityStateBuilder().moveExecutionsToSingleActivityId(executionIds, endId).changeState(); 终止流程
runtimeService.startProcessInstanceByKey(String processDefinitionKey, Map<String, Object> variables) 根据部署流程key启动一个流程
runtimeService.startProcessInstanceById(String processDefinitionId, Map<String, Object> variables) 根据部署流程id启动一个流程
runtimeService.createProcessInstanceQuery().processInstanceId(processInstance.getId()) 查询流程实例	
runtimeService.createExecutionQuery() 获取流程执行对象
runtimeService.signalEventReceived("my-signal"); 信号捕获节点触发
runtimeService.messageEventReceived("my-message",executions.getId()); 消息触发
runtimeService.startProcessInstanceByMessage("my-message"); 流程基于message启动
runtimeService.startProcessInstanceWithForm(String processDefinitionId, String outcome, Map<String,Object> properties, String taskName);  附带表单数据启动流程实例
TaskService
任务管理类。可以从这个类中获取任务的信息。
流转的节点审批
taskService.createTaskQuery().list() 待办任务列表
taskService.createTaskQuery().taskId(taskId).singleResult();  待办任务详情
taskService.setOwner("taskId","user") 设置流程发起人(委托人)
taskService.saveTask(task); 修改任务
taskService.setAssignee() 设置审批人
taskService.addComment() 设置审批备注
taskService.getTaskComments("任务id")	查询审批批注
taskService.getTaskEvents("任务id")	查询任务日志记录		
taskService.getProcessInstanceComments(processInstanceId); 查看任务详情(也就是都经过哪些人的审批,意见是什么)
taskService.delegateTask(taskId, delegater); 委派任务
taskService.claim(taskId, userId);认领任务(指定审批人)
taskService.unclaim(taskId); 取消认领
taskService.complete(taskId, completeVariables); 完成任务
taskService.completeTaskWithForm(String taskId, String formDefinitionId, String outcome, Map<String,Object> properties); 附带表单数据完成任务
参数变量
taskService.setVariable("任务id","键","值") 设置普通变量		
taskService.setVariableLocal("任务id","键","值")	设置本地变量	
taskService.getVariables("任务id") 获取普通变量	
taskService.getVariablesLocal(("任务id")	获取本地变量	
runtimeService.getVariables(task.getExecutionId()) 通过流获取变量	
任务授权
taskService.addGroupIdentityLink()新增组任务授权
taskService.addUserIdentityLink() 新增人员任务授权
taskService.deleteGroupIdentityLink() 删除组任务授权
taskService.deleteUserIdentityLink() 删除人员任务授权
taskService.addCandidateUser("user") 添加候选人
taskService.addCandidateGroup("group") 添加候选组	
taskService.createTaskQuery().taskCandidateUser("user").taskUnassigned().list() 查询候选人列表有user但是没指定代办人任务
taskService.createTaskQuery().taskAssignee("user").list() 查询代办人为user的任务	
taskService.getIdentityLinksForTask("taskId") 查询任务与人员之间的关系
附加信息	
taskService.createAttachment("类型","任务id","流程Id","附件名称","附件描述","流或者url) 上传附件	
taskService.getTaskAttachments("任务id")	 获取附件
	HistoryService
Flowable的历史管理类,可以查询历史信息,执行流程时,引擎会保存很多数据(根据配置),比如流程实例启动时间,任务的参与者, 完成任务的时间,每个流程实例的执行路径,等等。 这个服务主要通过查询功能来获得这些数据。
historyService.createHistoricProcessInstanceQuery().list() 查询流程实例列表(历史流程,包括未完成的)
historyService.createHistoricProcessInstanceQuery().list().foreach().getValue() 可以获取历史中表单的信息
historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); 根绝id查询流程实例
historyService.deleteHistoricProcessInstance() 删除历史流程
historyService.createHistoricDetailQuery() 历史任务流程活动详细信息	
historyService.createProcessInstanceHistoryLogQuery("流程实例id") 查询一个流程实例的所有其他数据	
historyService.createHistoricVariableInstanceQuery() 查询流程任务变量	
historyService.createHistoricTaskInstanceQuery() 查询任务实例		
historyService.deleteHistoricTaskInstance(taskid); 删除任务实例
historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).list()  流程实例活动节点列表 (当前进行到的那个节点的流程图使用)ManagementService
引擎管理类,提供了对Flowable 流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用,主要用于 Flowable 系统的日常维护。
managementService.executeCommand(new classA())  执行classA的内部方法
managementService.createTimerJobQuery()	查询定时工作	
managementService.createJobQuery() 查询一般工作
managementService.createSuspendedJobQuery() 查询中断工作managementService.createDeadLetterJobQuery() 查询无法执行的工作	
managementService.createTablePageQuery().tableName(managementService.getTableName(class)) 查询实体到所有数据	
managementService.executeCustomSql() 自定义sql查询	 IdentityService
它用于管理(创建,更新,删除,查询……)组与用户。请注意,Flowable实际上在运行时并不做任何用户检查。例如任务可以分派给任何用户,而引擎并不会验证系统中是否存在该用户。这是因为Flowable有时要与LDAP、Active Directory等服务结合使用。
identityService.newUser("userid") 创建一个用户		
identityService.newGroup("groupid") 创建一个组	
identityService.saveUser(user)	保存或者更新用户	
identityService.saveGroup(group) 保存或者更新组	
identityService.createUserQuery().userId(userId).singleResult();  获取审批用户的具体信息
identityService.createGroupQuery().groupId(groupId).singleResult(); 获取审批组的具体信息
FormService
可选服务。也就是说Flowable没有它也能很好地运行,而不必牺牲任何功能。这个服务引入了开始表单(start form)与任务表单(task form)的概念。 开始表单是在流程实例启动前显示的表单,而任务表单是用户完成任务时显示的表单。Flowable可以在BPMN 2.0流程定义中定义这些表单。表单服务通过简单的方式暴露这些数据。再次重申,表单不一定要嵌入流程定义,因此这个服务是可选的。
formService.getStartFormKey(processDefinition.getId())  部署流程的id获取表单key	
formService.getRenderedStartForm()查询表单json(无数据)
formService.getStartFormData(processDefinition.getId()).getFormProperties() 获取开始节点表单内容	
formService.submitStartFormData(processDefinition.getId(), "传值参数") 通过formservice启动流程	
formService.submitTaskFormData("taskId","传参数") 通过formservice提交task表单
formService.getTaskFormData("taskId")	通过taskid获取task节点表单内容DynamicBpmnService
可用于修改流程定义中的部分内容,而不需要重新部署它。例如可以修改流程定义中一个用户任务的办理人设置,或者修改一个服务任务中的类名。
流程图标介绍
BPMN 2.0是业务流程建模符号2.0的缩写。它由Business Process Management Initiative这个非营利协会创建并不断发展。作为一种标识,BPMN 2.0是使用一些符号来明确业务流程设计流程图的一整套符号规范,它能增进业务建模时的沟通效率。目前BPMN2.0是最新的版本,它用于在BPM上下文中进行布局和可视化的沟通。接下来我们先来了解在流程设计中常见的 符号。
BPMN2.0的基本符合主要包含:
事件图标
在Flowable中的事件图标启动事件,边界事件,中间事件和结束事件.

活动(任务)图标
活动是工作或任务的一个通用术语。一个活动可以是一个任务,还可以是一个当前流程的子处理流程; 其次,你还可以为活动指定不同的类型。常见活动如下:

结构图标
结构图标可以看做是整个流程活动的结构,一个流程中可以包括子流程。常见的结构有:

网关图标
网关用来处理决策,有几种常用网关需要了解:

常用任务类型有
- 用户任务
- Java Service任务
- 脚本任务
- 业务规则任务
- 多实例
- 手动任务
- Java接收任务
- Shell任务
- 补偿处理器
- Web Service任务
- 邮件任务
- Http任务
- Camel任务
- Mule任务
用户任务
“用户任务(user task)”指需要人工执行的任务。当流程执行到达用户任务时,流程实例会停止等待,直到用户触发完成任务动作。
用户任务用左上角有一个小用户图标的标准任务(圆角矩形)表示。

用户任务在XML中如下定义。其中id是必须属性,name是可选属性。
<userTask id="theTask" name="重要任务" />到期日期
每个任务都可以设置到期日期(due date)。
可以指定固定时间或相对时间,比如,当dueDate为“PT30M”时,表示到达任务30分钟后到期。
到期日期必须符合java.util.Date或java.util.String(ISO8601格式)。
实际应用,我们指定为变量值。
<userTask id="theTask" name="Important task" flowable:dueDate="${dateVariable}"/>任务的到期日期可以使用TaskService,或者在TaskListener中使用传递的DelegateTask修改。
任务指派
- 指派确定的办理人
<userTask id="theTask" name="重要任务" flowable:assignee="jinyangjie"/>- 指派潜在办理人
<userTask id="theTask" name="重要任务" flowable:candidateUsers="jinyangjie, zhangsan" />- 指派潜在办理组
<userTask id="theTask" name="重要任务" flowable:candidateGroups="leader, manager" />Java Service任务
Java Service任务(Java service task)用于调用Java类。Java Service不属于BPMN2.0规范,而是Flowable的自定义扩展。
服务任务用左上角有一个小齿轮图标的圆角矩形表示。

有三种方法声明如何调用Java逻辑,下面分别介绍:
- 调用固定的类
使用flowable:class属性提供全限定类名(fully qualified classname),指定流程执行时调用的类,该类必须实现JavaDelegate或ActivityBehavior接口。
<serviceTask id="javaService" flowable:class="com.example.service.MyJavaDelegate" />- 调用动态类
使用flowable:delegateExpression属性提供委托对象(delegation object)的表达式。该功能和flowable:class类似,同样需要实现JavaDelegate或ActivityBehavior接口,只不过这里不是指定一个具体的实现类,而是查询指定名称的Bean对象。
<serviceTask id="javaService" flowable:delegateExpression="${myDelegateExpressionBean}" />myDelegateExpressionBean是一个实现了JavaDelegate接口的bean,定义在Spring容器中。
- 调用类的指定方法或属性值
使用flowable:expression属性指定类的方法或属性值。同样的,该类需要实现JavaDelegate或ActivityBehavior接口。
<serviceTask id="javaService" flowable:expression="#{printer.printMessage()}" />将在名为printer的对象上调用printMessage方法(不带参数)。当然也可以为表达式中使用的方法传递变量。
属性值示例:
<serviceTask id="javaService" flowable:expression="#{printer.ready}" />会调用名为printer的bean的ready参数的getter方法,getReady(不带参数)。该值会被解析为执行的流程变量。
具体实现实例
下面是一个Java类的示例,用于将流程变量String改为大写。这个类通过实现org.flowable.engine.delegate.JavaDelegate接口,可以在流程执行中被调用。
同时,需要重写execute(DelegateExecution)方法实现业务逻辑。这个方法就是引擎将调用的方法。另外,通过该方法中的DelegateExecution参数可以访问流程实例的各种信息。
public class ToUppercase implements JavaDelegate {
      public void execute(DelegateExecution execution) {
        String var = (String) execution.getVariable("input");
        var = var.toUpperCase();
        execution.setVariable("input", var);
      }
}如果实现org.flowable.engine.impl.delegate.ActivityBehavior接口,可以访问更强大的引擎功能,例如,可以影响流程的控制流程。但注意这并不是好的实践,需要避免这么使用。
任务的返回值
服务执行的返回值(仅对使用表达式的服务任务),可以通过为服务任务定义的'flowable:resultVariable'属性设置为流程变量。可以是已经存在的,或者新的流程变量。 如果指定为已存在的流程变量,则流程变量的值会被服务执行的返回值覆盖。 如果不指定结果变量名,则服务任务的返回值将被忽略。
<serviceTask id="aMethodExpressionServiceTask"
        flowable:expression="#{myService.doSomething()}"
        flowable:resultVariable="myVar" />在上例中,服务执行的结果(调用‘doSomething()’方法的返回值),在服务执行完成后,会设置为名为‘myVar’的流程变量。
异常处理
当执行自定义逻辑时,通常需要捕获并在流程中处理特定的业务异常。Flowable提供了多种方式。
抛出BPMN错误
可以在服务任务或脚本任务的用户代码中抛出BPMN错误。可以在Java委托、脚本、表达式与委托表达式中,抛出特殊的FlowableException:BpmnError。引擎会捕获这个异常,并将其转发至合适的错误处理器,如错误边界事件或错误事件子流程。
public class ThrowBpmnErrorDelegate implements JavaDelegate {
      public void execute(DelegateExecution execution) throws Exception {
        try {
          executeBusinessLogic();
        } catch (BusinessException e) {
          throw new BpmnError("BusinessExceptionOccurred");
        }
      }
    }构造函数的参数是错误代码。错误代码决定了处理这个错误的错误处理器。
这个机制只应该用于业务错误,需要通过流程中定义的错误边界事件或错误事件子流程处理。技术错误应该通过其他异常类型表现,并且通常不在流程内部处理。
异常映射
可以使用mapException扩展,直接将Java异常映射至业务异常(错误)。单映射是最简单的形式:
<serviceTask id="servicetask1" flowable:class="...">
      <extensionElements>
        <flowable:mapException
              errorCode="myErrorCode1">com.example.SomeException</flowable:mapException>
      </extensionElements>
    </serviceTask>在上面的代码中,如果服务任务抛出org.flowable.SomeException的实例,引擎会捕获该异常,并将其转换为带有给定errorCode的BPMN错误。然后就可以像普通BPMN错误完全一样地处理。其他的异常没有映射,仍将抛出至API调用处。
也可以在单行中使用includeChildExceptions属性,映射特定异常的所有子异常。
<serviceTask id="servicetask1" flowable:class="...">
      <extensionElements>
        <flowable:mapException errorCode="myErrorCode1"
               includeChildExceptions="true">com.example.SomeException</flowable:mapException>
      </extensionElements>
    </serviceTask>上面的代码中,Flowable会将SomeException的任何直接或间接的子类,转换为带有指定错误代码的BPMN错误。 当未指定includeChildExceptions时,视为“false”。
默认映射
默认映射最常用。默认映射是一个不指定类的映射,可以匹配任何Java异常:
 <serviceTask id="servicetask1" flowable:class="...">
      <extensionElements>
        <flowable:mapException errorCode="myErrorCode1"/>
      </extensionElements>
    </serviceTask>除了默认映射,会按照从上至下的顺序检查映射,使用第一个匹配的映射。只在所有映射都不能成功匹配时使用默认映射。 只有第一个不指定类的映射会作为默认映射。默认映射忽略includeChildExceptions。
异常顺序流
还有种推荐用法,在发生异常时,将流程执行路由至另一条路径。下面是一个例子。
<serviceTask id="servicetask1" flowable:class="com.example.ThrowsExceptionBehavior">
    </serviceTask>
    <sequenceFlow id="no-exception" sourceRef="javaService" targetRef="theEnd" />
    <sequenceFlow id="exception" sourceRef="javaService" targetRef="fixException" />服务任务有两条出口顺序流,命名为exception与no-exception。在发生异常时,使用顺序流ID控制流程流向:
    public class ThrowsExceptionBehavior implements ActivityBehavior {
      public void execute(DelegateExecution execution) {
        String var = (String) execution.getVariable("var");
        String sequenceFlowToTake = null;
        try {
          executeLogic(var);
          sequenceFlowToTake = "no-exception";
        } catch (Exception e) {
          sequenceFlowToTake = "exception";
        }
        DelegateHelper.leaveDelegate(execution, sequenceFlowToTake);
      }
    }
脚本任务
脚本任务(script task)是自动执行的活动。当流程执行到达脚本任务时,会执行相应的脚本。
脚本任务用左上角有一个小“脚本”图标的标准BPMN 2.0任务(圆角矩形)表示。

脚本任务使用script与scriptFormat元素定义。
<scriptTask id="theScriptTask" scriptFormat="groovy">
      <script>
        sum = 0
        for ( i in inputArray ) {
          sum += i
        }
      </script>
    </scriptTask>默认情况下,JavaScript包含在每一个JDK中,因此不需要添加任何JAR文件。如果想使用其它脚本引擎,则需要在classpath中添加相应的jar,并使用适当的名字。例如,Flowable单元测试经常使用Groovy。Groovy脚本引擎与groovy-all JAR捆绑在一起。添加如下依赖:
    <dependency>
        <groupId>org.codehaus.groovy</groupId>
        <artifactId>groovy-all</artifactId>
        <version>2.x.x<version>
    </dependency>脚本中的变量
到达脚本引擎的执行中,所有的流程变量都可以在脚本中使用。在这个例子里,脚本变量‘inputArray’实际上就是一个流程变量(一个integer的数组)。
 <script>
        sum = 0
        for ( i in inputArray ) {
          sum += i
        }
    </script>在脚本中设置变量的例子:
    <script>
        def scriptVar = "test123"
        execution.setVariable("myVar", scriptVar)
    </script>
注意:下列名字是保留字,不能用于变量名:out,out:print,lang:import,context,elcontext。
脚本任务的结果
脚本任务的返回值,可以通过为脚本任务定义的‘flowable:resultVariable’属性设置为流程变量。可以是已经存在的,或者新的流程变量。如果指定为已存在的流程变量,则流程变量的值会被脚本执行的结果值覆盖。如果不指定结果变量名,则脚本结果值将被忽略。
<scriptTask id="theScriptTask" scriptFormat="juel" flowable:resultVariable="myVar">
  <script>#{echo}</script>
</scriptTask>在上面的例子中,脚本执行的结果(解析表达式‘#{echo}’的值),将在脚本完成后,设置为名为‘myVar’的流程变量。
业务规则任务
在企业应用中,推荐做法是使用可维护的规则库来管理复杂多变的业务规则,将业务代码和规则分开维护,一旦规则有变动,只需修改预设规则即可,而不会影响到业务代码。
业务规则任务可以根据流程变量的值处理预设的业务规则。Flowable支持目前最流行的规则引擎——Drools。只需把含有业务规则任务的流程文件和规则引擎文件“.drl”一同打包部署到系统中,同时添加Drools的jar包,即可实现Flowable驱动规则引擎。
业务规则任务显示为带有表格图标的圆角矩形。

要执行业务规则,需要定义输入与结果变量。输入变量可以用流程变量的列表定义,使用逗号分隔。输出变量只能有一个变量名,如果没有指定结果变量名,默认为org.flowable.engine.rules.OUTPUT。
    <process id="simpleBusinessRuleProcess">
      <startEvent id="theStart" />
      <sequenceFlow sourceRef="theStart" targetRef="businessRuleTask" />
      <businessRuleTask id="businessRuleTask" flowable:ruleVariablesInput="${order}"
          flowable:resultVariable="rulesOutput" />
      <sequenceFlow sourceRef="businessRuleTask" targetRef="theEnd" />
      <endEvent id="theEnd" />
    </process>
也可以将业务规则任务配置为只执行部署的.drl文件中的一组规则。要做到这一点,需要指定规则名字的列表,用逗号分隔。
    <businessRuleTask id="businessRuleTask" flowable:ruleVariablesInput="${order}"
          flowable:rules="rule1, rule2" />
这样只会执行rule1与rule2。
也可以定义需要从执行中排除的规则列表。
<businessRuleTask id="businessRuleTask" flowable:ruleVariablesInput="${order}"
          flowable:rules="rule1, rule2" exclude="true" />
这个例子中,除了rule1与rule2之外,其它所有与流程定义一起部署的规则都会被执行。
注意:集成Drools的业务规则任务,是企业应用中的重要内容,需要重点掌握。
多实例
多实例活动(multi-instance activity)是在业务流程中,为特定步骤定义重复的方式。在编程概念中,多实例类似for each结构:可以为给定集合中的每一条目,顺序或并行地,执行特定步骤,甚至是整个子流程。
网关和事件不能设置为多实例。
按照BPMN2.0规范的要求,用于为每个实例创建执行的父执行,会提供下列变量:
- nrOfInstances:实例总数。
- nrOfActiveInstances:当前活动的(即未完成的)实例数量。对于顺序多实例,这个值总为1。
- nrOfCompletedInstances:已完成的实例数量。
  可以调用execution.getVariable(x)方法获取这些值。
另外,每个被创建的执行,都有局部变量(对其他执行不可见,也不存储在流程实例级别):
- loopCounter:给定实例在for-each循环中的index。
如果一个活动是多实例,将通过在该活动底部的三条短线表示。三条竖线代表实例会并行执行,而三条横线代表顺序执行。

要将活动变成多实例,该活动的XML元素必须有multiInstanceLoopCharacteristics子元素
    <multiInstanceLoopCharacteristics isSequential="false|true">
     ...
    </multiInstanceLoopCharacteristics>
isSequential属性代表了活动的实例为顺序还是并行执行。
有4种不同方法可以配置数量。
指定数字(实例数量)
    <multiInstanceLoopCharacteristics isSequential="false|true">
      <loopCardinality>5</loopCardinality>
    </multiInstanceLoopCharacteristics>
表达式
使用解析为正整数的表达式:
    <multiInstanceLoopCharacteristics isSequential="false|true">
      <loopCardinality>${nrOfOrders-nrOfCancellations}</loopCardinality>
    </multiInstanceLoopCharacteristics>
指定集合
另一个定义实例数量的方法,是使用loopDataInputRef子元素,指定一个集合型流程变量的名字。对集合中的每一项,都会创建一个实例。可以使用inputDataItem子元素,将该项设置给该实例的局部变量。在下面的XML示例中展示:
    <userTask id="miTasks" name="My Task ${loopCounter}" flowable:assignee="${assignee}">
      <multiInstanceLoopCharacteristics isSequential="false">
        <loopDataInputRef>assigneeList</loopDataInputRef>
        <inputDataItem name="assignee" />
      </multiInstanceLoopCharacteristics>
    </userTask>
假设变量assigneeList包含[kermit, gonzo, fozzie]。上面的代码会创建三个并行的用户任务。每一个执行都有一个名为assignee的(局部)流程变量,含有集合中的一项,并在这个例子中被用于指派用户任务。
loopDataInputRef与inputDataItem的缺点是名字很难记,并且由于BPMN 2.0概要的限制,不能使用表达式。Flowable通过在multiInstanceCharacteristics上提供collection与elementVariable属性解决了这些问题:
    <userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
      <multiInstanceLoopCharacteristics isSequential="true"
         flowable:collection="${myService.resolveUsersForTask()}" flowable:elementVariable="assignee" >
      </multiInstanceLoopCharacteristics>
    </userTask>
请注意collection属性会作为表达式进行解析。如果表达式解析为字符串而不是一个集合,不论是因为本身配置的就是静态字符串值,还是表达式计算结果为字符串,这个字符串都会被当做变量名,在流程变量中用于获取实际的集合。
例如,下面的代码片段会让引擎查找存储在assigneeList流程变量中的集合:
条件型数量
多实例活动在所有实例都完成时结束。然而,也可以指定一个表达式,在每个实例结束时进行计算。当表达式计算为true时,将销毁所有剩余的实例,并结束多实例活动,继续执行流程。这个表达式必须通过completionCondition子元素定义。
    <userTask id="miTasks" name="My Task" flowable:assignee="${assignee}">
      <multiInstanceLoopCharacteristics isSequential="false"
         flowable:collection="assigneeList" flowable:elementVariable="assignee" >
        <completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6 }</completionCondition>
      </multiInstanceLoopCharacteristics>
    </userTask>
在这个例子里,会为assigneeList集合中的每个元素创建并行实例。当60%的任务完成时,其他的任务将被删除,流程继续运行。
手动任务
手动任务(manual task)用来定义在BPM引擎不能完成的任务。对于引擎来说,手动任务将当做一个空任务来处理,在流程执行到达手动任务时,自动继续执行流程。
手动任务用左上角有一个小“手”图标的标准BPMN 2.0任务(圆角矩形)表示。

<manualTask id="myManualTask" name="Call client for more information" />Java接收任务
接收任务(receive task),是等待特定消息到达的简单任务。当流程执行到达接收任务时,将保持等待状态,直到引擎接收到特定的消息,触发流程穿过接收任务继续执行。
接收任务用左上角有一个消息图标的标准BPMN 2.0任务(圆角矩形)表示。消息图标是白色的(对应的黑色消息图标代表发送的含义)。

<receiveTask id="waitState" name="wait" />使用方法
要使流程实例从接收任务的等待状态中继续执行,需要使用到达接收任务的执行id,调用runtimeService.signal(executionId)。下面的代码片段展示了如何操作:
    ProcessInstance pi = runtimeService.startProcessInstanceByKey("receiveTask");
    Execution execution = runtimeService.createExecutionQuery()
      .processInstanceId(pi.getId())
      .activityId("waitState")
      .singleResult();
    runtimeService.trigger(execution.getId());
Shell任务
Shell任务(Shell task)可以运行Shell脚本与命令。请注意Shell任务不是BPMN 2.0规范的“官方”任务(因此也没有专用图标)。
Shell任务实现为特殊的服务任务,将服务任务的type定义为‘shell’进行设置。

Shell任务参数
| 参数 | 必填 | 类型 | 描述 | 默认值 | 
| command | 是 | String | 要执行的Shell命令 | |
| arg0-5 | 否 | String | 参数0至参数5 | |
| wait | 否 | true/false | 是否等待Shell进程终止 | true | 
| redirectError | 否 | true/false | 是否将标准错误(standard error)并入标准输出(standard output) | false | 
| cleanEnv | 否 | true/false | 是否避免Shell进程继承当前环境 | false | 
| outputVariable | 否 | String | 保存输出的变量名 | |
| errorCodeVariable | 否 | String | 保存结果错误码的变量名 | |
| directory | 否 | String | Shell进程的默认目录 | 
下面的XML代码片段是使用Shell任务的例子。将会运行”cmd /c echo EchoTest” Shell脚本,等待其结束,并将其结果存入resultVar。
    <serviceTask id="shellEcho" flowable:type="shell" >
      <extensionElements>
        <flowable:field name="command" stringValue="cmd" />
        <flowable:field name="arg1" stringValue="/c" />
        <flowable:field name="arg2" stringValue="echo" />
        <flowable:field name="arg3" stringValue="EchoTest" />
        <flowable:field name="wait" stringValue="true" />
        <flowable:field name="outputVariable" stringValue="resultVar" />
      </extensionElements>
    </serviceTask>
补偿处理器
如果要使用一个活动补偿另一个活动的影响,可以将其声明为补偿处理器(compensation handler)。补偿处理器不在正常流程中执行,而只在流程抛出补偿事件时才会执行。
补偿处理器不得有入口或出口顺序流。
补偿处理器必须通过单向的连接,关联一个补偿边界事件。
如果一个活动是补偿处理器,则会在其下部中间显示补偿事件图标。下面摘录的流程图展示了一个带有补偿边界事件的服务任务,并关联至一个补偿处理器。请注意补偿处理器图标显示在”cancel hotel reservation(取消酒店预订)”服务任务的下部中间。

要将一个活动声明为补偿处理器,需要将isForCompensation属性设置为true:
    <serviceTask id="undoBookHotel" isForCompensation="true" flowable:class="...">
    </serviceTask>
- Web Service任务:调用外部的Web Service资源。
- 邮件任务:用于发送邮件。
- Http任务:用于发出Http请求。
- Camel任务:集成消息路由框架Camel。
- Mule任务:集成企业系统总线框架Mule。
Flowable组件具体使用
任务分配
固定分配
固定分配就是我们前面介绍的,在绘制流程图或者直接在流程文件中通过Assignee来指定的方式


表达式分配
Flowable使用UEL进行表达式解析。UEL代表Unified Expression Language,是EE6规范的一部分.Flowable支持两种UEL表达式: UEL-value 和UEL-method
值表达式
值表达式 Value expression: 解析为一个值。默认情况下,所有流程变量都可以使用。(若使用Spring)所有的Spring bean也可以用在表达式里。例如
${myVar}
${myBean.myProperty}

先部署流程,然后在启动流程实例的时候绑定表达式对应的值
/**
 * 启动流程实例
 */
@Test
public void testRunProcess(){
	// 获取流程引擎对象
	ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
	// 启动流程实例通过 RuntimeService 对象
	RuntimeService runtimeService = processEngine.getRuntimeService();
	// 设置 assignee 的取值
	Map<String,Object> variables = new HashMap<>();
	variables.put("myVar","张三") ;
	// 启动流程实例,第一个参数是流程定义的id
	ProcessInstance processInstance = runtimeService
			.startProcessInstanceById("MyHolidayUI:1:4", variables);// 启动流程实例
	// 输出相关的流程实例信息
	System.out.println("流程定义的ID:" + processInstance.getProcessDefinitionId());
	System.out.println("流程实例的ID:" + processInstance.getId());
	System.out.println("当前活动的ID:" + processInstance.getActivityId());
}方法表达式
方法表达式 Method expression: 调用一个方法,可以带或不带参数。当调用不带参数的方法时,要确保在方法名后添加空括号(以避免与值表达式混淆)。传递的参数可以是字面值(literal value),也可以是表达式,它们会被自动解析。例如:
${printer.print()}
${myBean.addNewOrder('orderName')}
${myBean.doSomething(myVar, execution)}myBean是Spring容器中的个Bean对象,表示调用的是bean的addNewOrder方法
监听器分配
关于监听器的介绍前面已经详细说过了,我们可以使用监听器来完成很多Flowable的流程业务。
我们在此处使用监听器来完成负责人的指定,那么我们在流程设计的时候就不需要指定assignee
创建自定义监听器:
/**
 * 自定义的监听器
 */
public class MyTaskListener implements TaskListener {
    @Override
    public void notify(DelegateTask delegateTask) {
        System.out.println("监听器触发了:" + delegateTask.getName());
        if("提交请假流程".equals(delegateTask.getName()) &&
                "create".equals(delegateTask.getEventName())){
            // 指定任务的负责人
            delegateTask.setAssignee("小明");
        }else {
            delegateTask.setAssignee("小张");
        }
    }
}
流程变量
流程实例按步骤执行时,需要使用一些数据。在Flowable中,这些数据称作变量(variable),并会存储在数据库中。变量可以用在表达式中(例如在排他网关中用于选择正确的出口路径),也可以在Java服务任务(service task)中用于调用外部服务(例如为服务调用提供输入或结果存储),等等。
流程实例可以持有变量(称作流程变量 process variables);用户任务以及执行(executions)——流程当前活动节点的指针——也可以持有变量。流程实例可以持有任意数量的变量,每个变量存储为ACT_RU_VARIABLE数据库表的一行。
所有的startProcessInstanceXXX方法都有一个可选参数,用于在流程实例创建及启动时设置变量。例如,在RuntimeService中:
ProcessInstance startProcessInstanceByKey(String processDefinitionKey, Map<String, Object> variables);
也可以在流程执行中加入变量。例如,(RuntimeService):
void setVariable(String executionId, String variableName, Object value);
void setVariableLocal(String executionId, String variableName, Object value);
void setVariables(String executionId, Map<String, ? extends Object> variables);
void setVariablesLocal(String executionId, Map<String, ? extends Object> variables);读取变量方法(请注意TaskService中有类似的方法。这意味着任务与执行一样,可以持有局部变量,其生存期为任务持续的时间。)
Map<String, Object> getVariables(String executionId);
Map<String, Object> getVariablesLocal(String executionId);
Map<String, Object> getVariables(String executionId, Collection<String> variableNames);
Map<String, Object> getVariablesLocal(String executionId, Collection<String> variableNames);
Object getVariable(String executionId, String variableName);
<T> T getVariable(String executionId, String variableName, Class<T> variableClass);
注意:由于流程实例结束时,对应在运行时表的数据跟着被删除。所以,查询一个已经完结流程实例的变量,只能在历史变量表中查找。
历史变量
历史变量,存入act_hi_varinst表中。在流程启动时,流程变量会同时存入历史变量表中;在流程结束时,历史表中的变量仍然存在。可理解为“永久代”的流程变量。
获取已完成的、id为’XXX’的流程实例中,所有的HistoricVariableInstances(历史变量实例),并以变量名排序。
historyService.createHistoricVariableInstanceQuery()
		.processInstanceId("XXX")
		.orderByVariableName.desc().list();全局变量
流程变量的默认作用域是流程实例。当一个流程变量的作用域为流程实例时,可以称为 global 变量
注意:如: Global变量:userId(变量名)、zhangsan(变量值)
global 变量中变量名不允许重复,设置相同名称的变量,后设置的值会覆盖前设置的变量值。
局部变量
任务和执行实例仅仅是针对一个任务和一个执行实例范围,范围没有流程实例大, 称为 local 变量。
Local 变量由于在不同的任务或不同的执行实例中,作用域互不影响,变量名可以相同没有影响。Local 变量名也可以和 global 变量名相同,没有影响。
变量例子:

部署流程
@Test
public void deploy(){
    ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
    RepositoryService repositoryService = processEngine.getRepositoryService();
    Deployment deploy = repositoryService.createDeployment()
        .addClasspathResource("出差申请单.bpmn20.xml")
        .name("请假流程...")
        .category("请假") // 分类
        .tenantId("dpb") // 租户id
        .deploy();
    System.out.println("deploy.getId() = " + deploy.getId());
    System.out.println("deploy.getName() = " + deploy.getName());
    System.out.println("deploy.getCategory() = " + deploy.getCategory());
}启动流程实例:并且指定全局流程变量
/**
 * 在启动流程实例的时候设置流程变量
 */
@Test
public void runProcess(){
	// 获取流程引擎对象
	ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
	// 启动流程实例通过 RuntimeService 对象
	RuntimeService runtimeService = processEngine.getRuntimeService();
	// 设置流程变量
	Map<String,Object> variables = new HashMap<>();
	// 设置assignee的取值
	variables.put("assignee0","张三");
	variables.put("assignee1","李四");
	variables.put("assignee2","王五");
	variables.put("assignee3","赵财务");
	// 启动流程实例,第一个参数是流程定义的id
	ProcessInstance processInstance = runtimeService
			.startProcessInstanceById("evection:1:4",variables);// 启动流程实例
	// 输出相关的流程实例信息
	System.out.println("流程定义的ID:" + processInstance.getProcessDefinitionId());
	System.out.println("流程实例的ID:" + processInstance.getId());
	System.out.println("当前活动的ID:" + processInstance.getActivityId());
}完成Task任务,同时也可以指定流程变量
/**
 * 完成任务时指定流程变量
 */
@Test
public void completeTask(){
	// 获取流程引擎对象
	ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
	TaskService taskService = processEngine.getTaskService();
	Task task = taskService.createTaskQuery()
			.processDefinitionId("evection:1:4")
			.taskAssignee("李四")
			.singleResult();
	// 添加流程变量
	Map<String, Object> map = task.getProcessVariables();
	map.put("num",4);
	// 完成任务
	taskService.complete(task.getId(),map);
}当然我们也可以在处理流程之外通过Task编号来修改流程变量
/**
 * 通过当前任务设置
 */
@Test
public void currentTask(){
	//   当前待办任务id
	//  获取processEngine
	ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
	TaskService taskService = processEngine.getTaskService();
	Task task = taskService.createTaskQuery()
			.processDefinitionId("evection:1:4")
			.taskAssignee("王五")
			.singleResult();
	// 添加流程变量
	Map<String, Object> map = task.getProcessVariables();
	map.put("num",1);
	//  一次设置多个值 设置局部变量
	taskService.setVariables(task.getId(), map);
}表单引擎与变量
在Flowable中除了变量可以存放数据,表单也可以存放数据与读取数据
在实际业务中,流程伴随着各种各样的表单,Flowable引擎将表单数据统一作为流程变量存入变量表中。所以,对于Flowable引擎,可以完全独立于表单运行,因为可以用流程变量替代表单数据。
但一般的,我们需要结构化的数据,表单仍然是我们推荐的用法。
表单定义有两种方法,内置表单和外部表单。
常用的是外置表单
表单支持下列表单字段类型:
- Text: 渲染为文本框
- Multiline text: 渲染为多行文本框
- Number: 渲染为只允许数字值的文本框
- CheckBox: 渲染为复选框
- Date: 渲染为日期框
- Dropdown: 渲染为下拉选择框,候选值由字段定义配置
- Radio buttons: 渲染为单选按钮,候选值由字段定义配置
- People: 渲染为选人框,可以选择用户身份表中的用户
- Group of people: 渲染为选组框,可以选择组身份表中的组
- Upload: 渲染为上传框
- Expression: 渲染为一个标签,可以在标签文字中使用JUEL表达式,以使用变量及/或其他动态值
内置表单
以请假为例,XML内容:
<process id="leave" name="请假流程-内置表单">
    <startEvent id="start">
        <extensionElements>
            <flowable:formProperty id="startDate" name="请假开始事件" type="date" 
              datePattern="dd-MMM-yyyy" required="true" readable="true" writeable="true"/>
            <flowable:formProperty id="endDate" name="请假结束事件" type="date" 
              datePattern="dd-MMM-yyyy" required="true" readable="true" writeable="true"/>
            <flowable:formProperty id="reason" name="请假原因" type="string" 
              required="true" readable="true" writeable="true"/>
            <flowable:formProperty id="leaveType" type="enum" name="请假类型">
              <flowable:value id="personalLeave" name="事假" />
              <flowable:value id="annualLeave" name="年假" />
            </flowable:formProperty>
        </extensionElements>
    </startEvent>
</process>使用方法:
StartFormData FormService.getStartFormData(String processDefinitionId)或者
TaskFormData FormService.getTaskFormData(String taskId)外部表单
根据表单文件自行渲染的任务表单,称为外部表单。
<process id="leave" name="请假流程-内置表单">
        <startEvent id="start" flowable:formKey="form1"></startEvent>
</process>flowable:formKey=”form1″中的”form1″对应表单定义文件的”key”值。
- 表单定义文件的后缀为.form。
- 表单的JSON定义以key、name和description开头。
- 表单引擎通过属性key来辨别表单在整个表单引擎中的唯一身份。对于来源相同的同一个表单定义的版本系统也是基于属性key运作的。
- 第二部分是一个数组类型fields,表单定义的字段在这里阐明。
- 第三部分是可选的,用来定义表单的结果outcomes。示例如下:
表单的定义
{
    "key": "form1",
    "name": "My first form",
    "fields": [
        {
            "id": "input1",
            "name": "Input1",
            "type": "text",
            "required": false,
            "placeholder": "empty"
        }
    ],
    "outcomes": [
        {
            "id": "null",
            "name": "Accept"
        },
        {
            "id": "null",
            "name": "Reject"
        }
    ]
}在springboot环境下,resources/forms目录下任何.form后缀的表单定义文件都会被自动部署。
例如,将表单定义内容保存为leave.form文件,放入resources/forms目录下。
注意:实际应用中,应当让前端流程设计器生成指定格式的表单定义文件,通过接口方式,更新部署流程定义及表单定义资源。
获取及提交表单参数
实际上,渲染表单所需的所有数据都组装在下面两个方法:
StartFormData FormService.getStartFormData(String processDefinitionId)
TaskFormdata FormService.getTaskFormData(String taskId)可以通过下面两个方法提交表单参数:
ProcessInstance FormService.submitStartFormData(String processDefinitionId, Map<String,String> properties)
    void FormService.submitTaskFormData(String taskId, Map<String,String> properties)runtimeService.startProcessInstanceWithForm和formService.submitStartFormData都能填写完成表单并开始流程
也就是说其实表单填写的数据都是放在variables里的
表单参数FormProperty的具体信息:
public interface FormProperty {
  /**
   * 在{@link FormService#submitStartFormData(String, java.util.Map)}
   * 或{@link FormService#submitTaskFormData(String, java.util.Map)}
   * 中提交参数时使用的key
   */
  String getId();
  /** 显示标签 */
  String getName();
  /** 在本接口中定义的类型,例如{@link #TYPE_STRING} */
  FormType getType();
  /** 可选。这个参数需要显示的值 */
  String getValue();
  /** 这个参数是否可以读取:在表单中显示,并可通过
   * {@link FormService#getStartFormData(String)}
   * 与{@link FormService#getTaskFormData(String)}
   * 方法访问。
   */
  boolean isReadable();
  /** 用户提交表单时是否可以包含这个参数? */
  boolean isWritable();
  /** 输入框中是否必填这个参数 */
  boolean isRequired();
}
获取及提交表单数据
FormModel RuntimeService.getStartFormModel(String processDefinitionId, String processInstanceId);提交表单数据的方法:
// 附带表单数据启动流程实例
    ProcessInstance RuntimeService.startProcessInstanceWithForm(String processDefinitionId, String outcome, Map<String,Object> properties, String taskName);
    // 附带表单数据完成任务
    void TaskService.completeTaskWithForm(String taskId, String formDefinitionId, String outcome, Map<String,Object> properties);表单数据实际存放在流程变量表,所以,用流程变量的方法同样可以获取及提交表单数据。
表单部署
流程部署
@Test
public void DeployProcess(){
    Deployment holiday = repositoryService.createDeployment()
            .addClasspathResource("请假流程.bpmn20.xml")
            .name("请假流程")
            .deploy();
}
表单部署
@Test
public void  DeployForm(){
    Deployment deployment = repositoryService.createDeploymentQuery().deploymentName("请假流程").singleResult();
    formRepositoryService.createDeployment()
            .addClasspathResource("OA请假.form")
            .name("外置表单")
            .parentDeploymentId(deployment.getId())
            .deploy();
}
启动一下 流程
@Test
public void StatrProcess(){
    ProcessDefinition leave_key = repositoryService.createProcessDefinitionQuery().processDefinitionKey("leave_key").singleResult();
    Map<String,Object> map=new HashMap<>();
    map.put("days","4");
    map.put("reason","维护世界和平");
    runtimeService.startProcessInstanceWithForm(leave_key.getId(),
            "请假",map,
            "OA请假表单");
}获取任务表单数据
@Test
public void getTaskFormData(){
//71e05286-df35-11ec-abb7-666ee0fc370d 这个参数是 ACT_RU_TASK的ID_
    FormInfo taskFormModel = taskService.getTaskFormModel("71e05286-df35-11ec-abb7-666ee0fc370d");
    SimpleFormModel formModel = (SimpleFormModel) taskFormModel.getFormModel();
    List<FormField> fields = formModel.getFields();
    fields.forEach(e->{
        System.out.println("e.getId() = " + e.getId());
        System.out.println("e.getName() = " + e.getName());
        System.out.println("e.getType() = " + e.getType());
        System.out.println("e.getValue() = " + e.getValue());
    });
}完成表单任务
@Test
public void comleteProcess(){
    Map<String,Object> map=new HashMap<>();
    map.put("days","6");
    map.put("reason","我要去旅游了");
    taskService.completeTaskWithForm("a8c61c99-df31-11ec-84ea-666ee0fc370d","618f9738-df2f-11ec-b007-666ee0fc370d","xx",map);
}自定义表单字段类型
在实际应用中,Flowable提供的表单字段类型并不能完全满足需求,往往我们需要自定义表单字段类型。
所有自定义字段类型需要继承一个表达类型抽象类“org.flowable.engine.form.AbstractFormType”。
比如,定义一个”卡片”自定义类型:
public class CardFormType extends AbstractFormType {
    // 定义表单类型的标识符
    @Override
    public String getName() {
        return "card";
    }
    // 把表单中的值转换为实际的对象(实际处理逻辑根据具体业务而定)
    @Override
    public Object convertFormValueToModelValue(String propertyValue) {
        return propertyValue;
    }
    // 把实际对象的值转换为表单中的值(实际处理逻辑根据具体业务而定)
    @Override
    public String convertModelValueToFormValue(Object modelValue) {
        return (String) modelValue;
    }
    
}新建配置类,注册自定义字段类型解析类
@Configuration
public class ApplicationConfig extends WebMvcConfigurerAdapter {
    @Bean
    public BeanPostProcessor activitiConfigurer() {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
                if (bean instanceof SpringProcessEngineConfiguration) {
                    List<AbstractFormType> customFormTypes = Arrays.<AbstractFormType>asList(new CardFormType());
                    ((SpringProcessEngineConfiguration)bean).setCustomFormTypes(customFormTypes);
                }
                return bean;
            }
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                return bean;
            }
        };
    }
}
自定义表单引擎
Flowable支持自定义表单引擎以适应各种场景。只需要实现接口org.flowable.engine.impl.form.FormEngine,然后在引擎中注册自定义的表单引擎实现类即可。
public class MyFormEngine implements FormEngine {
    // 表单引擎的名称
    @Override
    public String getName() {
        return "MyFormEngine";
    }
    // 实际处理逻辑根据具体业务而定
    @Override
    public Object renderStartForm(StartFormData startFormData) {
        return "MyStartData";
    }
    // 实际处理逻辑根据具体业务而定
    @Override
    public Object renderTaskForm(TaskFormData taskFormData) {
        return "MyTaskData";
    }
}注册方法与自定义表单字段类型相似,在配置类中加入以下语句:
List<FormEngine> customFormEngines = Arrays.<FormEngine>asList(new MyFormEngine());
    ((SpringProcessEngineConfiguration)bean).setCustomFormEngines(customFormEngines);使用方法:
Object FormService.getRenderedStartForm(String processDefinitionId, "myFormEngine");
Object FormService.getRenderedTaskForm(String taskId);任务待签、待办、转办、委派等
在流程定义中在任务结点的 assignee 固定设置任务负责人,在流程定义时将参与者固定设置在.bpmn 文件中,如果临时任务负责人变更则需要修改流程定义,系统可扩展性差。针对这种情况可以给任务设置多个候选人或者候选人组,可以从候选人中选择参与者来完成任务。
对于变更任务负责人,候选人或组并不是必须的,我们可以从候选人或组中去选择新的负责人,也可以指定候选人或组意外的负责人
候选人
定义流程图,同时指定候选人,多个候选人会通过,连接

部署和启动流程实例
/**
 * 部署流程
 */
@Test
public void deploy(){
	ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
	RepositoryService repositoryService = processEngine.getRepositoryService();
	Deployment deploy = repositoryService.createDeployment()
			.addClasspathResource("请假流程-候选人.bpmn20.xml")
			.name("请求流程-候选人")
			.deploy();
	System.out.println("deploy.getId() = " + deploy.getId());
	System.out.println(deploy.getName());
}
/**
 * 启动流程实例
 */
@Test
public void runProcess(){
	ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
	RuntimeService runtimeService = processEngine.getRuntimeService();
	// 给流程定义中的UEL表达式赋值
	Map<String,Objet> variables = new HashMap<>();
	variables.put("myVar","张三");
	variables.put("zhengge1","李四");
	variables.put("zhengge2","王五");
	runtimeService.startProcessInstanceById("holiday-candidate:1:4",variables);
}任务的查询
根据当前登录的用户,查询对应的候选任务
/**
     * 根据登录的用户查询对应的可以拾取的任务
     *
     */
    @Test
    public void queryTaskCandidate(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        TaskService taskService = processEngine.getTaskService();
        List<Task> list = taskService.createTaskQuery()
                //.processInstanceId("2501")
                .processDefinitionId("holiday-candidate:1:4")
                .taskCandidateUser("李四") # 注意
                .list();
        for (Task task : list) {
            System.out.println("task.getId() = " + task.getId());
            System.out.println("task.getName() = " + task.getName());
        }
    }任务的拾取
知道了我有可拾取的任务后,拾取任务。
/**
     * 拾取任务
     *    一个候选人拾取了这个任务之后其他的用户就没有办法拾取这个任务了
     *    所以如果一个用户拾取了任务之后又不想处理了,那么可以退还
     */
    @Test
    public void claimTaskCandidate(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        TaskService taskService = processEngine.getTaskService();
        Task task = taskService.createTaskQuery()
                //.processInstanceId("2501")
                .processDefinitionId("holiday-candidate:1:4")
                .taskCandidateUser("李四")
                .singleResult();
        if(task != null){
            // 拾取对应的任务
            taskService.claim(task.getId(),"李四");
            System.out.println("任务拾取成功");
        }
    }任务的归还
/**
 * 退还任务
 *    一个候选人拾取了这个任务之后其他的用户就没有办法拾取这个任务了
 *    所以如果一个用户拾取了任务之后又不想处理了,那么可以退还
 */
@Test
public void unclaimTaskCandidate(){
	ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
	TaskService taskService = processEngine.getTaskService();
	Task task = taskService.createTaskQuery()
			//.processInstanceId("2501")
			.processDefinitionId("holiday-candidate:1:4")
			.taskAssignee("张三")
			.singleResult();
	if(task != null){
		// 拾取对应的任务
		taskService.unclaim(task.getId());
		System.out.println("归还拾取成功");
	}
}任务变更负责人
/**
 * 任务的交接
 * 如果我获取了任务,但是不想执行,那么我可以把这个任务交接给其他的用户
 */
@Test
public void taskCandidate(){
	ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
	TaskService taskService = processEngine.getTaskService();
	Task task = taskService.createTaskQuery()
			//.processInstanceId("2501")
			.processDefinitionId("holiday-candidate:1:4")
			.taskAssignee("李四")
			.singleResult();
	if(task != null){
		// 任务的交接
		taskService.setAssignee(task.getId(),"王五");
		System.out.println("任务交接给了王五");
	}
}OWNER 和 ASSIGNEE,Claim 的区别
claim其实和assignee所表达的效果是一致的在数据库上都是ASSIGNEE_字段
如果某个task之前已经被签收claim了,再次执行claim命令的时候就会抛出异常,记一个任务只能被签收一次。而setAssignee是可以不断的覆盖负责人的。
在act_ru_task表中有OWNER_和ASSIGNEE_两个字段。
ASSIGNEE_(受理人):task任务的受理人,就是执行TASK的人,这个又分两种情况(有值,NULL)
- 有值的情况:XML流程里面定义的受理人,TASK会直接填入这个人;
- NULL:XML没有指定受理人或者只指定了候选组;
 没有值的时候,可以使用签收功能去指定受理人,就是候选组里面谁签收谁就成了受理人。
OWNER_(委托人):受理人委托其他人操作该TASK的时候,受理人就成了委托人OWNER_,其他人就成了受理人ASSIGNEE_
- owner字段就是用于受理人委托别人操作的时候运用的字段。
Task task=taskService.createTaskQuery().singleResult();
//签收
taskService.claim(task.getId(), "billy");
taskService.setAssignee(task.getId(), "billy");
logger.info(taskService.createTaskQuery().singleResult().getAssignee());
//结果:billy
//委派
taskService.delegateTask(task.getId(), "cc");
logger.info(taskService.createTaskQuery().singleResult().getOwner());
logger.info(taskService.createTaskQuery().singleResult().getAssignee());
//结果:owner是Billy,assignee是cc任务委派
taskService.setOwner(taskA.getId(), 委托人.getId());
taskService.setAssignee/claim(taskA.getId(), 被委托人.getId());其实这个两个方法的作用是一致的,都是改变了数据库中的OWNER_和ASSIGNEE_两个字段,第一个方法是同时改变,后一个是一个一个改变填充。
任务委派会有的绕,需要解释一下
@Test
public void delegate(){
    String taskId = "e94bace6-aa39-11ea-851b-000ec6dd34b8";
    String userId = "被委派人";
    processService.delegate(taskId,userId);
    System.out.println("委派成功");
}此时act_ru_task表中任务实例增加一个owner,办理人变成了被委派人。DELEGATION_字段的值为PENDING(等待)。

委派人此时查询任务,根据办理人查询时查询不到的,此时任务的办理人时被委派人。所以委派人查询任务是根据owner。具体代码如下:
@Test
public void searchTaskByOwner(){
	String owner = "委派人";
	List<Task> tasks = taskService.createTaskQuery().taskOwner(owner).list();
	if(null != tasks && !tasks.isEmpty()){
		for (Task task:tasks) {
			System.out.println("待办任务ID: "+task.getId());
			System.out.println("待办任务定义key: "+task.getTaskDefinitionKey());
			System.out.println("流程实例ID: "+task.getProcessInstanceId());
			System.out.println("流程定义ID: "+task.getProcessDefinitionId());
			System.out.println("待办任务name: "+task.getName());
			System.out.println("待办任务所有者: "+task.getOwner());
			System.out.println("待办任务办理人: "+task.getAssignee());
		}
	}
}
被委托人解决任务:
@Test
public void resolveTask(){
	String taskId = "93182e09-aa37-11ea-ba0c-000ec6dd34b8";
	taskService.resolveTask(taskId);
	System.out.println("被委派人解决任务");
}

此时任务并没有被完成,只是回到了委派人。然后可以看到,上图中有DELEGATION_显示为RESOLVED(已解决)。
然后才是委派人完成任务:
@Test
public void completeTask(){
	String taskId = "e94bace6-aa39-11ea-851b-000ec6dd34b8";
	taskService.complete(taskId);
	System.out.println("委派人完成任务");
}
任务的完成
正常的任务处理
/**
 * 完成任务
 */
@Test
public void completeTask(){
	ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
	TaskService taskService = processEngine.getTaskService();
	Task task = taskService.createTaskQuery()
			//.processInstanceId("2501")
			.processDefinitionId("holiday-candidate:1:4")
			.taskAssignee("王五")
			.singleResult();
	if(task != null){
		// 完成任务
		taskService.complete(task.getId());
		System.out.println("完成Task");
	}
}候选人组
当候选人很多的情况下,我们可以分组来处理。先创建组,然后把用户分配到这个组中。
用户管理
我们需要先单独维护用户信息。后台对应的表结构是ACT_ID_USER.
/**
     * 维护用户
     */
    @Test
    public void createUser(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        // 通过 IdentityService 完成相关的用户和组的管理
        IdentityService identityService = processEngine.getIdentityService();
        User user = identityService.newUser("田佳");
        user.setFirstName("田");
        user.setLastName("jia");
        user.setEmail("tianjia@qq.com");
        identityService.saveUser(user);
    }
Group管理
维护对应的Group信息,后台对应的表结构是ACT_ID_GROUP
 /**
     * 创建用户组
     */
    @Test
    public void createGroup(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        IdentityService identityService = processEngine.getIdentityService();
        // 创建Group对象并指定相关的信息
        Group group = identityService.newGroup("group2");
        group.setName("开发部");
        group.setType("type1");
        // 创建Group对应的表结构数据
        identityService.saveGroup(group);
    }
用户分配组
用户和组是一个多对多的关联关联,我们需要做相关的分配,后台对应的表结构是ACT_ID_MEMBERSHIP
/**
     * 将用户分配给对应的Group
     */
    @Test
    public void userGroup(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        IdentityService identityService = processEngine.getIdentityService();
        // 根据组的编号找到对应的Group对象
        Group group = identityService.createGroupQuery().groupId("group1").singleResult();
        List<User> list = identityService.createUserQuery().list();
        for (User user : list) {
            // 将用户分配给对应的组
            identityService.createMembership(user.getId(),group.getId());
        }
    }
候选人组应用
搞清楚了用户和用户组的关系后我们就可以来使用候选人组的应用了

然后我们把流程部署和运行,注意对UEL表达式赋值,关联上Group
/**
     * 部署流程
     */
    @Test
    public void deploy(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        RepositoryService repositoryService = processEngine.getRepositoryService();
        Deployment deploy = repositoryService.createDeployment()
                .addClasspathResource("请假流程-候选人组.bpmn20.xml")
                .name("请求流程-候选人")
                .deploy();
        System.out.println("deploy.getId() = " + deploy.getId());
        System.out.println(deploy.getName());
    }
    /**
     * 启动流程实例
     */
    @Test
    public void runProcess(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        IdentityService identityService = processEngine.getIdentityService();
        Group group = identityService.createGroupQuery().groupId("group1").singleResult();
        RuntimeService runtimeService = processEngine.getRuntimeService();
        // 给流程定义中的UEL表达式赋值
        Map<String,Object> variables = new HashMap<>();
        // variables.put("g1","group1");
        variables.put("g1",group.getId()); // 给流程定义中的UEL表达式赋值
        runtimeService.startProcessInstanceById("holiday-group:1:17504",variables);
    }对应表结构中就有对应的体现

任务的拾取和完成
然后完成任务的查询拾取和处理操作
/**
     * 根据登录的用户查询对应的可以拾取的任务
     *
     */
    @Test
    public void queryTaskCandidateGroup(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        // 根据当前登录的用户找到对应的组
        IdentityService identityService = processEngine.getIdentityService();
        // 当前用户所在的组
        Group group = identityService.createGroupQuery().groupMember("邓彪").singleResult();
        TaskService taskService = processEngine.getTaskService();
        List<Task> list = taskService.createTaskQuery()
                //.processInstanceId("2501")
                .processDefinitionId("holiday-group:1:17504")
                .taskCandidateGroup(group.getId())
                .list();
        for (Task task : list) {
            System.out.println("task.getId() = " + task.getId());
            System.out.println("task.getName() = " + task.getName());
        }
    }
    /**
     * 拾取任务
     *    一个候选人拾取了这个任务之后其他的用户就没有办法拾取这个任务了
     *    所以如果一个用户拾取了任务之后又不想处理了,那么可以退还
     */
    @Test
    public void claimTaskCandidate(){
        String userId = "田佳";
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        // 根据当前登录的用户找到对应的组
        IdentityService identityService = processEngine.getIdentityService();
        // 当前用户所在的组
        Group group = identityService.createGroupQuery().groupMember(userId).singleResult();
        TaskService taskService = processEngine.getTaskService();
        Task task = taskService.createTaskQuery()
                //.processInstanceId("2501")
                .processDefinitionId("holiday-group:1:17504")
                .taskCandidateGroup(group.getId())
                .singleResult();
        if(task != null) {
            // 任务拾取
            taskService.claim(task.getId(),userId);
            System.out.println("任务拾取成功");
        }
    }  
   /**
     * 完成任务
     */
    @Test
    public void completeTask(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        TaskService taskService = processEngine.getTaskService();
        Task task = taskService.createTaskQuery()
                //.processInstanceId("2501")
                .processDefinitionId("holiday-group:1:17504")
                .taskAssignee("邓彪")
                .singleResult();
        if(task != null){
            // 完成任务
            taskService.complete(task.getId());
            System.out.println("完成Task");
        }
    }网关
网关用来控制流程的流向
排他网关
排他网关(exclusive gateway)(也叫异或网关 XOR gateway,或者更专业的,基于数据的排他网关 exclusive data-based gateway),用于对流程中的决策建模。当执行到达这个网关时,会按照所有出口顺序流定义的顺序对它们进行计算。选择第一个条件计算为true的顺序流(当没有设置条件时,认为顺序流为true)继续流程。
请注意这里出口顺序流的含义与BPMN 2.0中的一般情况不一样。一般情况下,会选择所有条件计算为true的顺序流,并行执行。而使用排他网关时,只会选择一条顺序流。当多条顺序流的条件都计算为true时,会且仅会选择在XML中最先定义的顺序流继续流程。如果没有可选的顺序流,会抛出异常。
排他网关用内部带有’X’图标的标准网关(菱形)表示,’X’图标代表异或的含义。请注意内部没有图标的网关默认为排他网关。BPMN 2.0规范不允许在同一个流程中混合使用有及没有X的菱形标志。

案例

/**
     * 部署流程
     */
    @Test
    public void deploy(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        RepositoryService repositoryService = processEngine.getRepositoryService();
        Deployment deploy = repositoryService.createDeployment()
                .addClasspathResource("请假流程-排他网关.bpmn20.xml")
                .name("请求流程-排他网关")
                .deploy();
        System.out.println("deploy.getId() = " + deploy.getId());
        System.out.println(deploy.getName());
    }
    /**
     * 启动流程实例
     */
    @Test
    public void runProcess(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        RuntimeService runtimeService = processEngine.getRuntimeService();
        // 给流程定义中的UEL表达式赋值
        Map<String,Object> variables = new HashMap<>();
        // variables.put("g1","group1");
        variables.put("num",3); // 给流程定义中的UEL表达式赋值
        runtimeService.startProcessInstanceById("holiday-exclusive:1:4",variables);
    }
    /**
     * 启动流程实例
     */
    @Test
    public void setVariables(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        RuntimeService runtimeService = processEngine.getRuntimeService();
        // 给流程定义中的UEL表达式赋值
        Map<String,Object> variables = new HashMap<>();
        // variables.put("g1","group1");
        variables.put("num",4); // 给流程定义中的UEL表达式赋值
        runtimeService.setVariables("12503",variables);
    }
    /**
     * 完成任务
     */
    @Test
    public void completeTask(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        TaskService taskService = processEngine.getTaskService();
        Task task = taskService.createTaskQuery()
                //.processInstanceId("2501")
                .processDefinitionId("holiday-exclusive:1:4")
                .taskAssignee("zhangsan")
                .singleResult();
        if(task != null){
            // 完成任务
            taskService.complete(task.getId());
            System.out.println("完成Task");
        }
    }如果从网关出去的线所有条件都不满足的情况下会抛出系统异常

但是要注意任务没有介绍,还是原来的任务,我们可以重置流程变量
 @Test
    public void setVariables(){
        ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
        RuntimeService runtimeService = processEngine.getRuntimeService();
        // 给流程定义中的UEL表达式赋值
        Map<String,Object> variables = new HashMap<>();
        // variables.put("g1","group1");
        variables.put("num",4); // 给流程定义中的UEL表达式赋值
        runtimeService.setVariables("12503",variables);
    }前面我们可以直接在连接线上定义条件,那为什么还要有排他网关呢?直接在线上的情况,如果条件都不满足,流程就结束了,是异常结束!
并行网关
网关也可以建模流程中的并行执行。在流程模型中引入并行的最简单的网关,就是并行网关(parallel gateway)。它可以将执行分支(fork)为多条路径,也可以合并(join)多条入口路径的执行。
并行网关的功能取决于其入口与出口顺序流:
- 分支:所有的出口顺序流都并行执行,为每一条顺序流创建一个并行执行。
- 合并:所有到达并行网关的并行执行都会在网关处等待,直到每一条入口顺序流都到达了有个执行。然后流程经过该合并网关继续。
请注意,如果并行网关同时具有多条入口与出口顺序流,可以同时具有分支与合并的行为。在这种情况下,网关首先合并所有入口顺序流,然后分裂为多条并行执行路径。
与其他网关类型有一个重要区别:并行网关不计算条件。如果连接到并行网关的顺序流上定义了条件,会直接忽略该条件。

当我们执行了创建请假单后,到并行网关的位置的时候,在ACT_RU_TASK表中就有两条记录

然后同时在ACT_RU_EXECUTION中有三条记录,一个任务对应的有两个执行实例

包含网关
包含网关可以看做是排他网关和并行网关的结合体。 和排他网关一样,你可以在外出顺序流上定义条件,包含网关会解析它们。 但是主要的区别是包含网关可以选择多于一条顺序流,这和并行网关一样。
包含网关的功能是基于进入和外出顺序流的:
- 分支: 所有外出顺序流的条件都会被解析,结果为true的顺序流会以并行方式继续执行, 会为每个顺序流创建一个分支。
- 汇聚:所有并行分支到达包含网关,会进入等待状态, 直到每个包含流程token的进入顺序流的分支都到达。 这是与并行网关的最大不同。换句话说,包含网关只会等待被选中执行了的进入顺序流。 在汇聚之后,流程会穿过包含网关继续执行。

事件网关
基于事件的网关(event-based gateway)提供了根据事件做选择的方式。网关的每一条出口顺序流都需要连接至一个捕获中间事件。当流程执行到达基于事件的网关时,与等待状态类似,网关会暂停执行,并且为每一条出口顺序流创建一个事件订阅。
请注意:基于事件的网关的出口顺序流与一般的顺序流不同。这些顺序流从不实际执行。相反,它们用于告知流程引擎:当执行到达一个基于事件的网关时,需要订阅什么事件。有以下限制:
- 一个基于事件的网关,必须有两条或更多的出口顺序流。
- 基于事件的网关,只能连接至intermediateCatchEvent(捕获中间事件)类型的元素(Flowable不支持在基于事件的网关之后连接“接收任务 Receive Task”)。
- 连接至基于事件的网关的intermediateCatchEvent,必须只有一个入口顺序流。
租户案例
什么是租户
在多个系统同时使用同一套数据库的流程引擎时,这些系统就是租户
举个例子:我现在有一个通用的流程引擎平台,现在 A,B 两个系统接入平台,那么 A 系统就只能看到租户为 A 的流程,B 只能看到租户为 B 的流程
优点
提高了流程模型的复用性,比如我两个系统都用这一个模型,那我直接根据租户,生成两个系统各自的流程定义即可,而不需要两个模型
设置租户
该方式是在流程模型发布生成流程定义时候设置(tenantId 即租户)
repositoryService.createDeployment()
                    .name(modelData.getName())
                    .addBytes(processName, bpmnBytes)
                    // 设置租户
                    .tenantId(tenantId)
                    .deploy();可以变更租户
    /**
     * 更换流程定义租户
     * @param deploymentId 部署ID
     * @param tenantId 新租户
     * @return
     */
    @PutMapping(value = "/process/tenant")
    public Data changeProcessDefinedTenant(@RequestParam(value = "deploymentId") String deploymentId,
                                           @RequestParam(value = "tenantId") String tenantId) {
        repositoryService.changeDeploymentTenantId(deploymentId, tenantId);
        return new MultipartData().plugin().code(10000).message("操作成功").attach();
    }查询租户的定义与实例数据
// 租户流程定义列表查询
repositoryService.createProcessDefinitionQuery().processDefinitionTenantId(tenantId).list()
// 租户流程实例列表查询
runtimeService.createProcessInstanceQuery().processInstanceTenantId(tenantId).list()批量查询租户列表
租户毕竟是系统,如果我系统使用的是公共的一套用户,这时一个用户就有对应多个系统的情况,那么查就要一次查多个租户的数据
可惜,官方并没有给出租户相关数据的 IN 查询
    /**
     * 流程实例列表查询 批量查询租户列表
     * @param tenantList 租户列表
     */
    @PostMapping(value = "/info")
    public Data getFlowInfo(@RequestBody List<String> tenantList,
                            @RequestParam(defaultValue = "10") Integer pageSize,
                            @RequestParam(defaultValue = "1")Integer pageNum) {
        // 把租户列表转化为逗号分隔的字符串
        String tenantIds = "\"" + Joiner.on("\",\"").join(tenantList) + "\"";
        int start = (pageNum - 1) * pageSize;
        String sql = "SELECT * FROM " + managementService.getTableName(ProcessInstance.class) +
                " WHERE ID_ = PROC_INST_ID_ " +
                "AND TENANT_ID_ IN ("+ tenantIds +") " +
                "ORDER BY START_TIME_ DESC ";
        sql=sql.replaceAll("\"","'");
        // 获取数据总条数
        Integer total = runtimeService.createNativeProcessInstanceQuery().sql(sql).list().size();
        // 拼接分页 limit 10 offset 0
        sql = sql + "LIMIT "+ pageSize +" offset "+ start;
        System.out.println(sql);
        // 获取当前页数据
        List<ProcessInstance> processInstanceList = runtimeService.createNativeProcessInstanceQuery().sql(sql).list();
        List<ProcessInstanceVo> list = new ArrayList<>();
        processInstanceList.forEach(processInstance -> list.add(new ProcessInstanceVo(processInstance)));
        return new MultipartData().plugin().code(10000).message("操作成功").data(list).attach().include("total",total);
    }多人会签
会签任务:一个任务需要两个或者两个以上的成员参与进行审批审批条件是多样,并且可配置通过的权重比例。可以理解为投票,但是也有可能 某一个审批者有一票否决权。
多人会签是指一个任务需要多个人来处理,案例讲解
其实就是多实例,上面有多实例的一些讲解
这里画一个简单的图
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="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:flowable="http://flowable.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.flowable.org/processdef" exporter="Flowable Open Source Modeler" exporterVersion="6.7.2">
  <process id="duoren-3" name="duoren-3" isExecutable="true">
    <startEvent id="startEvent1" flowable:formFieldValidation="true"></startEvent>
    <userTask id="sid-9AEE7D68-6146-4526-AB50-05B3A5F1554F" name="多人任务" flowable:formFieldValidation="true">
      <extensionElements>
        <flowable:taskListener event="create" expression="${mulitiInstanceTaskListener.completeListener(execution)}"></flowable:taskListener>
      </extensionElements>
      <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="persons" flowable:elementVariable="person">
        <extensionElements></extensionElements>
        <loopCardinality>3</loopCardinality>
        <completionCondition>${mulitiInstanceCompleteTask.completeTask(execution)}</completionCondition>
      </multiInstanceLoopCharacteristics>
    </userTask>
    <endEvent id="sid-C8E5071E-5F53-4E1E-91FD-1DCD476A158E"></endEvent>
    <sequenceFlow id="sid-596BCEDC-94CD-43C2-AB09-F1B67167C2B1" sourceRef="sid-9AEE7D68-6146-4526-AB50-05B3A5F1554F" targetRef="sid-C8E5071E-5F53-4E1E-91FD-1DCD476A158E"></sequenceFlow>
    <sequenceFlow id="sid-4A27C302-65AE-4E7B-ABB3-FE7C938ADD82" sourceRef="startEvent1" targetRef="sid-9AEE7D68-6146-4526-AB50-05B3A5F1554F"></sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_duoren-3">
    <bpmndi:BPMNPlane bpmnElement="duoren-3" id="BPMNPlane_duoren-3">
      <bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
        <omgdc:Bounds height="30.0" width="30.0" x="90.0" y="163.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-9AEE7D68-6146-4526-AB50-05B3A5F1554F" id="BPMNShape_sid-9AEE7D68-6146-4526-AB50-05B3A5F1554F">
        <omgdc:Bounds height="80.0" width="100.0" x="195.0" y="138.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="sid-C8E5071E-5F53-4E1E-91FD-1DCD476A158E" id="BPMNShape_sid-C8E5071E-5F53-4E1E-91FD-1DCD476A158E">
        <omgdc:Bounds height="28.0" width="28.0" x="364.0" y="164.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="sid-4A27C302-65AE-4E7B-ABB3-FE7C938ADD82" id="BPMNEdge_sid-4A27C302-65AE-4E7B-ABB3-FE7C938ADD82" flowable:sourceDockerX="15.0" flowable:sourceDockerY="15.0" flowable:targetDockerX="50.0" flowable:targetDockerY="40.0">
        <omgdi:waypoint x="119.94999906759469" y="178.0"></omgdi:waypoint>
        <omgdi:waypoint x="194.99999999996822" y="178.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="sid-596BCEDC-94CD-43C2-AB09-F1B67167C2B1" id="BPMNEdge_sid-596BCEDC-94CD-43C2-AB09-F1B67167C2B1" flowable:sourceDockerX="50.0" flowable:sourceDockerY="40.0" flowable:targetDockerX="14.0" flowable:targetDockerY="14.0">
        <omgdi:waypoint x="294.95000000000005" y="178.0"></omgdi:waypoint>
        <omgdi:waypoint x="364.0" y="178.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>
这张图需要注意的是下面的3条竖线,代表着并行,也就是多实例
Parallel 并行,指的如果我们配置了3人会签,3人可以同时在待办看到此任务并处理
sequential 串行,指的是如果我们配置了3人会签,则会需要串行执行,前一个人办理了后一个人才能看到

这里在用户任务节点绑定了一个监听器,监听create行为,该监听器我们是通过UEL表达式来实现的,mulitiInstanceTaskListener是我们注入到Spring容器中的对象,这个监听器是任务监听器,使用的是方法表达式 Method expression: 调用一个方法,可以带或不带参数,而不是去实现TaskListener接口。
/** 
启动时监听任务
*/
@Component("mulitiInstanceTaskListener")
public class mulitiInstanceTaskListener implements Serializable {
    public void completeListener(DelegateExecution execution){
        System.out.println("任务"+execution.getId());
        System.out.println("多人"+execution.getVariable("persons"));
        System.out.println("单人"+execution.getVariable("person"));
    }
}
/**
 * 多人会签判断
 */
@Component("mulitiInstanceCompleteTask")
public class MulitiInstanceCompleteTask implements Serializable {
    /**
     * 触发的方法
     *
     * @return false 会签任务没结束
     *         true 任务结束
     */
    public Boolean completeTask(DelegateExecution execution){
        System.out.println("总的会签任务数量"+execution.getVariable("nrOfInstances"));
        System.out.println("当前活动的实例的数量,即还没有完成的实例数量"+execution.getVariable("nrOfActiveInstances"));
        System.out.println("已经完成的实例的数量"+execution.getVariable("nrOfCompletedInstances"));
        // 一个人同意就通过
        Boolean flag = (Boolean) execution.getVariable("flag");
        System.out.println(flag);
        return flag;
    }
}
上面的三个变量是Flowable中自带的可用变量
- 
nrOfInstances:该会签环节中总共有多少个实例
- 
nrOfActiveInstances:当前活动的实例的数量,即还没有完成的实例数量。
- 
nrOfCompletedInstances:已经完成的实例的数量

- 
Loop cardinality:设置为3表示只循环3次,也就是三个人会签(次数也可以用变量来代替比如${num},只需要传num的变量就行)
- 
Collection:表示要循环的集合,我们给的是persons,后面需要在流程变量中赋值
- 
Element variable:表示循环的变量
- 
Completion condition:表示任务结束的条件,也就是多人会签的结束条件,在此处我们用的是UEL表达式,mulitiInstanceCompleteTask表示的是我们注入到Spring容器中的对象,同样也是表达式的写法

不一样的UI绘图,名称与位置不同,但是参数都是一样的
	@Test
	void startFlow() throws Exception{
		Map<String,Object> map = new HashMap<>();
		// 设置多人会签的数据
		map.put("persons", Arrays.asList("张三","李四","王五","赵六"));
		ProcessInstance processInstance = runtimeService
				.startProcessInstanceById("duoren-3:1:ef873610-9e42-11ee-ad53-22ea421acd47",map);
	}在启动流程实例的时候,我们需要设置相关的参数,在流程定义的时候设置的persons在此处我们就需要设置了,设置为Arrays.asList(“张三”,“李四”,“王五”,“赵六”),这里设置了4个元素,在流程定义里定义了3个,表示只会循环3次,启动流程后,在Task中可以看到只有3个任务

同时控制也有对应的输出,触发了Task监听的创建事件

会签处理任务
启动流程后我们发下在Task中产生了3条任务,我们先通过TaskService来完成其中一个任务,设置一个标志flag为false,来控制会签还没有结束,同时Task中另外两个任务还在

	@Test
	void completeTask1(){
		Map<String,Object> map = new HashMap<>();
		map.put("flag",false);
		taskService.complete("1c798f2f-9e43-11ee-964a-22ea421acd47",map); // 任务ID
		System.out.println("complete ....");
	}
当任务执行完成时会同步触发会签完成表达式中对象方法。有如下的输出

同时Task表中的记录还有两条

然后当我们在完成一个任务,这时设置flag为true,会发现在这个多人处理中,最多3个人处理在第二个人处理后就结束了
	@Test
	void completeTask1(){
		Map<String,Object> map = new HashMap<>();
		map.put("flag",true);
		taskService.complete("1c81f3a2-9e43-11ee-964a-22ea421acd47",map);
		System.out.println("complete ....");
	}
同时来看看表结构中的记录,发现没有了,多人会签就结束啦,这里是3个人只有有一个人同意就会通过。
数据库表中流程实例、活动实例、任务实例
在我们查询流程实例的时候常常会将这几个内容搞混,所以现在要整理一下

ProcessInstance
通过runtimeService.startProcessInstance()方法启动,引擎会创建一个流程实例(ProcessInstance)。
简单来说流程实例就是根据一次(一条)业务数据用流程驱动的入口,两者之间是一对一的关系。
引擎会创建一条数据到ACT_RU_EXECUTION表,同时也会根据history的级别决定是否查询相同的历史数据到ACT_HI_PROCINST表。
启动流程和业务关联区别:
- 对于自定义表单来说启动的时候会传入businessKey作为业务和流程的关联属性
- 对于动态表单来说不需要使用businessKey关联,因为所有的数据都保存在引擎的表中
- 对于外部表单来说businessKey是可选的,但是一般不会为空,和自定义表单类似
Execution
初学者最搞不懂的就是ProcessInstance与Execution之间的关系,要分两种情况说明。
Execution的含义就是一个流程实例(ProcessInstance)具体要执行的过程对象。
不过在说明之前先声明两者的对象映射关系:
ProcessInstance(1)—>Execution(N),其中N >= 1。
Task
刚刚说了ProcessInstance是和业务一对一关联的,和业务数据最亲密;Task是和用户最亲密的(UserTask),用户每天的待办事项就是一个个的Task对象。
Execution和Task是一对一关系,Task可以是任何类型的Task实现,可以是用户任务(UserTask)、Java服务(ServiceTask)等,在实际流程运行中只不过面向对象不同,用户任务需要有人完成(complete),Java服务需要有系统自动执行(execution)。
Activity
Activity包含了流程中所有的活动数据,例如开始事件、各种分支(排他、并行等)、以及刚刚提到的Task执行记录。
有些人认为Activity和Task是多对一关系,其实不是,是一对一的关系,每一次操作的task背后实际记录更详细的活动。
Flowable与SpringBoot结合
Spring Boot是一个应用框架。可以轻松地创建独立运行的,生产级别的,基于Spring的应用,并且可以“直接运行”。它基于约定大于配置的原则使用Spring框架与第三方库,使你可以轻松地开始使用。大多数Spring Boot应用只需要很少的Spring配置。
Spring Boot提倡约定大于配置。要开始工作,只需在项目中添加flowable-spring-boot-starter或flowable-spring-boot-starter-rest依赖。
<dependency>
    <groupId>org.flowable</groupId>
    <artifactId>flowable-spring-boot-starter</artifactId>
    <version>6.7.2</version>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>就这么简单。这个依赖会自动向classpath添加正确的Flowable与Spring依赖。现在可以编写Spring Boot应用了
启动服务依赖包会帮你做很多事情
- 自动创建了内存数据库(因为classpath中有PGSQL驱动),并传递给Flowable流程引擎配置
- 创建并暴露了Flowable的ProcessEngine、CmmnEngine、DmnEngine、FormEngine、ContentEngine及IdmEngine bean
- 所有的Flowable服务都暴露为Spring bean
- 创建了Spring Job Executor
并且可以自动部署:
- processes目录下的任何BPMN 2.0流程定义都会被自动部署。创建processes目录,并在其中创建示例流程定义。
- cases目录下的任何CMMN 1.1事例都会被自动部署。
- forms目录下的任何Form定义都会被自动部署。
Flowable配置参数
# flowable配置
flowable:
  check-process-definitions: true #是否需要自动部署流程定义。
  cmmn:
    async:
      executor:
        async-job-lock-time: 300000 #异步作业在被异步执行器取走后的锁定时间(以毫秒计)。在这段时间内,其它异步执行器不会尝试获取及锁定这个任务。'
        default-async-job-acquire-wait-time: 10000 #异步作业获取线程在进行下次获取查询前的等待时间(以毫秒计)。只在当次没有取到新的异步作业,或者只取到很少的异步作业时生效。默认值= 10秒。'
        default-queue-size-full-wait-time: 0 #异步作业(包括定时器作业与异步执行)获取线程在队列满时,等待执行下次查询的等待时间(以毫秒计)。默认值为0(以向后兼容)'
        default-timer-job-acquire-wait-time: 1000 #定时器作业获取线程在进行下次获取查询前的等待时间(以毫秒计)。只在当次没有取到新的定时器作业,或者只取到很少的定时器作业时生效。默认值= 10秒。'
        max-async-jobs-due-per-acquisition: 1 # (译者补)单次查询的异步作业数量。默认值为1,以降低乐观锁异常的可能性。除非你知道自己在做什么,否则请不要修改这个值。'
        retry-wait-time: 500 #(译者补不了了)'
        timer-lock-time: 300000 #定时器作业在被异步执行器取走后的锁定时间(以毫秒计)。在这段时间内,其它异步执行器不会尝试获取及锁定这个任务。'
    async-executor-activate: true # 是否启用异步执行器。'
    deploy-resources: true #是否部署资源。默认值为''true''。'
    deployment-name: SpringBootAutoDeployment #CMMN资源部署的名字。'
    enable-safe-xml: true # 在解析CMMN XML文件时进行额外检查。参见 https://www.flowable.org/docs/userguide/index.html#advanced.safe.bpmn.xml # 不幸的是,部分平台(JDK 6,JBoss)上无法使用这个功能,因此如果你所用的平台在XML解析时不支持StaxSource,需要禁用这个功能。'
    enabled: false # 是否启用CMMN引擎。'
    resource-location: classpath*:/cases/ # CMMN资源的路径。'
    resource-suffixes: # **.cmmn,**.cmmn11,**.cmmn.xml,**.cmmn11.xml # 需要扫描的资源后缀名。'
    servlet:
      load-on-startup: -1 # 启动时加载CMMN servlet。'
      name: Flowable CMMN Rest API # CMMN servlet的名字。'
      path: /cmmn-api # CMMN servlet的context path。'
  content:
    enabled: false # 是否启动Content引擎。'
    servlet:
      load-on-startup: -1 # 启动时加载Content servlet。'
      name: Flowable Content Rest API # Content servlet的名字。'
      path: /content-api # Content servlet的context path。'
    storage:
      create-root: true # 如果根路径不存在,是否需要创建?'
      root-folder: # 存储content文件(如上传的任务附件,或表单文件)的根路径。'
  custom-mybatis-mappers: # 需要添加至引擎的自定义Mybatis映射的FQN。'
  custom-mybatis-x-m-l-mappers: # 需要添加至引擎的自定义Mybatis XML映射的路径。'
  database-schema: # 如果数据库返回的元数据不正确,可以在这里设置schema用于检测/生成表。'
  database-schema-update: true # 数据库schema更新策略。'
  db-history-used: true # 是否要使用db历史。'
  deployment-name: SpringBootAutoDeployment # 自动部署的名称。'
  dmn:
    deploy-resources: true # 是否部署资源。默认为''true''。'
    deployment-name: SpringBootAutoDeployment # DMN资源部署的名字。'
    enable-safe-xml: true # 在解析DMN XML文件时进行额外检查。参见 https://www.flowable.org/docs/userguide/index.html#advanced.safe.bpmn.xml。不幸的是,部分平台(JDK 6,JBoss)上无法使用这个功能,因此如果你所用的平台在XML解析时不支持StaxSource,需要禁用这个功能。'
    enabled: false # 是否启用DMN引擎。'
    history-enabled: false # 是否启用DMN引擎的历史。'
    resource-location: classpath*:/dmn/ # DMN资源的路径。'
    resource-suffixes: # **.dmn,**.dmn.xml,**.dmn11,**.dmn11.xml # 需要扫描的资源后缀名。'
    servlet:
      load-on-startup: -1 # 启动时加载DMN servlet。'
      name: Flowable DMN Rest API # DMN servlet的名字。'
      path: /dmn-api # DMN servlet的context path。'
    strict-mode: true # 如果希望避免抉择表命中策略检查导致失败,可以将本参数设置为false。如果检查发现了错误,会直接返回错误前一刻的中间结果。'
  form:
    deploy-resources: true # 是否部署资源。默认为''true''。'
    deployment-name: SpringBootAutoDeployment # Form资源部署的名字。'
    enabled: false # 是否启用Form引擎。'
    resource-location: classpath*:/forms/ # Form资源的路径。'
    resource-suffixes: # **.form # 需要扫描的资源后缀名。'
    servlet:
      load-on-startup: -1 # 启动时加载Form servlet。'
      name: Flowable Form Rest API # Form servlet的名字。'
      path: /form-api # Form servlet的context path。'
  history-level: audit # 要使用的历史级别。'
  idm:
    enabled: false # 是否启用IDM引擎。'
    ldap:
      attribute:
        email: # 用户email的属性名。'
        first-name: # 用户名字的属性名。'
        group-id: # 用户组ID的属性名。'
        group-name: # 用户组名的属性名。'
        group-type: # 用户组类型的属性名。'
        last-name: # 用户姓的属性名。'
        user-id: # 用户ID的属性名。'
      base-dn: # 查找用户与组的DN(标志名称 distinguished name)。'
      cache:
        group-size: -1 # 设置{@link org.flowable.ldap.LDAPGroupCache}的大小。这是LRU缓存,用于缓存用户及组,以避免每次都查询LDAP系统。'
#      custom-connection-parameters: '# 用于设置所有没有专用setter的LDAP连接参数。查看 http://docs.oracle.com/javase/tutorial/jndi/ldap/jndi.html
#                介绍的自定义参数。参数包括配置链接池,安全设置,等等。'
      enabled: false # 是否启用LDAP IDM 服务。'
      group-base-dn: # 组查找的DN。'
      initial-context-factory: com.sun.jndi.ldap.LdapCtxFactory # 初始化上下文工厂的类名。'
      password: # 连接LDAP系统的密码。'
      port: -1 # LDAP系统的端口。'
      query:
        all-groups: # 查询所有组所用的语句。'
        all-users: # 查询所有用户所用的语句。'
        groups-for-user: # 按照指定用户查询所属组所用的语句'
        user-by-full-name-like: # 按照给定全名查找用户所用的语句。'
        user-by-id: # 按照userId查找用户所用的语句。'
      search-time-limit: 0 # 查询LDAP的超时时间(以毫秒计)。默认值为''0'',即“一直等待”。'
      security-authentication: simple # 连接LDAP系统所用的''java.naming.security.authentication''参数的值。'
      server: # LDAP系统的主机名。如''ldap://localhost''。'
      user: # 连接LDAP系统的用户ID。'
      user-base-dn: # 查找用户的DN。'
    password-encoder: # 使用的密码编码类型。'
    servlet:
      load-on-startup: -1 # 启动时加载IDM servlet。'
      name: Flowable IDM Rest API # IDM servlet的名字。'
      path: /idm-api # IDM servlet的context path。'
  mail:
    server:
      default-from: flowable@localhost # 发送邮件时使用的默认发信人地址。'
      host: localhost # 邮件服务器。'
      password: # 邮件服务器的登录密码。'
      port: 1025 # 邮件服务器的端口号。'
      use-ssl: false # 是否使用SSL/TLS加密SMTP传输连接(即SMTPS/POPS)。'
      use-tls: false # 使用或禁用STARTTLS加密。'
      username: # 邮件服务器的登录用户名。如果为空,则不需要登录。'
  process:
    async:
      executor:
        async-job-lock-time: 300000 # 异步作业在被异步执行器取走后的锁定时间(以毫秒计)。在这段时间内,其它异步执行器不会尝试获取及锁定这个任务。'
        default-async-job-acquire-wait-time: 10000 # 异步作业获取线程在进行下次获取查询前的等待时间(以毫秒计)。只在当次没有取到新的异步作业,或者只取到很少的异步作业时生效。默认值= 10秒。'
        default-queue-size-full-wait-time: 0 # 异步作业(包括定时器作业与异步执行)获取线程在队列满时,等待执行下次查询的等待时间(以毫秒计)。默认值为0(以向后兼容)'
        default-timer-job-acquire-wait-time: 10000 # 定时器作业获取线程在进行下次获取查询前的等待时间(以毫秒计)。只在当次没有取到新的定时器作业,或者只取到很少的定时器作业时生效。默认值= 10秒。'
        max-async-jobs-due-per-acquisition: 1 # (译者补)单次查询的异步作业数量。默认值为1,以降低乐观锁异常的可能性。除非你知道自己在做什么,否则请不要修改这个值。'
        retry-wait-time: 500 # ???(译者补不了了)'
        timer-lock-time: 300000 # 定时器作业在被异步执行器取走后的锁定时间(以毫秒计)。在这段时间内,其它异步执行器不会尝试获取及锁定这个任务。'
    async-executor-activate: true # 是否启用异步执行器。'
    definition-cache-limit: -1 # 流程定义缓存中保存流程定义的最大数量。默认值为-1(缓存所有流程定义)。'
    enable-safe-xml: true # 在解析BPMN XML文件时进行额外检查。参见 https://www.flowable.org/docs/userguide/index.html#advanced.safe.bpmn.xml。不幸的是,部分平台(JDK 6,JBoss)上无法使用这个功能,因此如果你所用的平台在XML解析时不支持StaxSource,需要禁用这个功能。'
    servlet:
      load-on-startup: -1 # 启动时加载Process servlet。'
      name: Flowable BPMN Rest API # Process servlet的名字。'
      path: /process-api # Process servelet的context path。'
  process-definition-location-prefix: classpath*:/processes/ # 自动部署时查找流程的目录。'
#  process-definition-location-suffixes: # **.bpmn20.xml,**.bpmn # ''processDefinitionLocationPrefix''路径下需要部署的文件的后缀(扩展名)。'
  db-identity-used: false
management:
  endpoint:
    flowable:
      #      cache:
      #        time-to-live: 0ms # 缓存响应的最大时间。'
      enabled: true # 是否启用flowable端点。'Flowable通用接口封装
先准备3个实体类来接受参数
FlowInfo
@Data
public class FlowInfo {
    String processInstanceId;
    String businessKey;
    String name;
    Boolean suspended;
    Boolean ended;
    String tenantId;
    String deploymentId;
    String processDefinitionId;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    Date startTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    Date endTime;
    String startUserId;
    // 当前节点
    String currentTask;
    // 当前办理人
    String assignee;
}Process
@Data
public class Process {
	String id;
	String deploymentId;
	String name;
	String resourceName;
	String key;
	String tenantId;
	String diagramresourceName;
}TaskInfo
@Data
public class TaskInfo {
	String taskId;
	String processInstanceId;
	String executionId;
	String businessKey;
	String processName;
	String taskName;
	String starter;
	String assignee;
	String startTime;
	String createTime;
	String formKey;
	String tenantId;
	Integer pageSize = 10;
	Integer pageNum = 1;
}ProcessInstanceVo
/**
 * 流程实例 Vo
 * ProcessInstanceVo
 **/
@Data
public class ProcessInstanceVo {
    private String id;
    private String processDefinitionKey;
    private String processDefinitionId;
    private String businessKey;
    private String processDefinitionName;
    private Date startTime;
    private String deploymentId;
    private String description;
    private String name;
    private Integer processDefinitionVersion;
    private Map<String, Object> processVariables;
    private String startUserId;
    private String callbackId;
    private String callbackType;
    private String tenantId;
    private String localizedDescription;
    private String localizedName;
    private String processInstanceId;
    private String activityId;
    private String parentId;
    private String rootProcessInstanceId;
    private String superExecutionId;
    private Boolean isSuspended;
    private Boolean isEnded;
    public ProcessInstanceVo(){}
    public ProcessInstanceVo(ProcessInstance processInstance){
        id = processInstance.getId();
        processDefinitionKey = processInstance.getProcessDefinitionKey();
        processDefinitionId = processInstance.getProcessDefinitionId();
        businessKey = processInstance.getBusinessKey();
        processDefinitionName = processInstance.getProcessDefinitionName();
        startTime = processInstance.getStartTime();
        deploymentId = processInstance.getDeploymentId();
        description = processInstance.getDescription();
        name = processInstance.getName();
        processDefinitionVersion = processInstance.getProcessDefinitionVersion();
        processVariables = processInstance.getProcessVariables();
        startUserId = processInstance.getStartUserId();
        callbackId = processInstance.getCallbackId();
        callbackType = processInstance.getCallbackType();
        tenantId = processInstance.getTenantId();
        localizedDescription = processInstance.getLocalizedDescription();
        localizedName = processInstance.getLocalizedName();
        processInstanceId = processInstance.getProcessInstanceId();
        activityId = processInstance.getActivityId();
        parentId = processInstance.getParentId();
        rootProcessInstanceId = processInstance.getRootProcessInstanceId();
        superExecutionId = processInstance.getSuperExecutionId();
        isSuspended = processInstance.isSuspended();
        isEnded = processInstance.isEnded();
    }
}
部署管理接口
FlowController
/**
 * 几个概念总结:
 * deploymentId ---- 是上传文件返回的部署ID,一次可以部署多个流程图,产生多个流程定义ID
 * ProcessDefinitionId --- 流程定义Id,在部署完成之后会产生
 * ProcessInstanceId --- 是流程实例Id,启动流程的时候生成,可以通过流程定义Id来启动流程实例
 * ExecutionId:
 * 当流程中没有分支时,Execution等同于ProcessInstance,甚至连ID也相同
 * 当流程中存在分支(fork, parallel gateway),则在分支口会形成子Execution,在下一个gateway才会合并(joined)
 *
 * 一个Process Instance(流程实例)是一个ProcessDefinition(流程定义)的执行
 * 一个Process Instance(流程实例)可以有许多同时执行的步骤(concurrent executions)
 */
/**
 * 部署管理接口
 * 流程管理
 * @author xujiahui
 */
@RestController
@RequestMapping("/manage")
@Slf4j
public class FlowController {
    private final RepositoryService repositoryService;
    private final ProcessEngineConfiguration configuration;
    private final RuntimeService runtimeService;
    private final HistoryService historyService;
    private final TaskService taskService;
    public FlowController(RepositoryService repositoryService,
                          ProcessEngineConfiguration configuration,
                          RuntimeService runtimeService,
                          HistoryService historyService,
                          TaskService taskService) {
        this.repositoryService = repositoryService;
        this.configuration = configuration;
        this.runtimeService = runtimeService;
        this.historyService = historyService;
        this.taskService = taskService;
    }
    /**
     * 文件类型
     */
    enum FileType{
        zip,
        bpmn,
        xml
    }
    /**
     * 上传一个工作流文件
     * tenantId 租户ID
     */
    @PostMapping(value = "/upload/workflow")
    public Data fileUpload(@RequestParam(value = "tenantId",required = false) String tenantId,
                           @RequestParam MultipartFile uploadFile) {
        try {
            String filename = uploadFile.getOriginalFilename();
            InputStream is = uploadFile.getInputStream();
            assert filename != null;
            DeploymentBuilder deploymentBuilder;
            if (filename.endsWith(FileType.zip.name())) {
                deploymentBuilder = repositoryService.createDeployment().name(filename).addZipInputStream(new ZipInputStream(is));
            } else if (filename.endsWith(FileType.bpmn.name()) || filename.endsWith(FileType.xml.name())) {
                deploymentBuilder = repositoryService.createDeployment().name(filename).addInputStream(filename, is);
            } else {
                return new MultipartData().plugin().code(11111).message("文件格式错误").attach();
            }
            if(tenantId != null){
                deploymentBuilder = deploymentBuilder.tenantId(tenantId);
            }
            deploymentBuilder.deploy();
        } catch (Exception e) {
            e.printStackTrace();
            return new MultipartData().plugin().code(11111).message("部署失败").attach();
        }
        return new MultipartData().plugin().code(10000).message("部署成功").attach();
    }
    /**
     * 查询已部署工作流列表
     */
    @GetMapping(value = "/process/lists")
    @ResponseBody
    public Data getList(@RequestParam(required = false) String key,
                        @RequestParam(required = false) String name,
                        @RequestParam(value = "tenantId",required = false) String tenantId
                        ) {
        ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery();
        if (StringUtils.isNotEmpty(key)) {
            query.processDefinitionKey(key);
        }
        if (StringUtils.isNotEmpty(name)) {
            query.processDefinitionName(name);
        }
        if (StringUtils.isNotEmpty(tenantId)) {
            query.processDefinitionTenantId(tenantId);
        }
        List<ProcessDefinition> list = query.list();
        List<Process> mylist = new ArrayList<Process>();
        for (int i = 0; i < list.size(); i++) {
            Process p =new Process();
            p.setDeploymentId(list.get(i).getDeploymentId());
            p.setId(list.get(i).getId());
            p.setKey(list.get(i).getKey());
            p.setName(list.get(i).getName());
            p.setResourceName(list.get(i).getResourceName());
            p.setDiagramresourceName(list.get(i).getDiagramResourceName());
            p.setTenantId(list.get(i).getTenantId());
            mylist.add(p);
        }
        return new MultipartData().plugin().code(10000).message("操作成功").data(mylist).attach();
    }
    /**
     * 删除一次部署的工作流
     * @param deploymentId 部署id
     */
    @DeleteMapping(value = "/remove")
    public Data remove(@RequestParam("deploymentId") String deploymentId) {
        repositoryService.deleteDeployment(deploymentId, true);
        return new MultipartData().plugin().code(10000).message("删除成功").attach();
    }
    /**
     * 查看工作流图片
     * @param processDefinitionId processDefinitionId 进程定义 ID
     * 例子: processDefinitionId=hiddenTrouble2:1:6601be52-124f-11ed-833b-5689f5011ef4
     * 表 ACT_RE_PROCDEF 的 ID
     */
    @GetMapping(value = "/show/resource")
    public void showresource(@RequestParam("processDefinitionId") String processDefinitionId,
                       HttpServletResponse response) throws Exception {
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
        ProcessDiagramGenerator diagramGenerator = configuration.getProcessDiagramGenerator();
        InputStream is = diagramGenerator.generateDiagram(bpmnModel, "png",  "宋体", "宋体", "宋体", configuration.getClassLoader(), true);
        ServletOutputStream output = response.getOutputStream();
        IOUtils.copy(is, output);
    }
    /**
     * 查看工作流定义
     * @param deploymentId deploymentId 部署ID
     * @param resource resourceName
     */
    @GetMapping(value = "/show/process/definition")
    public void showProcessDefinition(@RequestParam("deploymentId") String deploymentId,
                                      @RequestParam(value="resource") String resource,
                       HttpServletResponse response) throws Exception {
        InputStream is = repositoryService.getResourceAsStream(deploymentId, resource);
        ServletOutputStream output = response.getOutputStream();
        IOUtils.copy(is, output);
    }
    /**
     * 启动流程实例__通过流程定义name
     * 实例启动成功,返回当前活动任务
     * @param name 流程定义name
     * @param tenantId 租户ID
     * @param userId 用户ID
     * @param variables 变量
     */
    @PostMapping(value = "/start/key")
    public Data startByName(@RequestParam("name")String name,
                            @RequestParam(value = "tenantId",required = false) String tenantId,
                            @RequestParam(value = "userId" ,required = false)String userId,
                            @RequestParam(required=false) Map<String, Object> variables) {
        ProcessInstance pi;
        if(tenantId!=null){
            pi=runtimeService.startProcessInstanceByKeyAndTenantId(name,variables,tenantId);
        }else{
            pi = runtimeService.startProcessInstanceByKey(name, variables);
        }
        log.info("流程实例ID:{}---流程定义ID:{}", pi.getId(), pi.getProcessDefinitionId());
        // 从历史任务中获取task和运行任务中获取task是一致的
        HistoricTaskInstance historicTaskInstance = historyService.createHistoricTaskInstanceQuery().processInstanceId(pi.getProcessInstanceId()).unfinished().singleResult();
        taskService.setAssignee(historicTaskInstance.getId(),userId);
        return new MultipartData().plugin().code(10000).message("操作成功").data(historicTaskInstance).attach();
    }
    /**
     * 启动流程实例--通过流程定义ID
     * 实例启动成功,返回当前活动任务
     * @param processDefinitionId 流程定义ID
     * @param userId 用户ID
     * @param variables 变量
     */
    @PostMapping(value = "/start/id")
    public Data startById(@RequestParam("processDefinitionId") String processDefinitionId,
                          @RequestParam(value = "userId",required = false)String userId,
                          @RequestParam(required=false) Map<String, Object> variables) {
        ProcessInstance pi = runtimeService.startProcessInstanceById(processDefinitionId, variables);
        log.info("流程实例ID:{}---流程定义ID:{}", pi.getId(), pi.getProcessDefinitionId());
        HistoricTaskInstance historicTaskInstance = historyService.createHistoricTaskInstanceQuery().processInstanceId(pi.getProcessInstanceId()).unfinished().singleResult();
        taskService.setAssignee(historicTaskInstance.getId(),userId);
        return new MultipartData().plugin().code(10000).message("操作成功").data(historicTaskInstance).attach();
    }
    /**
     * 更换流程定义租户
     * @param deploymentId 部署ID
     * @param tenantId 新租户
     * @return
     */
    @PutMapping(value = "/process/tenant")
    public Data changeProcessDefinedTenant(@RequestParam(value = "deploymentId") String deploymentId,
                                           @RequestParam(value = "tenantId") String tenantId) {
        repositoryService.changeDeploymentTenantId(deploymentId, tenantId);
        return new MultipartData().plugin().code(10000).message("操作成功").attach();
    }
}
流程监控
FlowMonitorController
/**
 * 流程监控
 * @author xujiahui
 */
@RestController
@RequestMapping("/monitor")
public class FlowMonitorController  {
    private final RuntimeService runtimeService;
    private final TaskService taskService;
    private final HistoryService historyService;
    private final RepositoryService repositoryService;
    private final ProcessEngine processEngine;
    private final ManagementService managementService;
    public FlowMonitorController(RuntimeService runtimeService,
                                 TaskService taskService,
                                 HistoryService historyService,
                                 RepositoryService repositoryService,
                                 ProcessEngine processEngine,
                                 ManagementService managementService) {
        this.runtimeService = runtimeService;
        this.taskService = taskService;
        this.historyService = historyService;
        this.repositoryService = repositoryService;
        this.processEngine = processEngine;
        this.managementService = managementService;
    }
    /**
     * 查询所有正在运行的流程实例列表
     * @param bussinessKey  processInstanceBusinessKey 实例id
     * @param name  processDefinitionName
     * @param tenantId  租户ID
     * @param processDefinitionId  流程定义ID
     * @param processInstanceId  流程实例ID
     * @param pageSize 分页大小
     * @param pageNum 分页
     */
    @GetMapping(value = "/list/process")
    public Data getList(@RequestParam(required = false) String bussinessKey,
                        @RequestParam(required = false) String name,
                        @RequestParam(value = "tenantId",required = false) String tenantId,
                        @RequestParam(value = "processDefinitionId",required = false) String processDefinitionId,
                        @RequestParam(value = "processInstanceId",required = false) String processInstanceId,
                        @RequestParam(defaultValue = "10") Integer pageSize,
                        @RequestParam(defaultValue = "1") Integer pageNum) {
        int start = (pageNum - 1) * pageSize;
        ProcessInstanceQuery condition = runtimeService.createProcessInstanceQuery();
        if (StringUtils.isNotEmpty(bussinessKey)) {
            condition.processInstanceBusinessKey(bussinessKey);
        }
        if (StringUtils.isNotEmpty(name)) {
            condition.processDefinitionName(name);
        }
        if (StringUtils.isNotEmpty(tenantId)) {
            condition.processInstanceTenantId(tenantId);
        }
        if (StringUtils.isNotEmpty(processDefinitionId)) {
            condition.processDefinitionId(processDefinitionId);
        }
        if (StringUtils.isNotEmpty(processInstanceId)) {
            condition.processInstanceId(processInstanceId);
        }
        List<ProcessInstance> processList = condition.orderByStartTime().desc().listPage(start, pageSize);
        int total = condition.orderByStartTime().desc().list().size();
        List<FlowInfo> flows = new ArrayList<>();
        processList.stream().forEach(p -> {
            FlowInfo info = new FlowInfo();
            info.setProcessInstanceId(p.getProcessInstanceId());
            info.setBusinessKey(p.getBusinessKey());
            info.setName(p.getProcessDefinitionName());
            info.setStartTime(p.getStartTime());
            info.setStartUserId(p.getStartUserId());
            info.setSuspended(p.isSuspended());
            info.setEnded(p.isEnded());
            info.setTenantId(p.getTenantId());
            info.setDeploymentId(p.getDeploymentId());
            info.setProcessDefinitionId(p.getProcessDefinitionId());
            // 查看当前活动任务
            Task task =  taskService.createTaskQuery().processInstanceId(p.getProcessInstanceId()).singleResult();
            info.setCurrentTask(task.getName());
            info.setAssignee(task.getAssignee());
            flows.add(info);
        });
        return new MultipartData().plugin().code(10000).message("操作成功").data(flows).attach().include("total",total);
    }
    /**
     * 查询所有流程实例列表-包含在运行和已结束
     * @param bussinesskey processInstanceBusinessKey 流程id
     * @param name processDefinitionName
     * @param tenantId 租户ID
     * @param processDefinitionId 流程定义ID
     * @param processInstanceId 流程实例ID
     * @param pageSize 分页大小
     * @param pageNum 分页
     */
    @GetMapping(value = "/list/history/process")
    public Data listHistoryProcess(@RequestParam(required = false) String bussinesskey,
                                   @RequestParam(required = false) String name,
                                   @RequestParam(value = "tenantId",required = false) String tenantId,
                                   @RequestParam(value = "processDefinitionId",required = false) String processDefinitionId,
                                   @RequestParam(value = "processInstanceId",required = false) String processInstanceId,
                                   @RequestParam(defaultValue = "10") Integer pageSize,
                                   @RequestParam(defaultValue = "1") Integer pageNum) {
        int start = (pageNum - 1) * pageSize;
        HistoricProcessInstanceQuery condition = historyService.createHistoricProcessInstanceQuery();
        if (StringUtils.isNotEmpty(bussinesskey)) {
            condition.processInstanceBusinessKey(bussinesskey);
        }
        if (StringUtils.isNotEmpty(name)) {
            condition.processDefinitionName(name);
        }
        if (StringUtils.isNotEmpty(tenantId)) {
            condition.processInstanceTenantId(tenantId);
        }
        if (StringUtils.isNotEmpty(processDefinitionId)) {
            condition.processDefinitionId(processDefinitionId);
        }
        if (StringUtils.isNotEmpty(processInstanceId)) {
            condition.processInstanceId(processInstanceId);
        }
        List<HistoricProcessInstance> processList = condition.orderByProcessInstanceStartTime().desc().listPage(start, pageSize);
        int total = condition.orderByProcessInstanceStartTime().desc().list().size();
        List<FlowInfo> flows = new ArrayList<>();
        processList.stream().forEach(p -> {
            FlowInfo info = new FlowInfo();
            info.setProcessInstanceId(p.getId());
            info.setBusinessKey(p.getBusinessKey());
            info.setName(p.getProcessDefinitionName());
            info.setStartTime(p.getStartTime());
            info.setEndTime(p.getEndTime());
            info.setStartUserId(p.getStartUserId());
            info.setTenantId(p.getTenantId());
            info.setDeploymentId(p.getDeploymentId());
            info.setProcessDefinitionId(p.getProcessDefinitionId());
            if (p.getEndTime() == null) {
                info.setEnded(false);
                // 查看当前活动任务
                Task task =  taskService.createTaskQuery().processInstanceId(p.getId()).singleResult();
                info.setCurrentTask(task.getName());
                info.setAssignee(task.getAssignee());
            } else {
                info.setEnded(true);
            }
            flows.add(info);
        });
        return new MultipartData().plugin().code(10000).message("操作成功").data(flows).attach().include("total",total);
    }
    /**
     * 查询一个流程的活动历史
     * @param processInstanceId  processInstanceId 流程id
     * @param pageSize 分页大小
     * @param pageNum 分页
     */
    @GetMapping(value = "/history")
    public Data history(@RequestParam("processInstanceId") String processInstanceId,
                        @RequestParam(defaultValue = "10") Integer pageSize,
                        @RequestParam(defaultValue = "1")Integer pageNum) {
        int start = (pageNum - 1) * pageSize;
        HashSet<String> set = new HashSet<>();
        set.add("userTask");
        List<HistoricActivityInstance> history = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).activityTypes(set).orderByHistoricActivityInstanceStartTime().asc().listPage(start, pageSize);
        int total = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).activityTypes(set).orderByHistoricActivityInstanceStartTime().asc().list().size();
        return new MultipartData().plugin().code(10000).message("操作成功").data(history).attach().include("total",total);
    }
    /**
     * 查询所有正在运行的执行实例列表
     * @param processInstanceId  processInstanceId 流程id
     */
    @GetMapping(value = "/list/executions")
    public Data listExecutions(@RequestParam(value = "processInstanceId",required = false) String processInstanceId,
                               @RequestParam(defaultValue = "10") Integer pageSize,
                               @RequestParam(defaultValue = "1") Integer pageNum) {
        int start = (pageNum - 1) * pageSize;
        ExecutionQuery executionQuery = runtimeService.createExecutionQuery();
        if (StringUtils.isNotEmpty(processInstanceId)) {
            executionQuery.processInstanceId(processInstanceId);
        }
        List<Execution> executionList = executionQuery.orderByProcessInstanceId().desc().listPage(start, pageSize);
        int total = executionQuery.orderByProcessInstanceId().desc().list().size();
        List<FlowInfo> flows = new ArrayList<>();
        executionList.stream().forEach(p -> {
            FlowInfo info = new FlowInfo();
            info.setProcessInstanceId(p.getProcessInstanceId());
            info.setSuspended(p.isSuspended());
            info.setEnded(p.isEnded());
            info.setTenantId(p.getTenantId());
            flows.add(info);
        });
        return new MultipartData().plugin().code(10000).message("操作成功").data(flows).attach().include("total",total);
    }
    /**
     * 流程图进度追踪
     * @param processInstanceId
     * @param response
     */
    @GetMapping(value = {"/trace/process"})
    public void traceProcess(@RequestParam("processInstanceId") String processInstanceId,
                             HttpServletResponse response) {
        // 根据流程实例ID获得当前处于活动状态的ActivityId合集
        ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
        String processDefinitionId = pi.getProcessDefinitionId();
        List<String> highLightedActivities = new ArrayList<>();
        // 获得活动的节点
        List<HistoricActivityInstance> highLightedActivityList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list();
        List<String> highLightedFlows = new ArrayList<>();
        for (HistoricActivityInstance tempActivity : highLightedActivityList) {
            String activityId = tempActivity.getActivityId();
            highLightedActivities.add(activityId);
            if ("sequenceFlow".equals(tempActivity.getActivityType())) {
                highLightedFlows.add(tempActivity.getActivityId());
            }
        }
        // 获取流程图
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
        ProcessEngineConfiguration engConf = processEngine.getProcessEngineConfiguration();
        ProcessDiagramGenerator diagramGenerator = engConf.getProcessDiagramGenerator();
        InputStream in = diagramGenerator.generateDiagram(bpmnModel, "bmp", highLightedActivities, highLightedFlows, "宋体",
                "宋体", "宋体", engConf.getClassLoader(), 1.0, true);
        OutputStream out = null;
        byte[] buf = new byte[1024];
        int length;
        try {
            out = response.getOutputStream();
            while ((length = in.read(buf)) != -1) {
                out.write(buf, 0, length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IoUtil.closeSilently(out);
            IoUtil.closeSilently(in);
        }
    }
    /**
     * 挂起流程实例
     * @param processInstanceId
     * @return
     */
    @GetMapping(value = "/suspend")
    public Data suspend(@RequestParam("processInstanceId") String processInstanceId) {
        runtimeService.suspendProcessInstanceById(processInstanceId);
        return new MultipartData().plugin().code(10000).message("挂起成功").attach();
    }
    /**
     * 唤醒一个挂起的流程实例
     * @param processInstanceId
     */
    @GetMapping(value = "/run")
    public Data rerun(@RequestParam("processInstanceId") String processInstanceId) {
        runtimeService.activateProcessInstanceById(processInstanceId);
        return new MultipartData().plugin().code(10000).message("唤醒成功").attach();
    }
    /**
     * 查询一个流程的变量
     */
    @GetMapping(value = "/variables")
    public Data variables(@RequestParam("processInstanceId") String processInstanceId,
                          @RequestParam(defaultValue = "10") Integer pageSize,
                          @RequestParam(defaultValue = "1")Integer pageNum) {
        int start = (pageNum - 1) * pageSize;
        List<HistoricVariableInstance> variables = historyService.createHistoricVariableInstanceQuery().processInstanceId(processInstanceId).orderByVariableName().asc().listPage(start, pageSize);
        int total = historyService.createHistoricVariableInstanceQuery().processInstanceId(processInstanceId).orderByVariableName().asc().list().size();
        return new MultipartData().plugin().code(10000).message("操作成功").data(variables).attach().include("total",total);
    }
    /**
     * 修改任务变量
     * @param processInstanceId processInstanceId 流程实例ID
     * @param variables 变量
     * @return
     */
    @PostMapping("/process/variable")
    public Data updateProcessVariables(@RequestParam(value = "processInstanceId") String processInstanceId,
                                       @RequestParam(required = false) Map<String, Object> variables){
        Task currentActivity = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
        taskService.setVariables(currentActivity.getId(),variables);
        return new MultipartData().plugin().code(10000).message("操作成功").attach();
    }
    /**
     * 查询所有任务的变量
     * @param processInstanceId 流程实例ID
     * @return
     */
    @GetMapping(value = "/all/variables")
    public Data allVariables(@RequestParam(value = "processInstanceId") String processInstanceId){
        Task currentActivity = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
        Map<String, Object> variables = taskService.getVariables(currentActivity.getId());
        return new MultipartData().plugin().code(10000).message("操作成功").data(variables).attach();
    }
    /**
     * @author wph
     * @Description TODO 查询出流程
     * @date 2022/8/30 16:27
     *
     * @param processDefinitionId
     * @return com.linktopa.corex.multipart.core.Data
     */
    @GetMapping(value = "/process/task")
    public Data getProcessTask(@RequestParam("processDefinitionId") String processDefinitionId) {
        try {
            BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
            List<org.flowable.bpmn.model.Process> processes = bpmnModel.getProcesses();
            org.flowable.bpmn.model.Process process = processes.get(0);
            Collection<FlowElement> flowElements = process.getFlowElements();
            List<UserTask> taskstest = new ArrayList<>();
            List<MultipartData> tasks = new ArrayList<>();
            for (FlowElement flowElement : flowElements) {
                MultipartData task = new MultipartData();
                //            System.out.println(flowElement.getClass());
                if (flowElement instanceof UserTask) {
                    System.out.println(flowElement.getClass());
                    taskstest.add((UserTask) flowElement);
                    task.include("taskName",flowElement.getName());
                    task.include("taskDefinitionKey",flowElement.getId());
                    task.include("taskCategory",((UserTask) flowElement).getCategory());
                    task.include("assignee",((UserTask) flowElement).getAssignee());
                    tasks.add(task);
                }
            }
            return new MultipartData().plugin().code(10000).message("查询流程定义任务成功").data(tasks).attach();
        } catch (Exception e) {
            return new MultipartData().plugin().code(11111).message("查询流程定义任务成功").attach();
        }
    }
    /**
     * 查询批注信息
     * @param taskId 任务ID
     * @return
     * @throws Exception
     */
    @GetMapping(value = "/comment")
    public Data getTaskComments(String taskId) throws Exception {
        List<Comment> taskComments = taskService.getTaskComments(taskId);
        return new MultipartData().plugin().code(10000).message("操作成功").data(taskComments).attach();
    }
    /**
     * 分页查询流程实例或用户历史任务列表(所有)
     * @param processInstanceId 流程实例ID
     * @param userId 用户ID
     * @param tenantId 租户ID
     * @param pageNum 页码
     * @param pageSize 数量
     * @return
     */
    @GetMapping(value = "/task/pageList/instanceId")
    public Data pageListByInstanceId(@RequestParam(value = "processInstanceId",required = false) String processInstanceId,
                                     @RequestParam(value = "tenantId",required = false) String tenantId,
                                     @RequestParam(value = "userId",required = false) String userId,
                                     @RequestParam(defaultValue = "10") Integer pageSize,
                                     @RequestParam(defaultValue = "1")Integer pageNum) {
        int start = (pageNum - 1) * pageSize;
        HistoricTaskInstanceQuery condition = historyService.createHistoricTaskInstanceQuery();
        if (StringUtils.isNotEmpty(processInstanceId)) {
            condition.processInstanceId(processInstanceId);
        }
        if (StringUtils.isNotEmpty(userId)) {
            condition.taskAssignee(userId);
        }
        if (StringUtils.isNotEmpty(tenantId)) {
            condition.taskTenantId(tenantId);
        }
        List<HistoricTaskInstance> list = condition.orderByTaskCreateTime().desc().listPage(start, pageSize);
        int total = condition.orderByTaskCreateTime().desc().list().size();
        return new MultipartData().plugin().code(10000).message("操作成功").data(list).attach().include("total",total);
    }
    /**
     * 流程实例列表查询 批量查询租户列表
     * @param tenantList 租户列表
     */
    @PostMapping(value = "/info")
    public Data getFlowInfo(@RequestBody List<String> tenantList,
                            @RequestParam(defaultValue = "10") Integer pageSize,
                            @RequestParam(defaultValue = "1")Integer pageNum) {
        // 把租户列表转化为逗号分隔的字符串
        String tenantIds = "\"" + Joiner.on("\",\"").join(tenantList) + "\"";
        int start = (pageNum - 1) * pageSize;
        String sql = "SELECT * FROM " + managementService.getTableName(ProcessInstance.class) +
                " WHERE ID_ = PROC_INST_ID_ " +
                "AND TENANT_ID_ IN ("+ tenantIds +") " +
                "ORDER BY START_TIME_ DESC ";
        sql=sql.replaceAll("\"","'");
        // 获取数据总条数
        Integer total = runtimeService.createNativeProcessInstanceQuery().sql(sql).list().size();
        // 拼接分页 limit 10 offset 0
        sql = sql + "LIMIT "+ pageSize +" offset "+ start;
        System.out.println(sql);
        // 获取当前页数据
        List<ProcessInstance> processInstanceList = runtimeService.createNativeProcessInstanceQuery().sql(sql).list();
        List<ProcessInstanceVo> list = new ArrayList<>();
        processInstanceList.forEach(processInstance -> list.add(new ProcessInstanceVo(processInstance)));
        return new MultipartData().plugin().code(10000).message("操作成功").data(list).attach().include("total",total);
    }
}
/**
     * 历史流程图进度追踪
     * @param processDefinitionId
     * @param processInstanceId
     * @param response
     */
    @GetMapping(value = {"history/trace/process"})
    public void historyTraceProcess(@RequestParam("processDefinitionId") String processDefinitionId,
                                    @RequestParam("processInstanceId") String processInstanceId,
                             HttpServletResponse response) {
        List<String> highLightedActivities = new ArrayList<>();
        // 获得活动的节点
        List<HistoricActivityInstance> highLightedActivityList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list();
        List<String> highLightedFlows = new ArrayList<>();
        for (HistoricActivityInstance tempActivity : highLightedActivityList) {
            String activityId = tempActivity.getActivityId();
            highLightedActivities.add(activityId);
            if ("sequenceFlow".equals(tempActivity.getActivityType())) {
                highLightedFlows.add(tempActivity.getActivityId());
            }
        }
        // 获取流程图
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
        ProcessEngineConfiguration engConf = processEngine.getProcessEngineConfiguration();
        ProcessDiagramGenerator diagramGenerator = engConf.getProcessDiagramGenerator();
        InputStream in = diagramGenerator.generateDiagram(bpmnModel, "bmp", highLightedActivities, highLightedFlows, "宋体",
                "宋体", "宋体", engConf.getClassLoader(), 1.0, true);
        OutputStream out = null;
        byte[] buf = new byte[1024];
        int length;
        try {
            out = response.getOutputStream();
            while ((length = in.read(buf)) != -1) {
                out.write(buf, 0, length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IoUtil.closeSilently(out);
            IoUtil.closeSilently(in);
        }
    }
/**
     * 通过流程实例终止流程
     * @param processInstanceId
     * @return
     */
    @DeleteMapping("/terminate")
    public Data stopProcessInstanceById(@RequestParam(value = "processInstanceId",required = false) String processInstanceId) {
        Data date;
        Task currentActivity = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
        if (processInstance != null) {
            // 找到每个元素,包括节点和顺序流
            Process mainProcess = repositoryService.getBpmnModel(processInstance.getProcessDefinitionId()).getMainProcess();
            Collection<FlowElement> flowElements = mainProcess.getFlowElements();
            for (FlowElement flowElement:
                    flowElements) {
                System.out.println(flowElement.getId());
            }
            String endNodeId = flowElements.stream().filter(EndEvent.class::isInstance).collect(Collectors.toList()).get(0).getId();
            //执行终止
            runtimeService.createChangeActivityStateBuilder().processInstanceId(processInstanceId).moveActivityIdTo(currentActivity.getTaskDefinitionKey(),endNodeId).changeState();
            date = new MultipartData().plugin().code(10000).message("操作成功,流程结束").attach();
        }else {
            date = new MultipartData().plugin().code(11111).message("操作失败,找不到流程实例").attach();
        }
        return date;
    }
任务接口
TaskController
/**
 * 任务接口
 * @author xujiahui
 */
@RestController
@RequestMapping("/task")
public class TaskController {
    private final RuntimeService runtimeService;
    private final TaskService taskService;
    private final  FormService formService;
    public TaskController(RuntimeService runtimeService,
                          TaskService taskService,
                          FormService formService) {
        this.runtimeService = runtimeService;
        this.taskService = taskService;
        this.formService = formService;
    }
    /**
     * 查询待办任务列表
     * @param param 信息
     * @return
     */
    @GetMapping("/list")
    public Data taskList(TaskInfo param)
    {
        TaskQuery condition = taskService.createTaskQuery();
        if (StringUtils.isNotEmpty(param.getAssignee())) {
            condition.taskAssignee(param.getAssignee());
        }
        if (StringUtils.isNotEmpty(param.getTaskName())) {
            condition.taskName(param.getTaskName());
        }
        if (StringUtils.isNotEmpty(param.getProcessName())) {
            condition.processDefinitionName(param.getProcessName());
        }
        if (StringUtils.isNotEmpty(param.getTaskId())) {
            condition.taskId(param.getTaskId());
        }
        if (StringUtils.isNotEmpty(param.getTenantId())) {
            condition.taskId(param.getTenantId());
        }
        int total = condition.active().orderByTaskCreateTime().desc().list().size();
        int start = (param.getPageNum()-1) * param.getPageSize();
        List<Task> taskList = condition.active().orderByTaskCreateTime().desc().listPage(start, param.getPageSize());
        List<TaskInfo> tasks = tasksInfo(taskList);
        return new MultipartData().plugin().code(10000).message("操作完成").data(tasks).attach().include("total",total);
    }
    /**
     * 填充任务信息
     */
    private List<TaskInfo> tasksInfo(List<Task> taskList){
        List<TaskInfo> tasks = new ArrayList<>();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        taskList.stream().forEach(a->{
            ProcessInstance process = runtimeService.createProcessInstanceQuery().processInstanceId(a.getProcessInstanceId()).singleResult();
            TaskInfo info = new TaskInfo();
            info.setAssignee(a.getAssignee());
            info.setBusinessKey(process.getBusinessKey());
            info.setCreateTime(sdf.format(a.getCreateTime()));
            info.setTaskName(a.getName());
            info.setExecutionId(a.getExecutionId());
            info.setProcessInstanceId(a.getProcessInstanceId());
            info.setProcessName(process.getProcessDefinitionName());
            info.setStarter(process.getStartUserId());
            info.setStartTime(sdf.format(process.getStartTime()));
            info.setTaskId(a.getId());
            info.setTenantId(a.getTenantId());
            String formKey = formService.getTaskFormData(a.getId()).getFormKey();
            info.setFormKey(formKey);
            tasks.add(info);
        });
        return tasks;
    }
    /**
     * 用taskid查询formkey
     **/
    @PostMapping("/form/info")
    public Data alllist(@RequestParam("taskId") String taskId) {
        String formKey = formService.getTaskFormData(taskId).getFormKey();
        return new MultipartData().plugin().code(10000).message("操作完成").data(formKey).attach();
    }
    /**
     * 办理一个用户任务
     * @param taskId 任务ID
     * @param userId 用户ID
     * @param nextUserId 下一个任务用户ID
     * @param processInstanceId 流程ID
     * @param variables 变量
     */
    @PostMapping(value = "/complete/task")
    public Data completeTask(@RequestParam("taskId") String taskId,
                             @RequestParam(value = "userId",required = false)String userId,
                             @RequestParam(value = "nextUserId",required = false)String nextUserId,
                             @RequestParam(value = "processInstanceId",required = false) String processInstanceId,
                             @RequestParam(required=false) Map<String, Object> variables) {
        if(userId!=null){
            taskService.setAssignee(taskId, userId);
        }
        if (variables == null) {
            taskService.complete(taskId);
        } else {
            taskService.complete(taskId, variables);
        }
        // 指定下一个任务的负责人
        if(nextUserId!=null && processInstanceId!=null){
            try {
                Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
                taskService.setAssignee(task.getId(), nextUserId);
            }catch (Exception e){
                return new MultipartData().plugin().code(10000).message("完成任务,无法指定下一个任务负责人").attach();
            }
        }
        return new MultipartData().plugin().code(10000).message("完成任务").attach();
    }
    /**
     * 任务移交
     * 【act_ru_task】设置assignee为转办人
     * @param taskId 任务ID
     * @param userId 用户ID(接受移交的用户)
     * @return 信息
     */
    @PostMapping(value = "/transfer")
    public Data transferTask(String taskId, String userId) throws Exception {
        taskService.setAssignee(taskId, userId);
        return new MultipartData().plugin().code(10000).message("移交成功").attach();
    }
    /**
     * 任务签收
     * @param taskId 任务ID
     * @param userId 用户ID
     * @return
     */
    @PostMapping(value = "/claim")
    public Data claim(String taskId, String userId) throws Exception {
        taskService.claim(taskId, userId);
        return new MultipartData().plugin().code(10000).message("任务签收成功").attach();
    }
    /**
     * 任务反签收
     * @param taskId 任务ID
     * @return
     * @throws Exception
     */
    @PostMapping(value = "/un/claim")
    public Data unClaim(String taskId) throws Exception {
        taskService.unclaim(taskId);
        return new MultipartData().plugin().code(10000).message("任务反签收成功").attach();
    }
    /**
     * 任务委派
     * 【act_ru_task】委派人:owner,被委派人:assignee,委派任务:delegateTask,任务回到委派人:resolveTask
     * @param taskId 任务ID
     * @param userId 用户ID
     */
    @PostMapping(value = "/delegate")
    public Data delegate(String taskId, String userId) throws Exception {
        taskService.delegateTask(taskId, userId);
        return new MultipartData().plugin().code(10000).message("任务委派成功").attach();
    }
    /**
     * 任务归还
     * 被委派人完成任务之后,将任务归还委派人
     * @param taskId 任务ID
     * @param variables 变量
     */
    @PostMapping(value = "/resolve")
    public Data reslove(String taskId,@RequestParam(required=false) Map<String, Object> variables) throws Exception {
        taskService.resolveTask(taskId,variables);
        return new MultipartData().plugin().code(10000).message("任务归还成功").attach();
    }
    /**
     * 添加批注信息
     * 批注信息:act_hi_comment
     * @param taskId 任务ID
     * @param instanceId 实例ID
     * @param message 批注内容
     */
    @PostMapping(value = "/comment/add")
    public Data addComment(String taskId, String instanceId, String message) throws Exception {
        Comment comment = taskService.addComment(taskId, instanceId, message);
        return new MultipartData().plugin().code(10000).message("添加成功").data(comment).attach();
    }
    /**
     * 任务撤回
     * 注意:当前与目标定义Key为设计模板时任务对应的ID,而非数据主键ID
     * @param processInstanceId 流程实例ID
     * @param currentTaskKey 当前任务定义Key
     * @param targetTaskKey 目标任务定义Key
     */
    @PostMapping(value = "/withdraw")
    public Data withdraw(String processInstanceId, String currentTaskKey, String targetTaskKey) {
        runtimeService.createChangeActivityStateBuilder()
                .processInstanceId(processInstanceId)
                .moveActivityIdTo(currentTaskKey, targetTaskKey)
                .changeState();
        return new MultipartData().plugin().code(10000).message("任务撤回成功").attach();
    }
    /**
     * 查询未签收任务列表
     * @param userId 用户ID
     * @param pageNum 页码
     * @param tenantId 租户ID
     * @param pageSize 数量
     * @return
     */
    @GetMapping(value = "/task/list/un/claim")
    public Data unClaim(@RequestParam("userId")String userId,
                        @RequestParam(value = "tenantId",required = false) String tenantId,
                        @RequestParam(defaultValue = "10") Integer pageSize,
                        @RequestParam(defaultValue = "1")Integer pageNum) {
        int start = (pageNum - 1) * pageSize;
        TaskQuery taskQuery = taskService.createTaskQuery();
        if (StringUtils.isNotEmpty(tenantId)) {
            taskQuery.taskTenantId(tenantId);
        }
        List<Task> taskList = taskQuery.taskCandidateUser(userId)
                .orderByTaskPriority().desc()
                .orderByTaskCreateTime().asc()
                .listPage(start, pageSize);
        int total = taskQuery.taskCandidateUser(userId)
                .orderByTaskPriority().desc()
                .orderByTaskCreateTime().asc().list().size();
        List<TaskInfo> tasks = tasksInfo(taskList);
        return new MultipartData().plugin().code(10000).message("操作成功").data(tasks).attach().include("total",total);
    }
}
Flowable问题记录
无法获取SpringBean
在我们使用监听器时通常情况下实现类注入的bean一直为null
应该是流程引擎启动时,依赖注入还未初始化完成,因为是groovy和java是在工程中混着用的
解决的方式有两种
1、使用@PostConstruct注解自启动时获取Bean
案例
@Slf4j
@Component
public class UserTaskListener implements TaskListener {
    @Autowired
    EventBusCenter eventBusCenter;
    private static UserTaskListener userTaskListener;
    /**
     * 监听器中无法获取spring bean配置
     */
    @PostConstruct
    public void init() {
        userTaskListener = this;
        userTaskListener.eventBusCenter = this.eventBusCenter;
    }
    @Override
    public void notify(DelegateTask delegateTask) {
        Map<String, VariableInstance> variableInstances = delegateTask.getVariableInstances();
        delegateTask.getVariable("userId");
        delegateTask.getVariables();
        delegateTask.getVariableNames();
        try {
            /**
             * 事件监听发送消息
             * 调用方法时必须这么调用userTaskListener.eventBusCenter
             */
            userTaskListener.eventBusCenter.postAsync(new message("1","userId",delegateTask.getVariable("userId").toString()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        log.info("UserTask监听:");
        log.info(delegateTask.getName());
        log.info(delegateTask.getAssignee());
        log.info(delegateTask.getEventName());
    }
}2、使用Flowable delegateExpressio调用动态类来获取Spring的依赖注入,获取spring bean配置
@Slf4j
@Component("myListener") // 使用flowable delegateExpressio调用动态类来获取Spring的依赖注入,获取spring bean配置
public class UserTaskListener2 implements TaskListener {
    @Autowired
    EventBusCenter eventBusCenter;
    @Override
    public void notify(DelegateTask delegateTask) {
        Map<String, VariableInstance> variableInstances = delegateTask.getVariableInstances();
        delegateTask.getVariable("userId");
        delegateTask.getVariables();
        delegateTask.getVariableNames();
        try {
            /**
             * 事件监听发送消息
             * 调用方法时必须这么调用userTaskListener.eventBusCenter
             */
            eventBusCenter.postAsync(new message("1","userId",delegateTask.getVariable("userId").toString()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        log.info("UserTask监听:");
        log.info(delegateTask.getName());
        log.info(delegateTask.getAssignee());
        log.info(delegateTask.getEventName());
    }
}
xml的代码配置
    <userTask id="faxian" name="发现隐患" flowable:formFieldValidation="true">
      <extensionElements>
        <flowable:taskListener event="create" delegateExpression="${myListener}"></flowable:taskListener>
      </extensionElements>
    </userTask>关键在于delegateExpression="${myListener},这是委托表达式的写法,括号中的值就是我们注册到Spring中的Bean值,必须要一致
 
                    
