Flowable
简介
提示
本篇博客相关的所有案例源码地址: flowable-demo
Flowable是一个轻量级、开源的业务流程引擎(BPMN 2.0)。一个最基本的 Flowable 流程开发过程是,使用流程设计器建模流程(流程定义),将流程定义文件(.bpmn 或 .bpmn20.xml)部署到流程引擎上,通过流程引擎提供的服务类执行流程实例(一个定义可以被部署为多个实例,如请假流程,多个人请假相当于多个实例),自动保存流程数据到数据库。
流程设计器
workflow-bpmn-modeler是一个使用 vue2 开发的 GitHub 开源项目,它看着简洁却五脏俱全,作为入门流程设计器再合适不过了。国产的 bpmn-process-designer 提供了 vue3 版本,不但支持流程设计,还提供预览、导入等功能,但部分功能收费,可作为进阶参考学习,直接访问官网提供的流程设计平台玩一下怎么定义流程。
Flowable 的数据表
Flowable 在初始化时,会自动创建若干张数据表,用于存储流程定义、流程实例、任务、变量、历史数据等,Flowable 的数据库名称均以 ACT_ 开头,如下:
数据表 描述 ACT_RE_* RE 代表 repository,存储流程定义、流程静态资源(图片、规则等)等数据,是 Flowable 中最核心的数据表,也是存储量最大的表 ACT_RU_* RU 代表 runtime,存储流程运行时数据,如流程实例、任务、变量、异步任务等数据 ACT_HI_* HI 代表 history,存储流程的历史数据,如流程实例、任务、变量、历史表单、操作记录等数据 ACT_GE_* GE 代表 general,存储通用数据,如二进制流程文件 ACT_ID_* ID 代表 identity,存储用户、组等数据,可选服务,不常用 Flowable 的服务类
Flowable 引擎提供了多种不同功能的服务类,如下(看个眼熟即可,刚开始无需考虑细节):

Flowable 服务类 服务名 描述 RepositoryService 管理流程定义和部署,主要处理静态信息,如加载 BPMN 2.0 XML 文件到流程引擎 RuntimeService 负责启动流程定义,一个定义可能有多个实例同时运行,主要处理动态信息 TaskService 所有与任务相关的操作都归属于它,包括创建、查询、分派、认领和完成等 IdentityService 支持组和用户的管理,但是 Flowable 实际上在运行时不会对用户进行任何检查,需要结合其他权限系统使用 FormService 可选服务,提供对表单的支持 HistoryService 提供对流程历史数据的访问,如流程实例的启动时间、谁执行了哪些任务、完成任务所需的时间、每个流程实例的执行路径等 ManagementService 它允许检索数据库表和表元数据的信息,读取元数据通常需结合业务,因此这个服务类很少使用
整合 SpringBoot
接下来,我们将通过 Spring Boot 启动一个最简单的请假流程实例。
创建流程定义
使用流程设计器创建一个非常简单的请假流程,可以
这个流程定义内容。这个流程中只包含开始、结束事件和一个用户任务,最终效果是:- 流程发起人启动流程时提交参数(如请假天数、流程发起人 id)
- 用户李四专门负责请假审批(可获取发起人提交的参数)
- 审批通过,流程结束
现在无需关心各节点具体细节,后面章节会分别叙述他们的功能,流程定义效果如下(注意图中的流程标识key参数,它表示流程定义的唯一标识,启动流程实例时会用到它):

