Flowable 学习章节
官网地址:https://www.flowable.org/ Flowable6.3中文教程:https://tkjohn.github.io/flowable-userguide/ Flowable6.4.2中文教程:http://web.wzhz.xyz/flowable/bpmn/ Flowable6.5.0中文教程: http://www.shareniu.com/flowable6.5_zh_document/bpm/index.html http://jeesite.com/docs/bpm/ https://jeesite.gitee.io/front/flowable/6.5.0/bpmn/ 可以在官网下载对应的jar包在本地部署运行,官方提供了下面的五个应用程序: Flowable Modeler:流程定义管理 Flowable Task:用户任务管理 Flowable IDM:用户组权限管理 Flowable REST API:流程引擎对外提供的API接口 Flowable Admin:后台管理
- Activiti与Flowable对比
- flowable6和activit7的对比中文翻译列表
- Flowable 框架
- Activiti Flowable transient变量
- Flowable Activiti 会签 多实例
- Flowable6.4 – 会签实现方案
- Flowable6.4 - 分派办理人
- Flowable6.4 – 加签和减签
- Flowable6.4 – 加签和减签的源码解析
- Flowable6.4 – 绘制流程图
- flowable 任务节点多实例使用
Activiti与Flowable对比
这里对比 Activiti7
和 Flowable6
对比框架简介
- jBPM,是始祖,但目前市场较小,不作为对比重点
- Activiti,2010年基于jBPM诞生
- Flowable,2016年基于Activiti诞生
- Comframe,亚信的工作流框架,从时间上推测,用的可能是jBPM2.0或者3.0
- 新零售工作流框架,从使用的ExtJS3和dwr推测,应该诞生于2010年前后,应该没有用开源工作流框架,功能可能比较简单
对比框架选择
- jBPM7主要与JBoss下的规则引擎集成较好,其他没有什么优势,不太建议
- Activiti6在主力Tijs Rademakers和Joram Barrez离开后,就没有开发了,一直停留在6.0.0,不太建议
- Salaboy可能是空降,对核心引擎不熟,也可能Alfresco想发展cloud方向,因此6.0以后直接启动7.0
- Flowable6以后,Tijs继续他的本行,开发引擎核心
因此,这里对比activiti7和flowable6
Roadmap对比
Activiti的roadmap
Flowable的roadmap
可以看出:
- Activiti7的方向在云化
- 而Flowable6的方向是继续它的引擎核心,新加了RESTful任务(这个activiti没有,我之前是通过写代码实现),新加了异步存历史数据从而提升效率等核心功能
- 冀正在他的博客里讲了Flowable比Activiti多的功能, 当然,因为冀正是flowable的commiter,所以没有说Activiti比Flowable多的功能,有点偏颇,可以作为参考。
功能与其他总体对比
公司的个人初步感觉:
结论
- 具体选择看看锦华和龚总觉得哪个功能更为重要
- 我个人根据开发效率、功能、总体印象排列如下
- flowable6
- activiti7
- activiti6
- AI Comframe(AIF)
- 新零售工作流
- jbpm7
- activiti5、flowable5、jbpm5/6/4/3,其他国产开源框架或商用框架等
附录
flowable6和activit7的对比中文翻译列表
Flowable6(比activit6多的功能)
- 1、flowable已经支持所有的历史数据使用mongdb存储,activiti没有。
- 2、flowable支持事务子流程,activiti没有。
- 3、flowable支持多实例加签、减签,activiti没有。
- 4、flowable支持httpTask等新的类型节点,activiti没有。
- 5、flowable支持在流程中动态添加任务节点,activiti没有。
- 6、flowable支持历史任务数据通过消息中间件发送,activiti没有。
- 7、flowable支持java11,activiti没有。
- 8、flowable支持动态脚本,,activiti没有。
- 9、flowable支持条件表达式中自定义juel函数,activiti没有。
- 10、flowable支持cmmn规范,activiti没有。
- 11、flowable修复了dmn规范设计器,activit用的dmn设计器还是旧的框架,bug太多。
- 12、flowable屏蔽了pvm,activiti6也屏蔽了pvm(因为6版本官方提供了加签功能,发现pvm设计的过于臃肿,索性直接移除,这样加签实现起来更简洁、事实确实如此,如果需要获取节点、连线等信息可以使用bpmnmodel替代)。
- 13、flowable与activiti提供了新的事务监听器。activiti5版本只有事件监听器、任务监听器、执行监听器。-
- 14、flowable对activiti的代码大量的进行了重构。
- 15、activiti以及flowable支持的数据库有h2、hsql、mysql、oracle、postgres、mssql、db2。其他数据- 库不支持的。使用国产数据库的可能有点失望了,需要修改源码了。
- 16、flowable支持jms、rabbitmq、mongodb方式处理历史数据,activiti没有。
- 17、等等
Activiti7(比activiti6多的功能)
- 清理老版本的代码
- 清理仓库和重构
- 域API + HAL API +运行时包
- 流程定义重写
- 流程实例
- 任务
- 流程定义支持XML/JSON/SVG三种风格
- 审计服务:用于审计信息的事件存储
- 身份管理和SSO(KeyCloak 实现)
- 改进,改进和新增内容
- 查询服务:运行时信息使用事件存储。
- 开启Security
- JPA——参考实现
- 基础设施启用服务
- 网关(Zuul)
- 应用程序注册表(Eureka)
- SSO 和IDM(Keycloak )
- 所有服务都启用了Docker
- 所有的服务都可以部署到Kubernetes
- Cloud 实例 8月的工作是让流程引擎和其他服务与一些基础设施服务协作在云环境中工作。我们创建了Activiti Cloud 启动器,以确保在这些环境中工作既简单又直观。 我们还得到了查询服务的初始实现,这将允许您在不影响(和影响)任何流程引擎运行时的情况下使用关于流程执行的数据。
- 您可以通过查看我们的activiti - cloud示例存储库来使用所有这些服务,在这里您将找到一组描述符,以使用Docker撰写和Kubernetes和一个JavaScript应用程序来启动我们的所有服务,该应用程序演示了如何与提供的所有服务交互。这个简单的示例显示了当您想要与我们的服务交互时,安全层(SSO)是如何启动的。
- 集成事件和云连接器
- 释放Maven中央仓库地址
- 分布式通知服务(设计和初始实现)
- 基础设施启用服务
- 示踪剂(Zipkin)
- ELK 堆栈支持(可选)
- Activiti Cloud 文档
- 验证的例子
- AWS
- CloudFoundry 9月将致力于完善我们现有的服务,以确保我们使用合适的工具来进行正确的基础设施建设。我们支持的环境越多(AWS,CloudFoundry,Kubernetes),我们需要替换和集成的内容越多。基于Kubernetes已经提供了服务注册中心的事实,我们正在考虑替换Eureka在Kubernetes上运行。 本月的高优先级将是集成事件生产者和消费者(云连接器)的初始实现,这将使我们能够消除对类路径扩展的需求,并提高我们服务的互操作性。 我们的目标是在月底前对我们的通知服务进行非常简单的实现,以演示我们的基础结构如何允许您构建反应性和上下文应用程序。
- 将提供对Zipkin的支持,以监视和排除服务之间的交互。
- 应用程序上下文服务——初始版本
- 提供基本的案例管理结构
- 发布/部署运行时包服务
- 分布式模型存储库服务(设计和初始实现)
- 新的决策运行时设计和初始实现(有别于Flowable)
- 流程引擎清理和重构
- BPMN2扩充
- 历史上的服务
- 定时执行器
- 计时器
- 电子邮件服务
总结:
-
Activiti7好像基本叫Activiti Cloud7,专注于cloud的开发,包括与Zuul、Eureka、Zipkin、Sping Cloud、Docker、Kubernetes、ELK、Jenkins(持续集成)等功能;同时还包括一些我们用不到的云方面的功能,包括Cloud Native(云原生?)、KNative(谷歌的serverless开源框架)、Istio(service mesh的一个开源实现)、JHispter(前端的微服务框架?)、AWS(肯定不会与阿里云整合的)等;还有一些我看不懂的云方面的技术名词。
-
而Flowable专注于工作流引擎在NoSQL、消息队列的实现,可以完全不用关系型数据库,通过消息队列异步也可以提高效率;还专注于CMMN、DMN等流程规范、规则引擎方面的功能;还有如JUnit5(单元测试)、Jupiter(代码审查)等的功能;未来也打算开发K8s的整合功能。也就是主要专注于工作流引擎核心的功能。
附录:
Activiti7最新开发路线图(中文翻译)
Flowable最新版(6.4/6.3.1/6.3/6.2/6.1.1/6.1) 新特性(中文翻译)
Flowable v5 和v6版本的区别 http://www.shareniu.com/article/85.htm
Flowable 框架
Flowable 框架学习笔记
Flowable 入门介绍
官网地址:flowable.org
Flowable BPMN 用户手册 (v 6.4.2-SNAPSHOT)
Flowable BPMN 用户手册 (v 6.5.0-SNAPSHOT)
可以在官网下载对应的jar包在本地部署运行,官方提供了下面的五个应用程序:
-
Flowable Modeler:流程定义管理
-
Flowable Task:用户任务管理
-
Flowable IDM:用户组权限管理
-
Flowable REST API:流程引擎对外提供的API接口
-
Flowable Admin:后台管理
初识Flowable五大引擎
Flowable有五大引擎,每个之间都是相互独立互不影响。
ProcessEngine是里面最核心也是最重要的一个引擎,如果失去它那Flowable也就意义了。
流程引擎使用架构
Flowable引擎在使用前需要先通过配置来初始化ProcessEngine。
初始化ProcessEngineConfiguration一般有两种方式:
-
通过Spinrg配置文件进行依赖注入,通过flowable.cfg.xml文件来初始化ProcessEngineConfiguration(这里的文件名必须为flowable.cfg.xml,否则Flowable识别不到)
-
通过编写程序的方式来构造ProcessEngineConfiguration对象
ProcessEngineConfiguration在初始化过程中会同时初始化数据库,如果数据库已经存在,则不会做创建更新操作,如果数据库不存在,则会默认执行数据库创建脚本。
流程引擎初体验
-
简单了解Bpmn
-
Task任务:
用户任务(userTask)
系统任务(serviceTask )
-
Event事件:
定时器事件(timerEventDefinition)
-
Gateway网关:
排他网关(exclusive gateway)
-
-
目标:实现以下简化版的请假流程
-
步骤1:定义相应的BPMN文件
-
步骤2:配置flowable.cfg.xml
-
步骤3:将流程定义添加到Repository仓储中
-
步骤4:Runtime开始一个流程实例
Flowable的用户权限体系
在接入Flowable的用户权限体系的时候,有四种方式:
-
使用Flowable提供的默认IdmEngine进行用户体系管理,该引擎包含了用户、组的概念。
-
集成LDAP,实现轻量级用户权限管理。通过IdentityService进行认证,用于由IdentityService处理所有认证业务的场景。
-
实现IdmIdentityService接口,自定义实现用户、组的查询
-
接入自定义的权限体系
用户id => 获取到租户id、角色id集、部门id集
-
单用户(assignee="用户id")、多用户(candidateUsers="用户id1,用户id2")
-
单角色、多角色(candidateGroups=":角色id1,:角色id2")
-
单部门、多部门(candidateGroups="部门id1:,部门id2:")
-
角色或部门(candidateGroups="角色id1:, :部门id1")
-
角色且部门
-
使用Flowable工作流引擎的时候,不可避免就需要考虑相应的用户权限,
根据官方文档提供的教程,实现Flowable的用户权限体系总共有两大类:
Flowable提供的IdmEngine身份识别引擎
使用Flowable提供的IdmEngine,也有三种方案:
-
方案一: Flowable默认提供的IdmEngine已经实现IdmIdentityService接口,包含对用户、组、权限等的操作;
-
方案二: 集成LDAP来实现轻量级用户权限管理。LDAP内部已经实现IdmIdentityService接口,包含对用户、组、权限等的操作;
-
方案三: 自定义实现IdmIdentityService接口,实现对用户、组、权限的操作;
自定义身份识别引擎
有时候在项目上已经实现了自己的用户体系,接入Flowable的工作流引擎时,就需要考虑如何将自己的用户体系映射到Flowable工作流引擎的数据权限上去。Flowable提供的数据权限体系是用户、组、租户的结构体系,因为这里提供一个映射的设计思路:
在实际项目中,往往用户会存在租户、角色、部门之间的对应关系,因而在对应到Flowable的用户、组、租户的时候可以用以下思路来处理:
具体的案例如下:
用户A(id值为1) - 角色A(id值为1) - 部门A(id值为1) - 区域A(id值为1)(某个租户的用户A是角色A,所在部门为部门A,所属区域为区域A)
根据上述的映射关系转换成如下
-
用户转换后的id:1
-
角色转换后的id:R1
-
部门转换后的id:O1
-
区域转换后的id:A1
将映射关系对应到Flowable中
-
用户:1
-
组:R1、O1、A1
知道如果映射后,现在的问题就是在哪里去实现这部分的映射关系,以下提供几种实现的思路,目前还没正式验证过,仍处于实验阶段:
-
一、修改官方的源码
已知对bpmn的用户任务进行解析的方法类为UserTaskXMLConverter,通过该类的源码可以了解相关的解析机制,在此基础上对自己需要的节点添加相关的解析操作
-
二、找到官方提供的自定义配置,修改自定义配置属性(还未找到方案)
-
三、在前端设计bpmn文件的时候,通过相关的方法对id进行映射后生成最终的bpmn文件
-
四、自定义权限的表达式,例如:
<userTask id="approveTask" name="刘备审批" flowable:assignee="#{idmTest.org('emp')}"/>
其中idmTest.org就是用来自定义返回的id集的方法
数据库介绍(34张表)
ACT_RE_ *:RE代表repository。具有此前缀的表包含静态信息,例如流程定义和流程资源(图像,规则等)。
ACT_RU_ *:RU代表runtime。这些是包含运行时的流程实例,用户任务,变量,作业等的运行时数据的运行时表。Flowable仅在流程实例执行期间存储运行时数据,并在流程实例结束时删除记录。这使运行时表保持小而快。
ACT_HI_ *:HI代表history。这些是包含历史数据的表,例如过去的流程实例,变量,任务等。
ACT_GE_ *:general数据,用于各种用例。
ACT_ID_ *:Idm的用户、组
表分类 | 表名 | 表说明 |
---|---|---|
一般数据(2) | ACT_GE_BYTEARRAY | 通用的流程定义和流程资源 |
ACT_GE_PROPERTY | 系统相关属性 | |
流程历史记录(8) | ACT_HI_ACTINST | 历史的流程实例 |
ACT_HI_ATTACHMENT | 历史的流程附件 | |
ACT_HI_COMMENT | 历史的说明性信息 | |
ACT_HI_DETAIL | 历史的流程运行中的细节信息 | |
ACT_HI_IDENTITYLINK | 历史的流程运行过程中用户关系 | |
ACT_HI_PROCINST | 历史的流程实例 | |
ACT_HI_TASKINST | 历史的任务实例 | |
ACT_HI_VARINST | 历史的流程运行中的变量信息 | |
用户用户组表(9) | 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 | 系统登录日志表 | |
ACT_ID_USER | 用户表 | |
流程定义表(3) | ACT_RE_DEPLOYMENT | 部署单元信息 |
ACT_RE_MODEL | 模型信息 | |
ACT_RE_PROCDEF | 已部署的流程定义 | |
运行实例表(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 | 运行时变量表 | |
其他表(2) | ACT_EVT_LOG | 事件日志表 |
ACT_PROCDEF_INFO | 流程定义信息 |
ACT_HI_ACTINST 流程实例的历史运行节点表
ACT_HI_TASKINST 流程实例的历史任务表
ACT_HI_VARINST 流程实例的历史运行节点的变量表
ACT_HI_PROCINST 流程历史部署记录
ACT_HI_IDENTITYLINK 对应ACT_RU_IDENTITYLINK
的历史记录表
ACT_RE_DEPLOYMENT 流程部署
ACT_RE_PROCDEF 流程定义表
ACT_RU_EXECUTION 流程实例执行过程的所有节点记录
ACT_RU_IDENTITYLINK 流程实例运行过程中,各节点对应的用户
ACT_RU_TASK 流程实例运行时的任务表
ACT_RU_VARIABLE 流程实例运行时节点的变量表
ACT_GE_BYTEARRAY 资源文件表
Activiti Flowable transient变量
Activiti Flowable transient变量
Activiti6 增加了瞬时变量( transient
变量)。接下来展示如何使用 Activiti6 提供的 transient
变量。
Activiti6 版本之前所有的变量都会持久化到数据库表( act_ru_variable 或者 act_hi_varinst ),变量存储于 act_hi_varinst 表前提是开启了历史数据的归档开关。瞬时变量使用起来跟持久化的变量一样一样的。唯一的区别是他不会持久化到数据库表。下面对瞬时变量进行一些说明:
瞬时变量可以存活下来知道下一步的等待状态,前提是流程实例的数据可以存储到数据库。
瞬态变量隐藏具有相同名称的持久性变量。
1.1 瞬时变量的操作
void setTransientVariable(String variableName, Object variableValue);
void setTransientVariableLocal(String variableName, Object variableValue);
void setTransientVariables(MaptransientVariables);
void setTransientVariablesLocal(MaptransientVariables);
Object getTransientVariable(String variableName);
Object getTransientVariableLocal(String variableName);
MapgetTransientVariables();
MapgetTransientVariablesLocal();
void removeTransientVariable(String variableName);
void removeTransientVariableLocal(String variableName);
接下来演示瞬时变量的使用过程,操作起来非常的简单。
首先定义一个流程文档(名称process.bpmn20.xml),该流程图如下所示:
部署流程文档如下代码所示:
public void addInputStreamTest() throws Exception {
//定义的文件信息的流读取
InputStream inputStream = DeploymentBuilderTest.class.getClassLoader().getResourceAsStream("com/shareniu/activiti6/transientvars/process.bpmn20.xml");
//流程定义的分类
String category="variabletypeTest";
DeploymentBuilder deploymentBuilder = repositoryService.createDeployment().category(category).addInputStream("variabletype.bpmn", inputStream);
//部署
Deployment deploy = deploymentBuilder.deploy();
}
启动流程实例时,我们传递的变量是定期的变量。他们将保留和审核历史记录会保留,因为实在没有理由为什么这不应该是这样。
流程实例启动完毕之后,会首先执行‘execute HTTP call’,那么会立刻触发ExecuteHttpCallDelegate类中的逻辑,该类的定义如下所示:
public class ExecuteHttpCallDelegate implements JavaDelegate {
public void execute(DelegateExecution execution) {
execution.setTransientVariable("response", "分享牛(shareniu.com)");
execution.setTransientVariable("responseStatus",200);
}
}
上述的两个变量response和responseStatus都为瞬时变量。
接下来看一下ProcessResponseExecutionListener类的定义如下所示:
public class ProcessResponseExecutionListener implements ExecutionListener {
public void notify(DelegateExecution execution) {
Listlist=new ArrayList<>();
list.add("shareniu1");
list.add("shareniu2");
execution.setTransientVariable("searchResults",list );
}
}
Ok,上述工作完毕之后直接启动新的流程实例如下所示:
public void startProcessInstanceById() throws Exception{
runtimeService.startProcessInstanceById("transient-vars:1:37504");
}
act_ru_variable表的变化如下所示:
看到上图可知response、responseStatus这两个瞬时变量没有被存储,但是searchResults瞬时变量被存储了,那么问题来了,什么时候瞬时变量会转化为持久化变量呢?关于这一点可以参考随后的章节。
Flowable Activiti 会签 多实例
Flowable Activiti 会签 多实例
在实际的业务中,可能存在存在这么一种情况,当流程运行到某一个环节时,可能需要同时多个人的参与,才可以完成此环节。此时就可以用到activiti的多实例来解决此问题。
将一个节点设置成多实例:
要把一个节点设置为多实例,节点xml元素必须设置一个 multiInstanceLoopCharacteristics
子元素。
当 isSequential=true
时,表示的顺序执行,即虽然该节点有多条任务,但只有上一条执行完,才可以执行下一条。
当 isSequential=false
时,表示的并行执行,即该节点下的多条任务可以同时执行。
设置会签环节的参与者
activiti:collection
:用于执行该会签环节的参与参与的人,此处是使用的一个名叫pers的流程变量
activiti:elementVariable
:此处表示的是每一个分支都有一个名叫per的流程变量,和上方的activiti:assignee
结合使用就可以执行该分支应该由谁来处理。
指定会签环节的结束条件
当画红线的部分返回一个true的时候,该会签环节结束。进入下一个流程执行的环节.
completionCondition中写的是juel表达式。其中的mulitiInstance如果是和spring整合了,就是spring管理的bean的id,否则就是流程变量的key.
会签环节中涉及的几个默认的流程变量
1.nrOfInstances 该会签环节中总共有多少个实例
2.nrOfActiveInstances 当前活动的实例的数量,即还没有 完成的实例数量。
3.nrOfCompletedInstances 已经完成的实例的数量
代码
1.分配会签环节的人:
/**
* 分配下一环节会签的人
* @author huan
*/
public class AssgineeMultiInstancePer implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) throws Exception {
System.out.println("设置会签环节的人员.");
execution.setVariable("pers", Arrays.asList("张三", "李四", "王五", "赵六"));
}
}
2.多实例判断完成的条件:(注意:有于我没有和spring整合,所以此类要实现Serializable接口)
/**
* 多实例完成的条件判断
* @author huan
*/
public class MulitiInstanceCompleteTask implements Serializable {
private static final long serialVersionUID = 1L;
public boolean completeTask(DelegateExecution execution) {
System.out.println("总的会签任务数量:" + execution.getVariable("nrOfInstances") + "当前获取的会签任务数量:" + execution.getVariable("nrOfActiveInstances") + " - " + "已经完成的会签任务数量:" + execution.getVariable("nrOfCompletedInstances"));
System.out.println("I am invoked.");
return false;
}
}
3.会签环节中的一个监听器:
/**
* 测试会签过程中监听器的执行情况
* @author huan
*/
public class TestLinstener implements TaskListener {
private static final long serialVersionUID = -5754522101489239675L;
@Override
public void notify(DelegateTask delegateTask) {
System.out.print(delegateTask.getId() + " - " + delegateTask.getProcessInstanceId() + " - " + delegateTask.getEventName() + " - " + delegateTask.getTaskDefinitionKey());
}
}
4.测试代码:
/**
* 测试会签
* @author huan
*/
public class TestMultiInstance {
@Test
public void testProcess() {
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RepositoryService repositoryService = processEngine.getRepositoryService();
RuntimeService runtimeService = processEngine.getRuntimeService();
TaskService taskService = processEngine.getTaskService();
Deployment deploy = repositoryService.createDeployment()//
.name("会签流程测试")//
.addInputStream("multiInstances.bpmn", this.getClass().getResourceAsStream("multiInstances.bpmn"))//
.addInputStream("multiInstances.png", this.getClass().getResourceAsStream("multiInstances.png"))//
.deploy();
System.out.println(deploy.getId() + " " + deploy.getName());
Map<String, Object> variables = new HashMap<String, Object>();
variables.put("mulitiInstance", new MulitiInstanceCompleteTask());
ProcessInstance pi = runtimeService.startProcessInstanceByKey("multiInstances",variables);
System.out.println(pi.getId() + " " + pi.getActivityId());
Task task1 = taskService.createTaskQuery().processInstanceId(pi.getId()).taskAssignee("张三").singleResult();
System.out.println(task1.getId() + " - " + task1.getAssignee() + " - " + task1.getProcessInstanceId() + " - " + task1.getProcessDefinitionId());
Task task2 = taskService.createTaskQuery().processInstanceId(pi.getId()).taskAssignee("李四").singleResult();
System.out.println(task2.getId() + " - " + task2.getAssignee() + " - " + task2.getProcessInstanceId() + " - " + task2.getProcessDefinitionId());
Task task3 = taskService.createTaskQuery().processInstanceId(pi.getId()).taskAssignee("王五").singleResult();
System.out.println(task3.getId() + " - " + task3.getAssignee() + " - " + task3.getProcessInstanceId() + " - " + task3.getProcessDefinitionId());
Task task4 = taskService.createTaskQuery().processInstanceId(pi.getId()).taskAssignee("赵六").singleResult();
if (task4 != null) {
System.out.println(task4.getId() + " - " + task4.getAssignee() + " - " + task4.getProcessInstanceId() + " - " + task4.getProcessDefinitionId());
}
Task task5 = taskService.createTaskQuery().processInstanceId(pi.getId()).taskAssignee("钱七").singleResult();
System.out.println(task5);
taskService.complete(task1.getId());
taskService.complete(task2.getId());
taskService.complete(task3.getId());
Task task6 = taskService.createTaskQuery().processInstanceId(pi.getId()).taskAssignee("钱七").singleResult();
System.out.println(task6);
taskService.complete(task4.getId());
Task task7 = taskService.createTaskQuery().processInstanceId(pi.getId()).taskAssignee("钱七").singleResult();
System.out.println(task7);
taskService.complete(task7.getId());
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(pi.getId()).singleResult();
if (null == processInstance) {
System.out.println("流程完成.");
}
}
}
5.流程文件:
<?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:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="multiInstances" name="流程会签测试" isExecutable="true">
<startEvent id="startevent1" name="Start"></startEvent>
<sequenceFlow id="flow1" sourceRef="startevent1" targetRef="A001"></sequenceFlow>
<serviceTask id="A001" name="设置下一环节的人" activiti:class="com.huan.activiti.liuyang.会签.AssgineeMultiInstancePer"></serviceTask>
<userTask id="B001" name="会签环节" activiti:assignee="${per}">
<extensionElements>
<activiti:taskListener event="complete" class="com.huan.activiti.liuyang.会签.TestLinstener"></activiti:taskListener>
</extensionElements>
<multiInstanceLoopCharacteristics isSequential="false" activiti:collection="pers" activiti:elementVariable="per">
<completionCondition>${mulitiInstance.completeTask(execution)}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
<sequenceFlow id="flow2" sourceRef="A001" targetRef="B001"></sequenceFlow>
<userTask id="C001" name="会签后的环节" activiti:assignee="钱七"></userTask>
<sequenceFlow id="flow3" sourceRef="B001" targetRef="C001"></sequenceFlow>
<endEvent id="endevent1" name="End"></endEvent>
<sequenceFlow id="flow4" sourceRef="C001" targetRef="endevent1"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_multiInstances">
<bpmndi:BPMNPlane bpmnElement="multiInstances" id="BPMNPlane_multiInstances">
<bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
<omgdc:Bounds height="35.0" width="35.0" x="100.0" y="240.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="A001" id="BPMNShape_A001">
<omgdc:Bounds height="71.0" width="117.0" x="190.0" y="222.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="B001" id="BPMNShape_B001">
<omgdc:Bounds height="55.0" width="105.0" x="380.0" y="230.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="C001" id="BPMNShape_C001">
<omgdc:Bounds height="55.0" width="105.0" x="561.0" y="230.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
<omgdc:Bounds height="35.0" width="35.0" x="740.0" y="240.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
<omgdi:waypoint x="135.0" y="257.0"></omgdi:waypoint>
<omgdi:waypoint x="190.0" y="257.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
<omgdi:waypoint x="307.0" y="257.0"></omgdi:waypoint>
<omgdi:waypoint x="380.0" y="257.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
<omgdi:waypoint x="485.0" y="257.0"></omgdi:waypoint>
<omgdi:waypoint x="561.0" y="257.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
<omgdi:waypoint x="666.0" y="257.0"></omgdi:waypoint>
<omgdi:waypoint x="740.0" y="257.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
7.流程图:
Flowable6.4 – 会签实现方案
Flowable6.4 – 会签实现方案
之前介绍过多实例,但只是理论上如何实现,本文将介绍一种能够实际应用的会签方案。
前面几篇文章描述过如何为一个UserTask节点增加扩展属性,多实例UserTask节点也可以按照之前的方法增加扩展属性。
但是分配办理人时,与普通的UserTask节点稍有不同,不能直接从UserTask节点的扩展属性内获取办理人信息。
如何获得办理人?
首先, 声明一个辅助处理器,用来帮助获得UserTask节点的办理人和检查是否结束多实例节点的办理。主要代码如下:
/**
* 获得当前节点的处理者列表
* @param execution 当前执行实例
* @return 处理者列表
*/
public List<DealerInfo> getList(DelegateExecution execution) {
FlowElement flowElement = execution.getCurrentFlowElement();
UserTask userTask = (UserTask) flowElement;
UserTaskExtension userTaskExtension = FlowUtil.getUserTaskExtension(userTask);
//DealerInfo是一个实体,用来表示办理人的具体信息
//从扩展属性内读取UserTask的办理人信息
//返回一个集合,该集合的数量影响了UserTask多实例的数量
return userTaskExtension.getDealers();
}
/**
* 获取会签是否结束
* @param execution 当前执行实例
* @return 是否结束
*/
public boolean getComplete(DelegateExecution execution) {
Integer nrOfCompletedInstances = (Integer) execution.getVariable("nrOfCompletedInstances");
Integer nrOfInstances = (Integer) execution.getVariable("nrOfInstances");
int agreeCount = 0, rejectCount = 0, abstainCount = 0;
Map<String, Object> vars = execution.getVariables();
for (String key : vars.keySet()) {
//会签投票以SIGN_VOTE+TaskId标识
//获得会签投票的统计结果
if (key.contains(FlowConst.SIGN_VOTE) && !key.equals(FlowConst.SIGN_VOTE_RESULT)) {
Integer value = (Integer) vars.get(key);
//统计同意、驳回、弃权票数
//省略代码若干......
}
}
//以下为一段简单的规则,可以按情况实现自己的会签规则
if (!nrOfCompletedInstances.equals(nrOfInstances)) {
//必须等所有的办理人都投票
return false;
} else {
//会签全部完成时,使用默认规则结束
if (rejectCount > 0) {
//有反对票,则最终的会签结果为不通过
//移除SIGN_VOTE+TaskId为标识的参数
removeSignVars(execution);
//增加会签结果参数,以便之后流转使用
execution.setVariable(FlowConst.SIGN_VOTE_RESULT, false);
//会签结束
return true;
} else {
//没有反对票时,则最终的会签结果为通过
removeSignVars(execution);
execution.setVariable(FlowConst.SIGN_VOTE_RESULT, true);
return true;
}
}
}
然后, 对流程图内的UserTask节点进行设置:
上图中的Collection设置为:${mutiInstanceHandler.getList(execution)}
。
上图中的Completion设置为:${mutiInstanceHandler.getComplete(execution)}
。
一定要注意的是:Element Var,这个参数将决定多实例中每一个UserTask的办理人。
接下来,设置办理人,之前的教程可以参考《Flowable6.4 - 分派办理人》,主要的代码如下:
/**
* 分配办理人员
*/
@Override
protected void handleAssignments(TaskService taskService, String assignee, String owner, List<String> candidateUsers, List<String> candidateGroups, TaskEntity task, ExpressionManager expressionManager, DelegateExecution execution) {
if (null != this.userTaskExtension) {
//由扩展属性读取办理人规则
List<DealerInfo> dealerInfos = getDealerInfo();
if (null == dealerInfos || dealerInfos.size() == 0) {
throw new RuntimeException("处理者信息不存在");
}
if (hasMultiInstanceCharacteristics())
//多实例节点分配处理者
//获得传参,流程图中的Element Var
Object objDealer = execution.getVariable("dealerInfo");
if (objDealer instanceof DealUserInfo) {
DealUserInfo userInfo = (DealUserInfo) objDealer;
assignee = userInfo.getId();
} else if (objDealer instanceof DealRoleInfo) {
DealRoleInfo roleInfo = (DealRoleInfo) objDealer;
task.addGroupIdentityLink(roleInfo.getId(), "role");
}
}
}
super.handleAssignments(taskService, assignee, owner, candidateUsers, candidateGroups, task, expressionManager, execution);
}
最后, 在Complete流程的时候,按照调用方传回的信息,设置投票的值即可:
//设置会签投票结果
variables.put(FlowConst.SIGN_VOTE + "_" + taskId, signVoteType.getCode());
如何使用会签结果?
在需要判断会签结果的Sequence Flow Condition中直接设置:
Flowable6.4 - 分派办理人
Flowable6.4 - 分派办理人
这次分享的是从一个开源项目的代码里面学来的,有兴趣的同学可以去阅读一下该项目的源码,还是有很多可以参考的地方,项目地址如下:
https://gitee.com/threefish/NutzFw.git
首先, 存储办理人的表:act_ru_identitylink。如果想为一个Task分配办理人,可以使用以下的API:
task.addCandidateGroup(String groupId);
task.addCandidateUser(String userId);
task.addCandidateGroups(Collection<String> candidateGroups);
task.addCandidateUsers(Collection<String> candidateUsers);
如果使用以上的API增加一个办理人,会在act_ru_identitylink表中增加一条记录,如下:
使用上述API设置的人员或者组,表中的TYPE_为“candidate”。如果阅读Flowable的源代码,就会发现原因:
//设置人员
public IdentityLinkEntity addCandidateUser(String taskId, String userId) {
return this.addTaskIdentityLink(taskId, userId, (String)null, "candidate");
}
//设置组
public IdentityLinkEntity addCandidateGroup(String taskId, String groupId) {
return this.addTaskIdentityLink(taskId, (String)null, groupId, "candidate");
}
但是, 有时候这种固定的属性无法满足我们的业务需要。比如,有时候期望设置部门、岗位、角色。这时候,就需要使用其它的API进行设置,如下:
通过上面的“identityLinkTyp”,就可以自定义TYPE_的值,如下图所示:
之后,就可以进入本次的主要内容了,如何为UserTask节点分配办理人。这里提供的一个方案是通过重写UserTaskActivityBehavior来实现。
关于如何重写UserTaskActivityBehavior,可以参考之前的文章,链接如下:
Flowable6.4 - Behavior使用初探
这里需要重写UserTaskActivityBehavior内的handleAssignments方法,主要的代码如下:
public class ExtUserTaskActivityBehavior extends UserTaskActivityBehavior {
private static final long serialVersionUID = 7711531472879418236L;
public ExtUserTaskActivityBehavior(UserTask userTask) {
super(userTask);
}
/**
* 分配办理人员
*/
@Override
protected void handleAssignments(TaskService taskService, String assignee, String owner, List<String> candidateUsers, List<String> candidateGroups, TaskEntity task, ExpressionManager expressionManager, DelegateExecution execution) {
//此处可以根据业务逻辑自定义
super.handleAssignments(taskService, assignee, owner, candidateUsers, candidateGroups, task, expressionManager, execution);
}
}
比如NutzFW这个开源项目就是通过如下的过程设置的:
通过扩展UserTask节点属性,设置办理人。
当触发handleAssignments方法时,读取UserTask节点属性。
根据节点属性设置办理人。
主要的设置代码如下:
case SINGLE_USER:
//单人情况下,直接设置办理人
assignee = taskExtensionDTO.getAssignee();
break;
case MULTIPLE_USERS:
//多人情况下,设置candidateUsers
candidateUsers = taskExtensionDTO.getCandidateUsers().stream().map(CandidateUsersDTO::getUserName).collect(Collectors.toList());
break;
case USER_ROLE_GROUPS:
//角色时,设置group
candidateGroups = taskExtensionDTO.getCandidateGroups().stream().map(CandidateGroupsDTO::getRoleCode).collect(Collectors.toList());
break;
Flowable6.4 – 加签和减签
Flowable6.4 – 加签和减签
趁着旅游归来的短暂休息,了解一下Flowable中的加签和减签操作。主要是以下两个方法来实现:
runtimeService.addMultiInstanceExecution(String activityId, String parentExecutionId, Map<String, Object> executionVariables)
runtimeService.deleteMultiInstanceExecution(String executionId, boolean executionIsCompleted)
依然是先上流程图:
其中会签节点是多实例节点,此流程的关键xml片段如下:
<process id="TestMutiTask" isExecutable="true">
<startEvent id="Start1" name="开始"></startEvent>
<userTask id="UserTask1" name="处理"></userTask>
<sequenceFlow id="sid-D14A5BC6-A61E-461F-AD33-0042E91B8B13" sourceRef="Start1" targetRef="UserTask1"></sequenceFlow>
<userTask id="UserTask2" name="会签">
<multiInstanceLoopCharacteristics isSequential="false" flowable:collection="${subProcessHelper.getUserNames()}" flowable:elementVariable="assignee">
<completionCondition>${subProcessHelper.isComplete(execution)}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
<sequenceFlow id="sid-E6847EF6-F54F-409B-AF8B-DCA62ECDC76F" sourceRef="UserTask1" targetRef="UserTask2"></sequenceFlow>
<userTask id="UserTask3" name="审批"></userTask>
<sequenceFlow id="sid-AF828B84-DDAA-4056-88C5-9D4F6EA9F725" sourceRef="UserTask2" targetRef="UserTask3"></sequenceFlow>
<endEvent id="End1" name="结束"></endEvent>
<sequenceFlow id="sid-C49B6256-0827-4CF5-8A47-5860A107142A" sourceRef="UserTask3" targetRef="End1"></sequenceFlow>
</process>
subProcessHelper.getUserNames():会返回一个List<String>
集合,会签节点会根据此集合的数量生成相对应的实例。
subProcessHelper.isComplete(execution):用来判断会签节点是否完成,这里设置的条件为“已完成数量/总数量>2/3
”。
调用加签和减签的方法如下:
/**
* 增加流程执行实例
* @param nodeId
* @param proInstId
* @param assigneeStr 以逗号隔开的字符串
*/
@RequestMapping(value = "addExecution/{nodeId}/{proInstId}/{assignees}")
public void addExecution(@PathVariable("nodeId") String nodeId,
@PathVariable("proInstId") String proInstId,
@PathVariable("assignees") String assigneeStr) {
String[] assignees = assigneeStr.split(",");
for (String assignee : assignees) {
runtimeService.addMultiInstanceExecution(nodeId, proInstId, Collections.singletonMap("assignee", (Object) assignee));
}
}
/**
* 删除流程执行实例
* @param excutionId
* @param complated 是否完成此流程执行实例
*/
@RequestMapping(value = "delExecution/{excutionId}/{complated}")
public void delExecution(@PathVariable("excutionId") String excutionId,
@PathVariable("complated") Boolean complated) {
runtimeService.deleteMultiInstanceExecution(excutionId, complated);
}
启动流程,将流程跳转至会签节点,如下图所示:
act_ru_task
此时请求加签方法:
http://localhost:8080/flowabledemo/flow/addExecution/UserTask2/55001/test004
流程会增加一个新的子实例,并且会增加相对应的参数,如下:
act_ru_task
此时如果请求减签的方法:
http://localhost:8080/flowabledemo/flow/delExecution/55034/0
流程中相对应的Task和Variable会被删除:
以上,就是本次试验的内容,需要注意的是,在减签时,如果Task正好是该多实例节点中的最后一个,将导致流程无法继续流转。下次可以分析一下源码,看看为何会这样。
Flowable6.4 – 加签和减签的源码解析
Flowable6.4 – 加签和减签的源码解析
上一篇简单实现了一下加签和减签的操作,这次主要是看看Flowable是如何实现加签和减签的。
首先,加签。
Flowable实现加签主要是通过下面的方法实现的:
runtimeService.addMultiInstanceExecution(String activityId, String parentExecutionId, Map<String, Object> executionVariables)
跟踪代码进入其方法体,发现执行了下面这个命令:
AddMultiInstanceExecutionCmd(activityId, parentExecutionId, executionVariables)
这里的三个参数所代表的的意义是:
activityId:流程节点的标识。
parentExecutionId:流程执行实例标识,proInstId。
executionVariables:所要传入的参数。
查看AddMultiInstanceExecutionCmd
这个类的源码,主要关注方法execute()
,此方法就是加签操作实现的关键。
关键的代码加注释,如下:
@Override
public Execution execute(CommandContext commandContext) {
ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager();
//获得multi instance execution,即IS_MI_ROOT
ExecutionEntity miExecution = searchForMultiInstanceActivity(activityId, parentExecutionId, executionEntityManager);
if (miExecution == null) {
throw new FlowableException("No multi instance execution found for activity id " + activityId);
}
if (Flowable5Util.isFlowable5ProcessDefinitionId(commandContext, miExecution.getProcessDefinitionId())) {
throw new FlowableException("Flowable 5 process definitions are not supported");
}
//创建新的流程执行实例
ExecutionEntity childExecution = executionEntityManager.createChildExecution(miExecution);
childExecution.setCurrentFlowElement(miExecution.getCurrentFlowElement());
//获得BPMN模型中节点的配置信息
BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(miExecution.getProcessDefinitionId());
Activity miActivityElement = (Activity) bpmnModel.getFlowElement(miExecution.getActivityId());
MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = miActivityElement.getLoopCharacteristics();
//设置流程参数nrOfInstances
Integer currentNumberOfInstances = (Integer) miExecution.getVariable(NUMBER_OF_INSTANCES);
miExecution.setVariableLocal(NUMBER_OF_INSTANCES, currentNumberOfInstances + 1);
//设置子流程执行实例的参数
if (executionVariables != null) {
childExecution.setVariablesLocal(executionVariables);
}
//如果是并行,需要执行操作,生成Task记录
if (!multiInstanceLoopCharacteristics.isSequential()) {
miExecution.setActive(true);
miExecution.setScope(false);
childExecution.setCurrentFlowElement(miActivityElement);
CommandContextUtil.getAgenda().planContinueMultiInstanceOperation(childExecution, miExecution, currentNumberOfInstances);
}
return childExecution;
}
需要注意的就是最后部分的操作,因为节点有并行和串行的区分,所以需要不同的处理。
再看,减签。
Flowable实现加签主要是通过下面的方法实现的:
runtimeService.deleteMultiInstanceExecution(String executionId, boolean executionIsCompleted)
跟踪代码进入其方法体,发现执行了下面这个命令:
DeleteMultiInstanceExecutionCmd(executionId, executionIsCompleted)
这里的两个参数所代表的的意义是:
executionId:需要删除的流程执行实例标识。
executionIsCompleted:是否完成此流程执行实例。查看
DeleteMultiInstanceExecutionCmd这个类的源码,主要关注方法execute(),此方法就是加签操作实现的关键。
关键的代码加注释,如下:
@Override
public Void execute(CommandContext commandContext) {
ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager();
ExecutionEntity execution = executionEntityManager.findById(executionId);
//获得BPMN模型中节点的配置信息
BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(execution.getProcessDefinitionId());
Activity miActivityElement = (Activity) bpmnModel.getFlowElement(execution.getActivityId());
MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = miActivityElement.getLoopCharacteristics();
if (miActivityElement.getLoopCharacteristics() == null) {
throw new FlowableException("No multi instance execution found for execution id " + executionId);
}
if (!(miActivityElement.getBehavior() instanceof MultiInstanceActivityBehavior)) {
throw new FlowableException("No multi instance behavior found for execution id " + executionId);
}
if (Flowable5Util.isFlowable5ProcessDefinitionId(commandContext, execution.getProcessDefinitionId())) {
throw new FlowableException("Flowable 5 process definitions are not supported");
}
//删除指定的流程执行实例和与其关联的数据
ExecutionEntity miExecution = getMultiInstanceRootExecution(execution);
executionEntityManager.deleteChildExecutions(execution, "Delete MI execution", false);
executionEntityManager.deleteExecutionAndRelatedData(execution, "Delete MI execution", false);
//获得循环的索引值,以便之后重新设置
int loopCounter = 0;
if (multiInstanceLoopCharacteristics.isSequential()) {
//如果是串行,则获得当前的索引值
SequentialMultiInstanceBehavior miBehavior = (SequentialMultiInstanceBehavior) miActivityElement.getBehavior();
loopCounter = miBehavior.getLoopVariable(execution, miBehavior.getCollectionElementIndexVariable());
}
//如果设置为流程执行实例已经完成,则已完成数量+1,并且索引值也+1
//如果设置为流程执行实例未完成,则流程实例数量-1,索引值不变
if (executionIsCompleted) {
Integer numberOfCompletedInstances = (Integer) miExecution.getVariable(NUMBER_OF_COMPLETED_INSTANCES);
miExecution.setVariableLocal(NUMBER_OF_COMPLETED_INSTANCES, numberOfCompletedInstances + 1);
loopCounter++;
} else {
Integer currentNumberOfInstances = (Integer) miExecution.getVariable(NUMBER_OF_INSTANCES);
miExecution.setVariableLocal(NUMBER_OF_INSTANCES, currentNumberOfInstances - 1);
}
//生成一个新的流程执行实例(个人觉得这个是专为串行准备的)
ExecutionEntity childExecution = executionEntityManager.createChildExecution(miExecution);
childExecution.setCurrentFlowElement(miExecution.getCurrentFlowElement());
//如果是串行,需要执行一次生成Task,并且设置正确的loopCounter
if (multiInstanceLoopCharacteristics.isSequential()) {
SequentialMultiInstanceBehavior miBehavior = (SequentialMultiInstanceBehavior) miActivityElement.getBehavior();
miBehavior.continueSequentialMultiInstance(childExecution, loopCounter, childExecution);
}
return null;
}
所以,通过分析上述源码,如果是并行的多实例节点,并且删除了最后一个流程执行实例,会发现没有了Task,导致整个流程中断。
Flowable6.4 – 绘制流程图
Flowable6.4 – 绘制流程图
一般需要流程图的场景:
-
发起流程时,需要从全局了解整体情况、所涉及的经办人,便于必要时进行催办或者发起线下沟通。
-
流程运行时或结束后,查看流程所经历的办理过程。
在Flowable中,流程图的绘制可以参见:
org.flowable.image.impl.DefaultProcessDiagramGenerator
本文将分成两部分,简单介绍一下流程图的绘制和办理节点的高亮现实。
首先,如何绘制流程图。
总共三个步骤:
-
获得流程定义的BpmnModel。
-
根据BpmnModel获得图片流。
-
输出图片流。
直接上代码了,基本上都是Flowable Api的使用:
/**
* @param type 输入的图片类型(png或jpg)
* @param modelId 流程模型标识
* @param response HttpServletResponse
*/
@ResponseBody
@RequestMapping(value = "diagram/{type}/{modelId}")
public void getJpgDiagram(@PathVariable(value = "type") String type,
@PathVariable(value = "modelId") String modelId,
HttpServletResponse response) {
try {
//根据modelId或者BpmnModel
Model modelData = repositoryService.getModel(modelId);
ExtBpmnJsonConverter jsonConverter = new ExtBpmnJsonConverter();
byte[] modelEditorSource = repositoryService.getModelEditorSource(modelData.getId());
JsonNode editorNode = new ObjectMapper().readTree(modelEditorSource);
BpmnModel bpmnModel = jsonConverter.convertToBpmnModel(editorNode);
//获得图片流
DefaultProcessDiagramGenerator diagramGenerator = new DefaultProcessDiagramGenerator();
InputStream inputStream = diagramGenerator.generateDiagram(
bpmnModel,
type,
Collections.emptyList(),
Collections.emptyList(),
"宋体",
"宋体",
"宋体",
null,
1.0,
false);
//输出为图片
IOUtils.copy(inputStream, response.getOutputStream());
response.setHeader("Content-Disposition", "attachment; filename=" + bpmnModel.getMainProcess().getId() + "." + type);
response.flushBuffer();
} catch (Exception e) {
e.printStackTrace();
}
}
需要注意的是,如果未指定文字信息,中文就会以方框显示。如果设定了文字,但是输出还是以方框显示,可以尝试在设置Configuration时,加入:
configuration.setActivityFontName("宋体");
configuration.setLabelFontName("宋体");
效果如下:
然后,如何高亮显示已经办理的节点。
除了上面的三步之外,需要预先提取高亮的节点和Sequence Flow。即设置上文中的diagramGenerator.generateDiagram方法中的两个emptyList。
依然是直接上代码:
/**
* @param type 输入的图片类型(png或jpg)
* @param modelId 流程模型标识
* @param instId 流程实例标识
* @param response HttpServletResponse
*/
@ResponseBody
@RequestMapping(value = "acitvityDiagram/{type}/{modelId}/{instId}")
public void getJpgActivityDiagram(@PathVariable(value = "type") String type,
@PathVariable(value = "modelId") String modelId,
@PathVariable(value = "instId") String instId,
HttpServletResponse response) {
//获得已经办理的历史节点
List<HistoricActivityInstance> activityInstances = historyService.createHistoricActivityInstanceQuery().processInstanceId(instId).orderByHistoricActivityInstanceStartTime().asc().list();
List<String> activties = new ArrayList<>();
List<String> flows = new ArrayList<>();
for (HistoricActivityInstance activityInstance : activityInstances) {
if ("sequenceFlow".equals(activityInstance.getActivityType())) {
//需要高亮显示的连接线
flows.add(activityInstance.getActivityId());
} else {
//需要高亮显示的节点
activties.add(activityInstance.getActivityId());
}
}
try {
//根据modelId或者BpmnModel
Model modelData = repositoryService.getModel(modelId);
ExtBpmnJsonConverter jsonConverter = new ExtBpmnJsonConverter();
byte[] modelEditorSource = repositoryService.getModelEditorSource(modelData.getId());
JsonNode editorNode = new ObjectMapper().readTree(modelEditorSource);
BpmnModel bpmnModel = jsonConverter.convertToBpmnModel(editorNode);
//获得图片流
DefaultProcessDiagramGenerator diagramGenerator = new DefaultProcessDiagramGenerator();
InputStream inputStream = diagramGenerator.generateDiagram(
bpmnModel,
type,
activties,
flows,
"宋体",
"宋体",
"宋体",
null,
1.0,
false);
//输出图片
IOUtils.copy(inputStream, response.getOutputStream());
response.setHeader("Content-Disposition", "attachment; filename=" + bpmnModel.getMainProcess().getId() + "." + type);
response.flushBuffer();
} catch (Exception e) {
e.printStackTrace();
}
}
最终的效果如下:
flowable 任务节点多实例使用
flowable 任务节点多实例使用
我们在使用Flowable 工作流引擎的时候,最常用的肯定是任务节点,因为在OA系统、审批系统、办公自动化系统中核心的处理就是流程的运转,在流程运转的时候,可能我们有这样的一个需求,在一个任务节点的时候,我们需要多个人对这个节点进行审批,比如实际中这样一个例子,假如是一个部门的投票,这个部门有5个人,那么当5个人都投票的时候大概分为如下几种:
-
部门所有人都去投票,当所有人都投票完成的时候,这个节点结束,流程运转到下一个节点。(所有的人都需要投票)
-
部门所有人都去投票,只要有任意2/3的人同意,这个节点结束,流程运转到下一个节点。(部分人投票只要满足条件就算完成)。
-
部门中有一个部门经理,只要部门经理投票过了,这个节点结束,流程运转到下一个节点(一票否决权)。
-
部门中根据职位不同,不同的人都不同的权重,当满足条件的时候,这个节点结束,流程运转到下一个节点。比如说所有的人员权重加起来是1,a有0.2的权重,其他的四个人分别是0.1的权重,我们可以配置权重达到0.3就可以走向下一个节点,换言之a的权重是其他人的2倍,那就是a的投票相当于2个人投票。这种需求还是很常见的。
-
部门所有人都去投票,a投票结束到b,b开始投票结束到c,一直如此,串行执行。最终到最后一个人再统计结果,决定流程的运转。
上面的五种情况,我们可以提取出来一些信息,我们的activiti 工作流引擎,必须支持如下功能,才能满足上面的需求:
-
任务节点可以配置自定义满足条件。
-
任务节点必须支持串行、并行。
-
任务节点必须支持可以指定候选人或者候选组。
-
任务节点必须支持可以循环的次数。
-
任务节点必须支持可以自定义权重。
-
任务节点必须支持加签、减签。(就是动态的修改任务节点的处理人)
因为实际上的需求可能比上面的几种情况更加的复杂,上面的6个满足条件,工作流支持前4个,后面的2个条件是不支持的,所以我们必须要扩展activiti 工作流引擎才能使用5、6等的功能。下面我们将详细的介绍前四种条件的使用,在掌握基本使用之后,我们在后面的章节中将详细的介绍,5、6这两种功能以及可能更加复杂的操作。
串行、并行配置
为了演示如何使用,我们采用由浅入深的使用,结合流程图、流程定义xml、以及代码和数据库的变化来阐释每一个配置的使用以及含义。
流程的详细定义如下图所示:
流程的详细定义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: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">
<process id="multiInstance" name="multiInstance" isExecutable="true">
<documentation>multiInstance</documentation>
<startEvent id="startEvent1"></startEvent>
<userTask id="A" name="多实例测试" flowable:candidateUsers="shareniu1,shareniu2,shareniu3,shareniu4" xmlns:shareniu="http://www.shareniu.com/" shareniu:shareniuext="" shareniu:shareniuextboolen="false">
<multiInstanceLoopCharacteristics isSequential="true">
<loopCardinality>2</loopCardinality>
</multiInstanceLoopCharacteristics>
</userTask>
<sequenceFlow id="sid-6D482A8A-F796-4E2B-AB2C-ABBFD2DA428D" sourceRef="startEvent1" targetRef="A"></sequenceFlow>
<userTask id="shareniu-B" name="shareniu-B" xmlns:shareniu="http://www.shareniu.com/" shareniu:shareniuext="" shareniu:shareniuextboolen="false"></userTask>
<sequenceFlow id="sid-BA8FC337-40DC-493B-805C-F213B7C4A17D" sourceRef="A" targetRef="shareniu-B"></sequenceFlow>
<endEvent id="sid-4AC81F5B-49F8-4135-B68E-1C182D004080"></endEvent>
<sequenceFlow id="sid-DF962BB4-EA4C-4D86-9A91-214EB2EC1A47" sourceRef="shareniu-B" targetRef="sid-4AC81F5B-49F8-4135-B68E-1C182D004080"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_multiInstance">
<bpmndi:BPMNPlane bpmnElement="multiInstance" id="BPMNPlane_multiInstance">
<bpmndi:BPMNShape bpmnElement="startEvent1" id="BPMNShape_startEvent1">
<omgdc:Bounds height="30.0" width="30.0" x="100.0" y="130.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="A" id="BPMNShape_A">
<omgdc:Bounds height="80.0" width="100.0" x="165.0" y="105.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="shareniu-B" id="BPMNShape_shareniu-B">
<omgdc:Bounds height="80.0" width="100.0" x="315.0" y="120.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-4AC81F5B-49F8-4135-B68E-1C182D004080" id="BPMNShape_sid-4AC81F5B-49F8-4135-B68E-1C182D004080">
<omgdc:Bounds height="28.0" width="28.0" x="442.0" y="146.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sid-DF962BB4-EA4C-4D86-9A91-214EB2EC1A47" id="BPMNEdge_sid-DF962BB4-EA4C-4D86-9A91-214EB2EC1A47">
<omgdi:waypoint x="414.9499999999902" y="160.0"></omgdi:waypoint>
<omgdi:waypoint x="442.0" y="160.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-BA8FC337-40DC-493B-805C-F213B7C4A17D" id="BPMNEdge_sid-BA8FC337-40DC-493B-805C-F213B7C4A17D">
<omgdi:waypoint x="264.95000000000005" y="145.0"></omgdi:waypoint>
<omgdi:waypoint x="290.0" y="145.0"></omgdi:waypoint>
<omgdi:waypoint x="290.0" y="160.0"></omgdi:waypoint>
<omgdi:waypoint x="314.99999999998477" y="160.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-6D482A8A-F796-4E2B-AB2C-ABBFD2DA428D" id="BPMNEdge_sid-6D482A8A-F796-4E2B-AB2C-ABBFD2DA428D">
<omgdi:waypoint x="129.94999817301806" y="145.0"></omgdi:waypoint>
<omgdi:waypoint x="165.0" y="145.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
xml配置文件的部分含义如下:
- flowable:candidateUsers="shareniu1,shareniu2,shareniu3,shareniu4" 这个节点可以4个人审核。
<loopCardinality>2</loopCardinality>
循环2次结束。<multiInstanceLoopCharacteristics isSequential="true">
串行并行的配置。
串行的配置
修改<multiInstanceLoopCharacteristics isSequential="true">
中的isSequential为true是串行,isSequential为false是并行。我们测试串行。下面的代码展示启动流程因为是以一个节点所以部署启动后,直接进入多实例任务。
流程的部署
@Test
public void addBytes() {
byte[] bytes = IoUtil.readInputStream(
ProcessengineTest.class.getClassLoader()
.getResourceAsStream("com/shareniu/shareniu_flowable_study/bpmn/ch3/multiInstance.bpmn20.xml"),
"multiInstance.bpmn20.xml");
String resourceName = "multiInstance.bpmn";
Deployment deployment = repositoryService.createDeployment().addBytes(resourceName, bytes).deploy();
System.out.println(deployment);
}
流程的启动
@Test
public void startProcessInstanceByKey() {
String processDefinitionKey = "multiInstance";
ProcessInstance startProcessInstanceByKey = runtimeService.startProcessInstanceByKey(processDefinitionKey);
System.out.println(startProcessInstanceByKey);
}
我们按照上面的步骤启动一个流程看一下数据库的变化。
ACT_RU_TASK表的数据有了如下图所示:
ACT_RU_IDENTITYLINK表的数据有了如下图所示:
ACT_RU_IDENTITYLINK权限表中,确实把我们设置的人员信息设置进去了,shareniu1,shareniu2,shareniu3,shareniu4现在就有代办信息了。
ACT_RU_VARIABLE表的数据有了如下图示:
上面重要的变量需要解释一下,要不然还真不好理解,多任务是怎么运转的。
-
nrOfInstances 实例总数。
-
nrOfCompletedInstances 当前还没有完成的实例 nr是number单词缩写 。
-
loopCounter 已经循环的次数。
-
nrOfActiveInstances 已经完成的实例个数。
下面我们结束一个任务看一下,流程走到那个节点了。
完成任务
@Test
public void complete() {
String taskId="15011";
taskService.complete(taskId);
}
接下来看一下数据库表的变化。
ACT_RU_VARIABLE表的数据有了如下图所示:
上面我们仔细的发现,可以看到
nrOfCompletedInstances、loopCounter、nrOfActiveInstances都加1了,确实多任务就是参考这几个值的变化进行判断的。
因为我们设置了循环2次,所以我们看看ACT_RU_IDENTITYLINK还有一个任务,因为我们是并行处理的。
所以我们在结束新的任务看一下流程是不是真的结束了,如果结束了,那么我们循环次数的配置就是正确的。
完成任务
@Test
public void complete() {
String taskId="17502";
taskService.complete(taskId);
}
下面看一下ACT_RU_TASK,里面没有任务信息了,所以侧面证明循环次数的配置就是正确的。
接下来我们测试并行任务。除了isSequential="false",其他的配置是一样的。
并行的配置测试
除了isSequential="false",其他的配置跟上面的串行是一样一样的。
重新部署测试,部署后我们启动一个新的流程测试。
ACT_RU_TASK表如下:
一次性的有2个任务需要处理,因为我们循环的是2次,所以直接就是2个。
ok串行、并行就讲解到这里。
串行、并行总结
我们配置的是循环2次,看以看到不管是并行还是串行,两个代办任务结束之后,流程直接跳转到下一个状态,但是
我们并没有配置结束条件,所以上面的例子,也可以看出来,如果不配置默认的通过条件,则默认条件是1,后面的源码章节会给大家说明这一点的。
通过条件的配置
在上面的串行、并行实例中,我们没有设置通过条件,但是程序按照配置的循环的次数,然后跳转到了下一个状态,可以侧面印证,如果不配置通过条件则默认值就是1.
下面的代码详细的介绍通过条件的配置,具体的配置代码如下:
<process id="multiInstance" name="multiInstance"
isExecutable="true">
<documentation>multiInstance</documentation>
<startEvent id="startEvent1"></startEvent>
<userTask id="A" name="多实例测试"
flowable:candidateUsers="shareniu1,shareniu2,shareniu3,shareniu4">
<multiInstanceLoopCharacteristics
isSequential="true">
<loopCardinality>4</loopCardinality>
<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.25}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
<sequenceFlow
id="sid-6D482A8A-F796-4E2B-AB2C-ABBFD2DA428D" sourceRef="startEvent1"
targetRef="A"></sequenceFlow>
<userTask id="shareniu-B" name="shareniu-B"
xmlns:shareniu="http://www.shareniu.com/" shareniu:shareniuext=""
shareniu:shareniuextboolen="false"></userTask>
<sequenceFlow
id="sid-BA8FC337-40DC-493B-805C-F213B7C4A17D" sourceRef="A"
targetRef="shareniu-B"></sequenceFlow>
<endEvent id="sid-4AC81F5B-49F8-4135-B68E-1C182D004080"></endEvent>
<sequenceFlow
id="sid-DF962BB4-EA4C-4D86-9A91-214EB2EC1A47" sourceRef="shareniu-B"
targetRef="sid-4AC81F5B-49F8-4135-B68E-1C182D004080"></sequenceFlow>
</process>
配置描述
<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.25}</completionCondition>
nrOfCompletedInstances、nrOfInstances 变量描述上面已经描述了,我们这里设置的条件是大于1/4的人完成任务,任务就结束了。下面我们代码部署流程,启动流程后进行测试:
ACT_RU_TASK表如下:
我们随便结束一个任务,看一下ACT_RU_TASK表变化。
@Test
public void complete() {
String taskId="40003";
taskService.complete(taskId);
}
执行上面的代码,我们很神奇的发现,ACT_RU_TASK表中的其他任务没有了,因为我们配置了4个人,通过条件是1/4,所以任意一个人结束了,流程就结束了。这里我们测试的是并行,串行也是一样的,读者可以自行测试验证。
动态的配置
上面的几种方式,我们定义xml的时候,循环的次数是固定写在xml中的,也就是说我们配置的是循环2次,那么所有的流程实例都是循环2次,这样就不灵活了,程序当然是灵活了比较好,所以在实际开发中,我们可以使用下面的这种方式操作,使程序更加的灵活。
程序的xml配置如下所示:
<process id="multiInstance" name="multiInstance"
isExecutable="true">
<documentation>multiInstance</documentation>
<startEvent id="startEvent1"></startEvent>
<userTask id="A" name="多实例测试" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics
isSequential="false" flowable:collection="assigneeList"
flowable:elementVariable="assignee">
<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.25}
</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
<sequenceFlow
id="sid-6D482A8A-F796-4E2B-AB2C-ABBFD2DA428D" sourceRef="startEvent1"
targetRef="A"></sequenceFlow>
<userTask id="shareniu-B" name="shareniu-B"
xmlns:shareniu="http://www.shareniu.com/" shareniu:shareniuext=""
shareniu:shareniuextboolen="false"></userTask>
<sequenceFlow
id="sid-BA8FC337-40DC-493B-805C-F213B7C4A17D" sourceRef="A"
targetRef="shareniu-B"></sequenceFlow>
<endEvent id="sid-4AC81F5B-49F8-4135-B68E-1C182D004080"></endEvent>
<sequenceFlow
id="sid-DF962BB4-EA4C-4D86-9A91-214EB2EC1A47" sourceRef="shareniu-B"
targetRef="sid-4AC81F5B-49F8-4135-B68E-1C182D004080"></sequenceFlow>
</process>
动态配置如下所示:
<userTask id="usertask1" name="多实例任务" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="false" activiti:collection="assigneeList" activiti:elementVariable="assignee">
<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.25}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
参数说明:
-
activiti:assignee="${assignee}"
-
activiti:elementVariable="assignee" 多实例任务依赖上面的配置${assignee}
-
activiti:collection="assigneeList"
三个参数结合决定了,当前节点的处理人来自assigneeList集合,注意这里是集合信息而不是字符串,所以程序的运行时候变量的赋值,如下所示:
@Test
public void startProcessInstanceByKey() {
Map<String, Object> vars = new HashMap<>();
String[] v = { "shareniu1", "shareniu2", "shareniu3", "shareniu4" };
vars.put("assigneeList", Arrays.asList(v));
String processDefinitionKey = "multiInstance";
ProcessInstance startProcessInstanceByKey = runtimeService.startProcessInstanceByKey(processDefinitionKey,
vars);
// Sys
ok了,测试一下,确实程序如预期的所示,大功告成。
总结
参数的使用总结
-
flowable:candidateUsers="shareniu1,shareniu2,shareniu3,shareniu4" 这个节点可以4个人审核。
-
loopCardinality>2</loopCardinality>
循环2次结束。 -
<multiInstanceLoopCharacteristics isSequential="true">
串行并行的配置。<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.25}</completionCondition>
完成条件的配置。
这里我们还可以得出一个结论:
如果使用串行方式操作nrOfActiveInstances 变量始终是1,因为并行的时候才会去+1操作。
遗留点
上面的程序已经解决了常用的问题,关于会签、加签、减签、退签、权重配置、自定义通过条件配置(条件自定义通过)
这些问题,这个章节还没有具体的实现,关于这些问题,由于本章内容有限,我们就后续章节讲解吧。