Flowable动态多实例任务:从原理到实战,解决流程中参与者不确定性问题
1. 项目概述:为什么需要动态多实例?
在流程引擎的实际应用中,我们经常会遇到一种场景:一个任务需要由一组人来处理,但这组人的数量在流程设计时是未知的,只有在流程运行到该节点时才能确定。比如,一个报销审批流程,需要所有项目组成员会签,但项目组成员名单是动态的,可能随着项目进展而变化。再比如,一个文档需要发送给多个部门的负责人审阅,而涉及的部门数量取决于文档的类型。如果你还在为每个可能的参与者数量创建多个相同的任务节点,或者写一堆复杂的网关逻辑来判断,那说明你还没用上Flowable的“动态多实例”这个利器。
动态多实例,顾名思义,就是多实例任务的参与者集合是动态生成的。它与静态多实例(在流程设计时就固定了参与者列表,如user1, user2, user3)形成鲜明对比。静态多实例适合参与者固定的场景,比如固定的评审委员会;而动态多实例则解决了业务流程中最大的不确定性之一——人的不确定性。掌握它,意味着你的流程模型能更好地贴合现实世界的复杂性和灵活性,从“僵硬的图纸”变成“有生命的有机体”。
2. 核心概念与运行机制拆解
在深入代码之前,我们必须先厘清几个核心概念,这是理解动态多实例如何工作的基石。
2.1 多实例与动态多实例的本质区别
很多人容易混淆这两个概念。简单来说:
- 多实例(Multi-Instance):是一种活动(通常是
UserTask)的行为特性。它允许一个活动在运行时创建多个并行的或串行的实例。每个实例都是独立的,拥有自己的执行上下文(如任务变量)。 - 静态多实例:在BPMN XML中,通过
multiInstanceLoopCharacteristics元素定义,并使用collection属性指定一个固定的列表值(如${assigneeList},但这个列表在流程启动时就必须确定)。 - 动态多实例:同样使用
multiInstanceLoopCharacteristics,但其collection属性指向一个流程变量,这个变量的值(一个集合)可以在流程运行到该节点时,通过前置监听器、服务任务或其他方式动态计算或修改。关键在于,这个集合的内容在流程设计时是未知的。
2.2 关键属性解析
在BPMN 2.0规范中,多实例活动通过multiInstanceLoopCharacteristics元素配置。对于动态多实例,以下几个属性至关重要:
isSequential: 布尔值。false表示并行多实例(所有实例同时创建,常见于会签);true表示串行多实例(一个接一个地完成,常见于逐级审批)。collection:这是实现动态性的核心。它的值是一个表达式(如${dynamicUserList}),该表达式在运行时被解析,必须得到一个java.util.Collection、java.util.Iterable或数组。Flowable会遍历这个集合,为每个元素创建一个任务实例。elementVariable: 为集合中的每个元素指定一个变量名。在任务实例中,你可以通过这个变量名访问当前迭代的元素。例如,elementVariable="assignee",那么在任务表达式中就可以用${assignee}来指代当前任务的办理人。completionCondition: 完成条件。这是一个可选但强大的属性,允许你定义在多实例任务完成前,需要满足的条件。例如,${nrOfCompletedInstances/nrOfInstances >= 0.5}表示超过一半的实例完成时,整个多实例活动就完成(“多数决”)。
2.3 内置变量与生命周期
Flowable为每个多实例活动自动管理一组内置变量,在表达式中可以直接使用:
nrOfInstances: 实例的总数(即collection集合的大小)。nrOfActiveInstances: 当前活跃(未完成)的实例数。nrOfCompletedInstances: 已完成的实例数。loopCounter: 当前正在执行的实例的索引(从0开始)。
理解生命周期很重要:当流程执行到达动态多实例节点时,引擎会:
- 计算
collection表达式的值,得到集合A。 - 根据集合A的大小
n,创建n个任务实例(并行)或准备创建第一个实例(串行)。 - 为每个实例设置
elementVariable。 - 每个实例独立运行,可以独立完成或驳回。
- 根据
completionCondition(如果存在)或所有实例的完成状态,判断整个多实例活动是否完成,然后推动流程继续。
3. 实战:从建模到代码的完整实现
理论讲完,我们进入实战环节。我将通过一个“项目组月度报告会签”的场景,展示从BPMN设计到Java代码实现的完整链路。
3.1 BPMN 2.0 XML 模型定义
首先,我们设计流程。关键是在multiInstanceLoopCharacteristics中,将collection指向一个运行时变量。
<?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:flowable="http://flowable.org/bpmn" targetNamespace="http://flowable.org/bpmn"> <process id="dynamic_multi_instance_process" name="动态多实例会签流程" isExecutable="true"> <startEvent id="startEvent1" /> <sequenceFlow id="flow1" sourceRef="startEvent1" targetRef="genDynamicListTask" /> <!-- 服务任务:动态生成会签人员列表 --> <serviceTask id="genDynamicListTask" name="生成动态会签列表" flowable:class="com.example.flowable.listener.GenerateDynamicAssigneeListener" /> <sequenceFlow id="flow2" sourceRef="genDynamicListTask" targetRef="dynamicMultiInstanceTask" /> <!-- 动态多实例用户任务 --> <userTask id="dynamicMultiInstanceTask" name="项目报告会签"> <!-- 这里是关键:定义多实例循环特性 --> <multiInstanceLoopCharacteristics isSequential="false" <!-- 并行会签 --> flowable:collection="dynamicAssigneeCollection" <!-- 指向流程变量 --> flowable:elementVariable="singleAssignee"> <!-- 每个实例中的元素变量名 --> <!-- 可选:完成条件,此处为全部完成 --> <!-- <completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition> --> </multiInstanceLoopCharacteristics> </userTask> <sequenceFlow id="flow3" sourceRef="dynamicMultiInstanceTask" targetRef="endEvent1" /> <endEvent id="endEvent1" /> </process> </definitions>关键点解析:
flowable:collection="dynamicAssigneeCollection":这告诉Flowable,去流程变量中查找名为dynamicAssigneeCollection的变量,并将其值作为集合进行遍历。这个变量将在服务任务genDynamicListTask中设置。flowable:elementVariable="singleAssignee":在创建的每个会签任务实例中,都会有一个名为singleAssignee的局部变量,其值就是集合中当前遍历到的元素(比如一个用户ID)。我们可以在任务分配时使用它:flowable:assignee="${singleAssignee}"。但注意,在上面的XML中,我们直接在multiInstanceLoopCharacteristics里定义了elementVariable,通常也需要在userTask上配置flowable:candidateUsers或flowable:assignee来引用它,这里为了清晰先省略,后面在Java代码中演示另一种设置方式。
3.2 Java服务任务:动态生成参与者集合
接下来,实现那个用于生成动态列表的服务任务。这里我们使用一个实现了JavaDelegate接口的类。
package com.example.flowable.listener; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.JavaDelegate; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; @Component("generateDynamicAssigneeListener") public class GenerateDynamicAssigneeListener implements JavaDelegate { @Override public void execute(DelegateExecution execution) { // 模拟从数据库、外部接口或根据业务逻辑动态获取参与者列表 // 例如:根据当前项目ID,查询该项目下的所有成员 String processInstanceId = execution.getProcessInstanceId(); // 假设这是动态查询的结果 List<String> dynamicAssignees = fetchDynamicAssigneesFromService(processInstanceId); // 将动态生成的列表设置为流程变量,变量名必须与BPMN中的collection属性值一致 execution.setVariable("dynamicAssigneeCollection", dynamicAssignees); // 可选:记录日志,便于调试 execution.setVariable("assigneeListSize", dynamicAssignees.size()); System.out.println("动态生成的会签人员列表: " + dynamicAssignees); } private List<String> fetchDynamicAssigneesFromService(String processInstanceId) { // 这里模拟业务逻辑:可能是根据流程变量、表单数据、RPC调用结果来生成列表 // 示例1:固定逻辑 // return Arrays.asList("zhangsan", "lisi", "wangwu", "zhaoliu"); // 示例2:基于流程变量决定 String projectType = (String) execution.getVariable("projectType"); if ("urgent".equals(projectType)) { return Arrays.asList("manager_li", "director_wang"); // 紧急项目只需经理和总监 } else { return Arrays.asList("zhangsan", "lisi", "wangwu", "zhaoliu", "sunqi"); // 普通项目全体成员 } // 示例3:从数据库查询(需要注入Repository) // return projectMemberRepository.findUserIdsByProcessInstanceId(processInstanceId); } }注意:
execution.setVariable设置的变量是流程变量,在整个流程实例生命周期内有效(除非被覆盖)。而elementVariable(如singleAssignee)是任务局部变量,只在其对应的单个任务实例中有效。
3.3 流程启动与任务查询
现在,我们来启动这个流程并观察动态多实例的创建。
@Autowired private RuntimeService runtimeService; @Autowired private TaskService taskService; public void startDynamicMultiInstanceProcess() { // 1. 设置初始流程变量(可选,用于影响动态列表的生成) Map<String, Object> variables = new HashMap<>(); variables.put("projectType", "normal"); // 普通项目 variables.put("projectId", "PROJ-2023-001"); // 2. 启动流程实例 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey( "dynamic_multi_instance_process", // 流程定义Key variables ); System.out.println("流程实例启动成功,ID: " + processInstance.getId()); // 3. 稍等片刻,让服务任务执行完毕,然后查询生成的多实例任务 // 在实际应用中,这里可能需要异步等待或使用事件监听器。 // 我们直接查询该流程实例下所有的任务 List<Task> tasks = taskService.createTaskQuery() .processInstanceId(processInstance.getId()) .list(); System.out.println("当前流程实例下的任务数量: " + tasks.size()); for (Task task : tasks) { System.out.println("任务ID: " + task.getId() + ", 任务名称: " + getName() + ", 办理人: " + getAssignee() + ", 创建时间: " + getCreateTime()); // 可以查看任务的本地位变量 // Map<String, Object> taskLocalVariables = taskService.getVariablesLocal(task.getId()); // System.out.println("任务局部变量: " + taskLocalVariables); } }执行这段代码,如果fetchDynamicAssigneesFromService返回5个用户,那么你将在控制台看到创建了5个独立的“项目报告会签”任务,每个任务的assignee(或candidateUser)分别对应列表中的一个用户。
3.4 另一种常见模式:通过任务监听器分配
有时,我们可能不想在BPMN XML中写死flowable:assignee="${singleAssignee}",而是希望更灵活地在代码中分配。这时,可以在userTask上添加一个任务创建监听器。
修改BPMN XML中的userTask部分:
<userTask id="dynamicMultiInstanceTask" name="项目报告会签"> <extensionElements> <flowable:taskListener event="create" class="com.example.flowable.listener.MultiInstanceTaskAssignmentListener"/> </extensionElements> <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="dynamicAssigneeCollection" flowable:elementVariable="singleAssignee"> </multiInstanceLoopCharacteristics> </userTask>然后实现监听器:
@Component("multiInstanceTaskAssignmentListener") public class MultiInstanceTaskAssignmentListener implements TaskListener { @Override public void notify(DelegateTask delegateTask) { // 从任务局部变量中获取当前迭代的元素(即集合中的单个办理人) String assignee = (String) delegateTask.getVariableLocal("singleAssignee"); // 设置任务的办理人 delegateTask.setAssignee(assignee); // 你还可以在这里做更多事情,比如根据assignee设置任务优先级、到期时间等 if ("manager_li".equals(assignee)) { delegateTask.setPriority(100); } System.out.println("动态多实例任务创建,分配办理人: " + assignee); } }这种方式将业务逻辑从XML移到了Java代码中,提供了更强的控制力。
4. 高级应用与避坑指南
掌握了基础用法后,我们来看看一些更高级的场景和实践中容易踩的坑。
4.1 动态修改运行中的多实例集合
这是一个经典需求:会签过程中,突然需要增加或减少一个参与者。Flowable提供了API支持。
public void modifyRunningMultiInstance(String processInstanceId, String activityId, List<String> newAssigneeList) { // 假设我们要修改的节点ID是 `dynamicMultiInstanceTask` // 1. 首先,需要找到该多实例活动的执行实例(代表整个多实例活动,而非单个任务) List<Execution> executions = runtimeService.createExecutionQuery() .processInstanceId(processInstanceId) .activityId(activityId) // 多实例活动的ID .list(); if (executions.isEmpty()) { throw new RuntimeException("未找到运行中的多实例活动: " + activityId); } // 通常只有一个执行代表这个多实例活动本身 Execution multiInstanceExecution = executions.get(0); // 2. 设置新的集合到该执行的变量中(注意变量作用域) runtimeService.setVariable(multiInstanceExecution.getId(), "dynamicAssigneeCollection", newAssigneeList); // 3. **重要**:手动触发多实例行为的重新计算。这通常需要调用内部命令。 // 更常见的做法是,通过“事件子流程”或“边界事件”来响应变化,或者设计流程时就考虑变更路径。 // 直接运行时修改集合是高级操作,可能需要结合 `RuntimeService` 的 `trigger` 方法或自定义命令。 // 对于增加参与者,可以创建新任务;对于减少,可以尝试删除未完成的任务实例。 // 此处演示一种思路(非完整代码): // - 查询当前该多实例活动下所有未完成的任务。 // - 对比新旧列表,找出需要新增的 assignee,为每个新增的 assignee 调用 `taskService.createTaskQuery()...` 并手动创建新任务。 // - 找出需要移除的 assignee,找到其对应的未完成任务并删除 (`taskService.deleteTask(...)`)。 // 注意:这非常复杂,且容易破坏流程一致性。生产环境慎用,最好通过流程设计(如“加签”、“减签”子流程)来实现。 }实操心得:动态修改运行中的多实例是“雷区”。除非万不得已,应尽量避免。更好的架构设计是:将“确定参与者”这个动作作为一个独立的、可重复调用的服务或子流程。当需要变更时,走一个“变更审批”子流程,审批通过后,终止旧的多实例活动,然后带着新的参与者列表重新进入一个新实例。这样逻辑更清晰,数据一致性也更好保障。
4.2 串行动态多实例与循环计数器
串行多实例常用于逐级审批。loopCounter在这里非常有用。
<userTask id="serialDynamicTask" name="串行审批"> <multiInstanceLoopCharacteristics isSequential="true" flowable:collection="${approvalLevelList}" flowable:elementVariable="currentApprover"> </multiInstanceLoopCharacteristics> <extensionElements> <!-- 利用loopCounter设置不同的任务名称 --> <flowable:formProperty id="taskTitle" name="任务标题" expression="第${loopCounter+1}级审批" /> </extensionElements> </userTask>在监听器中,你可以通过delegateTask.getVariableLocal("loopCounter")获取当前是第几轮循环,从而执行不同的业务逻辑。
4.3 完成条件的灵活运用
completionCondition赋予了多实例活动更智能的完成逻辑。
<multiInstanceLoopCharacteristics isSequential="false" flowable:collection="${voterList}" flowable:elementVariable="voter"> <!-- 超过三分之二同意则通过 --> <completionCondition>${nrOfCompletedInstances > 0 && (agreeCount / nrOfInstances) > 0.666}</completionCondition> </multiInstanceLoopCharacteristics>为了实现这个条件,你需要在每个投票任务完成时,更新一个流程变量(如agreeCount)。这可以通过在任务完成事件(complete)的监听器中实现。
4.4 常见问题与排查技巧实录
问题1:动态多实例任务没有创建。
- 排查步骤:
- 检查集合变量:确保在到达多实例节点前,流程变量
dynamicAssigneeCollection已被正确设置且不为null。使用runtimeService.getVariable(executionId, “dynamicAssigneeCollection”)验证。 - 检查变量类型:确保设置的变量是
List、Array等集合类型,而不是字符串。Arrays.asList(“a”, “b”)是正确的,“a,b,c”是错误的。 - 查看日志:开启Flowable的DEBUG级别日志,搜索
Creating a new task instance相关的日志,看引擎是否尝试创建实例。 - 检查表达式:确认BPMN中的
flowable:collection属性值是否正确指向了流程变量名,没有拼写错误。
- 检查集合变量:确保在到达多实例节点前,流程变量
问题2:任务创建了,但办理人(assignee)为null。
- 排查步骤:
- 检查elementVariable:确认
elementVariable的名字(如singleAssignee)在任务分配表达式中被正确引用。例如,flowable:assignee="${singleAssignee}"。 - 检查集合内容:确保你放入
dynamicAssigneeCollection的集合里的每个元素都是有效的用户ID字符串,且不为null或空字符串。 - 使用任务监听器:如果分配逻辑复杂,改用任务创建监听器(
TaskListener),在notify方法中打印delegateTask.getVariableLocal(“singleAssignee”)进行调试。
- 检查elementVariable:确认
问题3:串行多实例卡住,不继续创建下一个实例。
- 排查步骤:
- 检查当前任务是否完成:确认上一个实例的任务是否已经调用
taskService.complete(taskId)。串行多实例只有在当前实例任务完成后,才会创建下一个。 - 检查完成条件:如果设置了
completionCondition,检查条件是否被意外满足,导致活动提前结束。 - 检查异常:查看是否有全局或局部的事件监听器抛出了未捕获的异常,阻塞了流程推进。
- 检查当前任务是否完成:确认上一个实例的任务是否已经调用
问题4:性能问题,当动态集合非常大时(如上千人),流程启动或任务查询变慢。
- 优化建议:
- 分页查询:在查询任务时,务必使用
.listPage(start, size)而不是.list()。 - 异步执行:考虑将生成超大列表的服务任务设置为异步(
flowable:async=”true”),避免阻塞流程事务。 - 业务拆分:从业务层面思考,是否需要真的为上千人创建独立任务?是否可以用“角色组”、“邮件通知”或“公告板”模式替代?有时,技术方案需要配合业务重构。
- 数据库索引:确保
ACT_RU_TASK表上的PROC_INST_ID_,ASSIGNEE_等字段有合适索引。
- 分页查询:在查询任务时,务必使用
一个我踩过的坑:曾经在collection表达式中使用了SpEL表达式调用一个返回List的Spring Bean方法,像这样${@userService.findApprovers(projectId)}。在单元测试中一切正常,但在生产环境的某些高并发场景下,偶尔会出现集合为null的情况。后来发现是因为Spring Bean的代理和作用域问题。解决方案:改为在明确的前置服务任务(JavaDelegate)中调用服务方法,将结果列表显式地设置为流程变量,再传递给多实例节点。这样更稳定,也更容易调试和记录日志。