Flowable动态多实例任务:从原理到实战,解决流程中参与者不确定性问题

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: 为集合中的每个元素指定一个变量名。在任务实例中你可以通过这个变量名访问当前迭代的元素。例如elementVariableassignee那么在任务表达式中就可以用${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 version1.0 encodingUTF-8? definitions xmlnshttp://www.omg.org/spec/BPMN/20100524/MODEL xmlns:flowablehttp://flowable.org/bpmn targetNamespacehttp://flowable.org/bpmn process iddynamic_multi_instance_process name动态多实例会签流程 isExecutabletrue startEvent idstartEvent1 / sequenceFlow idflow1 sourceRefstartEvent1 targetRefgenDynamicListTask / !-- 服务任务动态生成会签人员列表 -- serviceTask idgenDynamicListTask name生成动态会签列表 flowable:classcom.example.flowable.listener.GenerateDynamicAssigneeListener / sequenceFlow idflow2 sourceRefgenDynamicListTask targetRefdynamicMultiInstanceTask / !-- 动态多实例用户任务 -- userTask iddynamicMultiInstanceTask name项目报告会签 !-- 这里是关键定义多实例循环特性 -- multiInstanceLoopCharacteristics isSequentialfalse !-- 并行会签 -- flowable:collectiondynamicAssigneeCollection !-- 指向流程变量 -- flowable:elementVariablesingleAssignee !-- 每个实例中的元素变量名 -- !-- 可选完成条件此处为全部完成 -- !-- completionCondition${nrOfCompletedInstances nrOfInstances}/completionCondition -- /multiInstanceLoopCharacteristics /userTask sequenceFlow idflow3 sourceRefdynamicMultiInstanceTask targetRefendEvent1 / endEvent idendEvent1 / /process /definitions关键点解析flowable:collectiondynamicAssigneeCollection这告诉Flowable去流程变量中查找名为dynamicAssigneeCollection的变量并将其值作为集合进行遍历。这个变量将在服务任务genDynamicListTask中设置。flowable:elementVariablesingleAssignee在创建的每个会签任务实例中都会有一个名为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(); // 假设这是动态查询的结果 ListString dynamicAssignees fetchDynamicAssigneesFromService(processInstanceId); // 将动态生成的列表设置为流程变量变量名必须与BPMN中的collection属性值一致 execution.setVariable(dynamicAssigneeCollection, dynamicAssignees); // 可选记录日志便于调试 execution.setVariable(assigneeListSize, dynamicAssignees.size()); System.out.println(动态生成的会签人员列表: dynamicAssignees); } private ListString 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. 设置初始流程变量可选用于影响动态列表的生成 MapString, 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. 稍等片刻让服务任务执行完毕然后查询生成的多实例任务 // 在实际应用中这里可能需要异步等待或使用事件监听器。 // 我们直接查询该流程实例下所有的任务 ListTask 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()); // 可以查看任务的本地位变量 // MapString, 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 iddynamicMultiInstanceTask name项目报告会签 extensionElements flowable:taskListener eventcreate classcom.example.flowable.listener.MultiInstanceTaskAssignmentListener/ /extensionElements multiInstanceLoopCharacteristics isSequentialfalse flowable:collectiondynamicAssigneeCollection flowable:elementVariablesingleAssignee /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, ListString newAssigneeList) { // 假设我们要修改的节点ID是 dynamicMultiInstanceTask // 1. 首先需要找到该多实例活动的执行实例代表整个多实例活动而非单个任务 ListExecution 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 idserialDynamicTask name串行审批 multiInstanceLoopCharacteristics isSequentialtrue flowable:collection${approvalLevelList} flowable:elementVariablecurrentApprover /multiInstanceLoopCharacteristics extensionElements !-- 利用loopCounter设置不同的任务名称 -- flowable:formProperty idtaskTitle name任务标题 expression第${loopCounter1}级审批 / /extensionElements /userTask在监听器中你可以通过delegateTask.getVariableLocal(loopCounter)获取当前是第几轮循环从而执行不同的业务逻辑。4.3 完成条件的灵活运用completionCondition赋予了多实例活动更智能的完成逻辑。multiInstanceLoopCharacteristics isSequentialfalse flowable:collection${voterList} flowable:elementVariablevoter !-- 超过三分之二同意则通过 -- completionCondition${nrOfCompletedInstances 0 amp;amp; (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”)进行调试。问题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中调用服务方法将结果列表显式地设置为流程变量再传递给多实例节点。这样更稳定也更容易调试和记录日志。

最新新闻

日新闻

周新闻

月新闻