部署流程定义
使用 MySQL 作为持久化数据库,在配置中指定连接信息,Flowable 在启动时会自动检查和创建数据库表。
在 Spring Boot 中,resources/processes 目录下的任何流程定义文件(.bpmn 或 .bpmn20.xml)都会被自动部署(这个是可配置的),实际生产环境中,流程定义的内容通常由前端通过 API 的形式传递给后端,由后端保存到数据库,而不是将文件硬编码在项目目录中,下面提供了一个推荐的部署流程定义的参考代码。
<!-- Flowable 支持;artifactId 也可以使用 flowable-spring-boot-starter ,它是 flowable 全家桶,比较重量级。
我们使用其最核心的流程引擎 flowable-spring-boot-starter-process 就够了 -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-process</artifactId>
<version>${flowable.version}</version>
</dependency>
<!-- Spring Boot JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>server:
port: 8080
# Flowable配置
flowable:
# 历史记录级别;audit 记录节点、任务、变量等信息,它是默认值,也是推荐值
history-level: audit
# 每次应用启动时,检查或更新数据库表结构(生产环境可改为false)
database-schema-update: true
# 打开自动部署(自动加载 resources/processes 目录下的流程文件,生产环境建议关闭 )
check-process-definitions: true
# 邮件服务
mail:
server:
host: localhost
password: pass
port: 1025
use-ssl: true # 使用 ssl 通信
spring:
application:
name: flowable-demo
datasource:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
# 注意 nullCatalogMeansCurrent=true 这个配置项,它能解决 Flowable 首次启动时无法创建数据库表的问题(报错:xxx Table doesn't exist)
url: jdbc:mysql://localhost:3306/flowable-test?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8&nullCatalogMeansCurrent=true
username: root
password: root
hikari:
# 连接池名称,用于监控区分
pool-name: HikariCP
# 最小空闲连接数,建议设置为 CPU 核数的一半或业务低峰时的连接需求
minimum-idle: 10
# 最大连接池大小,建议设置为 CPU 核数 * 2 ~ 5,根据并发量和 MySQL 最大连接数调整
maximum-pool-size: 20
# 空闲连接存活时间,单位 ms(默认 600000,即 10 分钟);过短会频繁回收和创建,过长可能导致连接长时间闲置
idle-timeout: 600000
# 连接最大存活时间,单位 ms(默认 1800000,即 30 分钟);
# 建议 < MySQL wait_timeout(默认 28800 秒 = 8 小时),避免被 MySQL 强制断开
max-lifetime: 1800000
# 等待连接的最大时长,单位 ms(默认 30 秒),超过抛异常
connection-timeout: 30000
# 是否默认自动提交事务,建议保持 true,复杂事务交由 Spring 管理
auto-commit: true
# 检查连接有效性的超时时间,默认 5 秒
validation-timeout: 5000
# 连接泄露检测,单位 ms(比如 20 秒),超过未归还会打印 WARN 日志
leak-detection-threshold: 20000
# 连接保活时间,0 表示禁用;如果 MySQL 服务端的 wait_timeout 设置较短,可以启用(如 300000ms = 5 分钟)
keepalive-time: 0@Slf4j
@SpringBootTest
class FlowableDemoApplicationTests {
private final RepositoryService repositoryService;
private final RuntimeService runtimeService;
@Autowired
public FlowableDemoApplicationTests(RepositoryService repositoryService, RuntimeService runtimeService) {
this.repositoryService = repositoryService;
this.runtimeService = runtimeService;
}
@Test
void testDeploy() {
// 流程设计器设计的流程内容
String text = """
流程设计器设计的流程定义内容;可将流程设计器整合到自己项目中,由前端传递内容 ...
""";
// 部署流程到 flowable
repositoryService.createDeployment()
.addString("test.bpmn", text) // 字符串方式部署
.deploy();
// 验证部署
long count = repositoryService.createProcessDefinitionQuery().processDefinitionKey("流程定义 key").count();
Assertions.assertNotEquals(0, count);
// 获取流程定义 id
String deploymentId = repositoryService.createProcessDefinitionQuery().processDefinitionKey("流程定义 key")
.latestVersion().singleResult().getDeploymentId();
log.info("流程定义 id 为:{}", deploymentId);
}
}@Test
void testDelDeploy() {
// 删除正在运行的流程实例
String processInstanceId = "xxx";
runtimeService.deleteProcessInstance(processInstanceId, "删除原因");
// 删除流程定义
String deploymentId = "xxx";
repositoryService.deleteDeployment(deploymentId);
}提示
为了方便(懒得去跑测试用例),我们直接将第1节流程定义的内容粘贴到 process-demo.bpmn20.xml 文件中,放到 resources/processes 目录下,启动项目时让它自动部署。
可以试着多次启动项目,观察控制台输出和数据库表 act_re_deployment (流程部署表) 和 act_re_procdef (流程定义表) 的数据,每次启动都会在两张表中添加一条记录,但是他们会被添加上不同的版本号(设计如此,了解即可)。

