动态任务指派实战:Activiti TaskListener在Spring Boot中的高阶应用
审批流程中硬编码任务处理人?每次业务规则变更都要重新部署流程定义?这显然不符合现代敏捷开发的需求。今天我们就来彻底解决这个问题——通过Activiti的TaskListener实现动态任务指派与自动抄送机制,让流程引擎真正具备业务适应能力。
1. 为什么需要动态任务指派
传统流程设计中,我们常常在BPMN文件中直接指定activiti:assignee="zhangsan"。这种硬编码方式存在三个致命缺陷:
- 业务耦合度高:当组织架构调整时,必须修改流程定义并重新部署
- 规则灵活性差:无法根据表单数据动态计算处理人
- 扩展性不足:复杂的多条件指派逻辑难以实现
通过TaskListener的assignment和create事件,我们可以获取流程上下文中的所有变量,结合业务规则引擎动态决定:
- 任务处理人(assignee)
- 候选用户组(candidateGroups)
- 抄送列表(通过自定义扩展实现)
// 典型动态指派场景示例 public class DynamicAssignmentListener implements TaskListener { @Override public void notify(DelegateTask delegateTask) { String department = (String) delegateTask.getVariable("applyDepartment"); String loanAmount = (String) delegateTask.getVariable("loanAmount"); if("finance".equals(department)) { if(Integer.parseInt(loanAmount) > 100000) { delegateTask.setAssignee("CFO"); // 大额财务审批转CFO delegateTask.addCandidateGroup("audit"); // 同时需要审计会签 } else { delegateTask.setAssignee("financeManager"); } } } }2. TaskListener核心事件解析
理解事件触发顺序是正确使用监听器的关键。以下是四个核心事件的详细对比:
| 事件类型 | 触发时机 | 典型应用场景 | 注意事项 |
|---|---|---|---|
| create | 任务创建完成且所有参数设置后 | 初始化任务属性、设置默认处理人 | 此时assignee已确定 |
| assignment | 任务被指派给具体用户时 | 动态重新指派、添加候选组 | 在create之前触发 |
| complete | 任务完成前 | 数据校验、自动抄送 | 仍可访问任务变量 |
| delete | 任务删除前 | 清理关联资源 | 包括正常完成的情况 |
关键发现:
assignment事件在create之前触发,这与直觉相反。这是因为引擎设计时需要确保create事件触发时,assignee等关键属性已经确定。
3. Spring Boot集成实战
下面通过一个完整的Spring Boot示例展示生产级实现方案。
3.1 项目依赖配置
首先确保pom.xml包含必要依赖:
<dependencies> <dependency> <groupId>org.activiti</groupId> <artifactId>activiti-spring-boot-starter</artifactId> <version>7.1.0.M6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> </dependencies>3.2 动态指派监听器实现
创建带Spring依赖注入的监听器:
@Service public class DynamicUserAssignmentListener implements TaskListener { @Autowired private UserService userService; @Autowired private RuleEngine ruleEngine; @Override public void notify(DelegateTask delegateTask) { if(EVENTNAME_CREATE.equals(delegateTask.getEventName())) { // 从流程变量获取业务数据 String businessType = (String) delegateTask.getVariable("businessType"); String region = (String) delegateTask.getVariable("region"); // 通过规则引擎计算处理人 List<String> handlers = ruleEngine.calculateHandlers(businessType, region); if(!handlers.isEmpty()) { delegateTask.setAssignee(handlers.get(0)); if(handlers.size() > 1) { handlers.remove(0); delegateTask.addCandidateUsers(handlers); } } // 自动添加抄送 addCarbonCopy(delegateTask); } } private void addCarbonCopy(DelegateTask task) { // 实现抄送逻辑... } }3.3 流程定义配置
在BPMN 2.0 XML中配置监听器:
<userTask id="approvalTask" name="审批请求"> <extensionElements> <activiti:taskListener event="create" delegateExpression="${dynamicUserAssignmentListener}"/> </extensionElements> </userTask>4. 高级应用场景
4.1 基于表单数据的条件指派
当处理人需要根据表单填写内容动态确定时:
public void notify(DelegateTask delegateTask) { Map<String, Object> formData = (Map<String, Object>) delegateTask.getVariable("formData"); String riskLevel = (String) formData.get("riskLevel"); String amount = (String) formData.get("amount"); if("HIGH".equals(riskLevel) || new BigDecimal(amount).compareTo(new BigDecimal("1000000")) > 0) { delegateTask.setAssignee("seniorManager"); delegateTask.addCandidateGroup("riskCommittee"); } }4.2 自动抄送实现方案
通过complete事件实现任务完成后自动抄送:
@Service public class CarbonCopyListener implements TaskListener { @Autowired private NotificationService notificationService; @Override public void notify(DelegateTask delegateTask) { if(EVENTNAME_COMPLETE.equals(delegateTask.getEventName())) { List<String> ccList = (List<String>) delegateTask.getVariable("_carbonCopy"); if(ccList != null) { String comment = (String) delegateTask.getVariable("comment"); notificationService.sendCarbonCopy( delegateTask.getId(), ccList, comment ); } } } }5. 性能优化与避坑指南
在实际企业级应用中,我们总结了以下最佳实践:
避免循环依赖:
- 不要在监听器中注入
RuntimeService等流程引擎服务 - 使用
@Lazy解决Spring Bean的循环引用问题
- 不要在监听器中注入
性能优化技巧:
// 反模式:每次触发都查询数据库 User user = userRepository.findById(userId); delegateTask.setAssignee(user.getLoginName()); // 正解:提前在流程变量中缓存必要数据 delegateTask.setAssignee((String)delegateTask.getVariable("preCalculatedAssignee"));事务边界注意:
- 监听器中的操作与任务操作在同一个事务中
- 异常会导致整个任务操作回滚
测试策略:
@SpringBootTest public class TaskListenerTest { @Autowired private RuntimeService runtimeService; @Test public void testDynamicAssignment() { Map<String, Object> variables = new HashMap<>(); variables.put("department", "finance"); variables.put("amount", "150000"); ProcessInstance instance = runtimeService.startProcessInstanceByKey( "loanApproval", variables ); Task task = taskService.createTaskQuery() .processInstanceId(instance.getId()) .singleResult(); assertEquals("CFO", task.getAssignee()); } }
6. 扩展应用:会签任务动态配置
对于需要多人会签的场景,可以动态设置候选组并控制完成条件:
public void notify(DelegateTask delegateTask) { if(EVENTNAME_CREATE.equals(delegateTask.getEventName())) { String projectType = (String) delegateTask.getVariable("projectType"); if("strategic".equals(projectType)) { delegateTask.addCandidateGroup("boardMembers"); delegateTask.setVariable("nrOfApprovalsRequired", 3); } else { delegateTask.addCandidateGroup("departmentHeads"); delegateTask.setVariable("nrOfApprovalsRequired", 1); } } }对应的完成条件表达式:
<completionCondition>${nrOfApproved >= nrOfApprovalsRequired}</completionCondition>在电商退货流程中,我们成功应用这套方案将审批规则配置时间从原来的2天缩短到10分钟。某金融客户则通过动态指派实现了风险等级与审批层级的自动匹配,错误率下降90%。