多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Hot-Plug 实战:运行期管理 Solon 插件

Hot-Plug 实战:运行期管理 Solon 插件 业务模块每次更新都得重启整个应用吗小服务可能只是有点烦。可一旦你在跑一个挂了几十个集成模块的单体应用或者一个按区域下发路由规则的网关每一次重启都意味着一个中断窗口、一次连接排空外加围绕什么时候重启才安全的一番折腾。Solon 对这一类问题的答案是solon-hotplug—— 一个给业务插件提供热插拔hot-plug与热管理hot-management能力的基础扩展模块。这里的热意思正如你所期待更新扩展包时不需要重启主程序你可以通过接口或者 HTTP 端点、甚至数据库驱动的管理后台来管理插件生命周期。不过官方文档对这个取舍是坦诚的热插拔能力会带来新的开发约束。所以这篇文章的目标是讲清楚它是怎么工作的、约束有哪些、什么时候该用以及什么时候不该用。先定位它处在哪个位置动手之前先做个定位说明能省掉不少困惑。之前一篇文章介绍过 Solon 的两种 SPI 式扩展机制——E-Spi外部扩展把 jar 丢进目录、启动时发现加载和H-Spi热插拔。官方指引刻意保持保守一般情况下使用普通的外部扩展机制E-Spi。solon-hotplug就是实现 H-Spi 这一侧的模块。当你确实需要主进程持续运行时加载、启动、停止、卸载插件才应该找它。如果你的扩展包在部署时就是固定的E-Spi 更简单、约束更少。热插拔是一种要用纪律来换取的能力——而这篇要讲的正是这份纪律。基础 API加载与卸载一个 jar最底层的接口是PluginPackage。它是地基但文档注明通常你不会直接调它而是用管理 API。不过看一眼它其他一切都豁然开朗publicclassDemoApp{publicstaticvoidmain(String[]args){Solon.start(Test5App.class,args);FilejarFilenewFile(/xxx/xxx.jar);// 加载插件并启动PluginPackagejarPluginPluginPackage.loadJar(jarFile).start();// 卸载插件PluginPackage.unloadJar(jarPlugin);}}loadJar读取 jarstart()启动其中的插件unloadJar完成卸载。很简单。但如果你要按名称管理多个插件——真实的管理面需要的正是这个——那就升级到PluginManager。按名称热管理管理模型是给每个插件起一个名称指向一个jar 路径然后用load/start/stop/unload驱动它的生命周期。你可以在配置里声明注册表solon.hotplug:add1:/x/x/x.jar# 格式名称: jar文件add2:/x/x/x2.jar也可以在代码里注册插件——既然只是代码那从数据库读注册表、基于它搭出一个管理平台也完全可行PluginManager.add(add1,/x/x/x.jar);PluginManager.add(add2,/x/x/x2.jar);// PluginManager.remove(add2); // 从管理中移除一个插件接下来生命周期调用就很容易对外暴露了。这是文档里的一个最小示例通过 HTTP 启动和停止一个插件。publicclassApp{publicstaticvoidmain(String[]args){Solon.start(App.class,args,app-{// 启动一个插件app.router().get(start,ctx-{PluginManager.start(add1);ctx.output(OK);});// 停止一个插件app.router().get(stop,ctx-{PluginManager.stop(add1);ctx.output(OK);});});}}有两个行为值得记住因为它们让这个 API 很宽容start(add2)在插件未加载时会自动加载。unload(add2)在插件仍在运行时会先自动停止。所以四个操作可以安全地组合停止一个运行中的插件、换掉它的 jar、再重新启动——整个过程主应用从不宕机。纪律一个合格的插件必须清理什么这一部分才是真正决定热插拔能不能上生产的关键。当你停止一个插件时框架并不知道这个插件注册了哪些路由、任务、监听器或静态资源——只有插件自己知道。官方文档说得很直白与普通插件相比热插拔插件必须在preStop或stop中移除自己注册的资源这一点非常重要。官方示例在 stop 时反注册四类资源publicclassPlugin1ImplimplementsPlugin{AppContextcontext;StaticRepositorystaticRepository;Overridepublicvoidstart(AppContextcontext){this.contextcontext;// 扫描插件自身的组件this.context.beanScan(Plugin1Impl.class);// 注册自身的静态文件staticRepositorynewClassPathStaticRepository(context.getClassLoader(),plugin1_static);StaticMappings.add(/,staticRepository);}Overridepublicvoidstop()throwsThrowable{// 移除 HTTP 处理器基于前缀移除很方便Solon.app().router().remove(/user);// 移除定时任务JobManager.remove(job1);// 移除事件订阅context.beanForeach(bw-{if(bw.raw()instanceofEventListener){EventBus.unsubscribe(bw.raw());}});// 移除静态文件仓库StaticMappings.remove(staticRepository);}}四类资源每一类都有对应的清理调用插件在 start 时注册了什么在 stop 时它必须做什么通过context.beanScan(...)扫描组件遍历 bean用EventBus.unsubscribe反订阅EventListener通过StaticMappings.add(/, repo)注册静态文件StaticMappings.remove(repo)HTTP 路由Solon.app().router().remove(/user)用前缀整组移除定时任务JobManager.remove(job1)注意这里的不对称路由和任务按名称/前缀移除所以前缀要起得规整而事件监听器靠遍历自己的 bean 移除。如果插件漏掉其中一项第一次热替换看起来一切正常——第二次就开始泄漏幽灵插件的行为了。stop 方法不是可选的润色它是让热插拔安全的那份契约。约束如何打包一个能被拔出来的插件因为热插拔希望插件是领域独立的——尽量少跟其他东西交互这样它的资源才能被干净地拔走——文档给出了三条打包规则1. 包名必须独立。否则组件扫描器可能扫到别的插件的类。约定是主应用用xxx或xxx.main插件 1 用xxx.add1插件 2 用xxx.add2。2. 依赖要有意摆放。共享/公共依赖放进主程序包这样插件 jar 更小必须隔离的依赖放进插件包内部。3. 插件通过容器触达主程序而不是靠静态状态。用Solon.context().getBean(...)拿主程序的 bean用Solon.cfg()拿主程序的配置。这样依赖方向保持干净插件 → 容器 → 主程序资源绝不出现插件 → 插件内部状态。这三条规则就是热的实际代价。它们不难遵守但从第一天起就塑造了主应用与插件之间的边界设计。在 E-Spi 与 solon-hotplug 之间选择务实的看法是你的扩展需求处在一个光谱上。冷E-Spijar 放进外部目录启动时发现并装配。零额外约束对大多数场景完全够用。如果反正要发布才能变更这就是诚实的默认选择。热solon-hotplug运行时按名称管理 jar通过代码或管理面执行 load/start/stop/unload。代价是上面的打包纪律和 stop 清理契约。官方文档指出热插拔插件应当尽量保持领域独立并建议搭配DamiBus帮助解耦——设计插件边界时值得记在心里。如果你想看完整可运行的示例而不是片段官方仓库solon-examples/1.Solon/下有一个三模块演示demo1011-hotplug_common、demo1011-hotplug_main和demo1011-hotplug_plugin1。小结solon-hotplug给了 Solon 应用一个真正的运行时插件生命周期PluginPackage做一次性加载/卸载PluginManager做按名称的受管热替换外加给插件作者的一份清晰契约——在start里注册一切在stop里清理一切。这项能力对网关、区域化模块和长时间运行的服务尤其有用对它们而言一次重启就是一次生产事件。但诚实的结论还是文档开头那句常规扩展场景用 E-Spi只有当你确实需要运行时管理时才用热插拔。框架把热这条路径提供出来了却没有让冷这条路径变臃肿——对于一个以克制为哲学的框架这正是你希望被给予的那种取舍。
返回列表