启动一个流程实例
确保流程定义已经成功部署后,可以创建一个 RestController 来触发启动流程,假设用户 zhangsan 发起了这个请假流程,如下:
@SpringBootApplication
public class FlowableDemoApplication {
public static void main(String[] args) {
SpringApplication.run(FlowableDemoApplication.class, args);
}
}/**
* 使用 Flowable 处理请假流程示例
*/
@RestController
@RequestMapping("/leave")
@RequiredArgsConstructor
public class FlowableDemoController {
private final RuntimeService runtimeService;
private final TaskService taskService;
private final HistoryService historyService;
/**
* 发起一个流程
*
* @param employeeId 员工ID
* @param days 请假天数
* @return 流程实例ID
* 测试: localhost:8080/leave/start?employeeId=zhangsan&days=2
*/
@GetMapping("/start")
public String startLeaveProcess(@RequestParam("employeeId") String employeeId, @RequestParam("days") Integer days) {
// 设置流程参数
Map<String, Object> variables = new HashMap<>();
variables.put("initiator", employeeId);
variables.put("days", days);
ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder()
.processDefinitionKey("process_demo_key") // 注意,这里的 process_demo_key 是流程标识 key(唯一标识)
.variables(variables)
// .businessKey("") // 自己的业务 id(根据需要可选)
.start();
return "流程已启动,流程实例ID: " + processInstance.getId();
}
}提示
流程启动成功后,观察数据库表 act_ru_execution (流程实例表,保存所有正在运行的执行流) 和 act_hi_actinst (历史流程实例表) 的数据,可以看到新增了一些记录。
处理流程
现在流程已经被 zhangsan 启动,谁来处理这个流程呢?让我们回到流程定义,观察用户任务,在流程设计器中为这个用户任务指定了人员,如下:

