Flowable 工作流引擎进阶实战(高级篇)
本文承接《Flowable 工作流引擎入门教程(基础篇)》,深入任务分配、流程变量、监听器等进阶主题,并补充多实例、网关策略与历史查询等实战必会内容。
入门篇我们跑通了一个「经理审批」的请假流程,但现实业务的复杂度远不止如此:审批人可能是动态的、流程要携带业务数据、某些节点要在创建时自动指定负责人。这些都要靠本篇的进阶特性解决。
一、任务分配与流程变量
1.1 固定分配
最简单的方式:在画流程图或 BPMN 文件里直接写死 assignee:
xml
<userTask id="approveTask" name="经理审批" flowable:assignee="manager"/>
缺点是人固定死了,换个人就得改流程定义,只适合 demo。
1.2 表达式分配(UEL)
Flowable 使用 UEL(Unified Expression Language) 解析表达式,支持两种形式:
- 值表达式(Value expression):解析为一个值
- 方法表达式(Method expression):调用一个方法
scss
${myVar} // 值表达式,读取流程变量
${myBean.myProperty}
${printer.print()} // 方法表达式,无参要带空括号
${myBean.addNewOrder('orderName')} // 带字面量参数
${myBean.doSomething(myVar, execution)}
${myBean}默认指向 Spring 容器里的 Bean(需集成 Spring)。不集成 Spring 时,可通过ProcessEngineConfiguration注册自定义解析器。
值表达式实战:在 BPMN 里把审批人写成变量,启动时绑定:
java
@Test
public void testRunProcess() {
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
RuntimeService runtimeService = processEngine.getRuntimeService();
Map<String, Object> variables = new HashMap<>();
variables.put("assignee0", "张三");
variables.put("assignee1", "李四");
ProcessInstance instance = runtimeService
.startProcessInstanceById("MyHolidayUI:1:4", variables);
System.out.println("流程定义ID:" + instance.getProcessDefinitionId());
System.out.println("流程实例ID:" + instance.getId());
}
启动后查 ACT_RU_TASK 表,会发现任务的 assignee 已是 张三------说明 UEL 表达式被成功解析。
1.3 监听器分配
监听器能在流程运行到特定节点时触发自定义 Java 逻辑,连 assignee 都不用在流程定义里写。最适合「按业务规则动态指定负责人」。
先实现 TaskListener:
java
public class MyTaskListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
System.out.println("监听器触发了:" + delegateTask.getName());
if ("提交请假流程".equals(delegateTask.getName())
&& "create".equals(delegateTask.getEventName())) {
delegateTask.setAssignee("小明");
} else {
delegateTask.setAssignee("小张");
}
}
}
监听器可绑定的事件类型:
| 事件 | 触发时机 |
|---|---|
create |
任务创建后 |
assignment |
任务被分配后 |
complete |
任务完成后 |
delete |
任务被删除后 |
all |
以上全部 |
在 Flowable UI(Modeler)里把监听器关联到用户任务即可。部署运行后,Task 表的 assignee 会变成 小明,证明监听器生效。
二、流程变量详解
流程执行时携带的数据在 Flowable 里叫变量(variable),持久化在数据库。它有两个主要用途:
- 在表达式/网关 里判断走向(如
approved == true走通过分支); - 在服务任务里作为外部调用的入参或结果存储。
流程实例、用户任务、执行(execution)都可以持有变量,每个变量对应 ACT_RU_VARIABLE 表的一行。
2.1 设置变量的 API
启动时设置:
java
ProcessInstance startProcessInstanceByKey(String processDefinitionKey,
Map<String, Object> variables);
运行时追加/修改:
java
void setVariable(String executionId, String variableName, Object value);
void setVariableLocal(String executionId, String variableName, Object value);
void setVariables(String executionId, Map<String, ? extends Object> variables);
void setVariablesLocal(String executionId, Map<String, ? extends Object> variables);
2.2 全局变量 vs 局部变量
| 类型 | 作用域 | 特点 |
|---|---|---|
| Global | 整个流程实例 | 变量名不可重复,后设覆盖先设 |
| Local | 单个任务/执行实例 | 各任务互不影响,可与 global 同名共存 |
举例:出差申请里,userId=zhangsan 是全局变量;而某个会签节点里「本部门主管意见」这种只在本节点有意义的数据,就该用 Local,避免污染流程级命名空间。
2.3 实战案例:出差申请
需求:员工提交出差申请单 → 部门经理审批 → 根据金额走不同分支(≤5000 直接通过,>5000 需总监审批)→ 财务备案。
java
@Test
public void testBusinessTrip() {
RuntimeService rs = processEngine.getRuntimeService();
Map<String, Object> vars = new HashMap<>();
vars.put("applicant", "王五");
vars.put("amount", 8000); // 金额决定分支
vars.put("assigneeManager", "经理A");
vars.put("assigneeDirector", "总监B");
ProcessInstance pi = rs.startProcessInstanceByKey("businessTrip", vars);
// 网关条件示例:${amount <= 5000} 走经理直批;${amount > 5000} 走总监审批
}
配合 1.2 的表达式分配,两个审批节点的 assignee 分别绑定 assigneeManager / assigneeDirector,流程自动路由且动态指定负责人。
三、多实例任务(会签)
现实常有「多人同时审批,全部/任一通过才算完成」的场景,这叫多实例(multi-instance) 。在 BPMN 里通过 flowable:collection + flowable:elementVariable 配置:
xml
<userTask id="signTask" name="会签" flowable:assignee="${assignee}">
<multiInstanceLoopCharacteristics isSequential="false"
flowable:collection="${assigneeList}"
flowable:elementVariable="assignee">
<completionCondition>${nrOfCompletedInstances == nrOfInstances}</completionCondition>
</multiInstanceLoopCharacteristics>
</userTask>
isSequential="false":并行会签(同时发给所有人);completionCondition:nrOfCompletedInstances == nrOfInstances表示「全部完成才结束」(全票通过);改成>= 1就是「一票通过」。
四、网关策略进阶
| 网关 | 语义 | 典型用法 |
|---|---|---|
| 排他网关(XOR) | 只走一条满足条件的分支 | 金额判断、状态分流 |
| 并行网关(AND) | 同时走所有分支,汇聚时等全部 | 多系统并行通知 |
| 包容网关(OR) | 走所有满足条件的分支 | 多条件组合 |
排他网关的条件用
<conditionExpression>写 UEL;并行网关不需要条件,汇聚节点会等所有进入的分支都到达才继续。
五、历史查询与运维
流程跑完的数据沉淀在 ACT_HI_*(历史表),通过 HistoryService 查询:
java
List<HistoricProcessInstance> list = historyService.createHistoricProcessInstanceQuery()
.processDefinitionKey("holidayRequest")
.finished() // 只查已结束
.orderByProcessInstanceEndTime().desc()
.list();
for (HistoricProcessInstance hpi : list) {
System.out.println(hpi.getStartUserId() + " 耗时:" +
(hpi.getEndTime().getTime() - hpi.getStartTime().getTime()) + "ms");
}
常见历史查询对象:HistoricActivityInstance(节点级)、HistoricTaskInstance(任务级)、HistoricVariableInstance(变量级)。做「流程效率分析」「超时预警」都靠它们。
六、事务与异常处理
Flowable 的命令默认在一个数据库事务内执行。自定义 JavaDelegate(服务任务)里若抛异常,引擎会回滚当前事务,流程停在出错节点,可通过重试机制恢复:
java
public class CallExternalSystemDelegate implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) {
// 调用外部系统;失败抛异常 → 流程事务回滚,节点不前进
externalClient.notify(execution.getVariable("employee").toString());
}
}
对于「外部系统偶发超时」类问题,建议在 delegate 内部做幂等重试,或结合边界事件(Boundary Event)+ 定时器事件做补偿。
七、小结
进阶篇我们补齐了工作流真正落地所需的「动态能力」:
- 三种任务分配:固定(简单)、UEL 表达式(灵活)、监听器(规则驱动);
- 流程变量:全局/局部作用域划分,是流程的「血液」;
- 多实例会签:并行/串行、全票/一票规则;
- 三类网关:排他/并行/包容的取舍;
- 历史查询与事务:让流程可观测、可恢复。
掌握这些,你已经能应对绝大多数企业级审批流场景。下一步可以深入事件子流程、信号/消息捕获、定时边界事件,让流程具备更强的异步与补偿能力。
参考资料
- Flowable 官方用户手册:tkjohn.github.io/flowable-us...
- 邓澎波《Flowable 工作流引擎课件》系列