本文记录两个在 Spring Boot + Flowable 项目中实际踩到的坑,场景是:监听流程结束事件,将各审批节点的意见和得分写入业务记录表。
坑一:流程结束后在监听器里查 historyService,historicTasks 返回空列表
问题描述
监听 Flowable 流程结束事件,在事件处理方法里用 historyService 查询该流程下所有已完成的历史任务,结果 list 为空(size = 0),但数据库里明明有数据。
代码如下:
java
@Override
protected void onEvent(BpmProcessInstanceStatusEvent event) {
if (BpmProcessInstanceStatusEnum.isProcessEndStatus(event.getStatus())) {
syncReviewRecords(event);
}
}
private void syncReviewRecords(BpmProcessInstanceStatusEvent event) {
String processInstanceId = event.getId();
// ❌ 问题:查出来是空列表
List<HistoricTaskInstance> historicTasks = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.finished()
.orderByHistoricTaskInstanceEndTime().asc()
.list();
for (HistoricTaskInstance historicTask : historicTasks) {
// 永远不会进入循环......
}
}
原因分析
这是一个经典的 Flowable + Spring 事务时序问题,搞清楚整个调用链就能理解。
调用链梳理
用户点击"审批通过/拒绝"
→ taskService.complete(taskId) ← 触发 Flowable 引擎命令
→ Flowable 引擎内部处理流程推进
→ 流程节点全部完成,触发 PROCESS_COMPLETED 事件
→ BpmProcessInstanceEventListener.processCompleted()
→ processInstanceService.processProcessInstanceCompleted()
↓
★ 此处 Spring ApplicationEventPublisher.publishEvent() 同步发布事件
↓
→ 我们的监听器 onEvent() 被同步调用 ← 仍在同一个事务里!
→ historyService.createHistoricTaskInstanceQuery()...list()
→ 直接查数据库 → 返回空!
核心原因
Flowable 引擎使用 DbSqlSession 来管理数据库操作。它采用的是先缓冲、事务结束时统一 flush 的机制:
- 任务完成时,历史记录(
ACT_HI_TASKINST)的 INSERT/UPDATE 语句被写入内存 Session 缓冲区,并不会立即执行 SQL。 - Session 缓冲区中的所有语句要等到整个 Flowable 命令(即外层事务)提交时才统一 flush 到数据库。
而 Spring 的 ApplicationEventPublisher.publishEvent() 是同步 的,我们的监听器在事件发布的瞬间就被调用,此时整个外层事务尚未提交 ,Flowable 的历史数据还没写进数据库。
所以 historyService 去数据库里查,自然是空的。
用一张图理解
时间轴 ──────────────────────────────────────────────────────▶
[事务开始]
│
├─ Flowable 处理任务完成,历史数据写入 DbSqlSession 内存缓冲
│
├─ PROCESS_COMPLETED 事件触发
│
├─ publishEvent() → 我们的监听器执行 → historyService 查数据库 → 空!❌
│
[事务提交] → DbSqlSession flush → 历史数据真正写入数据库
解决方案
使用 Spring 的 TransactionSynchronizationManager 注册事务提交后的回调 (afterCommit),把实际的查询和写入逻辑推迟到事务提交之后再执行。此时 Flowable 历史数据已经落库,查询就能正常返回。
java
private void syncReviewRecords(BpmProcessInstanceStatusEvent event) {
String processInstanceId = event.getId();
Long taskId;
try {
taskId = Long.parseLong(event.getBusinessKey());
} catch (NumberFormatException e) {
log.error("[syncReviewRecords] businessKey 解析失败,businessKey={}", event.getBusinessKey(), e);
return;
}
Integer eventStatus = event.getStatus();
// ✅ 注册事务提交后的回调,等 Flowable 历史数据落库后再查询
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
doSyncReviewRecords(processInstanceId, taskId, eventStatus);
}
});
}
private void doSyncReviewRecords(String processInstanceId, Long taskId, Integer eventStatus) {
// ✅ 此时事务已提交,数据已落库,可以正常查到历史任务
List<HistoricTaskInstance> historicTasks = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.finished()
.includeTaskLocalVariables() // 见坑二,顺手加上
.orderByHistoricTaskInstanceEndTime().asc()
.list();
if (historicTasks.isEmpty()) {
log.warn("[doSyncReviewRecords] 未查询到已完成的历史任务,processInstanceId={}", processInstanceId);
return;
}
for (HistoricTaskInstance historicTask : historicTasks) {
// 正常处理每个节点的审批数据......
}
}
注意 :
afterCommit()回调运行时没有活跃事务 。如果回调里需要写数据库(如reviewRecordMapper.insert()),要确保被调用的 Service 方法上有@Transactional,Spring 会为其开启新事务。
扩展:为什么 afterCommit 里可以正常查?
afterCommit() 执行时,外层的 Flowable 事务已经提交完毕。此时 historyService.createHistoricTaskInstanceQuery() 会通过 Flowable 的 CommandExecutor 发起一个全新的命令,获取新的数据库连接,开启新事务查询,完全看得到已提交的历史数据。
类比理解
这就好比你在银行柜台办理存款,柜员把存款单填好放在抽屉里(DbSqlSession 缓冲),还没盖章提交。这时你跑去 ATM 查余额(historyService 查询),当然还是旧数字。等柜员把单子提交盖章(事务提交)之后,你再去 ATM 查,余额才会更新。
坑二:historicTask.getTaskLocalVariables() 拿不到审批意见
问题描述
调用 historicTask.getTaskLocalVariables() 返回空 Map,导致审批意见(TASK_REASON)始终为 null。
原因
Flowable 将任务本地变量单独存储在 ACT_HI_VARINST 表,查询 ACT_HI_TASKINST 时默认不 JOIN 变量表 。不显式声明的话,getTaskLocalVariables() 永远返回空 Map。
解决方案
在查询链上加 .includeTaskLocalVariables():
java
// ❌ 修复前:taskLocalVariables 永远是空 Map
List<HistoricTaskInstance> tasks = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.finished()
.list();
// ✅ 修复后:加上 includeTaskLocalVariables()
List<HistoricTaskInstance> tasks = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.finished()
.includeTaskLocalVariables() // ← 这一行
.list();
同理,如果需要读取流程变量,也要加 .includeProcessVariables()。
总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
监听器内 historyService 查询返回空 |
监听器与 Flowable 引擎同处一个事务,历史数据尚在内存缓冲未 flush | 用 TransactionSynchronizationManager.registerSynchronization().afterCommit() 推迟到事务提交后执行 |
getTaskLocalVariables() 返回空 Map |
Flowable 查询历史任务时默认不 JOIN 变量表 | 查询时加 .includeTaskLocalVariables() |
遇到 Flowable + Spring 集成问题,首先要搞清楚当前代码处于哪个事务阶段,这是排查此类问题最重要的切入点。