改一行模板代码,重启等三十秒;上线一个新插件,停机影响全量用户——这两件事几乎每个 Java 后端开发者都经历过。Solon 框架提供了一套从开发到生产的完整热加载/热插拔方案:Debug 模式解决开发阶段的热更新,E-Spi 解决插件扩展的隔离加载,H-Spi 解决生产环境的热插拔。三者覆盖了不同的场景和风险边界,理解它们的分工比单独掌握任何一个都重要。
Debug 模式:开发阶段的热更新
Solon 的 Debug 模式面向的是开发调试场景,核心目标是减少"改一行等半天"的痛苦。启用方式很简单:
# application.yml
solon.debug: true
开启后,Solon 会监听以下资源的变化并自动热更新:
- 配置文件(
application.yml等)——修改后即时刷新Solon.cfg()中的值 - 模板文件(FreeMarker、Thymeleaf 等)——模板引擎会重新加载最新模板
- 静态资源(HTML、CSS、JS)——静态文件服务直接返回最新内容
需要注意的是,Debug 模式不会热加载 Java 类的变更。它解决的是"非代码资源"的反复修改问题。如果你改了一个 Controller 的逻辑,仍然需要重启。这个设计是刻意的——Java 类的热替换涉及类加载器冲突、实例状态不一致等复杂问题,Debug 模式选择不碰这块,保持行为可预测。
实际开发中,Debug 模式最常见的收益场景是模板调试。下面是一个典型的 FreeMarker 渲染接口:
@Controller
public class PageController {
@Mapping("/hello")
public ModelAndView hello(Context ctx) {
ModelAndView mv = new ModelAndView("hello.ftl");
mv.put("title", "Solon 热加载演示");
mv.put("items", List.of("插件", "热插拔", "Debug"));
return mv;
}
}
对应的模板文件 hello.ftl 放在 src/main/resources/templates/ 下:
<h1>${title}</h1>
<ul>
<#list items as item>
<li>${item}</li>
</#list>
</ul>
Debug 模式开启后,你修改 hello.ftl 的排版或内容,刷新浏览器即可看到效果,不需要重启应用。改配置同理——比如你把 solon.app.name 从 "demo" 改成 "demo-v2",调用 Solon.cfg().get("solon.app.name") 立刻返回新值。
E-Spi:插件扩展的隔离加载
E-Spi(Extension SPI)解决的是插件扩展问题。它的核心思路是:每个插件用独立的类加载器加载,主应用和插件之间通过接口契约通信,而不是直接依赖具体的实现类。
这带来两个关键好处:
- 依赖隔离——插件可以引用自己的第三方库版本,不会和主应用冲突。比如主应用用 Mybatis 3.5,插件可以用 Mybatis 3.6,两者共存不冲突。
- 按需加载——插件 jar 放在指定目录,启动时扫描加载,不需要在 pom 里硬依赖。
一个典型的 E-Spi 插件定义如下:
// 主应用定义接口契约(在公共模块中)
public interface ReportPlugin {
String name();
byte[] generate(ReportRequest request);
}
// 插件实现(在独立 jar 模块中)
@SolonMain
public class PdfReportPlugin implements ReportPlugin {
@Override
public String name() {
return "pdf-report";
}
@Override
public byte[] generate(ReportRequest request) {
// 插件内部可以使用自己的 PDF 库版本
// 不会和主应用的依赖冲突
PdfDocument doc = new PdfDocument();
doc.addContent(request.getTitle());
doc.addTable(request.getData());
return doc.toBytes();
}
}
主应用加载和使用 E-Spi 插件:
@Component
public class ReportService {
// 通过 E-Spi 机制注入插件实例
@Spi("pdf-report")
private ReportPlugin pdfPlugin;
@Spi("excel-report")
private ReportPlugin excelPlugin;
public byte[] generateReport(String type, ReportRequest request) {
ReportPlugin plugin = switch (type) {
case "pdf" -> pdfPlugin;
case "excel" -> excelPlugin;
default -> throw new IllegalArgumentException("未知报表类型: " + type);
};
return plugin.generate(request);
}
}
插件 jar 的部署位置通常在 ext/ 或 plugins/ 目录,Solon 启动时自动扫描。目录结构:
my-app/
├── lib/
│ └── my-app-main.jar
├── ext/
│ ├── pdf-report-plugin.jar
│ └── excel-report-plugin.jar
└── application.yml
E-Spi 的局限在于:插件一旦加载,不能在运行时卸载或替换。它是"启动时加载、运行中固定"的模式。如果你需要运行时替换插件版本,就要用到 H-Spi。
H-Spi:生产级热插拔
H-Spi(Hot SPI)是 Solon 最重量级的热加载机制,面向生产环境的插件热插拔。它的核心能力是:在不停止主应用的前提下,加载新插件、卸载旧插件、替换插件版本。
H-Spi 的实现依赖三个关键设计:
- 独立类加载器——每个 H-Spi 插件实例拥有自己的
ClassLoader,卸载时释放类加载器,让 JVM 回收该插件的所有类和实例。 - 接口契约通信——主应用只持有接口引用,不持有实现类的任何直接引用。卸载时先清空接口引用,再释放类加载器,避免内存泄漏。
- 生命周期管理——插件有
init、start、stop、release四个阶段,热插拔时按顺序执行,确保资源正确释放。
下面是一个完整的 H-Spi 插件示例:
// 契约接口(公共模块)
public interface HotFeature {
String name();
void init(Context ctx);
Object execute(Map<String, Object> params);
void stop();
}
// 插件实现(独立 jar)
@SolonMain
public class AiSummaryFeature implements HotFeature {
private AiClient aiClient;
@Override
public String name() {
return "ai-summary";
}
@Override
public void init(Context ctx) {
// 初始化插件自己的资源
String apiKey = Solon.cfg().get("plugin.ai-summary.api-key");
aiClient = new AiClient(apiKey);
}
@Override
public Object execute(Map<String, Object> params) {
String text = (String) params.get("text");
return aiClient.summarize(text);
}
@Override
public void stop() {
// 释放资源,断开连接
if (aiClient != null) {
aiClient.close();
aiClient = null;
}
}
}
主应用通过 H-Spi API 管理插件的生命周期:
@Component
public class FeatureManager {
private final Map<String, HotFeature> features = new ConcurrentHashMap<>();
/** 加载并启动一个 H-Spi 插件 */
public void install(String pluginJarPath) {
// Solon H-Spi 加载机制:创建独立类加载器,实例化插件
HotFeature feature = SpiHotLoader.load(HotFeature.class, pluginJarPath);
feature.init(Solon.context());
features.put(feature.name(), feature);
System.out.println("插件已安装并启动: " + feature.name());
}
/** 卸载一个 H-Spi 插件 */
public void uninstall(String featureName) {
HotFeature feature = features.remove(featureName);
if (feature != null) {
feature.stop();
// SpiHotLoader 会释放该插件的类加载器
SpiHotLoader.release(featureName);
System.out.println("插件已卸载: " + featureName);
}
}
/** 热替换:卸载旧版本,加载新版本 */
public void replace(String featureName, String newPluginJarPath) {
uninstall(featureName);
install(newPluginJarPath);
}
/** 使用插件 */
public Object use(String featureName, Map<String, Object> params) {
HotFeature feature = features.get(featureName);
if (feature == null) {
throw new IllegalStateException("插件未安装: " + featureName);
}
return feature.execute(params);
}
}
一个简单的 REST 接口暴露热插拔操作:
@Controller
@Mapping("/feature")
public class FeatureController {
@Inject
private FeatureManager manager;
@Mapping("/install")
public String install(@Param String jarPath) {
manager.install(jarPath);
return "ok";
}
@Mapping("/uninstall")
public String uninstall(@Param String name) {
manager.uninstall(name);
return "ok";
}
@Mapping("/use")
public Object use(@Param String name, @Body Map<String, Object> params) {
return manager.use(name, params);
}
}
调用示例:
# 安装插件
curl "http://localhost:8080/feature/install?jarPath=/opt/plugins/ai-summary-v1.jar"
# 使用插件
curl -X POST "http://localhost:8080/feature/use?name=ai-summary" \
-H "Content-Type: application/json" \
-d '{"text": "Solon 是一个轻量级 Java 框架..."}'
# 热替换为新版本(先卸载再安装)
curl "http://localhost:8080/feature/uninstall?name=ai-summary"
curl "http://localhost:8080/feature/install?jarPath=/opt/plugins/ai-summary-v2.jar"
三种机制的边界与选择
把三种机制放在一起对比,选择逻辑就很清晰了:
| 机制 | 场景 | 类加载 | 运行时卸载 | 风险等级 |
|---|---|---|---|---|
| Debug 模式 | 开发阶段资源热更新 | 共享主类加载器 | 不支持 | 低 |
| E-Spi | 插件扩展、依赖隔离 | 独立类加载器 | 不支持 | 中 |
| H-Spi | 生产级热插拔 | 独立类加载器 | 支持 | 高 |
选择建议:
- 日常开发——开 Debug 模式,减少模板和配置的反复重启。
- 功能扩展——用 E-Spi,插件独立 jar、独立类加载器,启动时加载,稳定可靠。
- 需要运行时增减插件——用 H-Spi,但要严格遵循接口契约,确保
stop()方法释放所有资源。
H-Spi 的风险不可忽视。类加载器泄漏是 Java 热加载的经典坑——如果主应用有任何地方(哪怕是一个日志打印)直接引用了插件实现类的 Class 对象,该类加载器就无法被 GC 回收,反复热替换会导致 Metaspace 持续增长。防范手段:
- 主应用代码中永远只通过接口引用插件,不使用
instanceof检查实现类,不反射获取实现类的Class。 - 插件的
stop()方法必须关闭所有线程、连接、缓存,把内部引用全部清空。 - 热替换操作加限流和审批机制,不要让任意请求随意触发卸载。
实战清单
落地 Solon 热加载/热插拔时,按这个清单逐项确认:
- [ ] 开发环境
solon.debug: true已配置,模板和配置修改无需重启 - [ ] 插件接口契约放在独立的公共模块,主应用和插件 jar 都只依赖这个模块
- [ ] E-Spi 插件 jar 放在
ext/目录,启动时自动扫描加载 - [ ] H-Spi 插件的
stop()方法已释放所有线程、连接、内部引用 - [ ] 主应用代码中没有对插件实现类的直接引用或
instanceof检查 - [ ] H-Spi 热插拔操作有权限控制和操作日志,不暴露给未授权调用方
- [ ] 生产环境监控 Metaspace 使用量,设置合理的
-XX:MaxMetaspaceSize上限
三种机制不是互相替代的关系,而是分层协作:Debug 模式让开发快起来,E-Spi 让架构稳起来,H-Spi 让运维灵活起来。按场景选对工具,比追求"万能热加载"更务实。