升级 Spring Boot 4 后流程起不来、登录却报缺租户:Forge Admin 补上 applicationTaskExecutor 与旧 Sa-Tok

升级 Spring Boot 4 后流程起不来、登录却报缺租户:Forge Admin 补上 applicationTaskExecutor 与旧 Sa-Token Session

Forge Admin 是一套 Java 17 / Spring Boot / Vue 的开源后台框架,自带流程引擎、多租户、插件底座和鉴权会话。演示环境:www.dlforgelab.com:8084 。底座升到 Spring Boot 4.1.1 那天,现场不像「依赖树里多两行版本号」,更像两台机器同时报警:

  1. 流程服务起不来 :装配 ProcessEngine 时缺名为 applicationTaskExecutor 的 Bean;
  2. 管理端 Token 还在 Redis 里 ,业务接口却打出「设置租户上下文失败」「访问租户表...缺少租户上下文」------排障第一反应几乎全是改 TenantInterceptor。

两处都不是某个业务 Controller 写坏了,而是 Boot 4 的任务执行器装配约定变了 ,以及 Sa-Token 1.45 删掉了旧 Session 类型名 。Forge 在 commit 0ae90d31a(2026-10-13 10:11 CST)里用 AutoConfiguration 顺序、配置双保险、同包名空子类把坑钉死。开源仓库:gitee.com/ForgeLab/fo... (GitHub 镜像:github.com/yaomindong1... )。

TL;DR(可先收藏)

  1. Boot 4:上下文里已有自定义 Executor(如 flowEventExecutor、logTaskExecutor)时,默认不再 创建 applicationTaskExecutor。
  2. Flowable 8 的 ProcessEngineAutoConfiguration 按该 Qualifier 强制注入------缺 Bean,引擎就起不来。
  3. Forge 在 forge-plugin-flow 用 @AutoConfigureBefore(ProcessEngine...) + @ConditionalOnMissingBean(name="applicationTaskExecutor") 补齐;yml 加 spring.task.execution.mode=force 作双保险。
  4. Sa-Token ≤1.38 的 sa-token-redis-jackson 把 Session 写成 SaSessionForJacksonCustomized;1.45.0 已删该类 ;Jackson3 按 @class 解析 → InvalidTypeIdException。
  5. 读 Session 失败后,SessionHelper.getLoginUser → TenantInterceptor 打 ERROR → DefaultTenantLineHandler 抛「缺租户」------表象是租户,根因是反序列化。
  6. 修法:在 forge-starter-auth 保留同全限定名空子类 只读旧数据;或清 Redis Authorization:* 后重登。先查 Session,别先改租户拦截器。

版本矩阵(本稿场景)

组件 版本 / Artifact
Spring Boot 4.1.1(forge-server/.../forge-dependencies/pom.xml)
Flowable 8.0.0
Sa-Token 1.45.0(sa-token-spring-boot4-starter + sa-token-redis-jackson)
语言 / 前端 Java 17 / Vue 管理端
主修复提交 0ae90d31aba897ec959a1d3ef5d52443d03a1341

下面按「现象 → 根因 → 框架里怎么落」拆两个坑。重点不在背 API,而在 Forge 为什么把补丁放进 plugin / starter,而不是散落在业务模块。

坑 A:流程起不来------缺 applicationTaskExecutor

现象

按源码推演,发布前请作者验证:Boot 4 + Flowable 8 启动或装配 ProcessEngine 失败时,日志会指向缺少名为 applicationTaskExecutor 的 Bean。更让人懵的是:Forge 流程侧本身早就注册了业务线程池,并不是「完全没 Executor」。

触发「默认 Bean 被跳过」的自定义池至少有两处。流程事件异步池:

java 复制代码
// FlowAutoConfiguration.java L131--L141
@Bean(name = "flowEventExecutor")
public Executor flowEventExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(8);
    executor.setQueueCapacity(200);
    executor.setThreadNamePrefix("flow-event-");
    executor.setKeepAliveSeconds(60);
    executor.initialize();
    return executor;
}

日志 starter 里还有 LogThreadPoolConfig 的 @Bean("logTaskExecutor"),专门给异步落库操作日志用。这些池的本意 是隔离业务异步,不是顶替 Boot 默认的 applicationTaskExecutor------但在 Boot 4 的新约定下,它们会改变自动配置的分支选择。

根因:命名契约,不是「线程不够」

Boot 4 的 TaskExecutionAutoConfiguration 约定变了:只要上下文里已经有自定义 Executor,默认就不再创建 applicationTaskExecutor 。Flowable 8 的 ProcessEngineAutoConfiguration 却仍按该 Qualifier 强制注入。调用链可以压成四行:

