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。不过看一眼它,其他一切都豁然开朗:
public class DemoApp {
public static void main(String[] args) {
Solon.start(Test5App.class, args);
File jarFile = new File("/xxx/xxx.jar");
// 加载插件并启动
PluginPackage jarPlugin = PluginPackage.loadJar(jarFile).start();
// 卸载插件
PluginPackage.unloadJar(jarPlugin);
}
}
loadJar 读取 jar,start() 启动其中的插件,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 启动和停止一个插件。
public class App {
public static void main(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 时反注册四类资源:
public class Plugin1Impl implements Plugin {
AppContext context;
StaticRepository staticRepository;
@Override
public void start(AppContext context) {
this.context = context;
// 扫描插件自身的组件
this.context.beanScan(Plugin1Impl.class);
// 注册自身的静态文件
staticRepository = new ClassPathStaticRepository(context.getClassLoader(), "plugin1_static");
StaticMappings.add("/", staticRepository);
}
@Override
public void stop() throws Throwable {
// 移除 HTTP 处理器(基于前缀,移除很方便)
Solon.app().router().remove("/user");
// 移除定时任务
JobManager.remove("job1");
// 移除事件订阅
context.beanForeach(bw -> {
if (bw.raw() instanceof EventListener) {
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-Spi):jar 放进外部目录,启动时发现并装配。零额外约束,对大多数场景完全够用。如果反正要发布才能变更,这就是诚实的默认选择。
- 热(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,只有当你确实需要运行时管理时才用热插拔。 框架把"热"这条路径提供出来了,却没有让"冷"这条路径变臃肿——对于一个以克制为哲学的框架,这正是你希望被给予的那种取舍。

浙公网安备 33010602011771号