Fork me on GitHub

Activiti(4)--多实例实现会签功能

1. Activiti 多实例

多实例节点是在业务流程中定义重复环节的一种方式

从开发角度讲, 多实例类似于循环, 可以根据给定的集合, 为每个元素执行一个环境甚至一个子流程, 既可以顺序依次执行也可以并发同步执行.

多实例是在一个普通节点上添加额外的属性定义, 这样被多实例修饰的节点就会执行多次, 在 BPMN 规范中, 下面的节点都可以成为一个多实例节点:

  • UserTask
  • Script Task
  • Java Service Task
  • Web Service Task
  • Business Rule Task
  • Email Task
  • Manual Task
  • Receive Task
  • Sub-Process
  • Call Activity

每个上级流程为每个实例创建分支的时候都要提供如下变量:

  1. nrOfInstances: 实例总数
  2. nrOfActiveInstances: 当前活动的实例数量, 对于顺序执行的多实例, 该值始终为1
  3. nrOfCompletedInstances: 已经完成的实例数量
  4. loopCounter: 当前实例所在循环的索引值, 其他实例不可见, 不会保存到流程实例级别.

可以通过 execution.getVariable(String key) 方法获得这些变量

1.1. isSequential

表示节点时顺序执行还是并行执行, 默认为 false, 表示并行执行

1.2. 指定实例数量

实例的数量会在进入节点时进行计算, 但也可以直接指定

1
2
3
4
5
6
<multiInstanceLoopCharacteristics isSequential="false">
<!-- 可以使用loopCardinality子元素直接指定一个数字 -->
<loopCardinality>5</loopCardinality>
<!-- 也可以使用结果为整数的表达式 -->
<loopCardinality>${nrOfOrders-nrOfCancellations}</loopCardinality>
</multiInstanceLoopCharacteristics>

1.3. 接收并遍历集合

除此之外还可以通过 loopDataInputRef 元素设置一个类型为集合的流程变量名, 对于集合中的每个元素都会创建一个实例, 也可以通过 inputDataItem 子元素指定集合

1
2
3
4
5
6
<userTask id="someTask" name="Activiti is awesome!" activiti:assignee="${user}">
<multiInstanceLoopCharacteristics isSequential="false">
<loopDataInputRef>userList</loopDataInputRef>
<inputDataItem name="user"/>
</multiInstanceLoopCharacteristics>
</userTask>
  • loopDataInputRef 中的 userList 表示需要遍历的元素列表
  • inputDataItem 中的 user 表示每个分支都会拥有一个名为 user 的流程变量, 这个变量会包含集合中的对应元素, 在例子中用来设置用户任务的办理者, 也就是说 userTask 中的 activiti:assignee 属性的值需要和 inputDataItem 一致.

此外, 上述的变量名存在如下缺点:

  1. 名称复杂
  2. BPMN2.0 规定不能该节点不能包含表达式

Activiti 通过 multiInstanceCharacteristics 中设置 collectionelementVariable 属性来解决这个问题:

1
2
3
<userTask id="someTask" name="Activiti is awesome!" activiti:assignee="${user}">
<multiInstanceLoopCharacteristics isSequential="false" activiti:collection="${userList}" activiti:elementVariable="user"/>
</userTask>

二者实现的功能是相同的, 不过后者可以支持表达式, 这是我们动态配置用户任务属性的重要功能

1.4. 结束条件

多实例节点默认会在所有节点完成后结束, 也可以指定一个表达式在每个实例结束时执行, 如果表达式返回 true, 所有其他的实例都会销毁, 多实例节点也会结束, 流程会继续执行.

1
2
3
4
5
6
<userTask id="someTask" name="Activiti is awesome!" activiti:assignee="${user}">
<multiInstanceLoopCharacteristics isSequential="false" activiti:collection="${userList}" activiti:elementVariable="user">
<!-- 如果有 60% 的任务完成时, 其他任务会被删除, 流程继续进行 -->
<completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>

2. 会签逻辑

经过以上对 Activiti 多实例的介绍可知, 实现会签功能几个重要的点在于:

  1. 利用多实例完成动态实例的创建
  2. 根据业务设置合适的结束条件

2.1. 流程定义

2.1.1. XML 格式

1
2
3
4
5
6
7
8
9
<process id="my-process">
<startEvent id="start"/>
<sequenceFlow id="flow1" sourceRef="start" targetRef="someTask" />
<userTask id="someTask" name="Activiti is awesome!" activiti:assignee="${user}">
<multiInstanceLoopCharacteristics isSequential="false" activiti:collection="${userList}" activiti:elementVariable="user"/>
</userTask>
<sequenceFlow id="flow2" sourceRef="someTask" targetRef="end"/>
<endEvent id="end" />
</process>