text 复制代码
自定义 Executor 注册(flowEventExecutor / logTaskExecutor ...)
  → Boot4 TaskExecutionAutoConfiguration 跳过默认 applicationTaskExecutor
    → Flowable ProcessEngine 按 Qualifier 注入失败
      → 流程服务起不来

这是典型的框架级「命名契约」冲突:业务侧合理多池,引擎侧认死一个 Bean 名。只调大 corePoolSize、只查 Flowable 数据源,都碰不到根因。

Forge 怎么改:补在插件自动配置里

Forge 没有让每个业务服务手写一个 applicationTaskExecutor,而是把兼容收敛进 forge-plugin-flow------谁引入流程插件,谁自动带上装配补丁。

① 在 ProcessEngine 之前补 Bean (FlowApplicationTaskExecutorConfiguration.java L1--L35):

java 复制代码
@AutoConfiguration
@AutoConfigureBefore(name = "org.flowable.spring.boot.ProcessEngineAutoConfiguration")
public class FlowApplicationTaskExecutorConfiguration {

    public static final String APPLICATION_TASK_EXECUTOR_BEAN_NAME = "applicationTaskExecutor";

    @Bean(name = APPLICATION_TASK_EXECUTOR_BEAN_NAME)
    @ConditionalOnMissingBean(name = APPLICATION_TASK_EXECUTOR_BEAN_NAME)
    public AsyncTaskExecutor applicationTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("app-task-");
        executor.setKeepAliveSeconds(60);
        executor.initialize();
        return executor;
    }
}

三个细节值得抄进自己的框架:

  • @AutoConfigureBefore 用 类全名字符串,避免编译期强依赖 Flowable 装配类,插件模块边界更干净;
  • @ConditionalOnMissingBean(name=...) 保证别人已经提供同名 Bean 时不抢,可与 mode=force 共存;
  • Bean 名常量抽成 APPLICATION_TASK_EXECUTOR_BEAN_NAME,和 Flowable 约定的 Qualifier 字面量对齐,改名时只改一处。

② 写入 Boot 4 的 AutoConfiguration.imports,且排在 FlowAutoConfiguration 前面:

text 复制代码
com.mdframe.forge.starter.flow.config.FlowApplicationTaskExecutorConfiguration
com.mdframe.forge.starter.flow.config.FlowAutoConfiguration

顺序本身就是文档:先补齐引擎依赖的 Bean,再跑 Forge 自己的 Flow 自动配置(里面才注册 flowEventExecutor)。按源码推演,发布前请作者验证:若把两行顺序颠倒,在极端启动时序下仍可能先撞上 ProcessEngine 注入窗口------所以 imports 顺序不是「好看」,是契约。

③ 配置双保险 (forge-flow-server 的 application.yml L35--L38):

yaml 复制代码
# Boot 4:自定义 Executor 存在时默认不创建 applicationTaskExecutor;Flowable 8 强制依赖该 Bean
spring:
  task:
    execution:
      mode: force

mode=force 告诉 Boot:即便已有自定义 Executor,也强制创建默认任务执行器。Forge 选择 代码补 Bean + yml force 双轨------插件可随依赖落库,服务配置再兜一层,避免只改代码或只改配置时漏某个部署形态(单体 / 拆分 flow-server)。

表 1:Executor 装配链(可收藏)

环节 Boot 3 直觉 Boot 4 + Flowable 8 现实 Forge 落点
自定义 flowEventExecutor 等 与默认池无关 可导致默认 applicationTaskExecutor 不再创建 保留业务池,另补同名 Bean
Flowable ProcessEngine 常能吃到默认池 Qualifier 强制依赖该 Bean 名 @AutoConfigureBefore 抢在引擎前
只改业务代码 往往无效 根因在自动配置顺序 放进 forge-plugin-flow starter
运维开关 少见 spring.task.execution.mode=force application.yml 双保险
模块边界 随便写 @Configuration 可被扫包遗漏 META-INF/.../AutoConfiguration.imports 显式登记

坑 B:登录像租户坏了------旧 Session 类型读不出

现象:Token 还在,业务全像「缺租户」

升级后 Redis 里仍有 Authorization:*,Token 看着没过期。但读 Session 时出现:

text 复制代码
InvalidTypeIdException: Could not resolve type id
'cn.dev33.satoken.dao.SaSessionForJacksonCustomized'

