1. 微内核、插件化架构
公司的产品是采用微服务架构的,虽然微服务提供了灵活的组装能力,但业务多了之后,一个功能往往涉及多个微服务,已经很难清楚哪个微服务是服务哪个应用的,最终变成还是需要部署所有的微服务,于是资源占用也就上来了,而且我们主要是面向企业级用户,因此也就会疑问哪种构建方式更适合我们的产品?后来为了信创,遇上 Solon,把公司产品从 Spring、Spring Cloud 迁移到 Solon 和 Solon Cloud 上来。在学习 Solon 的时候,官网中介绍插件部分,SPI,E-SPI,H-SPI。如果你验证过 E-SPI,一定会想在 Solon 上构建一个插件化的系统,因为太方便了。那我们的产品是不是采用微内核、插件化的架构会更好呢?于是我在自己的实验项目(Ray)中,逐步验证自己的想法,后来又碰上微前端(MicroApp),我觉得确实可行了 —— 前后端都打包到一个 Jar 里面,一个插件就是一个应用。但是自己前端太菜,花了很多时间去搞微应用的集成。好在 AI 来了,解决了前端集成的问题。最近我让 AI 把我的半成品插件管理改了改,终于实现了我想要的效果 —— 一个主应用,其余的都是插件。
插件 = 一个 jar,一个插件就是一个应用。
| 后端代码 | top/goldenyear/ray/** + META-INF/solon/*.properties | Solon Plugin SPI,控制器/服务/监听器 |
| 前端产物 | ui/{app}/ | 微前端 dist,由后端统一 serve |
| SQL 脚本 | db/migration/{code}/ | Flyway 版本化迁移,每插件一条版本线 |
| 自描述清单 | plugin-{code}.yml(jar 根) | 编码/版本/依赖约束/前端应用声明 |
内置插件通过 E-SPI 的方式放在 modules 目录下,不需要的插件从目录中删掉就可以了。扩展的/外部的插件的安装、启停、升级、卸载全部经由 infra 插件管理界面完成,server 不重启。
那么今天就来聊聊 Solon 的插件系统。
2. Solon 插件基础:Plugin SPI
2.1 Plugin 接口与发现协议
Solon 的插件是标准 SPI:实现 Plugin 接口,在 META-INF/solon/ 下用一个 properties 文件声明,启动时内核会进行扫描。添加一个依赖(或把 jar 放进扩展目录),插件自动激活 —— 没有开关类配置,发现即装载。
public interface Plugin {
void start(AppContext context) throws Throwable; // 应用初始化后调用
default void prestop() throws Throwable {} // ::stop 之前
default void stop() throws Throwable {} // Solon::stop 时
}
声明文件 META-INF/solon/{packname}.properties(文件名要求全局唯一,否则可能无法正常加载,如果使用 Gradle 编译,还可能出现文件配置文件覆盖问题):
solon.plugin=top.goldenyear.ray.module.bpm.XPluginImp
solon.plugin.priority=0 # 数值越大越先启动,默认 0
Ray 在此协议上追加了一行自己的约定 —— 清单指针:
# ray-modules/ray-module-bpm/src/main/resources/META-INF/solon/top.goldenyear.ray.module.bpm.properties
solon.plugin=top.goldenyear.ray.module.bpm.XPluginImp
solon.plugin.priority=0
ray.plugin.descriptor=plugin-bpm.yml # ray 插件运行时凭这一行找到插件清单
最后一行的描述是 ray 插件的入口:插件运行时扫描所有 META-INF/solon/*.properties,凡带 ray.plugin.descriptor 键的,就是一个 ray 插件。复用 Solon 原生协议意味着同一个 jar 既能编译期内置、也能运行期外置加载,两种形态零差异。
2.2 插件在生命周期中的位置
理解插件机制的关键是它在应用生命周期中的确切位置:
#mermaid-svg-XwrgFA5MIAY7sm5I{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XwrgFA5MIAY7sm5I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XwrgFA5MIAY7sm5I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XwrgFA5MIAY7sm5I .error-icon{fill:#552222;}#mermaid-svg-XwrgFA5MIAY7sm5I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XwrgFA5MIAY7sm5I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XwrgFA5MIAY7sm5I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XwrgFA5MIAY7sm5I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XwrgFA5MIAY7sm5I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XwrgFA5MIAY7sm5I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XwrgFA5MIAY7sm5I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XwrgFA5MIAY7sm5I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XwrgFA5MIAY7sm5I .marker.cross{stroke:#333333;}#mermaid-svg-XwrgFA5MIAY7sm5I svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XwrgFA5MIAY7sm5I p{margin:0;}#mermaid-svg-XwrgFA5MIAY7sm5I .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-XwrgFA5MIAY7sm5I .cluster-label text{fill:#333;}#mermaid-svg-XwrgFA5MIAY7sm5I .cluster-label span{color:#333;}#mermaid-svg-XwrgFA5MIAY7sm5I .cluster-label span p{background-color:transparent;}#mermaid-svg-XwrgFA5MIAY7sm5I .label text,#mermaid-svg-XwrgFA5MIAY7sm5I span{fill:#333;color:#333;}#mermaid-svg-XwrgFA5MIAY7sm5I .node rect,#mermaid-svg-XwrgFA5MIAY7sm5I .node circle,#mermaid-svg-XwrgFA5MIAY7sm5I .node ellipse,#mermaid-svg-XwrgFA5MIAY7sm5I .node polygon,#mermaid-svg-XwrgFA5MIAY7sm5I .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XwrgFA5MIAY7sm5I .rough-node .label text,#mermaid-svg-XwrgFA5MIAY7sm5I .node .label text,#mermaid-svg-XwrgFA5MIAY7sm5I .image-shape .label,#mermaid-svg-XwrgFA5MIAY7sm5I .icon-shape .label{text-anchor:middle;}#mermaid-svg-XwrgFA5MIAY7sm5I .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XwrgFA5MIAY7sm5I .rough-node .label,#mermaid-svg-XwrgFA5MIAY7sm5I .node .label,#mermaid-svg-XwrgFA5MIAY7sm5I .image-shape .label,#mermaid-svg-XwrgFA5MIAY7sm5I .icon-shape .label{text-align:center;}#mermaid-svg-XwrgFA5MIAY7sm5I .node.clickable{cursor:pointer;}#mermaid-svg-XwrgFA5MIAY7sm5I .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XwrgFA5MIAY7sm5I .arrowheadPath{fill:#333333;}#mermaid-svg-XwrgFA5MIAY7sm5I .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XwrgFA5MIAY7sm5I .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XwrgFA5MIAY7sm5I .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XwrgFA5MIAY7sm5I .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XwrgFA5MIAY7sm5I .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XwrgFA5MIAY7sm5I .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XwrgFA5MIAY7sm5I .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XwrgFA5MIAY7sm5I .cluster text{fill:#333;}#mermaid-svg-XwrgFA5MIAY7sm5I .cluster span{color:#333;}#mermaid-svg-XwrgFA5MIAY7sm5I div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XwrgFA5MIAY7sm5I .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XwrgFA5MIAY7sm5I rect.text{fill:none;stroke-width:0;}#mermaid-svg-XwrgFA5MIAY7sm5I .icon-shape,#mermaid-svg-XwrgFA5MIAY7sm5I .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XwrgFA5MIAY7sm5I .icon-shape p,#mermaid-svg-XwrgFA5MIAY7sm5I .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XwrgFA5MIAY7sm5I .icon-shape .label rect,#mermaid-svg-XwrgFA5MIAY7sm5I .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XwrgFA5MIAY7sm5I .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XwrgFA5MIAY7sm5I .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XwrgFA5MIAY7sm5I :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
停机阶段
启动阶段
Init lambda
AppInitEndEvent
Plugin.start
AppPluginLoadEndEvent
Bean 扫描 + 注入
AppBeanLoadEndEvent
AppContext.start / @Init
AppLoadEndEvent
::Running::
AppPrestopEndEvent
Plugin.prestop
AppContext.stop / @Destroy
Plugin.stop
AppStopEndEvent
三个对插件开发最重要的时序:
2.3 插件能做什么:四个扩展点
插件不只是“被动启动的模块”,它可以在 start 时通过 AppContext 的四个扩展机制编程式地增强框架:
| beanBuilderAdd(anno, handler) | 注册 Bean 构建器 | @Controller 构建器注册路由 |
| beanInjectorAdd(anno, handler) | 注册字段注入器 | @Inject 注入器解析 Bean/配置 |
| beanInterceptorAdd(anno, interceptor) | 注册方法拦截器 | @Transaction 拦截器包裹方法调用 |
| beanExtractorAdd(anno, extractor) | 注册方法提取器 | @CloudJob 提取器收集任务方法 |
Solon 生态里的 solon-web、solon-data 等框架件,本质上都是带 META-INF/solon/*.properties 的插件。Ray 的 ray-framework-plugin、ray-framework-flyway、各业务模块同样如此 —— Solon 本身就是微内核、插件化的架构。
3. 装载双轨:E-SPI 与 H-SPI
编译打包到 Jar 能解决确定性的组装(通过 Gradle 参数等),但 Ray 还需要两种运行期形态,对应 Solon 官方的两个扩展装载机制:
| Solon 机制 | solon.extend 配置扩展目录 | solon-hotplug(org.noear:solon-hotplug) |
| 装载方式 | 内核启动时 AppClassLoader.addJar | PluginManager 静态 API,运行期任意时刻 |
| ClassLoader | 共享 AppClassLoader | 独享 PluginClassLoader(父委托到 AppClassLoader) |
| Bean 容器 | 同一 AppContext | 子 AppContext(copyTo 派生) |
| 生效时机 | 重启生效 | 即时生效 |
| 可卸载 | 否(类不可卸载) | 是(unload 关闭 ClassLoader) |
| ray 中的角色 | 内置模块(core/bpm/press/dev/ai) | 动态插件(如 ray-plugin-home) |
| 发布物命名 | ray-module-{code}-{version}.jar,在 modules/目录下 | ray-plugin-{code}-{version}.jar,在 plugins/目录下 |
发布物布局(package.sh 产出):
#mermaid-svg-c8hg1n6zyMgSaS3F{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-c8hg1n6zyMgSaS3F .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-c8hg1n6zyMgSaS3F .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-c8hg1n6zyMgSaS3F .error-icon{fill:#552222;}#mermaid-svg-c8hg1n6zyMgSaS3F .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-c8hg1n6zyMgSaS3F .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-c8hg1n6zyMgSaS3F .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-c8hg1n6zyMgSaS3F .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-c8hg1n6zyMgSaS3F .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-c8hg1n6zyMgSaS3F .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-c8hg1n6zyMgSaS3F .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-c8hg1n6zyMgSaS3F .marker{fill:#333333;stroke:#333333;}#mermaid-svg-c8hg1n6zyMgSaS3F .marker.cross{stroke:#333333;}#mermaid-svg-c8hg1n6zyMgSaS3F svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-c8hg1n6zyMgSaS3F p{margin:0;}#mermaid-svg-c8hg1n6zyMgSaS3F .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-c8hg1n6zyMgSaS3F .cluster-label text{fill:#333;}#mermaid-svg-c8hg1n6zyMgSaS3F .cluster-label span{color:#333;}#mermaid-svg-c8hg1n6zyMgSaS3F .cluster-label span p{background-color:transparent;}#mermaid-svg-c8hg1n6zyMgSaS3F .label text,#mermaid-svg-c8hg1n6zyMgSaS3F span{fill:#333;color:#333;}#mermaid-svg-c8hg1n6zyMgSaS3F .node rect,#mermaid-svg-c8hg1n6zyMgSaS3F .node circle,#mermaid-svg-c8hg1n6zyMgSaS3F .node ellipse,#mermaid-svg-c8hg1n6zyMgSaS3F .node polygon,#mermaid-svg-c8hg1n6zyMgSaS3F .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-c8hg1n6zyMgSaS3F .rough-node .label text,#mermaid-svg-c8hg1n6zyMgSaS3F .node .label text,#mermaid-svg-c8hg1n6zyMgSaS3F .image-shape .label,#mermaid-svg-c8hg1n6zyMgSaS3F .icon-shape .label{text-anchor:middle;}#mermaid-svg-c8hg1n6zyMgSaS3F .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-c8hg1n6zyMgSaS3F .rough-node .label,#mermaid-svg-c8hg1n6zyMgSaS3F .node .label,#mermaid-svg-c8hg1n6zyMgSaS3F .image-shape .label,#mermaid-svg-c8hg1n6zyMgSaS3F .icon-shape .label{text-align:center;}#mermaid-svg-c8hg1n6zyMgSaS3F .node.clickable{cursor:pointer;}#mermaid-svg-c8hg1n6zyMgSaS3F .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-c8hg1n6zyMgSaS3F .arrowheadPath{fill:#333333;}#mermaid-svg-c8hg1n6zyMgSaS3F .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-c8hg1n6zyMgSaS3F .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-c8hg1n6zyMgSaS3F .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c8hg1n6zyMgSaS3F .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-c8hg1n6zyMgSaS3F .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c8hg1n6zyMgSaS3F .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-c8hg1n6zyMgSaS3F .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-c8hg1n6zyMgSaS3F .cluster text{fill:#333;}#mermaid-svg-c8hg1n6zyMgSaS3F .cluster span{color:#333;}#mermaid-svg-c8hg1n6zyMgSaS3F div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-c8hg1n6zyMgSaS3F .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-c8hg1n6zyMgSaS3F rect.text{fill:none;stroke-width:0;}#mermaid-svg-c8hg1n6zyMgSaS3F .icon-shape,#mermaid-svg-c8hg1n6zyMgSaS3F .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c8hg1n6zyMgSaS3F .icon-shape p,#mermaid-svg-c8hg1n6zyMgSaS3F .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-c8hg1n6zyMgSaS3F .icon-shape .label rect,#mermaid-svg-c8hg1n6zyMgSaS3F .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c8hg1n6zyMgSaS3F .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-c8hg1n6zyMgSaS3F .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-c8hg1n6zyMgSaS3F :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
dist/ray-{version}-{profile}/
ray-server-{version}.jar平台运行时(零业务模块),java -jar 直跑
app-prod.yml外部配置
webapp/特殊站点体外模板根
modules/ · E-SPI 扩展目录(solon.extend)
plugins/ · H-SPI 动态插件运行目录
ray-module-core-{version}.jar
ray-module-{bpm,press,dev,ai}-{version}.jarprod 全量;minimal 仅 core
ray-plugin-home-{version}.jar随包预置,界面安装后生效
3.1 E-SPI:官方模块的装载
app.yml 一行配置即启用:
solon:
# 官方模块 E-SPI 体外扩展:启动时自动加载 modules/ 下全部 jar
# (AppClassLoader.addJar,共享 ClassLoader,重启生效)
extend: "modules"
内核启动时把 modules/ 下所有 jar 经 AppClassLoader.addJar 加入 classpath,后续流程与编译期内嵌完全一致。开发的时候,默认设置为 profileEnv=dev 把模块直接编译进 fat jar(E-SPI 文档明言"分/合自由、加载时机一样"),调试体验与单体无异。
build.properties
profileEnv=de
build.gradle
if (profileEnv == "dev") {
// dev 全内嵌单体(fat jar 直跑调试):core 与其余模块编译进 jar(02 §5.1)
implementation(project(":ray-modules:ray-module-core"))
implementation(project(":ray-modules:ray-module-bpm"))
implementation(project(":ray-modules:ray-module-press"))
implementation(project(":ray-modules:ray-module-ai"))
implementation(project(":ray-modules:ray-module-dev"))
implementation(project(":ray-plugins:ray-plugin-home"))
}
为什么官方模块不走 H-SPI 呢?
3.2 H-SPI:动态插件的热装载
solon-hotplug 的 PluginManager 全为静态方法、无状态:
PluginManager.add(code, jarFile); // 登记(name → file)
PluginPackage pkg = PluginManager.load(code); // 独立 PluginClassLoader,扫描 META-INF/solon
PluginManager.start(code); // 子 AppContext 初始化 + Plugin.start
PluginManager.stop(code); // Plugin.prestop/stop + context stop/clear
PluginManager.unload(code); // classLoader.removeJar + close
PluginManager.remove(code); // 移除登记
PluginClassLoader extends AppClassLoader,标准父委托。 PluginPackage.start() 创建子 AppContext,插件的 @Controller/@Component 在子上下文生效、路由注册到全局,可注入宿主 Bean,但宿主默认看不见插件 Bean。
4. Ray 插件运行时:ray-framework-plugin
Solon SPI 解决“代码怎么装进来”,剩下的四个问题由 ray-frameworks/ray-framework-plugin 来处理:清单怎么解析(descriptor/)、插件怎么统一建模(registry/)、前端怎么 serve(ui/)、SQL 怎么迁移(sql/)、安装怎么编排(install/)。
#mermaid-svg-6ceqeBqyU0Sr8If8{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6ceqeBqyU0Sr8If8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6ceqeBqyU0Sr8If8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6ceqeBqyU0Sr8If8 .error-icon{fill:#552222;}#mermaid-svg-6ceqeBqyU0Sr8If8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6ceqeBqyU0Sr8If8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6ceqeBqyU0Sr8If8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6ceqeBqyU0Sr8If8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6ceqeBqyU0Sr8If8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6ceqeBqyU0Sr8If8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6ceqeBqyU0Sr8If8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6ceqeBqyU0Sr8If8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6ceqeBqyU0Sr8If8 .marker.cross{stroke:#333333;}#mermaid-svg-6ceqeBqyU0Sr8If8 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6ceqeBqyU0Sr8If8 p{margin:0;}#mermaid-svg-6ceqeBqyU0Sr8If8 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-6ceqeBqyU0Sr8If8 .cluster-label text{fill:#333;}#mermaid-svg-6ceqeBqyU0Sr8If8 .cluster-label span{color:#333;}#mermaid-svg-6ceqeBqyU0Sr8If8 .cluster-label span p{background-color:transparent;}#mermaid-svg-6ceqeBqyU0Sr8If8 .label text,#mermaid-svg-6ceqeBqyU0Sr8If8 span{fill:#333;color:#333;}#mermaid-svg-6ceqeBqyU0Sr8If8 .node rect,#mermaid-svg-6ceqeBqyU0Sr8If8 .node circle,#mermaid-svg-6ceqeBqyU0Sr8If8 .node ellipse,#mermaid-svg-6ceqeBqyU0Sr8If8 .node polygon,#mermaid-svg-6ceqeBqyU0Sr8If8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6ceqeBqyU0Sr8If8 .rough-node .label text,#mermaid-svg-6ceqeBqyU0Sr8If8 .node .label text,#mermaid-svg-6ceqeBqyU0Sr8If8 .image-shape .label,#mermaid-svg-6ceqeBqyU0Sr8If8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-6ceqeBqyU0Sr8If8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6ceqeBqyU0Sr8If8 .rough-node .label,#mermaid-svg-6ceqeBqyU0Sr8If8 .node .label,#mermaid-svg-6ceqeBqyU0Sr8If8 .image-shape .label,#mermaid-svg-6ceqeBqyU0Sr8If8 .icon-shape .label{text-align:center;}#mermaid-svg-6ceqeBqyU0Sr8If8 .node.clickable{cursor:pointer;}#mermaid-svg-6ceqeBqyU0Sr8If8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6ceqeBqyU0Sr8If8 .arrowheadPath{fill:#333333;}#mermaid-svg-6ceqeBqyU0Sr8If8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6ceqeBqyU0Sr8If8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6ceqeBqyU0Sr8If8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6ceqeBqyU0Sr8If8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6ceqeBqyU0Sr8If8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6ceqeBqyU0Sr8If8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6ceqeBqyU0Sr8If8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6ceqeBqyU0Sr8If8 .cluster text{fill:#333;}#mermaid-svg-6ceqeBqyU0Sr8If8 .cluster span{color:#333;}#mermaid-svg-6ceqeBqyU0Sr8If8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6ceqeBqyU0Sr8If8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6ceqeBqyU0Sr8If8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-6ceqeBqyU0Sr8If8 .icon-shape,#mermaid-svg-6ceqeBqyU0Sr8If8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6ceqeBqyU0Sr8If8 .icon-shape p,#mermaid-svg-6ceqeBqyU0Sr8If8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6ceqeBqyU0Sr8If8 .icon-shape .label rect,#mermaid-svg-6ceqeBqyU0Sr8If8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6ceqeBqyU0Sr8If8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6ceqeBqyU0Sr8If8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6ceqeBqyU0Sr8If8 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
ray-frameworks/ray-framework-plugin(top.goldenyear.ray.framework.plugin)
descriptor/RayPluginDescriptor · DescriptorLoader(清单解析+校验)RequiresParser
registry/PluginRegistry(注册中心) · RayPluginInstancePluginState / PluginType / PluginOrigin
ui/UiRegistrar · RayUiStaticRepository(StaticRepository SPI)各 UiResourceStore
sql/FlywayMigrator(迁移门面,委托 ray-framework-flyway)
install/PluginInstaller(安装编排) · PluginStoreAdminGatewayBridge · EventBusPurge
integration/RayPluginFrameworkPlugin(Solon 插件入口,priority=5)
业务模块不直接依赖这个框架模块 —— 它们只提供清单与资源,零感知。
4.1 插件清单:plugin-{code}.yml
# ray-modules/ray-module-bpm/src/main/resources/plugin-bpm.yml(实际文件)
code: bpm # 全局唯一编码,^[a-z][a-z0-9-]*$,保留字 core
name: 工作流程
version: ${version} # 构建期经 processResources 以模块版本展开
description: 流程设计、流程审批与流程管理
author: ray
requires: "core>=1.0.0" # 依赖约束,安装时校验
apiVersion: "1"
apps: # 前端应用声明,一个插件可携带多个(core 携带 3 个)
– code: app–bpm
name: 工作流程
url: /bpm/ # 服务路径 = 静态资源前缀 = micro-app url
icon: mdi:chart–timeline–variant
sort: 30
parent: root # 应用树父节点
清单文件名带 {code} 而非统一的 plugin.yml,是因为 ray-server 的 fat jar 任务以 zipTree 展开 runtimeClasspath,同名资源在合并时会互相覆盖 —— 带上编码即天然唯一。DescriptorLoader 在安装时做全套校验:code 格式、url 必须 ^\\/[a-z0-9-]+\\/$、/ 前缀仅 core 可声明、jar 根必须恰有一个清单(“一 jar 一插件,一插件一应用”)。
4.2 统一模型与状态机
public class RayPluginInstance {
String code; // core / bpm / home / …
PluginType type; // CORE(仅 core)/ EXTERNAL(其余全部)
PluginOrigin origin; // CLASSPATH(E-SPI)/ PLUGINS(H-SPI)
PluginState state; // RESOLVED/INSTALLED/STARTED/STOPPED/UPGRADING/FAILED
RayPluginDescriptor descriptor;
ClassLoader classLoader; // E-SPI 为 AppClassLoader;H-SPI 为 PluginClassLoader
List<StaticRepository> repositories; // 本插件注册的静态仓库(停用时注销)
}
type 只有两类 —— core 是平台内核(插件管理、用户、权限、路由都在 core 内,停 core 等于停机,PluginInstaller 对其 stop/uninstall 请求直接拒绝,而非靠前端隐藏按钮);其余全部 EXTERNAL。origin 记录装载来源,决定可执行的操作集:
| 随 server 启动自动注册 | ✓(必) | ✓(在 classpath 即注册) | 按 infra_plugin.status 恢复 |
| 停用(stop) | ✗ 禁止 | ✓(摘前端入口;类不可卸) | ✓(完整:类卸载+路由反注册) |
| 卸载(uninstall) | ✗ | ✗(随发布物替换) | ✓ |
| 热升级(上传新 jar) | ✗ | ✗(拒绝上传,提示随发布物升级) | ✓ |
| SQL 迁移 / 前端注册 | ✓ | ✓(同一套机制) | ✓ |
生命周期状态机:
#mermaid-svg-BMlIfhSNAbmsvpIx{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BMlIfhSNAbmsvpIx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BMlIfhSNAbmsvpIx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BMlIfhSNAbmsvpIx .error-icon{fill:#552222;}#mermaid-svg-BMlIfhSNAbmsvpIx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BMlIfhSNAbmsvpIx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BMlIfhSNAbmsvpIx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BMlIfhSNAbmsvpIx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BMlIfhSNAbmsvpIx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BMlIfhSNAbmsvpIx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BMlIfhSNAbmsvpIx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BMlIfhSNAbmsvpIx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BMlIfhSNAbmsvpIx .marker.cross{stroke:#333333;}#mermaid-svg-BMlIfhSNAbmsvpIx svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BMlIfhSNAbmsvpIx p{margin:0;}#mermaid-svg-BMlIfhSNAbmsvpIx defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-BMlIfhSNAbmsvpIx g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-BMlIfhSNAbmsvpIx g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-BMlIfhSNAbmsvpIx g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-BMlIfhSNAbmsvpIx g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-BMlIfhSNAbmsvpIx g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-BMlIfhSNAbmsvpIx .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-BMlIfhSNAbmsvpIx .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-BMlIfhSNAbmsvpIx .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-BMlIfhSNAbmsvpIx .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-BMlIfhSNAbmsvpIx .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-BMlIfhSNAbmsvpIx .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-BMlIfhSNAbmsvpIx .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-BMlIfhSNAbmsvpIx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BMlIfhSNAbmsvpIx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BMlIfhSNAbmsvpIx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BMlIfhSNAbmsvpIx .edgeLabel .label text{fill:#333;}#mermaid-svg-BMlIfhSNAbmsvpIx .label div .edgeLabel{color:#333;}#mermaid-svg-BMlIfhSNAbmsvpIx .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-BMlIfhSNAbmsvpIx .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-BMlIfhSNAbmsvpIx .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-BMlIfhSNAbmsvpIx .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-BMlIfhSNAbmsvpIx .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-BMlIfhSNAbmsvpIx .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BMlIfhSNAbmsvpIx .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BMlIfhSNAbmsvpIx #statediagram-barbEnd{fill:#333333;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BMlIfhSNAbmsvpIx .cluster-label,#mermaid-svg-BMlIfhSNAbmsvpIx .nodeLabel{color:#131300;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-BMlIfhSNAbmsvpIx .note-edge{stroke-dasharray:5;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-note text{fill:black;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram-note .nodeLabel{color:black;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagram .edgeLabel{color:red;}#mermaid-svg-BMlIfhSNAbmsvpIx #dependencyStart,#mermaid-svg-BMlIfhSNAbmsvpIx #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-BMlIfhSNAbmsvpIx .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BMlIfhSNAbmsvpIx :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
上传/内置扫描
install(执行 SQL,写 infra_plugin)
start
stop
start
uninstall(数据保留/清理)
upgrade(上传新版本)
成功(vN+1)
失败
修复后手工 start
RESOLVED
INSTALLED
STARTED
STOPPED
UPGRADING
FAILED
清单校验通过,文件就绪
记录原因,人工介入,阻断自动重启
状态落库 infra_plugin(status=期望态/管理员意图,驱动重启后自动恢复;state=运行态,驱动界面显示与 FAILED 告警),磁盘/内存状态由 PluginRegistry 持有并与之对账。
4.3 classpath 插件的注册流程
RayPluginFrameworkPlugin(priority=5)的 start 做三件事:注册自研静态资源处理器(见 §4.5)、订阅宿主 DataSource、监听 AppPluginLoadEndEvent 触发扫描:
#mermaid-svg-qYGFu2obYiNS2iKg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qYGFu2obYiNS2iKg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qYGFu2obYiNS2iKg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qYGFu2obYiNS2iKg .error-icon{fill:#552222;}#mermaid-svg-qYGFu2obYiNS2iKg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qYGFu2obYiNS2iKg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qYGFu2obYiNS2iKg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qYGFu2obYiNS2iKg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qYGFu2obYiNS2iKg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qYGFu2obYiNS2iKg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qYGFu2obYiNS2iKg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qYGFu2obYiNS2iKg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qYGFu2obYiNS2iKg .marker.cross{stroke:#333333;}#mermaid-svg-qYGFu2obYiNS2iKg svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qYGFu2obYiNS2iKg p{margin:0;}#mermaid-svg-qYGFu2obYiNS2iKg .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-qYGFu2obYiNS2iKg .cluster-label text{fill:#333;}#mermaid-svg-qYGFu2obYiNS2iKg .cluster-label span{color:#333;}#mermaid-svg-qYGFu2obYiNS2iKg .cluster-label span p{background-color:transparent;}#mermaid-svg-qYGFu2obYiNS2iKg .label text,#mermaid-svg-qYGFu2obYiNS2iKg span{fill:#333;color:#333;}#mermaid-svg-qYGFu2obYiNS2iKg .node rect,#mermaid-svg-qYGFu2obYiNS2iKg .node circle,#mermaid-svg-qYGFu2obYiNS2iKg .node ellipse,#mermaid-svg-qYGFu2obYiNS2iKg .node polygon,#mermaid-svg-qYGFu2obYiNS2iKg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qYGFu2obYiNS2iKg .rough-node .label text,#mermaid-svg-qYGFu2obYiNS2iKg .node .label text,#mermaid-svg-qYGFu2obYiNS2iKg .image-shape .label,#mermaid-svg-qYGFu2obYiNS2iKg .icon-shape .label{text-anchor:middle;}#mermaid-svg-qYGFu2obYiNS2iKg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qYGFu2obYiNS2iKg .rough-node .label,#mermaid-svg-qYGFu2obYiNS2iKg .node .label,#mermaid-svg-qYGFu2obYiNS2iKg .image-shape .label,#mermaid-svg-qYGFu2obYiNS2iKg .icon-shape .label{text-align:center;}#mermaid-svg-qYGFu2obYiNS2iKg .node.clickable{cursor:pointer;}#mermaid-svg-qYGFu2obYiNS2iKg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qYGFu2obYiNS2iKg .arrowheadPath{fill:#333333;}#mermaid-svg-qYGFu2obYiNS2iKg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qYGFu2obYiNS2iKg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qYGFu2obYiNS2iKg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qYGFu2obYiNS2iKg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qYGFu2obYiNS2iKg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qYGFu2obYiNS2iKg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qYGFu2obYiNS2iKg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qYGFu2obYiNS2iKg .cluster text{fill:#333;}#mermaid-svg-qYGFu2obYiNS2iKg .cluster span{color:#333;}#mermaid-svg-qYGFu2obYiNS2iKg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qYGFu2obYiNS2iKg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qYGFu2obYiNS2iKg rect.text{fill:none;stroke-width:0;}#mermaid-svg-qYGFu2obYiNS2iKg .icon-shape,#mermaid-svg-qYGFu2obYiNS2iKg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qYGFu2obYiNS2iKg .icon-shape p,#mermaid-svg-qYGFu2obYiNS2iKg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qYGFu2obYiNS2iKg .icon-shape .label rect,#mermaid-svg-qYGFu2obYiNS2iKg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qYGFu2obYiNS2iKg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qYGFu2obYiNS2iKg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qYGFu2obYiNS2iKg :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
迁移失败
Solon 启动
内核 E-SPI:solon.extend 声明的 modules/下全部 jar → AppClassLoader.addJar
各 Solon 插件 start 完毕触发 AppPluginLoadEndEvent
scan():ScanUtil 扫描 META-INF/solon/*.properties中带 ray.plugin.descriptor 指针的条目
加载 plugin-{code}.yml,按 priority 注册到PluginRegistry(core 先,重复注册抛异常)
数据源就绪(AifeiDbManager.addStartEvent)migrateAndMount():逐插件
① Flyway migrate(table=flyway_{code},幂等;core 恒最先)
② 读 infra_plugin 期望态(status=0 则不挂前端)
③ UiRegistrar.mount(静态资源注册)→ STARTED
插件标 FAILED、前端不可见不阻断 server 与其他插件
"SQL 先于静态注册"是有意的:迁移失败的插件整体不可见,而不是带着旧表结构残喘。
4.4 SQL 迁移:一插件一实例一表
sql/FlywayMigrator 委托 ray-framework-flyway 的工厂,为每个插件构造独立 Flyway 实例:
Flyway.configure(cl) // 关键:资源扫描用插件 ClassLoader
.dataSource(ds)
.locations("classpath:db/migration/" + code) // 插件命名空间,防 fat jar 合并撞名
.table("flyway_" + code) // 独立 history 表:flyway_core/flyway_bpm/…
.validateOnMigrate(true) // checksum 防篡改
.cleanDisabled(true) // 永禁 clean
.outOfOrder(false) // 线性升级,乱序报错
.load();
版本线互不干扰,installed_by 列区分 system(启动自动)与操作人(安装/升级)。种子数据(账号、字典、应用行)经 R__seed.sql 可重复脚本承载(目前可能还不太合理),INSERT IGNORE 幂等。一个多插件共库的坑:Flyway 的 baselineOnMigrate 按"整库非空"判定存量,core 迁完库就非空,bpm 会被误判存量跳过基线 —— Ray 的解法是整轮首个插件迁移前判定一次“库中已有表且均无 flyway_{code} 版本线”才认定存量库。
4.5 前端统一发布:StaticRepository SPI
前端由后端 serve,落点是 solon-web-staticfiles 的 StaticRepository SPI——StaticMappings 支持运行期增删,天然匹配插件启停:
// 插件启动:UiRegistrar.mount → shell(root)应用挂 "/",微应用挂去尾斜杠前缀
RayStaticMappings.addUi("/", repo);
RayStaticMappings.addUi("/bpm", repo);
// 插件停用
StaticMappings.remove(repo);
资源来源以 UiResourceStore 接口屏蔽:ClasspathUiStore 走 AppClassLoader.getResource(E-SPI 模块),HotplugUiStore 走 PluginPackage.getResource(H-SPI 插件,jar 内 URL 直读、天然原子)。SPA fallback 与分级缓存(html no-cache+ETag 协商、hash 资产 immutable 长缓存)在 RayUiStaticRepository.find() 与自研的 RayUiStaticResourceHandler 内实现——原生 StaticResourceHandler 对无后缀路径直接 return,根本无法承载 SPA。
5. 动态插件的安装编排:PluginInstaller
install/PluginInstaller 是静态门面,infra 管理界面直调。核心是两段式安装,防止“一上传就改库”:
DB/FS/HSPIPluginInstaller插件管理 APIDB/FS/HSPIPluginInstaller插件管理 API#mermaid-svg-Fe3COuqHQ2FBqJA4{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Fe3COuqHQ2FBqJA4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .error-icon{fill:#552222;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .marker.cross{stroke:#333333;}#mermaid-svg-Fe3COuqHQ2FBqJA4 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Fe3COuqHQ2FBqJA4 p{margin:0;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Fe3COuqHQ2FBqJA4 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Fe3COuqHQ2FBqJA4 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Fe3COuqHQ2FBqJA4 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .sequenceNumber{fill:white;}#mermaid-svg-Fe3COuqHQ2FBqJA4 #sequencenumber{fill:#333;}#mermaid-svg-Fe3COuqHQ2FBqJA4 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .messageText{fill:#333;stroke:none;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .labelText,#mermaid-svg-Fe3COuqHQ2FBqJA4 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .loopText,#mermaid-svg-Fe3COuqHQ2FBqJA4 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Fe3COuqHQ2FBqJA4 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .noteText,#mermaid-svg-Fe3COuqHQ2FBqJA4 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .actorPopupMenu{position:absolute;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Fe3COuqHQ2FBqJA4 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Fe3COuqHQ2FBqJA4 .actor-man circle,#mermaid-svg-Fe3COuqHQ2FBqJA4 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Fe3COuqHQ2FBqJA4 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}管理员上传 jar1POST /infra/plugin/upload2落临时文件+解析清单+sha2563{token, 清单摘要}(10 分钟令牌)4precheck(界面展示待执行 SQL)5POST /infra/plugin/precheck6只读校验 code/版本/requires/app 冲突7Flyway info() 预检(只读)8待执行脚本列表9确认10POST /infra/plugin/confirm11串行锁(code 为键)+ 复查12落盘 → load → migrate → start → activate13安装结果(成功/失败+阶段)14管理员
首装(doFreshInstall)的装载顺序值得注意:
PluginManager.add(code, jar);
PluginPackage pkg = PluginManager.load(code); // ① 先 load:Flyway 要经插件
FlywayMigrator.migrate(code, pkg.getClassLoader()); // ClassLoader 扫 db/migration/{code}
PluginManager.start(code); // ② SQL 成功才启动后端 Bean
activate(...); // ③ 注册中心/网关桥/模型同步/挂前端
SQL 失败即 unload+remove+删 jar,不触达后端 Bean。升级(doInstallOver)先用临时 ClassLoader 跑 migrate —— “SQL 失败旧版本零感知继续服务” —— 然后落盘新 jar、拆旧装新,装新失败用旧 jar 原路恢复回滚。
activate 里两步是 H-SPI 实战出来的:
PluginRegistry.register(EXTERNAL, descriptor, PLUGINS, cl);
AdminGatewayBridge.bridge(code); // 子上下文控制器挂进宿主网关
AifeiDbManager.syncModels(cl); // ORM 模型映射 + 实例工厂补登记
UiRegistrar.mount(descriptor, store); // 静态资源
REST API 面(InfraPluginAdminController,/admin-api/infra/plugin/*):paginate/get/upload/precheck/confirm/start/stop/uninstall/sqlHistory/sqlAcceptChange(=Flyway repair)/listClasspath。
停机链路挂在 AppPrestopEndEvent,重启恢复挂在 AppLoadEndEvent:读 infra_plugin 中 origin=PLUGINS 且 status=1 的行逐件 add/load/migrate/start,FAILED 的不自动重试、单件失败不阻断 server。
6. 实战:写一个动态插件
以仓库里真实的首页插件 ray-plugins/ray-plugin-home 为例,完整走一遍。
6.1 目录结构
#mermaid-svg-F9jRgQha8m7NJP29{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-F9jRgQha8m7NJP29 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-F9jRgQha8m7NJP29 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-F9jRgQha8m7NJP29 .error-icon{fill:#552222;}#mermaid-svg-F9jRgQha8m7NJP29 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-F9jRgQha8m7NJP29 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-F9jRgQha8m7NJP29 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-F9jRgQha8m7NJP29 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-F9jRgQha8m7NJP29 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-F9jRgQha8m7NJP29 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-F9jRgQha8m7NJP29 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-F9jRgQha8m7NJP29 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-F9jRgQha8m7NJP29 .marker.cross{stroke:#333333;}#mermaid-svg-F9jRgQha8m7NJP29 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-F9jRgQha8m7NJP29 p{margin:0;}#mermaid-svg-F9jRgQha8m7NJP29 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-F9jRgQha8m7NJP29 .cluster-label text{fill:#333;}#mermaid-svg-F9jRgQha8m7NJP29 .cluster-label span{color:#333;}#mermaid-svg-F9jRgQha8m7NJP29 .cluster-label span p{background-color:transparent;}#mermaid-svg-F9jRgQha8m7NJP29 .label text,#mermaid-svg-F9jRgQha8m7NJP29 span{fill:#333;color:#333;}#mermaid-svg-F9jRgQha8m7NJP29 .node rect,#mermaid-svg-F9jRgQha8m7NJP29 .node circle,#mermaid-svg-F9jRgQha8m7NJP29 .node ellipse,#mermaid-svg-F9jRgQha8m7NJP29 .node polygon,#mermaid-svg-F9jRgQha8m7NJP29 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-F9jRgQha8m7NJP29 .rough-node .label text,#mermaid-svg-F9jRgQha8m7NJP29 .node .label text,#mermaid-svg-F9jRgQha8m7NJP29 .image-shape .label,#mermaid-svg-F9jRgQha8m7NJP29 .icon-shape .label{text-anchor:middle;}#mermaid-svg-F9jRgQha8m7NJP29 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-F9jRgQha8m7NJP29 .rough-node .label,#mermaid-svg-F9jRgQha8m7NJP29 .node .label,#mermaid-svg-F9jRgQha8m7NJP29 .image-shape .label,#mermaid-svg-F9jRgQha8m7NJP29 .icon-shape .label{text-align:center;}#mermaid-svg-F9jRgQha8m7NJP29 .node.clickable{cursor:pointer;}#mermaid-svg-F9jRgQha8m7NJP29 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-F9jRgQha8m7NJP29 .arrowheadPath{fill:#333333;}#mermaid-svg-F9jRgQha8m7NJP29 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-F9jRgQha8m7NJP29 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-F9jRgQha8m7NJP29 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F9jRgQha8m7NJP29 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-F9jRgQha8m7NJP29 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F9jRgQha8m7NJP29 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-F9jRgQha8m7NJP29 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-F9jRgQha8m7NJP29 .cluster text{fill:#333;}#mermaid-svg-F9jRgQha8m7NJP29 .cluster span{color:#333;}#mermaid-svg-F9jRgQha8m7NJP29 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-F9jRgQha8m7NJP29 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-F9jRgQha8m7NJP29 rect.text{fill:none;stroke-width:0;}#mermaid-svg-F9jRgQha8m7NJP29 .icon-shape,#mermaid-svg-F9jRgQha8m7NJP29 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-F9jRgQha8m7NJP29 .icon-shape p,#mermaid-svg-F9jRgQha8m7NJP29 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-F9jRgQha8m7NJP29 .icon-shape .label rect,#mermaid-svg-F9jRgQha8m7NJP29 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-F9jRgQha8m7NJP29 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-F9jRgQha8m7NJP29 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-F9jRgQha8m7NJP29 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
ray-plugins/ray-plugin-home/
build.gradle
src/main/
java/top/goldenyear/ray/plugin/home/
resources/
XPluginImp.java(Solon 插件入口)
controller/HomeAdminController.java
service/HomeService.java
META-INF/solon/ray-plugin-home.properties
plugin-home.yml(清单,jar 根)
db/migration/home/(SQL 版本线,可选)
6.2 四个必需文件
插件入口——与官方模块同构,beanScan 声明扫描包即可:
@Slf4j
public class XPluginImp implements Plugin {
@Override
public void start(AppContext context) {
context.beanScan(XPluginImp.class);
}
}
如果插件依赖宿主 Bean(内置模块 bpm 依赖宿主的 FlowEngine),需要在 start 里桥接——因为 H-SPI 子上下文的 copyTo 只复制构建器/注入器/拦截器/提取器,不复制宿主 Bean:
// ray-module-bpm 的 XPluginImp(依赖宿主 FlowEngine 的桥接写法)
FlowEngine engine = Solon.context().getBean(FlowEngine.class);
if (engine != null && appContext.getWrap(FlowEngine.class) == null) {
appContext.wrapAndPut(FlowEngine.class, engine); // dev 内嵌形态下为 no-op
}
SPI 声明 META-INF/solon/ray-plugin-home.properties:
solon.plugin=top.goldenyear.ray.plugin.home.XPluginImp
solon.plugin.priority=0
ray.plugin.descriptor=plugin-home.yml
清单 plugin-home.yml:
code: home
name: 首页
version: ${version}
description: 首页子应用(app–home)与欢迎页聚合接口
author: ray
requires: "core>=1.0.0"
apiVersion: "1"
apps:
– code: app–home
name: 首页
url: /home/
icon: material–symbols:dashboard–outline
sort: 1
parent: root
构建脚本 build.gradle 三个关键任务:
// ① 前端产物装配:从 ray-ui 仓库同步 dist 进构建目录(不污染 src/,不入 git)
tasks.register('syncUi', Sync) {
from(file("${rayUiHome}/apps/micro/home/dist")) { into 'ui/home' }
into layout.buildDirectory.dir('generated/plugin-ui')
}
sourceSets.main.resources.srcDir layout.buildDirectory.dir('generated/plugin-ui')
// ② 清单 ${version} 模板以模块版本展开(单一来源)
processResources {
filesMatching('plugin-*.yml') { expand version: moduleVersion }
}
// ③ 插件主发布物:classes + 清单 + ui/ + db/migration/ + META-INF/solon/,不带第三方依赖
tasks.register('pluginJar', Jar) {
archiveBaseName = 'ray-plugin-home'
from sourceSets.main.output
manifest { attributes 'Ray-Plugin-Code': 'home', 'Ray-Plugin-Version': "${project.version}" }
}
6.3 发布与安装
package.sh 按 ray-plugins/*/build/libs/ray-plugin-*-{version}.jar 收集进发布物 plugins/ 目录随包预置 —— 预置 jar 不自动装载,首次仍经管理界面上传/安装一次(两段式向导:上传自动预检 → 待执行 SQL + 备份必勾 → 执行),重启后按 infra_plugin.status 恢复。新插件加入 settings.gradle 后 ./gradlew :ray-plugins:ray-plugin-home:pluginJar 即得可安装的 jar。
6.4 插件开发规范
- 不得携带宿主未提供的第三方依赖:PluginClassLoader 父委托共享宿主 classpath,独有依赖会 NoClassDefFoundError;
- 表只操作本插件前缀({code}_*),脚本可重入(CREATE TABLE IF NOT EXISTS 等);
- 全局注册必须可注销:插件 Bean 避免注册全局监听/定时器,或在 Plugin.stop 注销 —— 被全局缓存/线程池/静态引用持有的类会阻止 ClassLoader 卸载,造成泄漏;
- 停机清理放 preStop(数据源仍可用),不订阅 AppStopEndEvent。
7. 落地踩坑实录
7.1 H-SPI 子上下文的两处"想当然"
① copyTo 不复制宿主 Bean。 文档语义容易读成"子上下文继承宿主一切",实际只复制构建器/注入器/拦截器/提取器四类扩展点。插件注入宿主 Bean 需模块自行桥接。
② "宿主不可见插件 Bean"对网关不成立 —— 但也不自动可见。 ray 的管理端控制器靠 tag=adminApi 打标,由宿主 AdminGateway 启动期扫描自身容器装载;H-SPI 子上下文的控制器根本进不去。框架的 AdminGatewayBridge 在插件 start 后把子上下文带该 tag 的 @Mapping 控制器手工 add 进宿主网关,卸载时 RoutingTable.remove(Class) 反注册 —— 并且要反射清扫 solon 4.0.6 的路由缓存:RoutingTableDefault.remove(Class) 不清缓存,stop 后再 start 同一插件会静默丢路由(200 → 重装 → 部分 404)。
7.2 运行期加载的类,字节码增强会失效
InstanceUtil 用 LambdaMetafactory 为 ORM 模型生成实例工厂,对插件 ClassLoader(以及 E-SPI 经 AppClassLoader.addJar 运行期加入)的类一律抛 Invalid caller——privateLookupIn 跨 unnamed module 丢失 full privilege access。ray 在 AifeiDbManager.syncModels/removeModels 里做了两处运行期补齐:@Table 模型映射在插件 start 后反射补登记;经公开的 setFactory API 为插件模型注册反射工厂(优先于 lambda 工厂,宿主类仍走 JIT 内联)。同理,停用落地为完整卸载链而非 stop 保留,重新启用走全新装载链 —— 既满足类卸载语义,也绕开路由缓存 bug。
7.3 停用语义:内嵌模块与动态插件不同
E-SPI 模块的类在共享 AppClassLoader 里,物理上不可卸 —— 停用只能摘前端入口(unmount 静态仓库),后端类与路由仍在;管理界面以"内置模块"徽标明示。H-SPI 插件的停用则是物理停止:unmount → 网关反注册 → PluginManager.stop/unload/remove → EventBus 清扫(EventBusPurge,处理历史插件的订阅残留) → ORM 模型移除,实测启停循环 200→404→200。
7.4 停机时序:prestop 而非 stop
最初的监听器订阅 AppStopEndEvent,那时数据库已关,stopAll() 查 infra_plugin 必抛异常。修正:订阅 AppPrestopEndEvent(容器 preStop、数据源仍可用),且 stop 列表从 PluginRegistry 内存态获取而非查库。
7.5 fat jar 资源合并撞名
zipTree 合并依赖时同名资源互相覆盖、胜出顺序不可控。对策:清单 plugin-{code}.yml、SQL 目录 db/migration/{code}/ 全部带编码命名空间;flyway 的 META-INF/services 注册文件手工做 union,保证 fat jar 内唯一。
7.6 modules / 缺失的行为设计
E-SPI 内置 core 模块缺失时,启动自然终止于宿主注入(如 IPermissionService 找不到) —— Solon 内核打印异常、停机 exit 1,不会半瘫运行。
8. 小结
Solon 提供的是“组装单元”与“两条装载轨道”;ray 做的是把前端产物、SQL 版本线、管理编排装进同一个 jar 里。Solon 的插件机制使得同一个插件 jar,编译期内嵌可调试、E-SPI 预置可分发、H-SPI 上传可热装,三种形态一条注册链。
网硕互联帮助中心


评论前必须登录!
注册