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

相关推荐
NHuan^_^20 分钟前
SpringBoot3 整合 SpringAI 实现ai助手(记忆)
java·人工智能·spring boot
前进的李工32 分钟前
MySQL大小写规则与存储引擎详解
开发语言·数据库·sql·mysql·存储引擎
Mr_Xuhhh34 分钟前
从ArrayList到LinkedList:理解链表,掌握Java集合的另一种选择
java·数据结构·链表
CoovallyAIHub40 分钟前
Sensors 2026 | 从无人机拍摄到跑道缺陷地图,机场巡检全流程自动化——Zadar机场全跑道验证
数据库·架构·github
错把套路当深情1 小时前
Java 全方向开发技术栈指南
java·开发语言
han_hanker1 小时前
springboot 一个请求的顺序解释
java·spring boot·后端
杰克尼1 小时前
SpringCloud_day05
后端·spring·spring cloud
MaCa .BaKa1 小时前
44-校园二手交易系统(小程序)
java·spring boot·mysql·小程序·maven·intellij-idea·mybatis
希望永不加班1 小时前
SpringBoot 静态资源访问(图片/JS/CSS)配置详解
java·javascript·css·spring boot·后端
oh LAN2 小时前
RuoYi-Vue-master:Spring Boot 4.x (JDK 17+) (环境搭建)
java·vue.js·spring boot