随后链路可以用源码钉死(不是玄学):

  1. SessionHelper.getLoginUser() 在无显式身份、无任务上下文时,最终走到 StpUtil.getTokenSession().get("loginUser")(L28--L40);
  2. TenantInterceptor.preHandle 反射调用 getLoginUser;若抛的不是「未登录」类异常,就会落入 log.error("设置租户上下文失败", e)(L104--L105);
  3. 租户上下文没建起来,访问带租户列的表时,DefaultTenantLineHandler 在严格模式下抛:访问租户表[...]时缺少租户上下文(L89--L92)。

于是排障同学很容易去改多租户拦截器、放宽 strictMode、甚至怀疑数据权限------全是误诊 。仓库 code-copilot/memory/pitfalls/security.md(发现日期 2026-10-13)写得很狠:先查 Session 反序列化,别先改租户拦截器。

根因:类型 ID 指向了已删除的类

  • Sa-Token ≤1.38 的 sa-token-redis-jackson 序列化 Session 时写入 "@class":"cn.dev33.satoken.dao.SaSessionForJacksonCustomized";
  • 1.45.0 起官方 jar 删掉该类;
  • Forge 使用 sa-token-spring-boot4-starter,走 Jackson3 按 @class 解析 → 类型找不到 → 整段登录态读失败。

Token 键还在,只是 JSON 里的类型 ID 指向了一个当前 classpath 没有的类。这比「Session 过期」更隐蔽:Redis TTL 正常、前端还带着 Authorization,业务却一片 500。

误诊链可以画成:

text 复制代码
StpUtil.getTokenSession()
  → Jackson3 按 @class 实例化
    → 缺类失败(InvalidTypeIdException)
      → SessionHelper.getLoginUser 失败
        → TenantInterceptor 反射失败打 ERROR「设置租户上下文失败」
          → DefaultTenantLineHandler「缺少租户上下文」
            → 误诊为「租户拦截器坏了」

Forge 怎么改:同包名空子类 + 单测钉契约

① 同包名空子类,只为读旧数据 (包必须是 cn.dev33.satoken.dao,一个字母都不能差):

java 复制代码
package cn.dev33.satoken.dao;

import cn.dev33.satoken.session.SaSession;

/**
 * Sa-Token ≤1.38 sa-token-redis-jackson 写入 Redis 的 Session 类型。
 * 1.45.0 起该类从官方 jar 移除......保留空子类仅为兼容读旧数据,新写入走 SaSession。
 */
public class SaSessionForJacksonCustomized extends SaSession {

    private static final long serialVersionUID = -7852311037327718900L;

    public SaSessionForJacksonCustomized() {}

    public SaSessionForJacksonCustomized(String id) {
        super(id);
    }
}

放在 forge-starter-auth 而不是某个业务模块:鉴权底座升级时所有管理端 / 网关侧读 Session 的路径一起受益。注释也写清边界------不为了「继续用旧类型写」 ,新写入走 SaSession。

② 单测用真实 Redis 字段形状 + SaJsonTemplateForJackson3 (SaSessionForJacksonCustomizedCompatTest L1--L32):

java 复制代码
// 字段形状取自本机 Redis 存量 Authorization:login:token-session:*(1.38 redis-jackson 写出)
String legacyJson = """
        {"@class":"cn.dev33.satoken.dao.SaSessionForJacksonCustomized",\
        "id":"Authorization:login:token-session:legacy-token",\
        "type":"Token-Session","loginType":"login","loginId":null,\
        "token":"legacy-token","createTime":1710000000000,\
        "dataMap":{"@class":"java.util.concurrent.ConcurrentHashMap"},\
        "tokenSignList":["java.util.Vector",[]]}
        """;

SaSession session = new SaJsonTemplateForJackson3().jsonToObject(legacyJson, SaSession.class);
assertThat(session).isInstanceOf(SaSessionForJacksonCustomized.class);
assertThat(session.getId()).isEqualTo("Authorization:login:token-session:legacy-token");

这类测试的价值不在「覆盖率数字」,而在 把生产 Redis 的字段形状固化进仓库:下次再升 Sa-Token / Jackson,单测先红,而不是客户环境先红。

③ 运维备选 :清 Redis 中 Authorization:* 后让用户重登------新会话写 SaSession,不再依赖旧类型名。兼容类与清键是 二选一或双保险:要平滑升级就留空子类;能接受全员重登、想减少幽灵类型,就清键。

表 2:误诊链 vs 真根因(可收藏)

你看到的日志 容易误判成 实际先查 Forge 依据
InvalidTypeIdException + SaSessionForJacksonCustomized Jackson 全局配置坏了 Redis Session JSON 的 @class CompatTest 字段形状
设置租户上下文失败 TenantInterceptor 逻辑错 getLoginUser / getTokenSession 是否已炸 Interceptor L68--L105
访问租户表[...]缺少租户上下文 多租户没开 / 表配置错 上游 Session 是否读出 LoginUser DefaultTenantLineHandler L89--L92
Redis 仍有 Token 「已登录却 500」玄学 键在 ≠ 能反序列化成当前 classpath 类型 pitfalls security.md 2026-10-13

