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

相关推荐
jwn9992 小时前
Spring Boot 整合 Keycloak
java·spring boot·后端
宁波阿成2 小时前
OpenClaw 在 Ubuntu 22.04.5 LTS 上的安装与问题处理记录
java·linux·ubuntu·openclaw·龙虾
mldlds2 小时前
SpringBoot详解
java·spring boot·后端
kang_jin2 小时前
Spring Boot 自动配置
java·spring boot·后端
sg_knight2 小时前
如何用 Claude Code 做大型项目重构与架构优化
java·重构·架构·llm·claude·code·claude-code
码不停蹄Zzz2 小时前
C语言——神奇的static
java·c语言·开发语言
RDCJM2 小时前
mysql表添加索引
数据库·mysql
yuweiade2 小时前
Spring Boot中使用Server-Sent Events (SSE) 实现实时数据推送教程
java·spring boot·后端
丈剑走天涯3 小时前
kubernetes java app 部署使用harbor私服 问题集合
java·容器·kubernetes