目录
多实例活动
是为业务流程中的某个步骤定义重复的一种方式。在编程概念中,多实例与 for each
结构相匹配:它允许对给定集合中的每个项目按顺序或并行地执行某个步骤或甚至一个完整的子流程。
多实例是一个有额外属性(所谓的 “多实例特性”)的常规活动,它将导致该活动在运行时被多次执行。以下活动可以成为多实例活动。
Service Task 服务任务
Send Task 发送任务
User Task 用户任务
Business Rule Task 业务规则任务
Script Task 脚本任务
Receive Task 接收任务
Manual Task 手动任务
(Embedded) Sub-Process (嵌入)子流程
Call Activity 发起活动
Transaction Subprocess 事务子流程
网关或事件不能成为多实例。
如果一个活动是多实例的,这将由活动底部的三条短线表示。三条垂直线表示实例将以并行方式执行,而三条水平线表示顺序执行。
按照规范的要求,每个实例所创建的执行的每个父执行将有以下变量:
nrOfInstances: 实例的总数量
nrOfActiveInstances: 当前活动的,即尚未完成的实例的数量。对于一个连续的多实例,这将永远是1。
nrOfCompletedInstances: 已经完成的实例的数量。
这些值可以通过调用 “execution.getVariable(x) “方法检索。
此外,每个创建的执行将有一个执行本地变量(即对其他执行不可见,也不存储在流程实例级别)。
loopCounter: 表示该特定实例的for each
循环中的索引
为了使一个活动成为多实例,活动xml元素必须有一个multiInstanceLoopCharacteristics
子元素。
<multiInstanceLoopCharacteristics isSequential="false|true">
...
</multiInstanceLoopCharacteristics>
isSequential
属性表示该活动的实例是按顺序执行还是并行执行。
多实例应用中我们需要指的具体生成几个实例任务。指派的方式可以通过loopCardinality
属性来指的。通过loopCardinality
来指定既可以是固定值也可以指定表达式(只要结果是整数即可)
<multiInstanceLoopCharacteristics isSequential="false">
<loopCardinality>3</loopCardinality>
</multiInstanceLoopCharacteristics>
或者
<multiInstanceLoopCharacteristics isSequential="false">
<loopCardinality>${num}</loopCardinality>
</multiInstanceLoopCharacteristics>
部署启动:
/**
* Deploy
*/
@Test
void testDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("multiInstance-demo1.bpmn20.xml")
.name("multiInstance-demo1")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* start process
*/
@Test
void startFlow() {
runtimeService.startProcessInstanceById("multiInstance-demo1:1:85c57d01-a718-11ee-b365-1a473d673661");
}
我们先审批一个任务:
/**
* 审批
*/
@Test
public void completeTask() {
taskService.complete("c92542cb-a718-11ee-bb4d-1a473d673661");
}
审批结束task表还剩两个任务。
我们依次都审批一下:
现在task表进行到了“多实例-串行”任务
我们审批一下:
审批结束后,发现走到了“多实例-串行”的第二个任务。
我们依次将第二和第三任务都审批完成,最后流程结束。
部署并启动:
/**
* Deploy
*/
@Test
void testDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("multiInstance-demo2.bpmn20.xml")
.name("multiInstance-demo2")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* start process
*/
@Test
void startFlow() {
Map<String,Object> map = new HashMap<>();
map.put("users", Arrays.asList("张三","李四","王五"));
runtimeService.startProcessInstanceById("multiInstance-demo2:1:9e822c93-a774-11ee-92a1-1a473d673661",map);
}
发现有三个并行的用户任务。
上面的例子我们一旦设置了3个用户并行节点,那就必须3个都审批才能流程结束,那怎么才能灵活点呢?比如3个节点里有1个用户审批就算流程结束。
${nrOfCompletedInstances/nrOfInstances >= 0.5 }
这句代码的意思是:审批完成数量如果大于等于0.5,就流程结束。我们设置张三、李四、王五三个用户并行节点,其实只要张三和李四审批通过,流程就结束了,因为3分之2大于0.5。
部署并启动:
/**
* Deploy
*/
@Test
void testDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("multiInstance-demo3.bpmn20.xml")
.name("multiInstance-demo3")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* start process
*/
@Test
void startFlow() {
Map<String,Object> map = new HashMap<>();
map.put("users", Arrays.asList("张三","李四","王五"));
runtimeService.startProcessInstanceById("multiInstance-demo3:1:47da4ebf-a777-11ee-b7e9-1a473d673661",map);
}
我们将其中两个审批:
/**
* 审批
*/
@Test
public void completeTask() {
taskService.complete("a3b2a811-a779-11ee-92e7-1a473d673661");
}
三个用户任务两个审批通过流程就结束了。
创建Java类:
/**
* 动态处理会签 完成条件
*/
@Component("multiInstanceDelegate")
public class MultiInstanceDelegate {
public boolean completeInstanceTask(DelegateExecution execution) {
// 获取当前多实例中的相关的参数
// 总得流程实例数量
Integer nrOfInstances = (Integer) execution.getVariable("nrOfInstances");
// 当前活跃的实例数量【没有审批的数量】
Integer nrOfActiveInstances = (Integer) execution.getVariable("nrOfActiveInstances");
// 当前已经审批的数量
Integer nrOfCompletedInstances = (Integer) execution.getVariable("nrOfCompletedInstances");
System.out.println("nrOfInstances = " + nrOfInstances);
System.out.println("nrOfActiveInstances = " + nrOfActiveInstances);
System.out.println("nrOfCompletedInstances = " + nrOfCompletedInstances);
return nrOfCompletedInstances > nrOfActiveInstances;
}
}
在xml中绑定类:
部署并启动:
/**
* Deploy
*/
@Test
void testDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("multiInstance-demo4.bpmn20.xml")
.name("multiInstance-demo4")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* start process
*/
@Test
void startFlow() {
Map<String,Object> map = new HashMap<>();
map.put("users", Arrays.asList("张三","李四","王五"));
runtimeService.startProcessInstanceById("multiInstance-demo3:2:1b63c2dd-a77c-11ee-b233-1a473d673661",map);
}
我们审批两个:
上面的案例都是在用户任务
中实现。我们也可以在Service Task
来实现。具体如下:
部署并启动:
/**
* Deploy
*/
@Test
void testDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("multiInstance-demo5.bpmn20.xml")
.name("multiInstance-demo5")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* start process
*/
@Test
void startFlow() {
runtimeService.startProcessInstanceById("multiInstance-demo5:1:917cf491-a77d-11ee-be15-1a473d673661");
}
将此用户任务审批:
/**
* 审批
*/
@Test
public void completeTask() {
taskService.complete("a90a0247-a77d-11ee-b929-1a473d673661");
}
多实例的场景也可以在子流程中来使用,具体我们通过案例来讲解。本质上和我们前面介绍的是差不多的。
部署并启动:
/**
* Deploy
*/
@Test
void testDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("multiInstance-demo6.bpmn20.xml")
.name("multiInstance-demo6")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* start process
*/
@Test
void startFlow() {
runtimeService.startProcessInstanceById("multiInstance-demo6:1:ad738209-a77e-11ee-a6e4-1a473d673661");
}
审批一下用户任务1:
结果出现了并行的两个子流程。
在实际的工作流审批中我们肯定 需要携带相关的数据的:
业务数据--流程实例绑定业务主键
表单数据--动态表单动态绑定
涉及到业务主键操作的方法:
// 在启动流程实例的时候绑定业务主键 1001231是我们自己随便设置的
runtimeService.startProcessInstanceById(processId,"1001231");
// 也可以通过runtimeService来动态的更新
runtimeService.updateBusinessKey();
// 通过 ProcessInstance 来获取对应的业务主键
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId("ffe242db-9720-11ee-80a7-c03c59ad2248")
.singleResult();
String businessKey = processInstance.getBusinessKey();
为了更好的介绍表单的内容我们先把FlowableUi的数据持久化到MySQL数据库中。我们来看看应该要如何来修改。先找到flowable-default.properties
这个属性文件。来修改数据库的配置信息,webapps\flowable-ui\WEB-INF\classes
在这个目录下
然后我们还需要添加MySQL的驱动到lib
目录中:
然后重启FlowableUI服务即可。
数据库有87张表。
我们拿一个文本、一个多行文本、一个日期:
分别对这三个组件编辑:
我们看数据库:
我们创建的表单在act_de_model表中。
其中model_editor_json字段是我们的表单组件json。
通过FlowableUI我们创建的Form表单,然后我们来看看应该要如何来部署表单,以及和流程关联后做流程操作以及相关的流程数据的查询出来。我们先通过单元测试来看看
部署这块我们需要通过FormRepositoryService
来处理。
我们现在resources下创建.form文件:
然后将我们刚才绘制的表单数据,act_de_model表中的model_editor_json字段值都复制到此文件中。
部署:
/**
* 1.部署流程
* 2.部署表单
* 3.启动带有表单的流程-->创建了对应的流程实例
*/
@Test
public void deployFormFlow(){
FormDeployment deploy = formRepositoryService.createDeployment()
.addClasspathResource("first.form")
.name("报销表单")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
}
表单部署会涉及到的几张表结构。
表名 | 作用 |
---|---|
act_fo_form_definition | Form表单定义表 |
act_fo_form_deployment | Form表单部署表 |
act_fo_form_resource | Form表单资源表 |
其实我们还有另一种方式来部署,采用addString方法:
@Autowired
FormRepositoryService formRepositoryService;
/**
* 1.部署流程
* 2.部署表单
* 3.启动带有表单的流程-->创建了对应的流程实例
*/
@Test
public void deployFormFlow(){
// 1.获取需要部署的form文件
String json = "{\"name\":\"报销流程表单\",\"key\":\"expenseAccountForm\",\"version\":0,\"fields\":[{\"fieldType\":\"FormField\",\"id\":\"amount\",\"name\":\"报销金额\",\"type\":\"integer\",\"value\":null,\"required\":true,\"readOnly\":false,\"overrideId\":true,\"placeholder\":\"0\",\"layout\":null},{\"fieldType\":\"FormField\",\"id\":\"reason\",\"name\":\"报销原因\",\"type\":\"text\",\"value\":null,\"required\":false,\"readOnly\":false,\"overrideId\":true,\"placeholder\":null,\"layout\":null},{\"fieldType\":\"FormField\",\"id\":\"expenseDate\",\"name\":\"报销日期\",\"type\":\"date\",\"value\":null,\"required\":false,\"readOnly\":false,\"overrideId\":true,\"placeholder\":null,\"layout\":null}],\"outcomes\":[]}";
FormDeployment deploy = formRepositoryService.createDeployment()
.addString("报销流程表单.form", json)
.name("报销表单")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
}
这里我们就直接从数据库中把表单的JSON数据拷贝出来存储在String类型中,然后通过formRepositoryService
来实现部署操作,此次需要注意如果通过非xxx.form
文件的方式部署,我们添加ResourceName
资源名称的时候,必须要加上.form
后缀,不然部署会失败。
然后我们来看看在具体流程
中是如何和表单关联起来的。先来看看在表单起始的时候就绑定。
然后保存流程即可,注意保存成功后在act_de_relation
中会生成一条流程
和表单
的对应管理,对应的在act_de_model
中会生成一条刚刚创建的流程信息如下:
对应的对应关系:
部署并启动:
/**
* Deploy
*/
@Test
void testDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("报销流程.bpmn20.xml")
.name("报销流程")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* 启动流程
*/
@Test
public void startFlowWithForm() {
Map<String,Object> map = new HashMap<>();
map.put("amount",10000);
map.put("reason","维护客户关系");
map.put("bxDate","2023-05-06");
runtimeService.startProcessInstanceWithForm("bxtask:1:eaca369b-a7af-11ee-9cdc-1a473d673661",null,map,"报销流程");
}
/**
* 获取流程绑定的表单数据
*/
@Test
public void getTaskFormInfo() {
// 流程定义ID 对应task表PROC_DEF_ID_字段
String proDefId = "bxtask:1:eaca369b-a7af-11ee-9cdc-1a473d673661";
// 流程实例ID 对应task表PROC_INST_ID_字段
String proInsId = "0967c741-a7b1-11ee-a085-1a473d673661";
FormInfo startFormModel = runtimeService.getStartFormModel(proDefId, proInsId);
System.out.println("startFormModel.getKey() = " + startFormModel.getKey());
System.out.println("startFormModel.getName() = " + startFormModel.getName());
System.out.println("startFormModel.getDescription() = " + startFormModel.getDescription());
// 获取表单对应的数据
SimpleFormModel formModel = (SimpleFormModel) startFormModel.getFormModel();
List<FormField> fields = formModel.getFields();
for (FormField field: fields) {
System.out.println("field.getId() = " + field.getId());
System.out.println("field.getName() = " + field.getName());
System.out.println("field.getValue() = " + field.getValue());
}
}
单个节点绑定其实就是在用户任务
上关联Form表单
,具体如下:
部署并启动:
/**
* Deploy
*/
@Test
void testDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("form-demo2.bpmn20.xml")
.name("form-demo2")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* start process
*/
@Test
void startFlow() {
Map<String,Object> map = new HashMap<>();
map.put("amount",999);
map.put("reason","我想报销");
map.put("bxDate","2023-05-06");
runtimeService.startProcessInstanceById("form-demo2:1:7fa62223-a7b4-11ee-8ab4-1a473d673661",map);
}
当我们再调用如下方法查询表单对应的数据时:
/**
* 获取流程绑定的表单数据
*/
@Test
public void getTaskFormInfo() {
// 流程定义ID 对应task表PROC_DEF_ID_字段
String proDefId = "form-demo2:1:7fa62223-a7b4-11ee-8ab4-1a473d673661";
// 流程实例ID 对应task表PROC_INST_ID_字段
String proInsId = "c611efbb-a7b4-11ee-a580-1a473d673661";
FormInfo startFormModel = runtimeService.getStartFormModel(proDefId, proInsId);
System.out.println("startFormModel.getKey() = " + startFormModel.getKey());
System.out.println("startFormModel.getName() = " + startFormModel.getName());
System.out.println("startFormModel.getDescription() = " + startFormModel.getDescription());
// 获取表单对应的数据
SimpleFormModel formModel = (SimpleFormModel) startFormModel.getFormModel();
List<FormField> fields = formModel.getFields();
for (FormField field: fields) {
System.out.println("field.getId() = " + field.getId());
System.out.println("field.getName() = " + field.getName());
System.out.println("field.getValue() = " + field.getValue());
}
}
我们将代码改为:
/**
* 获取绑定在单独节点上的表单数据
*/
@Test
public void getTaskFormNodeInfo() {
// 参数为task表的主键ID
FormInfo taskFormModel = taskService.getTaskFormModel("c6180a43-a7b4-11ee-a580-1a473d673661");
System.out.println("startFormModel.getKey() = " + taskFormModel.getKey());
System.out.println("startFormModel.getName() = " + taskFormModel.getName());
System.out.println("startFormModel.getDescription() = " + taskFormModel.getDescription());
// 获取表单对应的数据
SimpleFormModel formModel = (SimpleFormModel) taskFormModel.getFormModel();
List<FormField> fields = formModel.getFields();
for (FormField field: fields) {
System.out.println("field.getId() = " + field.getId());
System.out.println("field.getName() = " + field.getName());
System.out.println("field.getValue() = " + field.getValue());
}
}
我们将用户任务1审批,现在应该到了用户任务2:
我们用用户任务2的ID再次查询表单数据。
因为我们只在用户任务1节点上绑定了表单。
outcome
用来指定表单审批对应的结果。表单审批完成后根据outcome
的信息会在act_ru_variable
中会生成一个form_表单ID_outcome
这样一个流程变量,那么我们就可以根据这个流程变量来对应的路由到相关的流程中。比如:form_formbx2_outcome
注意:表单ID我们不用使用 -
拼接。
我们先在表单上定义两个结果,分别是接受和拒绝:
全部开始节点绑定表单:
排他网关两侧设置条件:
先部署表单:
/**
* 1.部署流程
* 2.部署表单
* 3.启动带有表单的流程-->创建了对应的流程实例
*/
@Test
public void deployFormFlow(){
FormDeployment deploy = formRepositoryService.createDeployment()
.addClasspathResource("first.form")
.name("报销表单")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
}
部署并启动流程:
/**
* Deploy
*/
@Test
void testDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("form-demo3.bpmn20.xml")
.name("form-demo3")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* 启动流程
*/
@Test
public void startFlowWithForm() {
Map<String,Object> map = new HashMap<>();
map.put("amount",666);
map.put("reason","我要报销");
map.put("bxDate","2023-12-31");
runtimeService.startProcessInstanceWithForm("form-demo3:1:3c1352cf-a7c1-11ee-a135-1a473d673661",null,map,"报销流程");
}
我们先从最简单的串行流程来分析,案例如下:
部署并启动:
/**
* 部署流程
*/
@Test
void backDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("back-demo1.bpmn20.xml")
.name("back-demo1")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* 启动流程
*/
@Test
public void startBackFlow() {
runtimeService.startProcessInstanceById("back-demo1:1:1f66c13f-a7de-11ee-b6b2-1a473d673661");
}
审批一直到用户4:
/**
* 审批
*/
@Test
public void completeBackTask() {
taskService.complete("5ca9e3c9-a7de-11ee-a405-1a473d673661");
}
任务回退,从用户4退回到用户3:
/**
* 任务回退
*/
@Test
public void backFlow() {
runtimeService.createChangeActivityStateBuilder().
processInstanceId("5ca5c514-a7de-11ee-a405-1a473d673661")
//第一个参数:后面的ID,这里是用户4 第二个参数:前面的ID,这里是用户3
.moveActivityIdTo("sid-E1666002-4CD3-49CD-9008-19A90600C1E0","sid-89C7AE8A-A433-47AA-A76C-A16EB6AB9B9A").changeState();
}
注意:moveActivityIdTo方法的参数是xml文件里<userTask>的id属性值。
当然,moveActivityIdTo方法也不光只能从后往前回退,也可以从前跳到后面某节点。
接下来我们在并行的场景中来看看各种回退的场景。具体案例流程如下:
部署并启动:
/**
* 部署流程
*/
@Test
void backDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("back-demo2.bpmn20.xml")
.name("back-demo2")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* 启动流程
*/
@Test
public void startBackFlow() {
runtimeService.startProcessInstanceById("back-demo2:1:f0ec9dae-a7e2-11ee-b807-1a473d673661");
}
现在走到“用户审批”:
审批:
将行政副总和业务负责人两个节点同时退回到用户审批节点:
/**
* 任务回退
*/
@Test
public void backFlow() {
runtimeService.createChangeActivityStateBuilder().
processInstanceId("532fa4ac-a7e3-11ee-8e15-1a473d673661")
// 因为是并行网关,所以要将usertask3和usertask2同时退回到usertask1节点
.moveActivityIdsToSingleActivityId(Arrays.asList("usertask3","usertask2"),"usertask1").changeState();
}
我们重新多层审批,直到最后走到“总经理”节点:
我们现在从“总经理”节点回退到“业务副总”和“行政副总”节点:
/**
* 任务回退
*/
@Test
public void backFlow() {
runtimeService.createChangeActivityStateBuilder().
processInstanceId("532fa4ac-a7e3-11ee-8e15-1a473d673661")
.moveSingleActivityIdToActivityIds("usertask5",Arrays.asList("usertask2","usertask4"))
.changeState();
}
最后我们来看看带有子流程的场景下如果有回退的情况应该要如何来处理,案例如下:
部署并启动流程:
/**
* 部署流程
*/
@Test
void backDeploy() {
Deployment deploy = repositoryService.createDeployment()
.addClasspathResource("back-demo3.bpmn20.xml")
.name("back-demo3")
.deploy();
System.out.println("deploy.getId() = " + deploy.getId());
System.out.println("deploy.getName() = " + deploy.getName());
}
/**
* 启动流程
*/
@Test
public void startBackFlow() {
runtimeService.startProcessInstanceById("back-demo3:1:76eba5cb-a7e8-11ee-abd5-1a473d673661");
}
审批:
/**
* 审批
*/
@Test
public void completeBackTask() {
taskService.complete("b86015f1-a7e8-11ee-88e0-1a473d673661");
}
从子流程“用户2”回退到用户1:
/**
* 任务回退
*/
@Test
public void backFlow() {
runtimeService.createChangeActivityStateBuilder().
processInstanceId("b85826ac-a7e8-11ee-88e0-1a473d673661")
.moveActivityIdTo("user2","user1")
.changeState();
}
?流程的撤销一般是流程的发起人感觉没有必要再做流程审批的推进了。想要结束流程的审批操作。这个时候我们可以直接通过runtimeService.deleteProcessInstance()
方法来实现相关的流程撤销操作。
比如我们上面的并行案例中。进入到并行节点后想要结束:
部署并启动...
审批:
现在我们进行流程的撤销:
/**
* 流程撤销
*/
@Test
public void deleteProcessInstance(){
runtimeService.deleteProcessInstance("aedd050c-a7ea-11ee-af22-1a473d673661","我是删除原因");
}
刚才的流程已经没有了。