三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Spring Boot集成Flowable:3个注解快速构建审批流

Spring Boot集成Flowable:3个注解快速构建审批流

1. 项目概述:当Spring Boot遇上Flowable

在后台管理系统的开发里,审批流是个绕不开的坎。无论是请假、报销还是工单流转,背后都是一套逻辑严谨的流程控制。传统做法是硬编码状态机,if-else满天飞,流程一改就得伤筋动骨。后来,我们引入了工作流引擎,把流程逻辑从业务代码里抽离出来,用可视化的方式定义,用引擎来驱动,这才算走上了正道。

Flowable,作为Activiti的一个分支,是目前Java生态里非常活跃的一款轻量级工作流引擎。它原生支持BPMN 2.0标准,与Spring Boot的集成堪称无缝。但很多朋友初上手时,面对Flowable那一套API和一堆数据库表,还是会觉得有点“重”,心想:我就想快速搭个请假审批,有必要搞这么复杂吗?

答案是:用对方法,一点都不复杂。今天要聊的,就是如何用Spring Boot接上Flowable,并且只靠3个核心注解,就能快速搭建并运行一个完整的请假审批流程。这3个注解,就像是三个“开关”,能帮你把Flowable引擎的能力,以最Spring Boot的方式注入到你的业务服务里,让流程的启动、任务处理和监听变得异常简单。我们不止讲怎么做,更会拆解为什么这么做,以及在实际操作中我踩过哪些坑、总结出哪些技巧。

2. 核心思路:注解驱动与职责分离

在深入代码之前,我们先理清整个方案的设计思路。核心目标是用最少的代码、最Spring Boot的方式,让工作流跑起来。这里的“少”,不是功能阉割,而是通过合理的抽象和框架集成,把繁琐的引擎API调用隐藏起来。

2.1 为什么是这三个注解?

我们选定的三个注解是:@ProcessRuntime@TaskRuntime@EventListener。它们分别对应了Flowable与Spring集成中的三个关键角色。

  1. @ProcessRuntime:这是流程实例的“发动机”。它的职责是启动流程、查询流程实例、挂起或激活流程。在请假场景里,“提交请假申请”这个动作,本质上就是使用ProcessRuntime启动一个定义好的“请假流程”。
  2. @TaskRuntime:这是用户任务的“操作台”。流程流转到某个用户任务(比如“经理审批”)时,具体的审批人就需要通过TaskRuntime来查询待办任务、签收任务、完成任务(同意或驳回)以及查询任务相关的变量和附件。
  3. @EventListener:这是流程的“耳朵”和“哨兵”。工作流引擎在运行过程中会抛出各种事件,比如任务创建了、任务完成了、流程结束了。通过@EventListener注解,我们可以让Spring Bean中的方法监听这些特定事件,从而在关键时刻执行业务逻辑,例如发送通知、更新业务状态、记录日志等。

这个组合实现了完美的职责分离。业务服务(如LeaveService)只关心启动流程和传递业务参数;任务处理(如TaskService)只关心如何操作具体的任务项;而事件监听器(如LeaveProcessEventListener)则负责处理流程推进过程中的副作用。三者通过Flowable引擎内部的事件机制松散耦合,任何一方的修改都不会直接影响其他方。

2.2 流程定义与业务数据的绑定策略

另一个关键思路是如何将Flowable流程实例与你的业务数据(比如一张t_leave请假单表)关联起来。硬编码在流程变量里?那会让流程变量变得臃肿且难以维护。

我们的策略是:使用业务主键(Business Key)进行松耦合关联

  • 在启动流程实例时,将业务实体的ID(如leaveId)作为businessKey传入。
  • 此后,在任何需要获取业务数据的场景(如任务办理、事件监听),都可以通过runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(leaveId)来反向找到流程实例,进而再通过leaveId去查询业务数据库。

这样做的好处是,流程引擎表只存储流程运行状态和必要的控制变量,完整的业务数据仍留在业务库中,两者通过一个简单的字符串键关联,清晰且易于扩展。