框架落地:为什么补丁要进 starter / plugin

Forge Admin 的模块习惯是「能力进底座,业务只消费」。这两处坑如果散落在各个 *-server:

  • flow 单体、flow 拆分服务、带流程的集成测试,都要各自抄一份 applicationTaskExecutor;
  • 管理端、开放能力入口凡是读 SessionHelper 的路径,都可能漏掉兼容类。

因此:

  • 坑 A 进 forge-plugin-flow:跟 Flowable 同生命周期,imports 顺序可控;
  • 坑 B 进 forge-starter-auth:跟 Sa-Token 同生命周期,包名兼容属于鉴权契约。

这和「业务里临时 new 一个线程池 / 临时 try-catch 吞掉租户异常」是两种产品观。后者能糊过演示,前者才能在 Boot 大版本升级时只改两处底座。

可带走的自检清单

升级 Boot 4 + Flowable 8 + Sa-Token 1.45 时,建议按这个顺序过一遍:

  1. 启动失败搜 applicationTaskExecutor:是否因自定义 Executor 导致默认 Bean 缺失?
  2. 有 Flowable:是否在 ProcessEngineAutoConfiguration 之前 补了同名 Bean?AutoConfiguration.imports 顺序对不对?
  3. 是否配置了 spring.task.execution.mode=force(或等价强制策略)?
  4. 登录后业务 500:先搜 InvalidTypeIdException / SaSessionForJacksonCustomized,再碰租户代码。
  5. 兼容类包名必须与 Redis 里 @class 逐字一致;单测用真实 JSON 形状,不要只测「能 new 出来」。
  6. 可接受全员重登时,清 Authorization:* 往往比长期维护幽灵类型更干净;要平滑升级则保留空子类。

按源码推演,发布前请作者验证:若环境从未用过 ≤1.38 的 redis-jackson Session,可能碰不到坑 B;但只要 Redis 里残留过旧 @class,Boot4 一上 Jackson3 就会集中爆发。坑 A 则几乎是「只要自定义了 Executor 又引了 Flowable 8」的稳定路径。

和本系列其它文的差异

  • 不是「插件独立 Flyway / @RequiresFeature / 离线许可证」(10-11);
  • 不是「复杂待办分页 COUNT(*) 解析失败」(10-12);
  • 本篇切口是 底座大版本升级的 Bean 契约 + Session 类型契约,故障反差在「流程起不来」和「登录却像租户全挂」。

Forge Admin 把兼容收敛在 starter / plugin 层:业务模块不用各自抄一遍补丁。源码检索关键字:FlowApplicationTaskExecutorConfiguration、SaSessionForJacksonCustomized、commit 0ae90d31a。

下一篇若继续写升级实战,会优先看 Admin JAR 主应用 classpath Flyway (与插件独立历史表不同切口),或 Boot 4 回归矩阵里其它「起得来但行为变了」的坑。欢迎 Star / Issue:gitee.com/ForgeLab/fo... 。

相关推荐
Sayai2 小时前
Hadoop 集群安全加固实战:YARN 未授权访问漏洞修复,firewalld 白名单 + Nginx 反向代理 Basic Auth 全流程
运维·hadoop·nginx·安全·网络安全
AIGC小尼2 小时前
本地 AI 漫剧流水线模型缓存与特征向量持久化:IPAdapter 预编码、特征池复用与内存池管理实战
人工智能·spring boot·缓存·预编码算法·comfyui·ai漫剧
砚凝霜2 小时前
【软考信息安全】第二十一章 网络设备安全增强技术与漏洞防护
网络·安全·web安全
acrel72 小时前
高速公路分布式光伏并网电能质量与安全保护方案应用
分布式·安全
IT研究室3 小时前
最新计算机毕业设计选题推荐-基于spring boot的幼儿园管理系统-网站-文档指导-Java-springboot
java·spring boot·课程设计
七夜zippoe3 小时前
Agent 权限边界:别让 AI 误删你的生产库(安全设计)
数据库·人工智能·安全·ai·权限边界·安全设计
aodunsoft3 小时前
安全月报 | 傲盾DDoS攻击防御2026年9月简报
网络·安全·ddos
威哥爱编程3 小时前
【AI全栈12-10】Spring Boot 用多模态做商品图审核 / 发票识别
java·spring boot·后端
威哥爱编程3 小时前
【AI全栈12-09】Spring Boot 用流式输出做打字机对话:点下去不再干等
java·spring boot·后端