插件运行时:装了就进路由,卸了就出清单
应用不是启动时 scanned 进来的,而是运行期从 plugins-dist 装载的。这篇拆开 PluginClassLoader、子 Spring 上下文、菜单幂等 upsert 与迁移执行四件事。
平台启动时,PluginClassPathLoader 在 Spring 之前执行,把 plugins-dist 下的插件 jar 挂到类加载路径上。之后每个应用由独立的 PluginClassLoader 加载,并创建自己的 Spring 子上下文。
为什么不用一个类加载器
- 应用之间依赖的第三方库版本可能不同,同一个类加载器会冲突。
- 卸载应用时要能释放它的类与静态状态,共享类加载器做不到。
- 应用之间的调用必须走平台 SDK 的契约 + 定位器,而不是直接抓对方的类。
平台依赖在应用的 pom 里一律是 provided,不打进应用 jar。这样应用只带自己的代码,平台升级时也不会出现两份 MyBatis-Plus / Spring。
安装不是「拷 jar」
一次安装至少要完成:校验包与签名、落位 jar、执行 migrations、写入应用实例、按 app.json 幂等 upsert 菜单与权限、登记前端产物。任何一步失败都要能回滚,不能留下「半装」状态。
菜单用 AppMenuPlanner 做幂等 upsert:同一个应用重复安装或升级,不会产生重复菜单;管理员改过应用声明的菜单,也会在下次同步时被纠正。菜单归属可追溯,管理员删不掉应用声明的菜单。
卸载与热加载
卸载时移除子上下文、关闭类加载器、清理路由清单与登录检查。开发模式下的 POST /app/dev/reload 只替换后端代码,不建表、不同步菜单、不部署前端产物——它不负责「首次安装」。
app-market:
plugin-runtime:
dev-mode: true
plugin-repo: ""
dev-base-dir: ""开发模式会从本地路径直接加载 jar,绕过包白名单与签名校验。能调用它的人等于能在宿主 JVM 里执行任意代码,绝不要在生产环境打开。
一句话总结
插件运行时的目标不是「让代码能跑」,而是「让装卸成为可重复、可回滚、可观测的操作」。这也是为什么安装、升级、卸载、启停各有一条完整的状态机,而不是几个脚本。