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元素配置。对于动态多实例,以下几个属性至关重要:

  1. isSequential: 布尔值。false表示并行多实例(所有实例同时创建,常见于会签);true表示串行多实例(一个接一个地完成,常见于逐级审批)。
  2. collection:这是实现动态性的核心。它的值是一个表达式(如${dynamicUserList}),该表达式在运行时被解析,必须得到一个java.util.Collectionjava.util.Iterable或数组。Flowable会遍历这个集合,为每个元素创建一个任务实例。
  3. elementVariable: 为集合中的每个元素指定一个变量名。在任务实例中,你可以通过这个变量名访问当前迭代的元素。例如,elementVariable="assignee",那么在任务表达式中就可以用${assignee}来指代当前任务的办理人。
  4. completionCondition: 完成条件。这是一个可选但强大的属性,允许你定义在多实例任务完成前,需要满足的条件。例如,${nrOfCompletedInstances/nrOfInstances >= 0.5}表示超过一半的实例完成时,整个多实例活动就完成(“多数决”)。

2.3 内置变量与生命周期

Flowable为每个多实例活动自动管理一组内置变量,在表达式中可以直接使用:

  • nrOfInstances: 实例的总数(即collection集合的大小)。
  • nrOfActiveInstances: 当前活跃(未完成)的实例数。
  • nrOfCompletedInstances: 已完成的实例数。
  • loopCounter: 当前正在执行的实例的索引(从0开始)。

理解生命周期很重要:当流程执行到达动态多实例节点时,引擎会:

  1. 计算collection表达式的值,得到集合A。
  2. 根据集合A的大小n,创建n个任务实例(并行)或准备创建第一个实例(串行)。
  3. 为每个实例设置elementVariable
  4. 每个实例独立运行,可以独立完成或驳回。
  5. 根据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:candidateUsersflowable: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 &amp;&amp; (agreeCount / nrOfInstances) > 0.666}</completionCondition> </multiInstanceLoopCharacteristics>

为了实现这个条件,你需要在每个投票任务完成时,更新一个流程变量(如agreeCount)。这可以通过在任务完成事件(complete)的监听器中实现。

4.4 常见问题与排查技巧实录

问题1:动态多实例任务没有创建。

  • 排查步骤
    1. 检查集合变量:确保在到达多实例节点前,流程变量dynamicAssigneeCollection已被正确设置且不为null。使用runtimeService.getVariable(executionId, “dynamicAssigneeCollection”)验证。
    2. 检查变量类型:确保设置的变量是ListArray等集合类型,而不是字符串。Arrays.asList(“a”, “b”)是正确的,“a,b,c”是错误的。
    3. 查看日志:开启Flowable的DEBUG级别日志,搜索Creating a new task instance相关的日志,看引擎是否尝试创建实例。
    4. 检查表达式:确认BPMN中的flowable:collection属性值是否正确指向了流程变量名,没有拼写错误。

问题2:任务创建了,但办理人(assignee)为null。

  • 排查步骤
    1. 检查elementVariable:确认elementVariable的名字(如singleAssignee)在任务分配表达式中被正确引用。例如,flowable:assignee="${singleAssignee}"
    2. 检查集合内容:确保你放入dynamicAssigneeCollection的集合里的每个元素都是有效的用户ID字符串,且不为null或空字符串。
    3. 使用任务监听器:如果分配逻辑复杂,改用任务创建监听器(TaskListener),在notify方法中打印delegateTask.getVariableLocal(“singleAssignee”)进行调试。

问题3:串行多实例卡住,不继续创建下一个实例。

  • 排查步骤
    1. 检查当前任务是否完成:确认上一个实例的任务是否已经调用taskService.complete(taskId)。串行多实例只有在当前实例任务完成后,才会创建下一个。
    2. 检查完成条件:如果设置了completionCondition,检查条件是否被意外满足,导致活动提前结束。
    3. 检查异常:查看是否有全局或局部的事件监听器抛出了未捕获的异常,阻塞了流程推进。

问题4:性能问题,当动态集合非常大时(如上千人),流程启动或任务查询变慢。

  • 优化建议
    1. 分页查询:在查询任务时,务必使用.listPage(start, size)而不是.list()
    2. 异步执行:考虑将生成超大列表的服务任务设置为异步(flowable:async=”true”),避免阻塞流程事务。
    3. 业务拆分:从业务层面思考,是否需要真的为上千人创建独立任务?是否可以用“角色组”、“邮件通知”或“公告板”模式替代?有时,技术方案需要配合业务重构。
    4. 数据库索引:确保ACT_RU_TASK表上的PROC_INST_ID_,ASSIGNEE_等字段有合适索引。

一个我踩过的坑:曾经在collection表达式中使用了SpEL表达式调用一个返回List的Spring Bean方法,像这样${@userService.findApprovers(projectId)}。在单元测试中一切正常,但在生产环境的某些高并发场景下,偶尔会出现集合为null的情况。后来发现是因为Spring Bean的代理和作用域问题。解决方案:改为在明确的前置服务任务(JavaDelegate)中调用服务方法,将结果列表显式地设置为流程变量,再传递给多实例节点。这样更稳定,也更容易调试和记录日志。