Flowable + Spring 集成踩坑:流程结束监听器查询历史任务为空 & 获取不到审批意见

本文记录两个在 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 集成问题,首先要搞清楚当前代码处于哪个事务阶段,这是排查此类问题最重要的切入点。

相关推荐
Cry丶6 分钟前
架构师实战:Spring Authorization Server 落地企业级“无感” SSO(附设计映射与源码级接口剖析)
spring·spring security·oauth2.0·authorization·sso·无感登录
敖正炀26 分钟前
Spring 深度内核-核心容器与扩展机制-Spring 循环依赖终极剖析:三级缓存与 AOP 代理的纠缠
spring
Navicat中国39 分钟前
使用 Navicat 导入向导导入 Excel 数据时,系统提示导入成功,表中也能看到数据,但行数统计显示为 0,这是什么原因?
数据库·excel·导入
小怪吴吴42 分钟前
idea 开发Android
android·java·intellij-idea
嘻嘻哈哈樱桃44 分钟前
牛客经典101题题解集--动态规划
java·数据结构·python·算法·职场和发展·动态规划
gmaajt1 小时前
Golang怎么做国际化多语言_Golang i18n教程【核心】
jvm·数据库·python
一次旅行1 小时前
IDEA安装CC GUI新手指南
java·ide·intellij-idea
超梦dasgg1 小时前
Spring AI 智能航空助手项目实战
java·人工智能·后端·spring·ai编程
折哥的程序人生 · 物流技术专研1 小时前
从“卡死”到“秒过”:WMS销售数据跨库回填的极限优化之旅
数据库·机器学习·oracle
李可以量化1 小时前
DeepSeek 量化交易实战:用标准化提示词模板实现 AI 辅助交易决策
大数据·数据库·人工智能