引言
在当今的企业级应用开发中,业务流程的自动化与规范化至关重要。Flowable 作为一款轻量级、高性能的开源 BPMN 2.0 工作流引擎,凭借其与 Spring Boot 的无缝集成能力,成为构建复杂审批流、任务流和自动化流程的首选。本文将基于 Spring Boot 3.x 与 Flowable 6.8.0,手把手带你实现一个包含流程部署、流程启动、任务审批 的完整工作流系统。我们将深入探讨如何处理指定处理人 与候选用户组(池)两种任务分配场景,并解析排他网关 与条件分支等核心流程控制元素。无论你是工作流新手还是希望升级技术栈的开发者,这篇指南都将为你提供清晰、可落地的代码与实践。
1. 环境与项目搭建
1.1 技术栈与版本
- Spring Boot: 3.2.5
- Flowable: 6.8.0
- Java: 17
- 数据库: MySQL 8.0 (Flowable 支持多种数据库)
- 构建工具: Maven
1.2 Maven 依赖
在 pom.xml 中添加 Flowable Spring Boot Starter 依赖。
xml
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>6.8.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.17</version>
</dependency>
1.3 配置文件
配置 application.yml,指定数据库连接及 Flowable 相关属性。
yaml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: root
druid:
#初始化时建立物理连接的个数
initial-size: 5
#最小连接池数量
min-idle: 5
#最大连接池数量 maxIdle已经不再使用
max-active: 20
#获取连接时最大等待时间,单位毫秒
max-wait: 60000
#用来检测连接是否有效的sql
validation-query: SELECT 1
main:
allow-circular-references: true
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
serialization:
WRITE_DATES_AS_TIMESTAMPS: true
flowable:
# 1. 开启异步执行器
# 作用:确保定时器事件、异步任务能被正常调度执行
async-executor-activate: false
# 2. 数据库策略
# 建议:SIT环境建议设为 false,由DBA统一管理脚本;若需自动建表可保持 true
# 选项说明:
# true: 启动时检查并更新表结构(开发/SIT常用)
# false: 启动时检查表结构,不更新,表不存在则报错(生产推荐)
# create-drop: 启动建表,关闭删表(测试用)
database-schema-update: true
# 3. 历史记录级别 (强烈建议添加)
# 作用:决定流程结束后保留多少数据
# 选项:
# none: 不保存任何历史记录(最快,但无法追溯)
# activity: 记录所有活动流转(常用)
# audit: 记录活动及变量信息(推荐,便于排查问题)
# full: 记录所有细节,包括表单属性等(最慢,数据量最大)
history-level: audit
# 4. 作业执行器 (可选,配合 async-executor-activate 使用)
# 作用:处理异步任务的核心组件
job-executor-activate: false # 在新版本中通常由 async-executor 接管,保持默认即可
启动应用后,Flowable 会自动在数据库中创建约 60 张表,用于存储流程定义、运行时实例、历史数据等。
2. 流程定义与部署
2.1 设计 BPMN 2.0 流程图
我们设计一个简单的"请假审批流程"。流程包含:
- 开始事件
- 用户任务"提交申请":申请人填写请假单。
- 排他网关:根据请假天数决定审批路径。
- 分支一(≤3天):直接由"部门经理"审批。
- 分支二(>3天):先由"部门经理"审批,再由"总经理"审批。
- 用户任务"人事归档":无论哪条分支,最终都由此任务结束。
- 结束事件
2.2 使用 Flowable Modeler 或代码部署
这里我们使用代码方式,将 bpmn 文件部署到引擎。
首先,在 src/main/resources/processes/ 下创建 leave-approval.bpmn20.xml。
xml
<?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="leaveApproval" name="请假审批流程" isExecutable="true">
<startEvent id="startEvent" name="开始"/>
<userTask id="submitApplication" name="提交申请" flowable:assignee="${applicant}">
<documentation>员工提交请假申请</documentation>
</userTask>
<sequenceFlow id="flow1" sourceRef="startEvent" targetRef="submitApplication"/>
<!-- 排他网关,根据条件决定流向 -->
<exclusiveGateway id="decisionGateway" name="请假天数判断"/>
<sequenceFlow id="flow2" sourceRef="submitApplication" targetRef="decisionGateway"/>
<!-- 分支一:天数 ≤ 3,流向经理审批 -->
<sequenceFlow id="flowToManager" sourceRef="decisionGateway" targetRef="managerApproval">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${days <= 3}]]>
</conditionExpression>
</sequenceFlow>
<!-- 分支二:天数 > 3,先经理后总经理 -->
<sequenceFlow id="flowToManagerThenCEO" sourceRef="decisionGateway" targetRef="managerApproval">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${days > 3}]]>
</conditionExpression>
</sequenceFlow>
<!-- 注意:此例中两条分支指向同一个"经理审批"任务,实际中可根据需要指向不同任务。这里为了简化,我们假设>3天也需要经理先批。更复杂的场景可使用并行网关。 -->
<userTask id="managerApproval" name="部门经理审批" flowable:candidateGroups="dept_manager">
<documentation>部门经理审批请假申请</documentation>
</userTask>
<sequenceFlow id="flow3" sourceRef="managerApproval" targetRef="ceoApproval">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${days > 3}]]>
</conditionExpression>
</sequenceFlow>
<userTask id="ceoApproval" name="总经理审批" flowable:candidateUsers="zhang_san,li_si">
<documentation>总经理审批(仅当请假天数>3天时)</documentation>
</userTask>
<sequenceFlow id="flow4" sourceRef="ceoApproval" targetRef="hrArchive"/>
<!-- 天数≤3天时,经理审批后直接跳至人事归档 -->
<sequenceFlow id="flowToHrDirect" sourceRef="managerApproval" targetRef="hrArchive">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${days <= 3}]]>
</conditionExpression>
</sequenceFlow>
<userTask id="hrArchive" name="人事归档" flowable:assignee="hr_staff">
<documentation>人事部门归档请假记录</documentation>
</userTask>
<sequenceFlow id="flow5" sourceRef="hrArchive" targetRef="endEvent"/>
<endEvent id="endEvent" name="结束"/>
</process>
</definitions>
关键点解析:
flowable:assignee="${applicant}":任务直接指定处理人,变量驱动。flowable:candidateGroups="dept_manager":任务候选组,组内成员均可签收处理。flowable:candidateUsers="zhang_san,li_si":任务候选用户,多个用户用逗号分隔。- 排他网关后的条件表达式
${days <= 3}使用了流程变量days。
2.3 部署服务类
创建 ProcessDeploymentService 用于部署流程定义。
java
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class ProcessDeploymentService {
@Autowired
private RepositoryService repositoryService;
/**
* 部署 classpath 下的 BPMN 文件
*/
public Deployment deployProcessFromClasspath(String bpmnFileName) {
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("processes/" + bpmnFileName)
.name("请假审批流程部署")
.deploy();
System.out.println("流程部署成功,部署ID: " + deployment.getId());
System.out.println("流程定义ID: " + deployment.getKey());
return deployment;
}
/**
* 查询所有已部署的流程定义
*/
public List<ProcessDefinition> listProcessDefinitions() {
return repositoryService.createProcessDefinitionQuery().list();
}
}
3. 流程启动与运行时管理
3.1 启动流程实例
流程部署后,即可通过流程定义ID(processDefinitionKey)启动实例。
java
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.HashMap;
@Service
public class ProcessRuntimeService {
@Autowired
private RuntimeService runtimeService;
/**
* 启动请假审批流程
* @param applicant 申请人ID
* @param days 请假天数
* @return 流程实例ID
*/
public String startLeaveProcess(String applicant, Integer days) {
Map<String, Object> variables = new HashMap<>();
variables.put("applicant", applicant);
variables.put("days", days);
// 还可以放入其他业务变量,如 reason、startDate 等
ProcessInstance processInstance = runtimeService
.startProcessInstanceByKey("leaveApproval", variables);
String processInstanceId = processInstance.getId();
System.out.println("流程启动成功,实例ID: " + processInstanceId);
return processInstanceId;
}
/**
* 查询某个用户的待办任务
*/
public List<Task> getTasksByAssignee(String assignee) {
return runtimeService.createTaskQuery()
.taskAssignee(assignee)
.list();
}
/**
* 查询候选组任务
*/
public List<Task> getTasksByCandidateGroup(String group) {
return runtimeService.createTaskQuery()
.taskCandidateGroup(group)
.list();
}
/**
* 查询候选用户任务
*/
public List<Task> getTasksByCandidateUser(String userId) {
return runtimeService.createTaskQuery()
.taskCandidateUser(userId)
.list();
}
}
4. 任务审批与处理人/候选组场景
4.1 处理"指定处理人"任务
在流程图中,"提交申请"和"人事归档"任务使用了 assignee。对于这类任务,只有指定的用户才能直接处理。
java
import org.flowable.engine.TaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TaskHandleService {
@Autowired
private TaskService taskService;
/**
* 处理指定给当前用户的任务(例如:提交申请、人事归档)
* @param taskId 任务ID
* @param userId 处理人ID(需与assignee一致)
* @param approvalResult 审批意见:同意/驳回
*/
public void handleAssigneeTask(String taskId, String userId, String approvalResult) {
// 1. 验证任务是否存在且指派给当前用户
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskAssignee(userId)
.singleResult();
if (task == null) {
throw new RuntimeException("任务不存在或您不是该任务的指定处理人");
}
// 2. 可设置任务变量(可选)
Map<String, Object> taskVariables = new HashMap<>();
taskVariables.put("approvalResult", approvalResult);
taskVariables.put("approver", userId);
taskService.setVariables(taskId, taskVariables);
// 3. 完成任务,流程向后推进
taskService.complete(taskId);
System.out.println("用户 " + userId + " 已完成任务: " + task.getName());
}
}
4.2 处理"候选用户组(池)"任务
"部门经理审批"任务设置了 candidateGroups="dept_manager"。这意味着 dept_manager 组内的任何成员都可以签收并处理该任务。
java
@Service
public class CandidateGroupTaskService {
@Autowired
private TaskService taskService;
/**
* 查询当前用户所属候选组的任务
*/
public List<Task> getGroupTasksForUser(String userId, List<String> groupIds) {
// 实际项目中,应根据 userId 查询其所属的用户组
// 这里简化:假设传入的 groupIds 就是用户所在的组
return taskService.createTaskQuery()
.taskCandidateGroupIn(groupIds) // 查询用户所在所有组的候选任务
.list();
}
/**
* 签收候选组任务(将任务从"候选池"变为指定给当前用户)
*/
public void claimGroupTask(String taskId, String userId) {
taskService.claim(taskId, userId);
System.out.println("用户 " + userId + " 已签收任务: " + taskId);
}
/**
* 处理已签收的组任务(后续处理同 assignee 任务)
*/
public void completeClaimedTask(String taskId, String userId, Map<String, Object> variables) {
Task task = taskService.createTaskQuery()
.taskId(taskId)
.taskAssignee(userId) // 签收后,assignee 变为当前用户
.singleResult();
if (task == null) {
throw new RuntimeException("任务不存在或您未签收此任务");
}
if (variables != null) {
taskService.setVariables(taskId, variables);
}
taskService.complete(taskId);
}
}
4.3 处理"候选用户"任务
"总经理审批"任务设置了 candidateUsers="zhang_san,li_si"。处理方式与候选组类似,但签收时使用 addCandidateUser 或直接 claim。
java
public void handleCandidateUserTask() {
// 查询候选用户任务
List<Task> candidateTasks = taskService.createTaskQuery()
.taskCandidateUser("zhang_san")
.list();
// 签收任务(如果该候选用户想处理)
taskService.claim(taskId, "zhang_san");
// 后续完成操作同上
}
5. 网关与分支规则详解
5.1 排他网关 (Exclusive Gateway)
排他网关(菱形)用于在多个流出顺序流中选择一条且仅一条 路径。选择依据是每个流上定义的条件表达式 (默认为 true 的流可作为默认路径)。
在我们的流程中:
- 当
${days <= 3}为true时,走flowToManager路径。 - 当
${days > 3}为true时,走flowToManagerThenCEO路径。 - 重要 :条件表达式应互斥,确保只有一条路径被选中。如果多个条件为真,引擎会选择第一个为真的顺序流。如果所有条件都不为真且没有默认流,引擎会抛出异常。
5.2 条件表达式与流程变量
条件表达式使用 JUEL (Java Unified Expression Language) 语法,可以访问流程变量。例如:
#{day > 5}(UEL 表达式)${days <= 3}(简化表达式)
变量可以在流程启动时传入,也可以在任务完成时通过 taskService.setVariable 设置。
5.3 其他常用网关
- 并行网关 (Parallel Gateway) :所有流出路径同时执行,常用于会签、并行审批。
- 包容网关 (Inclusive Gateway) :满足条件的流出路径都可以执行,用于多分支并行。
- 事件网关 (Event Gateway):基于事件(如消息、信号)决定路径。
6. 完整业务场景演示
下面我们通过一个 @SpringBootTest 测试类,串联整个流程。
java
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.test.Deployment;
import org.flowable.task.api.Task;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class LeaveApprovalProcessTest {
@Autowired
private ProcessDeploymentService deploymentService;
@Autowired
private ProcessRuntimeService runtimeService;
@Autowired
private TaskHandleService taskHandleService;
@Autowired
private CandidateGroupTaskService groupTaskService;
@Autowired
private TaskService taskService;
@Test
void testCompleteLeaveProcess_ShortLeave() {
// 1. 部署流程
deploymentService.deployProcessFromClasspath("leave-approval.bpmn20.xml");
// 2. 员工"张三"提交一个2天的请假申请
String processInstanceId = runtimeService.startLeaveProcess("zhang_san", 2);
assertNotNull(processInstanceId);
// 3. 张三完成"提交申请"任务(assignee任务)
List<Task> zhangTasks = runtimeService.getTasksByAssignee("zhang_san");
assertEquals(1, zhangTasks.size());
taskHandleService.handleAssigneeTask(zhangTasks.get(0).getId(), "zhang_san", "同意");
// 4. 流程到达"部门经理审批"(候选组任务)
// 假设"李四"是 dept_manager 组成员
List<Task> groupTasks = runtimeService.getTasksByCandidateGroup("dept_manager");
assertEquals(1, groupTasks.size());
Task managerTask = groupTasks.get(0);
// 李四签收并完成任务
taskService.claim(managerTask.getId(), "li_si");
Map<String, Object> vars = new HashMap<>();
vars.put("managerComment", "情况属实,准假");
taskService.complete(managerTask.getId(), vars);
System.out.println("部门经理李四已完成审批,审批意见: " + vars.get("managerComment"));
// 5. 由于请假天数为2天(≤3天),根据排他网关条件,流程应直接跳转到"人事归档"任务
// 验证流程实例是否仍在运行
long activeTaskCount = taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.active()
.count();
assertEquals(1, activeTaskCount, "流程应有一个活跃任务(人事归档)");
// 6. 查询人事归档任务(assignee为hr_staff)
List<Task> hrTasks = runtimeService.getTasksByAssignee("hr_staff");
assertEquals(1, hrTasks.size(), "应有一个人事归档任务");
Task hrTask = hrTasks.get(0);
assertEquals("人事归档", hrTask.getName());
// 7. HR人员完成归档任务
Map<String, Object> hrVars = new HashMap<>();
hrVars.put("archiveResult", "已归档至员工档案");
hrVars.put("archiveDate", new Date());
taskHandleService.handleAssigneeTask(hrTask.getId(), "hr_staff", "归档完成");
// 8. 验证流程已结束
long finalTaskCount = taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.active()
.count();
assertEquals(0, finalTaskCount, "流程应已结束,无活跃任务");
// 9. 查询历史流程实例,确认流程正常结束
HistoricProcessInstance historicProcess = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();
assertNotNull(historicProcess);
assertNotNull(historicProcess.getEndTime(), "流程实例应有结束时间");
System.out.println("请假流程(短假)测试完成,流程正常结束");
}
@Test
void testCompleteLeaveProcess_LongLeave() {
// 1. 部署流程(如果之前已部署,这里可以跳过)
deploymentService.deployProcessFromClasspath("leave-approval.bpmn20.xml");
// 2. 员工"王五"提交一个5天的请假申请(>3天)
String processInstanceId = runtimeService.startLeaveProcess("wang_wu", 5);
assertNotNull(processInstanceId);
// 3. 王五完成"提交申请"任务
List<Task> wangTasks = runtimeService.getTasksByAssignee("wang_wu");
assertEquals(1, wangTasks.size());
taskHandleService.handleAssigneeTask(wangTasks.get(0).getId(), "wang_wu", "同意");
// 4. 部门经理审批(候选组任务)
List<Task> groupTasks = runtimeService.getTasksByCandidateGroup("dept_manager");
assertEquals(1, groupTasks.size());
Task managerTask = groupTasks.get(0);
// 经理"赵六"签收并审批
taskService.claim(managerTask.getId(), "zhao_liu");
Map<String, Object> managerVars = new HashMap<>();
managerVars.put("managerComment", "请假时间较长,建议总经理审批");
managerVars.put("approvalResult", "同意");
taskService.complete(managerTask.getId(), managerVars);
System.out.println("部门经理赵六审批完成,流程进入总经理审批环节");
// 5. 验证流程进入总经理审批环节
List<Task> ceoTasks = runtimeService.getTasksByCandidateUser("zhang_san");
assertEquals(1, ceoTasks.size(), "应有一个总经理审批任务");
Task ceoTask = ceoTasks.get(0);
assertEquals("总经理审批", ceoTask.getName());
// 6. 总经理"张三"签收并审批
taskService.claim(ceoTask.getId(), "zhang_san");
Map<String, Object> ceoVars = new HashMap<>();
ceoVars.put("ceoComment", "同意,注意工作交接");
ceoVars.put("finalApproval", "批准");
taskService.complete(ceoTask.getId(), ceoVars);
System.out.println("总经理张三审批完成");
// 7. 流程进入人事归档环节
List<Task> finalHrTasks = runtimeService.getTasksByAssignee("hr_staff");
assertEquals(1, finalHrTasks.size());
Task finalHrTask = finalHrTasks.get(0);
// 8. HR完成归档
Map<String, Object> finalHrVars = new HashMap<>();
finalHrVars.put("archiveLocation", "长期假档案柜");
finalHrVars.put("note", "5天长假,已通知考勤部门");
taskHandleService.handleAssigneeTask(finalHrTask.getId(), "hr_staff", "归档完成");
// 9. 验证流程结束
long finalTaskCount = taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.active()
.count();
assertEquals(0, finalTaskCount, "长假流程应已结束");
HistoricProcessInstance historicProcess = historyService.createHistoricProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();
assertNotNull(historicProcess.getEndTime());
System.out.println("请假流程(长假)测试完成,流程正常结束");
}
@Test
void testProcessWithRejection() {
// 测试驳回场景:经理审批不通过,流程直接结束
deploymentService.deployProcessFromClasspath("leave-approval.bpmn20.xml");
String processInstanceId = runtimeService.startLeaveProcess("li_ming", 2);
// 申请人完成任务
List<Task> applicantTasks = runtimeService.getTasksByAssignee("li_ming");
taskHandleService.handleAssigneeTask(applicantTasks.get(0).getId(), "li_ming", "提交");
// 经理审批时选择"驳回"
List<Task> managerTasks = runtimeService.getTasksByCandidateGroup("dept_manager");
Task managerTask = managerTasks.get(0);
taskService.claim(managerTask.getId(), "manager_user");
// 设置驳回变量,并在BPMN中配置相应的网关条件
Map<String, Object> rejectVars = new HashMap<>();
rejectVars.put("approvalResult", "reject");
rejectVars.put("rejectReason", "项目期间,不予批准");
taskService.complete(managerTask.getId(), rejectVars);
// 验证流程结束(实际项目中需要在BPMN中配置驳回路径)
System.out.println("驳回场景测试完成");
}
### 6.1 测试运行效果展示
运行上述测试用例后,控制台会输出详细的流程执行日志。以下是测试运行时的关键节点输出示例:
**短假流程(2天)控制台输出:**
流程部署成功,部署ID: 2501
流程定义ID: leaveApproval
流程启动成功,实例ID: 5001
用户 zhang_san 已完成任务: 提交申请
用户 li_si 已签收任务: 12510
部门经理李四已完成审批,审批意见: 情况属实,准假
用户 hr_staff 已完成任务: 人事归档
请假流程(短假)测试完成,流程正常结束
**长假流程(5天)控制台输出:**
流程部署成功,部署ID: 2502
流程启动成功,实例ID: 5002
用户 wang_wu 已完成任务: 提交申请
用户 zhao_liu 已签收任务: 12520
部门经理赵六审批完成,流程进入总经理审批环节
用户 zhang_san 已签收任务: 12525
总经理张三审批完成
用户 hr_staff 已完成任务: 人事归档
请假流程(长假)测试完成,流程正常结束
6.2 Flowable 管理界面截图示例
如果你启用了 Flowable 的 REST API 和管理应用,可以在 http://localhost:8080/flowable-ui 查看流程执行情况:
-
流程定义列表 :展示已部署的请假审批流程

-
运行中流程实例 :显示当前活跃的流程实例及其状态

-
任务列表 :按处理人/候选组展示待办任务

-
历史流程查询 :查看已结束流程的完整执行轨迹

注意 :上图仅为示意,实际运行时可截取 Flowable UI 的真实界面。要启用管理界面,需添加
flowable-spring-boot-starter-ui依赖并配置相应权限。
6.3 关键验证点
通过测试代码和运行日志,我们可以验证:
- 流程路由正确性:2天假期直接经理审批→人事归档;5天假期经理审批→总经理审批→人事归档。
- 任务分配机制 :
- 指定处理人任务(提交申请、人事归档)只能由指定用户处理。
- 候选组任务(部门经理审批)可由组内任意成员签收处理。
- 候选用户任务(总经理审批)可由指定候选用户处理。
- 变量传递 :请假天数
days变量正确驱动排他网关分支选择。 - 流程完整性:所有测试用例均正常结束,历史记录完整。