3. 环境准备与基础配置

理论清晰了,我们开始动手。首先需要一个干净的Spring Boot工程。

3.1 依赖引入与版本选择

pom.xml中,关键依赖是flowable-spring-boot-starter。版本选择需要谨慎,需确保与你的Spring Boot版本兼容。以当前主流版本为例:

<properties> <java.version>17</java.version> <spring-boot.version>3.1.5</spring-boot.version> <flowable.version>7.0.0</flowable.version> </properties> <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Flowable 核心启动器 --> <dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>${flowable.version}</version> </dependency> <!-- 数据库(这里用H2内存数据库方便演示) --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok 简化代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

注意:Flowable 7.x 与 Spring Boot 3.x 兼容性较好。如果你使用的是Spring Boot 2.x,可能需要对应选择Flowable 6.x版本。引入starter后,Flowable会自动配置数据源、事务管理器以及引擎本身,并默认创建所需的数十张表。

3.2 应用配置与表结构初始化

application.yml中,我们需要进行一些基本配置:

spring: datasource: url: jdbc:h2:mem:flowable-db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true # 开启H2控制台,方便查看流程数据表 path: /h2-console # Flowable 配置 flowable: async-executor-activate: false # 开发环境可关闭异步执行器,简化调试 database-schema-update: true # 自动创建/更新表结构,生产环境建议设为`false`并通过脚本管理 history-level: audit # 历史级别,`audit`会保存流程实例、任务、变量等核心历史数据

启动应用,你会发现控制台打印了大量Creating表结构的SQL日志。此时访问http://localhost:8080/h2-console,连接上H2数据库,就能看到ACT_RE_*(存储流程定义)、ACT_RU_*(存储运行时数据)、ACT_HI_*(存储历史数据)和ACT_ID_*(存储身份数据)四大类表。这说明Flowable引擎已经就绪。

3.3 绘制并部署BPMN流程图

工作流的核心是流程定义。我们需要一个BPMN 2.0的XML文件来描述请假流程。你可以使用Flowable提供的在线设计器(通常集成在其UI应用中),或者使用Eclipse的BPMN 2.0插件、IntelliJ IDEA的Flowable插件来绘制。

这里给出一个最简化的“员工请假-经理审批”流程的XML定义 (leave-process.bpmn20.xml),应放在src/main/resources/processes/目录下:

<?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://www.flowable.org/processdef"> <process id="leaveApproval" name="员工请假审批流程" isExecutable="true"> <!-- 开始事件 --> <startEvent id="startEvent" name="提交申请"/> <!-- 员工提交请假申请任务 --> <userTask id="submitLeave" name="提交请假单" flowable:assignee="${applicantId}"> <extensionElements> <flowable:formProperty id="leaveType" name="请假类型" type="string" required="true"/> <flowable:formProperty id="duration" name="请假时长(天)" type="double" required="true"/> <flowable:formProperty id="reason" name="事由" type="string"/> </extensionElements> </userTask> <!-- 经理审批任务 --> <userTask id="managerApprove" name="经理审批" flowable:candidateUsers="${managerId}"> <extensionElements> <flowable:formProperty id="approvalResult" name="审批结果" type="enum"> <flowable:value id="agree" name="同意"/> <flowable:value id="reject" name="驳回"/> </flowable:formProperty> <flowable:formProperty id="comment" name="审批意见" type="string"/> </extensionElements> </userTask> <!-- 排他网关,根据审批结果决定流向 --> <exclusiveGateway id="decisionGateway" name="审批决策"/> <!-- 审批通过,流程结束 --> <endEvent id="endEventApproved" name="审批通过结束"/> <!-- 审批驳回,流程结束 --> <endEvent id="endEventRejected" name="审批驳回结束"/> <!-- 顺序流连接 --> <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="submitLeave"/> <sequenceFlow id="flow2" sourceRef="submitLeave" targetRef="managerApprove"/> <sequenceFlow id="flow3" sourceRef="managerApprove" targetRef="decisionGateway"/> <sequenceFlow id="flow4" sourceRef="decisionGateway" targetRef="endEventApproved"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${approvalResult == 'agree'}]]> </conditionExpression> </sequenceFlow> <sequenceFlow id="flow5" sourceRef="decisionGateway" targetRef="endEventRejected"> <conditionExpression xsi:type="tFormalExpression"> <![CDATA[${approvalResult == 'reject'}]]> </conditionExpression> </sequenceFlow> </process> </definitions>