<!-- 这是由流程设计器自动生成的部分代码,它和流程定义图中的内容等效 -->
<userTask id="user-task-001" name="用户任务" flowable:assignee="lisi">
<documentation>需要李四审批请假</documentation>
</userTask>可以发现,用户任务中 assignee 属性指定了李四来处理这个任务(下拉框是是写死的,值为 lisi),还可以结合你自己的系统,来动态指定这个值(可以是用户组、部门等等),后续会讲解,这里先别紧张,以最简单的方式达到目的,是理解 flowable 的最快途径。
下面的代码块包含了流程处理中几个经典的案例,如下:
/**
* 获取待办列表
*
* @param assignee 任务办理人
* @return 任务列表
* <p>
* 测试:localhost:8080/leave/pending/tasks?assignee=lisi
*/
@GetMapping("/pending/tasks")
public List<Map<String, Object>> getTasks(@RequestParam String assignee) {
// 1. 创建查询
List<Task> tasks = taskService.createTaskQuery()
.processDefinitionKey("process_demo_key") // 这里主要分离业务,如请假流程和报销流程可不能混杂在一起
.taskAssignee(assignee) // 指定办理人
.orderByTaskCreateTime().desc() // 按创建时间排序
// .listPage(0, 10) // 分页查询
.list(); // 不分页,查所有
// 2. 将Task对象转换为前端友好的Map结构
List<Map<String, Object>> result = new ArrayList<>();
for (Task task : tasks) {
Map<String, Object> taskInfo = new HashMap<>();
taskInfo.put("taskId", task.getId()); // 任务ID(办理任务时需要)
taskInfo.put("name", task.getName()); // 任务名称,如“提交请假申请”
taskInfo.put("createTime", task.getCreateTime()); // 创建时间
taskInfo.put("processInstanceId", task.getProcessInstanceId()); // 对应的流程实例ID
// 获取该流程实例的所有变量(这里就能获取到 张三 在提交流程时传递的请假天数等参数)
Map<String, Object> variables = runtimeService.getVariables(task.getProcessInstanceId());
taskInfo.put("variables", variables);
result.add(taskInfo);
}
return result;
}/**
* 办理任务
*
* @param taskId 要完成的任务ID
* @param remark 处理任务备注
* @return 完成结果
* <p>
* 测试:localhost:8080/leave/complete-task?taskId=xxx&remark=GoodJob
*/
@GetMapping("/complete-task")
public Map<String, Object> completeTask(@RequestParam("taskId") String taskId,
@RequestParam(value = "remark", required = false) String remark) {
Map<String, Object> variables = new HashMap<>();
if (remark != null) {
variables.put("remark", remark);
}
// 1. 先查询任务信息(在complete之前执行,不然执行后 task 就为空了)
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (task == null) throw new MyException("任务id有误!");
// 2. 保存需要的信息
String processInstanceId = task.getProcessInstanceId();
String processDefinitionId = task.getProcessDefinitionId();
// 3. 核心操作:完成任务,并设置流程变量
taskService.complete(taskId, variables);
// 4. 检查流程是否结束了
long activeTaskCount = taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.count();
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("taskCompleted", true);
result.put("processInstanceId", processInstanceId); // 流程实例 id
result.put("processDefinitionId", processDefinitionId); // 流程定义 id
result.put("processContinued", activeTaskCount > 0); // 流程是否进入下一步
result.put("processEnded", activeTaskCount == 0); // 流程是否结束
return result;
}/**
* 已办结任务列表
*
* @param userId 用户ID
* @return 已办任务列表
* <p>
* 测试: localhost:8080/leave/done/tasks?userId=lisi
*/
@GetMapping("/done/tasks")
public List<Map<String, Object>> getMyDoneTasks(@RequestParam String userId) {
List<HistoricTaskInstance> tasks = historyService.createHistoricTaskInstanceQuery()
.processDefinitionKey("process_demo_key")
.taskAssignee(userId) // 指定办理人
.finished() // 只查询已完成的
.orderByHistoricTaskInstanceEndTime().desc() // 按完成时间倒序
// .listPage(0, 10) // 分页查询
.list(); // 不分页,查所有
List<Map<String, Object>> result = new ArrayList<>();
for (HistoricTaskInstance task : tasks) {
Map<String, Object> taskMap = new HashMap<>();
taskMap.put("taskId", task.getId());
taskMap.put("taskName", task.getName());
taskMap.put("processInstanceId", task.getProcessInstanceId());
taskMap.put("startTime", task.getStartTime());
taskMap.put("endTime", task.getEndTime());
taskMap.put("duration", task.getDurationInMillis());
taskMap.put("assignee", task.getAssignee());
taskMap.put("deleteReason", task.getDeleteReason()); // 完成原因
// 获取该任务对应的流程实例的所有变量
Map<String, Object> variables = this.getProcessInstanceVariables(task.getProcessInstanceId());
taskMap.put("variables", variables);
// 如果觉得所有变量太多,也可以单独提取一些常用的业务变量(可选)
taskMap.put("businessKey", variables.get("businessKey"));
taskMap.put("approvalResult", variables.get("approved"));
taskMap.put("comments", variables.get("managerComment"));
result.add(taskMap);
}
return result;
}
/**
* 获取流程实例的所有历史变量
*
* @param processInstanceId 流程实例ID
* @return 变量Map
*/
private Map<String, Object> getProcessInstanceVariables(String processInstanceId) {
List<HistoricVariableInstance> variableInstances = historyService.createHistoricVariableInstanceQuery()
.processInstanceId(processInstanceId)
.list();
Map<String, Object> variables = new HashMap<>();
for (HistoricVariableInstance variable : variableInstances) {
variables.put(variable.getVariableName(), variable.getValue());
}
return variables;
}/**
* 查询我发起的流程列表
*
* @param userId 用户ID
* @return 我发起的流程列表
* <p>
* 测试: localhost:8080/leave/my-processes?userId=zhangsan
*/
@GetMapping("/my-processes")
public List<Map<String, Object>> getMyStartedProcesses(@RequestParam String userId) {
List<HistoricProcessInstance> processes = historyService.createHistoricProcessInstanceQuery()
.processDefinitionKey("process_demo_key")
.variableValueEquals("initiator", userId) // 指定发起人
.orderByProcessInstanceStartTime().desc() // 按开始时间倒序
// .listPage(0, 10) // 分页查询
.list(); // 不分页,查所有
List<Map<String, Object>> result = new ArrayList<>();
for (HistoricProcessInstance process : processes) {
Map<String, Object> processMap = new HashMap<>();
processMap.put("processInstanceId", process.getId());
processMap.put("processDefinitionName", process.getProcessDefinitionName());
processMap.put("startUserId", process.getStartUserId());
processMap.put("startTime", process.getStartTime());
processMap.put("endTime", process.getEndTime());
processMap.put("businessKey", process.getBusinessKey());
processMap.put("status", process.getEndTime() != null ? "已完成" : "进行中");
// 获取流程变量(无论流程是否结束都能获取)
processMap.put("variables", getProcessInstanceVariables(process.getId()));
result.add(processMap);
}
return result;
}我们以 lisi 的身份,执行一下上面的案例,效果如下:
zhangsan 启动请假流程后,下一步就到了用户任务节点,由于这个任务指定了 lisi 为处理人,此时查询 lisi 的待办任务列表,可以看到有一个任务,且谁请的假,请了几天也能看到:

lisi 根据待办列表中获取到的任务 id,调用办理接口即可:



截止目前,相信你已经对 flowable 的核心功能有了初步认识!接下来,我们将对 flowable 进行更深入的学习,包括:用户和组、流程变量和表单、事件和监听器、网关等常用功能。
用户和组
前面我们给用户任务指定了一个特定的处理人 lisi,用户任务还支持绑定多个候选人和候选组,默认情况下,任务只要被其中一个成员认领并处理完成,流程就会进入下一步;
Flowable 提供了 IdentityService 来管理用户和组,但通常情况下,我们自己的系统可能原本就存在用户、部门等功能(相当于用户和组),没有必要去额外使用 Flowable 的 IdentityService,下图是为用户任务指定候选组的示例:

<!-- 这是由流程设计器自动生成的,看看长啥样就行了 -->
<!-- 指定特定人员,assignee 可以传用户 id -->
<userTask id="bossApproval" name="部门领导审批" flowable:assignee="your_user_id_123"></userTask>
<!-- 多个候选组用逗号分隔 -->
<userTask id="hrApproval" name="HR审批" flowable:candidateGroups="dept_beijing,dept_shanghai"></userTask>
<!-- 多个候选人用逗号分隔 -->
<userTask id="teamTask" name="团队任务" flowable:candidateUsers="user_110,user_119,user_122"></userTask>上图流程设计器选择框数据是静态数据,我们只需找前端工程师重构一下流程设计器,替换掉选择器组件,使用我们自己系统的用户或部门 API 实现即可。
设计器前端改造我们就不讨论了,着重关注后端代码,假设现在用户任务的候选组和上图保持一致:
<userTask id="user-task-002" name="用户任务" flowable:candidateGroups="web,java"></userTask>将流程定义内容拷贝到 user-group-test.bpmn20.xml 文件中,放到 resources/processes 目录下即可。
我们假设现在王五来请假,李四属于 web 组成员,当王五提交请假流程时,李四的待办列表里将新增一条记录,处理流程示例如下(【办理】 和 【已办任务】 同上章一致,这里就不演示了):
/**
* 使用 Flowable 处理请假流程示例-用户组
*/
@RestController
@RequestMapping("/leave-group")
@RequiredArgsConstructor
public class FlowableUserGroupController {
private final RuntimeService runtimeService;
private final TaskService taskService;
private final MyFlowableUtil flowableUtil;
/**
* 发起一个流程
*
* @param employeeId 员工ID
* @param days 请假天数
* @return 流程实例ID
* 测试: localhost:8080/leave-group/start?employeeId=wangwu&days=30
*/
@GetMapping("/start")
public String startLeaveProcess(@RequestParam("employeeId") String employeeId, @RequestParam("days") Integer days) {
// 设置流程参数
Map<String, Object> variables = new HashMap<>();
variables.put("initiator", employeeId);
variables.put("days", days);
ProcessInstance processInstance = runtimeService.createProcessInstanceBuilder()
.processDefinitionKey("process_group_demo_key")
.variables(variables)
// .businessKey("") // 自己的业务 id(根据需要可选)
.start();
return "流程已启动,流程实例ID: " + processInstance.getId();
}
}/**
* 获取待办列表(包含当前用户关联的办理人、候选组、候选人相关所有任务)
*
* @param assignee 任务办理人
* @param group 所属部门(假设由前端传;实际由后端查询当前用户所属部门或组)
* @return 任务列表
* <p>
* 测试:localhost:8080/leave-group/pending/tasks?assignee=lisi&group=web
*/
@GetMapping("/pending/tasks")
public List<Map<String, Object>> getTasks(@RequestParam("assignee") String assignee,
@RequestParam("group") String group) {
// 1. 创建查询
List<Task> tasks = taskService.createTaskQuery()
.processDefinitionKey("process_group_demo_key")
// 使用 or 条件查询,任何满足其中一个条件的任务都会被查出
.or()
.taskAssignee(assignee) // 指定办理人
.taskCandidateGroup(group) // 查询当前用户所在候选组的任务(有可能一个用户同时在多个组,这里不做演示,写法去问问 AI 即可)
.taskCandidateUser(assignee) // 查询当前用户作为候选人的任务
.endOr() // 结束OR条件组
.orderByTaskCreateTime().desc()
.list(); // 不分页,查所有
// 2. 将Task对象转换为前端友好的Map结构
List<Map<String, Object>> result = new ArrayList<>();
for (Task task : tasks) {
Map<String, Object> taskInfo = new HashMap<>();
taskInfo.put("taskId", task.getId()); // 任务ID(完成任务时需要)
taskInfo.put("name", task.getName()); // 任务名称,如“提交请假申请”
taskInfo.put("createTime", task.getCreateTime()); // 创建时间
// ⭐ 标识任务类型,仅明确指定为办理人类型的任务才不用认领 ⭐
taskInfo.put("taskType", flowableUtil.determineTaskType(task, assignee, group));
taskInfo.put("processInstanceId", task.getProcessInstanceId()); // 对应的流程实例ID
// 获取该流程实例的所有变量
Map<String, Object> variables = runtimeService.getVariables(task.getProcessInstanceId());
taskInfo.put("variables", variables);
result.add(taskInfo);
}
return result;
}/**
* 认领任务
* 任务一旦被认领,将在其他人的待办列表中消失(直接将这个任务变成指定办理人任务类型);
* 根据待办列表提供的 taskType 字段判断是否需要认领或需要办理
*
* @param taskId 任务ID
* @param userId 认领用户ID
* @return 结果
* <p>
* 测试:localhost:8080/leave-group/claim-task?taskId=xxx&userId=lisi
*/
@GetMapping("/claim-task")
public String claimTask(@RequestParam("taskId") String taskId,
@RequestParam("userId") String userId) {
// 1. 检查任务是否存在且未被认领
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
if (task == null || task.getAssignee() != null) throw new MyException("任务已被他人认领!");
// 2. 执行认领操作()
taskService.claim(taskId, userId);
return "认领成功!";
}提示
在 OA 系统中,通常一个流程定义会对应一个特定的业务,按照业务划分菜单,典型的 OA 系统菜单结构示例:
待办事项
├── 请假审批 → processDefinitionKey = "leave_approval"
│ ├── 我的待办 (3)
│ ├── 我的已办
│ └── 我发起的
├── 费用报销 → processDefinitionKey = "expense_reimbursement"
│ ├── 待审批 (2)
│ ├── 已处理
│ └── 我的申请
└── 会议室预订 → processDefinitionKey = "meeting_room_booking"
├── 待确认 (1)
├── 已处理
└── 预订记录流程变量和表单
流程变量
在前面章节中已经简单使用了流程变量,当流程实例按步骤执行时,需要保存并使用一些数据(如请假天数、审批备注等),流程变量就是流程实例在运行过程中保存的数据。
流程变量的数据类型可以是基本类型(String、Integer 等),也支持复杂结构(如JSON 字符串),但要注意的是,流程变量不建议直接存放 Java 对象,而是建议将 Java 对象转为 JSON 字符串保存。
流程变量的作用域分为夸任务共享和仅用于当前任务,仅用于当前任务的变量被绑定到当前任务(taskId) 上,任务完成后在运行时查询不到,但历史表里会保留下来(注意 history level 设置为 audit 或更高)。
Flowable 的各个 xxxService 都可以通过调用 setVariable 或 setVariableLocal 设置流程变量,用法类似,以下为设置流程变量示例:
用户任务@Test void testVariable() { User user = new User("zhangsan", true, LocalDateTime.of(1998, 10, 13, 12, 0, 0)); String taskId = "a3735ece-923e-11f0-b3f8-0025c7031000"; taskService.setVariable(taskId, "info", JSONUtil.toJsonPrettyStr(user)); // 注意复杂类型需转换成 json taskService.setVariableLocal(taskId, "local", "这是当前任务变量,其他节点获取不到哦~"); Map<String, Object> variables = taskService.getVariables(taskId); // 打印所有变量,包括 共享变量 + 仅当前任务绑定变量 variables.forEach((k, v) -> log.info("打印所有变量:{}:{}", k, v)); variables.forEach((k, v) -> { taskService.removeVariable(taskId, k); log.info("删除变量:{}:{}", k, v); }); }服务任务/** * 复杂计算变量示例 * 服务任务(Service Task)是流程引擎提供的内置任务类型,用于执行一些复杂的业务逻辑,如调用外部服务、计算、数据库操作等,无需人工介入。 * 在流程设计器的服务任务中,为它绑定委托表达式(DelegateExecution),指定所在类路径即可,会自动执行 execute 方法 */ @Component public class CalcDelegate implements JavaDelegate { @Override public void execute(DelegateExecution execution) { // 获取 Integer qty = (Integer) execution.getVariable("qty"); Double price = (Double) execution.getVariable("price"); // 设置 execution.setVariable("total", qty * price); // 设置局部变量(只在当前 execution 有效) execution.setVariableLocal("tmpNote", "only for this step"); } }表单
在 Flowable 里,表单和用户任务(User Task) 紧密结合。Flowable 自带表单引擎,但需要定义一个 Json 格式的文件描述表单字段,实际开发中,我们更多的会高度自定义表单,而不是使用它自带的表单引擎。
在流程设计器中,可以给选中的用户任务绑定一个表单 key(formKey),后端将 formKey 附加到待办列表上,前端根据不同的 formKey 渲染出不同的表单页(formKey 可以和前端的组件名一致);

后端在待办列表中追加 formKey 字段:
// 1. 待办任务列表 List<Task> tasks = taskService.createTaskQuery() .processDefinitionKey("xxx") .or() .taskAssignee("xxx") // 指定办理人 .taskCandidateGroup("xxx") // 查询当前用户所在候选组的任务(有可能一个用户同时在多个组,这里不做演示,写法去问问 AI 即可) .taskCandidateUser("xxx") // 查询当前用户作为候选人的任务 .endOr() // 结束OR条件组 .orderByTaskCreateTime().desc() .list(); // 不分页,查所有 // 2. 将Task对象转换为前端友好的Map结构 List<Map<String, Object>> result = new ArrayList<>(); for (Task task : tasks) { Map<String, Object> taskInfo = new HashMap<>(); taskInfo.put("taskId", task.getId()); // 任务ID(完成任务时需要) // ⭐ 附加 formKey 属性,用于告知前端当前任务该用哪个表单组件 taskInfo.put("formKey", CharSequenceUtil.isBlank(task.getFormKey()) ? "" : task.getFormKey()); ... result.add(taskInfo); }
事件和监听器
在 Flowable 中,事件指流程运行过程中某些时间点或动作,如任务被创建、任务完成、流程启动等。而监听器需要绑定在事件上,比如在用户任务执行成功时发送邮件,执行成功表示事件,发送邮件表示监听器。
以用户任务为例,在流程设计器中,为它绑定监听器,你会发现可以同时绑定任务监听器和执行监听器:任务监听器仅适用于用户任务,监听时机(事件)包括任务本身的创建、完成、认领等等;执行监听器则比较通用,监听时机包括节点启动(start)、结束(end)、顺序流被执行(take,一般绑定到流程图的箭头上,表示走过);
下图表示添加执行监听器:

观察上图,你会发现在添加监听器时(无论是任务监听器还是执行监听器),可以选择三种调用方式:类(Java Class)、表达式(Java Expression)、委托表达式(Delegate Expression);他们的使用方式如下:
| 调用方式 | 说明 |
|---|---|
| 类 | 类需要根据场景实现如 JavaDelegate、ExecutionListener、TaskListener 等接口,而且流程设计器中要写全限定类名,非常不美观 |
| 表达式 | 写成 ${beanName.method(task)} 的形式,Flowable 会去 Spring 容器里找名为 beanName 的 Bean,然后调用它的方法,适合逻辑简单、只需要调用现有 Bean 的场景 |
| 委托表达式 | 写成 ${beanName} 的形式,引擎会从 Spring 容器里拿 beanName 这个 Bean,然后直接执行它,这是非常推荐的写法 |
由于委托表达式是最推荐的,本节仅演示它是如何实现的。假设我们为用户任务绑定了一个任务监听器,在任务完成时打印一下日志,实现方式如下:

@Slf4j
@Component("taskCompleteListener")
public class TaskCompleteListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
// 从任务或流程变量里取参数 initiator
Object initiator = delegateTask.getVariable("initiator");
log.info("【任务监听器】任务 {} 已完成,流程启动人是:{}", delegateTask.getName(), initiator);
}
}网关
网关主要用来控制流程走向,和流程变量结合使用,能让流程有条件地往不同路径走。仅需在流程设计器中,在与网关连接的流程线中绑定条件表达式即可:

至此,flowable 的关键技术点已介绍完毕,其他的如任务委派、会签、驳回、子任务等骚操作,由于笔者精力有限就不往下面写了,其实只要你将上面所有内容都过了一遍,大概理解了 Flowable 的整体流程,也就是一个举一反三的过程,随便问问 AI,相信你就能写出来了。

