云计算百科
云计算领域专业知识百科平台

基于 Solon 插件体系构建系统

1. 微内核、插件化架构

公司的产品是采用微服务架构的,虽然微服务提供了灵活的组装能力,但业务多了之后,一个功能往往涉及多个微服务,已经很难清楚哪个微服务是服务哪个应用的,最终变成还是需要部署所有的微服务,于是资源占用也就上来了,而且我们主要是面向企业级用户,因此也就会疑问哪种构建方式更适合我们的产品?后来为了信创,遇上 Solon,把公司产品从 Spring、Spring Cloud 迁移到 Solon 和 Solon Cloud 上来。在学习 Solon 的时候,官网中介绍插件部分,SPI,E-SPI,H-SPI。如果你验证过 E-SPI,一定会想在 Solon 上构建一个插件化的系统,因为太方便了。那我们的产品是不是采用微内核、插件化的架构会更好呢?于是我在自己的实验项目(Ray)中,逐步验证自己的想法,后来又碰上微前端(MicroApp),我觉得确实可行了 —— 前后端都打包到一个 Jar 里面,一个插件就是一个应用。但是自己前端太菜,花了很多时间去搞微应用的集成。好在 AI 来了,解决了前端集成的问题。最近我让 AI 把我的半成品插件管理改了改,终于实现了我想要的效果 —— 一个主应用,其余的都是插件。

插件 = 一个 jar,一个插件就是一个应用。

携带物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

三个对插件开发最重要的时序:

  • Plugin.start 早于 Bean 扫描。插件在 start 里做的事是“装配”:注册拦截器、桥接依赖,而不是使用业务 Bean。ray 的 XPluginImp 里 context.beanScan(XPluginImp.class) 显式声明扫描包,随后才轮到容器注入。
  • AppPluginLoadEndEvent 表示所有 Solon 插件已 start 完毕。ray 的插件运行时监听这个事件才开始扫描清单,保证 fat jar 内所有模块的文件都已就位。
  • AppPrestopEndEvent 阶段数据源仍可用,AppStopEndEvent 时库已关。在 Ray 的实际测试中踩过坑,插件停机清理必须放在 prestop。
  • 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 官方的两个扩展装载机制:

    E-SPI(体外扩展)H-SPI(热插拔)
    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 呢?

  • 自举死锁:infra_plugin 注册表由 core 自己的 SQL 版本线建表,而外置插件的启动要靠查这张表 —— “表由 core 建、core 的启动又靠表信息”,走不通;
  • 宿主启动期注入:ray-server(framework-server)启动即 @Inject core 的 IPermissionService 实现,H-SPI 子上下文的 Bean 宿主不可见;
  • 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: appbpm
    name: 工作流程
    url: /bpm/ # 服务路径 = 静态资源前缀 = micro-app url
    icon: mdi:charttimelinevariant
    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 记录装载来源,决定可执行的操作集:

    能力core(CORE)官方模块(E-SPI)动态插件(H-SPI)
    随 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: 首页子应用(apphome)与欢迎页聚合接口
    author: ray
    requires: "core>=1.0.0"
    apiVersion: "1"
    apps:
    code: apphome
    name: 首页
    url: /home/
    icon: materialsymbols:dashboardoutline
    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 上传可热装,三种形态一条注册链。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 基于 Solon 插件体系构建系统
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!