这个流程很简单:开始 → 员工提交任务 → 经理审批任务 → 根据审批结果(同意/驳回)网关决策 → 结束。注意flowable:assigneeflowable:candidateUsers,它们用于指定任务的办理人或候选人。这里使用了表达式${applicantId}${managerId},意味着这些值将在流程运行时动态传入。

将BPMN文件放在resources/processes/目录后,Flowable Spring Boot Starter会在应用启动时自动扫描并部署它。你可以在启动日志中看到类似Deployed processes: [leaveApproval (v1)]的信息。

4. 核心注解驱动开发实战

环境与流程定义都已就绪,现在进入核心环节:如何使用三个注解来驱动整个流程。

4.1 使用 @ProcessRuntime 启动流程

首先,我们创建一个请假服务LeaveService,并注入ProcessRuntime

import org.flowable.engine.RuntimeService; import org.flowable.engine.runtime.ProcessInstance; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Service @Slf4j @RequiredArgsConstructor public class LeaveService { // 关键注解注入:流程运行时服务 private final RuntimeService runtimeService; /** * 提交请假申请,启动流程 * @param applicantId 申请人ID * @param managerId 审批经理ID * @param leaveRequest 请假请求数据 * @return 启动的流程实例ID */ @Transactional public String submitLeaveRequest(String applicantId, String managerId, LeaveRequest leaveRequest) { // 1. 持久化业务数据(这里简化,直接使用DTO) // 实际项目中,应先保存 leaveRequest 到业务表,并获取生成的 leaveId String businessKey = "LEAVE_" + System.currentTimeMillis(); // 模拟业务主键 // 2. 设置流程变量 Map<String, Object> variables = new HashMap<>(); variables.put("applicantId", applicantId); variables.put("managerId", managerId); variables.put("leaveType", leaveRequest.getLeaveType()); variables.put("duration", leaveRequest.getDuration()); variables.put("reason", leaveRequest.getReason()); // 将业务主键也存入变量,方便后续查询 variables.put("businessKey", businessKey); // 3. 使用 ProcessRuntime 启动流程实例 // 参数:流程定义Key, 业务主键, 流程变量 ProcessInstance processInstance = runtimeService.startProcessInstanceByKey( "leaveApproval", // 对应BPMN文件中的 process id businessKey, variables ); String processInstanceId = processInstance.getId(); log.info("请假流程启动成功。流程实例ID: {}, 业务主键: {}", processInstanceId, businessKey); return processInstanceId; } } // 简单的请假请求DTO @Data class LeaveRequest { private String leaveType; // 年假、病假等 private Double duration; private String reason; }

关键点解析

  • RuntimeService是由Flowable自动配置的Bean,@Autowired或构造器注入即可。
  • startProcessInstanceByKey是最常用的启动方式,使用流程定义的id(即BPMN中的process id)。
  • businessKey是关联业务数据的生命线,务必使用有业务意义的唯一标识。
  • 启动流程是一个事务性操作,建议在Service方法上添加@Transactional注解,确保业务数据与流程实例创建的原子性。

4.2 使用 @TaskRuntime 处理审批任务

流程启动后,会自动流转到“提交请假单”任务,并因为assignee是申请人自己,所以该任务会自动完成(如果配置了flowable:skipExpression可能不会自动创建,这里我们假设需要手动完成)。接着会到达“经理审批”任务。现在,我们需要为经理提供一个查询待办和审批的接口。

创建TaskService,注入TaskService(注意是Flowable的,不是Spring的)。

import org.flowable.engine.TaskService; import org.flowable.task.api.Task; import org.flowable.task.api.TaskQuery; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.util.List; import java.util.Map; @Service @Slf4j @RequiredArgsConstructor public class TaskService { // 关键注解注入:任务服务 private final TaskService taskService; /** * 查询用户的待办任务 * @param userId 用户ID * @return 待办任务列表 */ public List<Task> getTodoTasks(String userId) { // 创建任务查询 TaskQuery taskQuery = taskService.createTaskQuery(); // 设置查询条件:候选人包含该用户 或 办理人是该用户 // 对于 candidateUsers 方式分配的任务,用户需要先“签收”才能成为办理人 List<Task> tasks = taskQuery .taskCandidateOrAssigned(userId) // 查询作为候选人或已被指派的任务 .orderByTaskCreateTime().desc() // 按创建时间倒序 .list(); // 可以进一步封装,将Task对象转换为前端需要的VO return tasks; } /** * 签收任务(将候选任务变为个人任务) * @param taskId 任务ID * @param userId 用户ID */ @Transactional public void claimTask(String taskId, String userId) { taskService.claim(taskId, userId); log.info("用户 {} 签收了任务 {}", userId, taskId); } /** * 完成任务(例如:经理审批) * @param taskId 任务ID * @param approvalResult 审批结果和意见 */ @Transactional public void completeTask(String taskId, Map<String, Object> approvalResult) { // 完成任务,并设置流程变量 // 这里传入的 approvalResult Map 应包含流程中需要的变量,如 approvalResult, comment taskService.complete(taskId, approvalResult); log.info("任务 {} 已完成,审批结果: {}", taskId, approvalResult); } /** * 获取任务关联的流程变量(用于渲染表单或展示详情) * @param taskId 任务ID * @return 流程变量Map */ public Map<String, Object> getTaskVariables(String taskId) { return taskService.getVariables(taskId); } }

实操心得

  • taskCandidateOrAssigned(userId)这个查询条件非常实用,它同时覆盖了“我是候选人”和“我已签收”两种状态的任务,避免了前端需要区分查询的麻烦。
  • taskService.complete(taskId, variables)方法中的variables参数至关重要。它会在完成任务的同时,将这些变量设置到流程实例的上下文中。在我们的流程里,网关决策依赖的approvalResult变量就是通过这里传入的。
  • 任务操作(claim,complete)通常也建议放在事务中,特别是当完成任务后需要同步更新业务状态时(虽然这个更新更推荐在监听器中做)。

4.3 使用 @EventListener 监听流程事件

流程在运行,但我们还需要知道它什么时候到了关键节点,以便执行一些“副作用”逻辑,比如发送通知、更新业务状态。这时就需要事件监听器。

创建一个事件监听器类,使用Spring的@EventListener注解来监听Flowable的事件。

import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener; import org.flowable.engine.delegate.event.FlowableEngineEntityEvent; import org.flowable.engine.delegate.event.FlowableEvent; import org.flowable.task.api.Task; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import lombok.extern.slf4j.Slf4j; @Component @Slf4j public class LeaveProcessEventListener { /** * 监听任务创建事件 * 当一个新的用户任务被创建时触发,可用于发送待办通知。 */ @EventListener(condition = "#event.type == 'TASK_CREATED'") public void onTaskCreated(FlowableEngineEntityEvent event) { Task task = (Task) event.getEntity(); String taskName = task.getName(); String processInstanceId = task.getProcessInstanceId(); log.info("[事件监听] 任务创建:任务名称='{}', 流程实例ID='{}'", taskName, processInstanceId); // 这里可以添加业务逻辑,例如: // 1. 查询任务候选人/办理人 // 2. 调用消息服务发送邮件、钉钉、微信通知 // 3. 记录任务创建日志到业务库 if ("经理审批".equals(taskName)) { String assignee = task.getAssignee(); // 如果是候选人方式,assignee可能为null,需要通过taskService.getIdentityLinksForTask获取候选人 log.info("--> 需要发送审批通知给用户: {}", assignee); // notificationService.sendToUser(assignee, "您有一个待审批的请假单"); } } /** * 监听任务完成事件 * 当一个用户任务被完成时触发,可用于更新业务状态。 */ @EventListener(condition = "#event.type == 'TASK_COMPLETED'") public void onTaskCompleted(FlowableEngineEntityEvent event) { Task task = (Task) event.getEntity(); String taskDefinitionKey = task.getTaskDefinitionKey(); // 对应BPMN中的任务ID,如 'managerApprove' String processInstanceId = task.getProcessInstanceId(); log.info("[事件监听] 任务完成:任务定义Key='{}', 流程实例ID='{}'", taskDefinitionKey, processInstanceId); // 关键业务逻辑:根据完成的任务节点,更新对应的业务数据状态 if ("managerApprove".equals(taskDefinitionKey)) { // 1. 获取流程变量中的审批结果和业务主键 // 注意:事件监听器中无法直接获取TaskService,需要通过RuntimeService获取变量 // 通常需要在事件触发时,将关键业务变量(如businessKey)也放入引擎的变量中 // 2. 根据businessKey找到业务实体,更新其状态为“已审批”或“已驳回” // 3. 记录审批日志 // businessService.updateStatus(businessKey, newStatus); } } /** * 监听流程实例结束事件 * 当整个流程实例结束时触发,可用于进行最终的资源清理或状态归档。 */ @EventListener(condition = "#event.type == 'PROCESS_COMPLETED'") public void onProcessCompleted(FlowableEngineEntityEvent event) { String processInstanceId = event.getProcessInstanceId(); log.info("[事件监听] 流程实例结束:ID='{}'", processInstanceId); // 例如:将业务单据状态标记为“流程已结束”,或触发后续的归档操作 } }

深度解析与避坑指南

  1. 事件类型:Flowable有丰富的事件类型(TASK_CREATED,TASK_COMPLETED,PROCESS_STARTED,PROCESS_COMPLETED,ACTIVITY_COMPLETED等)。@EventListenercondition属性使用SpEL表达式来过滤特定事件,非常灵活。
  2. 变量获取时机:这是最容易出错的地方。在TASK_COMPLETED事件中,任务实体task已经完成,通过taskService.getVariables(taskId)可能无法获取到任务完成时新设置的变量(如approvalResult)。更可靠的做法是:
    • 方案A:在完成任务(taskService.complete)后,立即在业务代码里更新状态,而不是依赖事件监听器。这更直接,事务也更容易控制。
    • 方案B:如果坚持在监听器中处理,可以通过RuntimeService获取流程实例级别的变量,因为任务完成时设置的变量会提升为流程变量。使用runtimeService.getVariables(processInstanceId)
  3. 事务边界:事件监听器默认在发布事件的同一事务中执行。这意味着如果监听器里抛异常,会导致触发该事件的操作(如完成任务)也回滚。务必确保监听器逻辑的健壮性,或者使用@Async@Transactional(propagation = Propagation.REQUIRES_NEW)将其放入独立事务中,避免影响主流程。
  4. 性能考量:高频事件(如ACTIVITY_STARTED)的监听器逻辑要尽可能轻量,避免阻塞主线程或拖慢流程引擎。

5. 构建REST API与前端交互

服务层准备好了,我们需要通过Controller暴露API给前端调用。这里构建一组最简化的RESTful接口。

import org.springframework.web.bind.annotation.*; import lombok.RequiredArgsConstructor; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/leave") @RequiredArgsConstructor public class LeaveController { private final LeaveService leaveService; private final TaskService taskService; // 注意这是我们自己写的TaskService @PostMapping("/submit") public ApiResponse<String> submit(@RequestBody LeaveSubmitRequest request) { // request 应包含 applicantId, managerId, leaveRequest String processInstanceId = leaveService.submitLeaveRequest( request.getApplicantId(), request.getManagerId(), request.getLeaveRequest() ); return ApiResponse.success("流程启动成功", processInstanceId); } @GetMapping("/tasks") public ApiResponse<List<Task>> getTasks(@RequestParam String userId) { List<Task> tasks = taskService.getTodoTasks(userId); return ApiResponse.success(tasks); } @PostMapping("/task/{taskId}/claim") public ApiResponse<Void> claimTask(@PathVariable String taskId, @RequestParam String userId) { taskService.claimTask(taskId, userId); return ApiResponse.success("签收成功"); } @PostMapping("/task/{taskId}/complete") public ApiResponse<Void> completeTask(@PathVariable String taskId, @RequestBody Map<String, Object> variables) { // variables 例如: {"approvalResult": "agree", "comment": "情况属实,同意"} taskService.completeTask(taskId, variables); return ApiResponse.success("任务完成"); } } // 简单的请求响应封装 @Data class ApiResponse<T> { private int code; private String msg; private T data; // 省略静态工厂方法 } @Data class LeaveSubmitRequest { private String applicantId; private String managerId; private LeaveRequest leaveRequest; }

前端应用(如Vue、React)就可以通过这些接口来:

  1. 提交请假单,启动流程。
  2. 用户登录后,查询自己的待办任务列表。
  3. 点击“办理”,签收任务。
  4. 在审批页面,看到从getTaskVariables获取的请假详情,并填写审批意见,点击“同意”或“驳回”来完成任务。

6. 流程监控、调试与进阶优化

一个可运行的基础流程搭建完毕了,但在实际开发中,我们还需要“看见”流程是如何运行的,以及处理一些更复杂的情况。

6.1 利用Flowable REST API与UI应用

Flowable Starter默认提供了一套REST API(/flowable-rest/service/)和一个管理UI(需要额外引入flowable-ui依赖)。对于开发调试而言,管理UI非常直观。你可以看到已部署的流程定义、正在运行的流程实例、各个节点的状态、流程变量等。虽然生产环境可能不会直接使用这个UI,但在开发阶段,它是排查流程流转问题不可或缺的工具。

6.2 流程变量管理策略

流程变量是连接流程引擎与业务数据的桥梁。管理不善会变成一团乱麻。

  • 精简原则:只把流程流转必需的数据作为流程变量(如applicantId,managerId,approvalResult)。完整的业务对象(如LeaveRequest)应以businessKey为索引,从业务库查询。
  • 序列化注意:如果变量值是复杂对象,Flowable会将其序列化后存入ACT_GE_BYTEARRAY表。确保该对象实现了Serializable接口,并考虑序列化兼容性问题。更推荐的做法是存ID或JSON字符串。
  • 作用域:变量有任务作用域和流程实例作用域。taskService.setVariableLocal设置的任务变量,只在当前任务有效。通常审批意见(comment)适合作为任务变量,而审批结果(approvalResult)这种影响网关决策的,必须在完成任务时通过taskService.complete(taskId, variables)设置为流程变量。

6.3 处理会签、或签等高级任务

实际审批中,经理审批可能不是一个人,而是需要多个部门负责人会签,或者其中任意一人同意即可(或签)。这在BPMN中可以通过“多实例活动”来实现。

修改managerApprove用户任务,为其添加多实例配置:

<userTask id="managerApprove" name="经理会签" flowable:candidateUsers="${managerIds}"> <extensionElements> <!-- ... 表单属性同上 ... --> </extensionElements> <!-- 多实例配置 --> <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="managerIds" flowable:elementVariable="singleManager"> <completionCondition>${nrOfCompletedInstances / nrOfInstances >= 0.5}</completionCondition> </multiInstanceLoopCharacteristics> </userTask>
  • isSequential="false"表示并行会签。
  • flowable:collection指定一个存储了候选人ID列表的流程变量(如managerIds,它是一个List)。
  • flowable:elementVariable指定在每次循环中,当前候选人的ID被赋值给哪个变量(singleManager)。
  • <completionCondition>是完成条件。这里示例是完成人数超过一半(50%)即通过。你可以写更复杂的表达式,如${nrOfCompletedInstances == nrOfInstances}(全部完成)或基于聚合变量判断。

在启动流程时,你需要传入一个List类型的managerIds变量。引擎会自动为每个候选人创建独立的子任务。后端TaskService的查询和办理逻辑基本不变,因为对于每个候选人来说,他们看到的还是自己的那个子任务。

6.4 集成业务表单与动态节点指派

我们例子中使用了BPMN内嵌的<extensionElements>定义简单表单。但在实际复杂业务中,表单通常由前端动态渲染,表单结构存储在另一张表或JSON配置中。此时,BPMN中只需定义任务节点,表单的Key(如formKey: 'leave_form')可以作为任务的一个属性。任务办理时,前端根据formKey去加载对应的表单配置。

节点指派也一样,assigneecandidateUsers可能不是写死的ID,而是通过一个表达式调用Spring Bean的方法动态计算出来,例如${ldapService.findDepartmentManager(applicantId)}。这需要在流程定义中配置flowable:delegateExpression或使用flowable:class指定一个实现了TaskAssignmentListener的Java类,从而在运行时动态解析负责人。

7. 常见问题排查与性能调优

在实际部署和运行中,你可能会遇到以下问题:

问题1:启动流程后,任务没有自动创建?

  • 检查点:首先确认流程是否成功部署(查看启动日志)。然后,检查启动流程时传入的变量是否正确。特别是assigneecandidateUsers表达式所引用的变量(如${applicantId})是否已正确设置并传入runtimeService.startProcessInstanceByKey的variables中。
  • 技巧:开启Flowable的调试日志logging.level.org.flowable=DEBUG,可以非常详细地看到引擎每一步的执行逻辑和SQL,是定位问题的利器。

问题2:查询待办任务速度慢?

  • 原因ACT_RU_TASK表数据量过大,且查询条件可能未走索引。
  • 优化
    1. 历史数据归档:定期将已结束的流程实例和历史任务从运行时表(ACT_RU_*)归档到历史表(ACT_HI_*),并清理运行时表。Flowable提供了HistoryService的相关API。
    2. 索引优化:确保ACT_RU_TASK表上的ASSIGNEE_,PROC_INST_ID_,CREATE_TIME_等字段有索引。Flowable自动创建的索引可能不全,需要根据查询模式补充。
    3. 分页查询:在TaskQuery上务必使用.listPage(start, size)进行分页,避免一次性拉取大量数据。

问题3:事件监听器导致事务回滚或性能瓶颈?

  • 事务:如前所述,考虑将非核心的监听逻辑(如发通知)异步化(@Async)并与主事务分离(@Transactional(propagation = Propagation.REQUIRES_NEW))。
  • 性能:对于像“发送短信”这种可能失败或耗时的操作,不要放在同步监听器里。可以将其放入消息队列(如RabbitMQ、Kafka),由消费者异步处理。监听器只负责发消息。

问题4:流程定义变更后,已运行的旧实例如何处理?

  • 策略:这是工作流管理的经典问题。Flowable支持流程定义的版本化。每次部署相同key的流程,都会生成一个新版本。默认情况下,新发起的流程实例会使用最新版本,而已运行的旧实例会继续沿用启动时的版本。这保证了运行中流程的稳定性。
  • 迁移:如果需要将旧实例迁移到新版本,通常非常复杂,需要手动编写迁移脚本,调整运行时数据。因此,在流程设计初期应尽量考虑周全,上线后对流程定义的修改要谨慎,非破坏性变更(如修改表单字段名)可以通过适配逻辑处理,而结构性变更(如增减节点)最好通过新启一个流程定义Key来处理。

通过以上七个部分的拆解,我们从设计思路、环境搭建、注解核心使用、API构建、高级特性到问题排查,完整地覆盖了用Spring Boot和Flowable搭建一个请假审批流程的全过程。这三个注解——@ProcessRuntime@TaskRuntime@EventListener——就像三把钥匙,帮你打开了Flowable集成Spring Boot的大门,让工作流开发回归到熟悉的Spring编程模型中,高效且清晰。

← 返回列表