2.1.2. BpmnModel 模型

用到的测试类:

1
2
3
4
5
6
7
8
9
10
@Slf4j
@Component
public class UsersBean implements Serializable {

public List<String> getUsers(String userId) {
log.info("userId: {}", userId);
return Arrays.asList(userId + "1", userId + "2", userId + "3");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Test
public void testExclusionGatewayModel() {
BpmnModel bpmnModel = new BpmnModel();
Process process = new Process();
process.setId("my-process");
StartEvent startEvent = new StartEvent();
startEvent.setId("startEvent");
UserTask someTask = new UserTask();
someTask.setId("someTask");
someTask.setName("Activiti is awesome!");
someTask.setAssignee("${user}");
MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = new MultiInstanceLoopCharacteristics();
multiInstanceLoopCharacteristics.setSequential(false);
multiInstanceLoopCharacteristics.setInputDataItem("${usersBean.getUsers(name)}");
multiInstanceLoopCharacteristics.setElementVariable("user");
multiInstanceLoopCharacteristics.setCompletionCondition("${nrOfCompletedInstances > 0}");
someTask.setLoopCharacteristics(multiInstanceLoopCharacteristics);
EndEvent endEvent = new EndEvent();
endEvent.setId("endEvent");
SequenceFlow flow1 = createSequence("startEvent", "someTask", "flow1", "flow1", null);
SequenceFlow flow2 = createSequence("someTask", "endEvent", "flow2", "flow2", null);
process.addFlowElement(startEvent);
process.addFlowElement(someTask);
process.addFlowElement(endEvent);
process.addFlowElement(flow1);
process.addFlowElement(flow2);
bpmnModel.addProcess(process);


// client
Deployment deployment = activitiRule.getRepositoryService().createDeployment()
.addBpmnModel("bpmn", bpmnModel)
.deploy();
log.info("deployment: {}", ToStringBuilder.reflectionToString(deployment, ToStringStyle.JSON_STYLE));
Map<String, Object> map = new HashMap<>();
map.put("usersBean", usersBean);
map.put("name", "wk");

ProcessInstance processInstance = activitiRule.getRuntimeService().startProcessInstanceByKey("my-process", map);
log.info("processInstance: {}", ToStringBuilder.reflectionToString(processInstance, ToStringStyle.JSON_STYLE));
List<Task> taskList = activitiRule.getTaskService().createTaskQuery().list();
log.info("当前 taskList 数量: {}", taskList.size());
for (Task task : taskList) {
log.info("task: {}", ToStringBuilder.reflectionToString(task, ToStringStyle.JSON_STYLE));
}
activitiRule.getTaskService().complete(taskList.get(0).getId());
log.info("其中一个节点完成审批");
taskList = activitiRule.getTaskService().createTaskQuery().list();
log.info("第一个节点审批完成后 taskList 数量: {}", taskList.size());
for (Task task : taskList) {
log.info("第一个节点审批完成后 task: {}", ToStringBuilder.reflectionToString(task, ToStringStyle.JSON_STYLE));
}
}

2.1.3. 日志输出

09:55:17,517 [main] INFO  org.destiny.activiti.GatewayExpressSpringTest  - processInstance: {"currentFlowElement":null,"currentActivitiListener":null,"processInstance":"ProcessInstance[4]","parent":null,"executions":[Multi instance root execution[ id '8' ] - activity 'someTask - parent '4'],"superExecution":null,"subProcessInstance":null,"tenantId":"","name":null,"description":null,"localizedName":null,"localizedDescription":null,"lockTime":null,"isActive":true,"isScope":true,"isConcurrent":false,"isEnded":false,"isEventScope":false,"isMultiInstanceRoot":false,"isCountEnabled":false,"eventName":null,"eventSubscriptions":[],"jobs":[],"timerJobs":[],"tasks":[],"identityLinks":[IdentityLinkEntity[id=24, type=participant, userId=wk1, processInstanceId=4], IdentityLinkEntity[id=27, type=participant, userId=wk2, processInstanceId=4], IdentityLinkEntity[id=30, type=participant, userId=wk3, processInstanceId=4]],"deleteReason":null,"suspensionState":1,"startUserId":null,"startTime":"Sun Dec 16 21:55:17 CST 2018","eventSubscriptionCount":0,"taskCount":0,"jobCount":0,"timerJobCount":0,"suspendedJobCount":0,"deadLetterJobCount":0,"variableCount":0,"identityLinkCount":0,"processDefinitionId":"my-process:1:3","processDefinitionKey":"my-process","processDefinitionName":null,"processDefinitionVersion":1,"deploymentId":null,"activityId":null,"activityName":null,"processInstanceId":"4","businessKey":null,"parentId":null,"superExecutionId":null,"rootProcessInstanceId":"4","rootProcessInstance":null,"forcedUpdate":false,"queryVariables":null,"isDeleted":false,"variableInstances":{usersBean=VariableInstanceEntity[id=6, name=usersBean, type=serializable, byteArrayValueId=5]},"usedVariablesCache":{},"transientVariabes":null,"cachedElContext":null,"id":"4","revision":1,"isInserted":true,"isUpdated":false,"isDeleted":false}
09:59:03,449 [main] INFO  org.destiny.activiti.GatewayExpressSpringTest  - 当前 taskList 数量: 3
09:59:03,450 [main] INFO  org.destiny.activiti.GatewayExpressSpringTest  - task: {"owner":null,"assigneeUpdatedCount":1,"originalAssignee":null,"assignee":"wk1","delegationState":null,"parentTaskId":null,"name":"Activiti is awesome!","localizedName":null,"description":null,"localizedDescription":null,"priority":50,"createTime":"Sun Dec 16 21:59:03 CST 2018","dueDate":null,"suspensionState":1,"category":null,"isIdentityLinksInitialized":false,"taskIdentityLinkEntities":[],"executionId":"14","execution":null,"processInstanceId":"4","processInstance":null,"processDefinitionId":"my-process:1:3","taskDefinitionKey":"someTask","formKey":null,"isDeleted":false,"isCanceled":false,"eventName":null,"currentActivitiListener":null,"tenantId":"","queryVariables":null,"forcedUpdate":false,"claimTime":null,"variableInstances":null,"usedVariablesCache":{},"transientVariabes":null,"cachedElContext":null,"id":"24","revision":1,"isInserted":false,"isUpdated":false,"isDeleted":false}
09:59:03,450 [main] INFO  org.destiny.activiti.GatewayExpressSpringTest  - task: {"owner":null,"assigneeUpdatedCount":1,"originalAssignee":null,"assignee":"wk2","delegationState":null,"parentTaskId":null,"name":"Activiti is awesome!","localizedName":null,"description":null,"localizedDescription":null,"priority":50,"createTime":"Sun Dec 16 21:59:03 CST 2018","dueDate":null,"suspensionState":1,"category":null,"isIdentityLinksInitialized":false,"taskIdentityLinkEntities":[],"executionId":"15","execution":null,"processInstanceId":"4","processInstance":null,"processDefinitionId":"my-process:1:3","taskDefinitionKey":"someTask","formKey":null,"isDeleted":false,"isCanceled":false,"eventName":null,"currentActivitiListener":null,"tenantId":"","queryVariables":null,"forcedUpdate":false,"claimTime":null,"variableInstances":null,"usedVariablesCache":{},"transientVariabes":null,"cachedElContext":null,"id":"27","revision":1,"isInserted":false,"isUpdated":false,"isDeleted":false}
09:59:03,451 [main] INFO  org.destiny.activiti.GatewayExpressSpringTest  - task: {"owner":null,"assigneeUpdatedCount":1,"originalAssignee":null,"assignee":"wk3","delegationState":null,"parentTaskId":null,"name":"Activiti is awesome!","localizedName":null,"description":null,"localizedDescription":null,"priority":50,"createTime":"Sun Dec 16 21:59:03 CST 2018","dueDate":null,"suspensionState":1,"category":null,"isIdentityLinksInitialized":false,"taskIdentityLinkEntities":[],"executionId":"16","execution":null,"processInstanceId":"4","processInstance":null,"processDefinitionId":"my-process:1:3","taskDefinitionKey":"someTask","formKey":null,"isDeleted":false,"isCanceled":false,"eventName":null,"currentActivitiListener":null,"tenantId":"","queryVariables":null,"forcedUpdate":false,"claimTime":null,"variableInstances":null,"usedVariablesCache":{},"transientVariabes":null,"cachedElContext":null,"id":"30","revision":1,"isInserted":false,"isUpdated":false,"isDeleted":false}
...
09:55:17,585 [main] INFO  org.destiny.activiti.GatewayExpressSpringTest  - 其中一个节点完成审批
09:55:17,587 [main] INFO  org.destiny.activiti.GatewayExpressSpringTest  - 第一个节点审批完成后 taskList 数量: 0

3. 多实例任务节点完成自定义条件

>