Skip to content

应用生命周期

五个动作,全部不需要重启

动作后端行为
部署落位 + 热加载(卸载旧插件 → 用新 jar 建上下文 → 注册接口)
安装本机缺产物 → 从市场按 packageUrl 拉包 → 落位 → 加载
升级同上 + 菜单幂等 upsert
卸载(保留数据,默认)unload(关子上下文 + 类加载器 + 注销接口)+ 回收本租户授权;菜单行与数据保留
卸载(彻底删除)上面这些 + 删菜单 / 删数据表 / 删迁移记录 / 删落位产物 / 删安装记录
停用 / 启用unload / 重新 load

落位与加载是两件事

这是整套顺序保证的关键。

方法行为谁用
deploy(versionId)落位 + 立即加载控制台「部署」按钮(它不跑迁移
deploy(versionId, false)只落位安装 / 升级

安装器传 false,加载交给排在迁移之后的 assertBackendLoaded —— 它本来就会"磁盘上有就装上"。

落位 → 变更表结构 → 切换代码。 所以插件在启动时机直接读新加的列是安全的。

实测:任务日志里迁移脚本那行在偏移 221,后端产物那行在 269 —— 迁移确实在前。

「落位」落的是两样东西

清单字段落位到默认值
assets.backendapp-market.deploy.plugin-dir 根下plugins-dist(必须与 -Dloader.path 一致)
assets.frontend 所在目录整棵树app-market.deploy.frontend-dir/<appCode>/../plus-ui/app-market-dist/remote-apps

菜单里的 component = remote:exam/exam/settings 只是视图键, 真正执行的是那份独立 ESM。

升级会强制重新落位

「文件存在就跳过」只在同版本重装时成立。升级时若沿用旧产物会同时踩两个静默坑: 前端还是旧版、迁移脚本根本不落位

状态机

UNINSTALLED → INSTALLING → INSTALLED → ENABLED ⇄ DISABLED
                   ↓            ↓          ↓
                 FAILED      UNINSTALLING ←┘
                   ↓            ↓
              (可重试)      UNINSTALLED

ENABLED / DISABLED → UPGRADING → (回到原启停状态)

几个细节:

  • 应用对外可见的判据只有 ENABLED
  • 升级保留原启停状态 —— 已停用的应用升级完仍然是停用;
  • 「已处于目标状态」对启用/停用/卸载给出可读提示,而不是让状态机抛错;
  • FAILED 可以重试(重新 install / upgrade / uninstall)。

存量部署上「安装」走不通

迁移脚本回填的安装实例状态是 enabled,而状态机里 enabled → installing 非法, 升级到同一版本也会被拒(「当前已是该版本」)。

所以把已有模块落地成插件只有两条路:

  1. 在上架端点**「部署」**(落位 + 热加载);
  2. 发布一个更高版本后升级

给客户交付时建议直接发布 1.0.1,让"升级"成为自然路径。

安装的完整顺序

① 生成 app_install_task 记录
② 检查本机有没有产物
   ├─ 已有 → 就地使用(同一版本重装)
   └─ 没有 / 正在升级 → 从市场按 packageUrl 下载
        ├─ 校验 MD5 / SHA256
        ├─ 安全检查:产物白名单 + 签名校验
        ├─ 解包落位
        └─ 记录 active-plugins.txt
③ 执行包内 migrations/*.sql(按文件名排序,幂等,只做 up)
④ 加载后端插件(先卸载旧的 → 建新上下文 → 注册接口)   ← 必须在 ③ 之后
⑤ 同步菜单与授权(按清单幂等 upsert)
⑥ 任务置为成功

最后一道断言:后端真的加载了吗

assertBackendLoaded 处理的是一件事:「磁盘上有一个 jar」与「JVM 把它挂到 classpath 上」 是两件完全不同的事。

它依次问:

  1. 插件运行时:这个 appCode 当前加载的是哪个 jar?
  2. classpath 探针:这个 jar 真的在当前 JVM 里吗?
  3. 都没有但磁盘上有 → 直接 load;
  4. 磁盘上也没有 → 拒绝,并把失败原因分成两条互不相同的处置建议: "还没部署 → 去点部署"vs"启动方式不对 → 用 -Dloader.path 启动"。

停用与启用

动作行为顺序为什么是这样
停用撤本租户菜单授权 + unload先撤授权,用户立刻看不到入口
启用load 再放出菜单反过来用户会先看到入口却打不开

加载失败必须让启用失败,不能"看起来启用了"。

卸载的两种模式

前端弹窗只给两个选项,默认落在可恢复的那条路

步骤谁做说明
撤本租户授权 + unload安装器两种模式都做
删菜单(含后代)+ 所有角色授权 + 归属映射清理服务菜单是全局行,要先过"别人还在用吗"这道闸
DROP 数据表清理服务表名来自迁移脚本里的 CREATE TABLE
删迁移记录清理服务必须删,否则重装时建表脚本被"已执行过"跳过,表再也建不回来
删产物(jar / 前端目录 / active-plugins.txt 记录)落位服务尽力而为:Windows 上被 JVM 占用的 jar 删不掉会如实报出来
删安装记录清理服务排在任务状态落库之后,否则终态无处可写

两道安全闸

闸一:别人还在用就不动全局的东西。sys_menu、数据表、本机产物都是一个部署内全局共享的。只要还有别的租户装着, 就跳过菜单 / 表 / 迁移记录 / 产物的删除,只在任务日志里说清楚。

判据必须排除自己那条记录 —— 此时它的状态还没被改成 uninstalled, 不排除的话每次卸载都会"发现还有一个租户在用",功能直接失效。

闸二:平台表前缀受保护。 只删迁移脚本声明过的表,且名字命中 sys_ / app_ / gen_ / flow_ / act_ / qrtz_ / snailjob_ / undo_log 前缀时一律跳过并告警 —— 防止一个写错的脚本(CREATE TABLE IF NOT EXISTS app_install)把平台表带下水。

刻意不做

  • 任务记录app_install_task)保留 —— 它是审计线索。代价是历史行会显示「(应用已删除)」。
  • 对象存储里的文件不删 —— 平台不知道它们属于谁,也不该替用户删文件。

卸载里三个踩过的坑

删菜单必须连后代一起删

归属映射里只有清单声明过的菜单,而 sys_menu 里还有按钮(menu_type='F')—— 它们历史上来自平台初始化 SQL,从来不在映射表里。

只删映射到的行,按钮就变成孤儿(父菜单没了、自己还挂在 parent_id 上), 菜单管理页里一堆没有父级的按钮。实测踩到过(23 个 gallery 按钮)。

清归属映射要按三条线索并行

menu_id ∈ 本次删掉的菜单
  ∪ menu_key ∈ 清单声明的键
  ∪ version_id ∈ 该应用已知版本

少一条就会留下"死映射":某条映射挂在软删除的旧版本上(version_id 已不在目录里), 前两条都找不到它。而菜单同步是按 appId 跨版本解析 key → menu_id 的 —— 下次安装/升级复用它指向的已删除菜单 IDupdateById 影响 0 行、映射照样写回, 结果是**「安装成功、状态 enabled,但一个菜单都没有」**。

配套地,菜单同步现在会自愈:同步前把"映射指向已不存在的菜单行"的项丢掉、 按新菜单重建,并 WARN 出被丢的键。

表名解析要先剥注释

只认 CREATE TABLEALTER / INSERT / DROP 都不构成"拥有"某张表。 并且必须先剥注释 —— 脚本头部常写「请用 CREATE TABLE IF NOT EXISTS」这类说明, 不剥就会把注释里的名字当成表名。

回滚能力

动作支持吗
升级失败后重试✅ 已成功的迁移会跳过
退回旧版本✅ 从市场选旧版本安装(菜单按旧版清单同步)
应用卸载✅ 两种模式
自动回滚表结构不支持 —— 迁移只做 up

相关阅读

基于 MIT 协议开源 · 文档与官网由源码生成