插件系统
受众:插件开发者、二次开发者 摘要:Plugin starter 首段提供静态插件元数据发现和本地生命周期 contract。它不是热加载框架,也不替代现有 Provider / SPI。
定位
spring-open-starter-plugin 用来让应用识别本地安装、classpath 描述文件或插件贡献者声明的插件元数据,并形成可查询、可审计、可文档化的插件注册表。spring-open-core-plugin 在此之上提供后台插件治理闭环:状态持久化、启停、安装 / 升级 / 卸载计划、生命周期执行记录和 Admin 管理页面。
首段只做五件事:
- 读取
spring-open-plugin.yml/spring-open-plugin.properties、META-INF/spring-open/plugins/*描述文件,或通过PluginContributor贡献的描述信息。 - 注册
PluginManager,供应用、后台或运维页查询插件列表、状态矩阵和生命周期计划。 - 约定插件可声明能力、扩展点、配置 key、权限码和资源入口。
- 约定本地插件安装、升级、降级、卸载的迁移脚本和回滚脚本,并生成可审计计划。
- 提供
PluginLifecycleExecutor和PluginLifecycleScriptRunnerSPI,在应用显式调用时按计划执行脚本并记录回滚结果;Core Plugin 会把执行结果落库供后台审计。
暂不做动态 jar 热加载、远程插件市场、插件沙箱、内置 SQL / 文件执行器、资源自动复制或独立 ClassLoader。插件状态和生命周期审计由 Core Plugin 持久化;插件真实业务 Bean、菜单、权限和 Provider 仍由对应模块或 Service Provider 管理。
配置
| 配置 | 默认值 | 说明 |
|---|---|---|
spring.open.plugin.enabled | true | 是否启用插件元数据发现 |
spring.open.plugin.locations | classpath 默认描述文件位置 | 插件描述文件 resource pattern,支持 classpath*: 和 file: |
spring.open.plugin.fail-on-invalid-descriptor | true | 描述文件缺少必填字段、解析失败或插件 id 重复时是否启动失败 |
spring.open.plugin.framework-version | 空 | 当前应用框架版本,用于插件兼容矩阵检查;为空时不限制 |
spring.open.plugin.states.<plugin-id>.enabled | 描述文件默认态 | 单个插件启用状态覆盖 |
默认扫描位置:
classpath*:META-INF/spring-open/plugins/*.propertiesclasspath*:META-INF/spring-open/plugins/*.ymlclasspath*:META-INF/spring-open/plugins/*.yamlclasspath*:spring-open-plugin.propertiesclasspath*:spring-open-plugin.ymlclasspath*:spring-open-plugin.yaml
本地安装目录可以这样接入:
spring:
open:
plugin:
locations:
- file:plugins/*.yml
- file:plugins/*.properties描述文件
Properties 示例:
plugin.id=sample-provider
plugin.name=Sample Provider
plugin.version=1.0.0
plugin.description=示例插件
plugin.vendor=ExampleVendor
plugin.homepage=https://example.com/plugins/sample-provider
plugin.enabled-by-default=true
plugin.capabilities=payment-provider,admin-view
plugin.extension-points[0]=payment.provider
plugin.extension-points[1]=admin.menu
plugin.dependencies[0]=base-provider
plugin.compatible-framework-versions[0]=0.7.*
plugin.config-keys[0]=spring.open.payment.providers.sample
plugin.permissions[0]=plugin:sample:view
plugin.resources[0]=classpath:/sample-provider/assets
plugin.install-scripts[0]=classpath:/sample-provider/install.sql
plugin.upgrade-scripts[0]=classpath:/sample-provider/upgrade-1.0.0.sql
plugin.downgrade-scripts[0]=classpath:/sample-provider/downgrade-0.9.0.sql
plugin.uninstall-scripts[0]=classpath:/sample-provider/uninstall.sql
plugin.rollback-scripts[0]=classpath:/sample-provider/rollback.sqlYAML 示例:
plugin:
id: sample-provider
name: Sample Provider
version: 1.0.0
description: 示例插件
vendor: ExampleVendor
homepage: https://example.com/plugins/sample-provider
enabled-by-default: true
capabilities:
- payment-provider
- admin-view
extension-points:
- payment.provider
- admin.menu
dependencies:
- base-provider
compatible-framework-versions:
- 0.7.*
config-keys:
- spring.open.payment.providers.sample
permissions:
- plugin:sample:view
resources:
- classpath:/sample-provider/assets
install-scripts:
- classpath:/sample-provider/install.sql
upgrade-scripts:
- classpath:/sample-provider/upgrade-1.0.0.sql
downgrade-scripts:
- classpath:/sample-provider/downgrade-0.9.0.sql
uninstall-scripts:
- classpath:/sample-provider/uninstall.sql
rollback-scripts:
- classpath:/sample-provider/rollback.sqlplugin.id 是必填项,建议使用 kebab-case。未配置 name 时展示名回退到 id,未配置 version 时回退为 0.0.0。
Java contract
| 类型 | 说明 |
|---|---|
PluginDescriptor | 插件元数据:id、名称、版本、能力、扩展点、配置 key、权限和资源 |
PluginContributor | 插件元数据贡献 SPI,推荐通过 @AutoService(PluginContributor.class) 注册,也兼容 META-INF/spring.factories |
PluginDescriptorLoader | 描述加载扩展点;默认组合资源描述文件、ServiceLoader 贡献和 Spring factories 贡献,可替换为自有安装目录或配置中心来源 |
PluginStateRegistry | 插件状态注册表,负责提供插件启停状态、状态来源和当前框架版本;默认从 PluginProperties 读取 |
PluginStatus | 插件状态视图,包含启停状态、状态来源、依赖检查明细和版本兼容矩阵 |
PluginLifecyclePlan | 插件安装、升级、降级或卸载的可审计执行计划,包含动作、版本、脚本、资源和提示 |
PluginLifecycleExecutor | 生命周期计划执行入口,按计划顺序执行脚本,失败时执行回滚脚本,并返回可审计结果 |
PluginLifecycleScriptRunner | 生命周期脚本执行 SPI;应用按脚本 URI 或内部指令协议注册 Runner |
PluginLifecycleExecutionResult | 生命周期执行结果,包含成功状态、已执行脚本、已回滚脚本、失败脚本、回滚失败脚本、资源入口、提示和摘要 |
PluginManager | 插件元数据管理入口,提供 list()、find(id)、exists(id)、statuses()、status(id)、planInstall()、planUpgrade()、planUninstall() |
DatabasePluginStateRegistry | Core Plugin 默认注册的数据库状态注册表,优先读取后台持久化状态,配置文件状态作为兜底 |
状态和兼容检查
Plugin starter 会通过 PluginStateRegistry 根据描述文件和配置计算插件状态:
ENABLED:插件启用,并通过依赖与兼容矩阵检查。DISABLED:插件被描述文件默认态或配置覆盖禁用。BLOCKED:插件依赖缺失,或依赖插件未处于可用状态。INCOMPATIBLE:插件声明的compatible-framework-versions不包含当前framework-version。
compatible-framework-versions 支持精确版本、* 和 0.7.* 这类前缀通配。
状态结果会保留可解释字段:
stateSource:DESCRIPTOR_DEFAULT表示来自描述文件默认态,CONFIGURED表示来自spring.open.plugin.states.<plugin-id>.enabled,自定义状态注册表可返回CUSTOM_REGISTRY或自定义来源。frameworkVersion:本次兼容矩阵检查使用的框架版本,来自PluginStateRegistry.frameworkVersion()。dependencyChecks:逐项返回依赖插件是否存在、依赖状态、是否可用和阻塞原因。compatibilityChecks:逐项返回框架版本、匹配模式、是否兼容和兼容问题;未声明兼容版本时会以*作为默认兼容矩阵行。
插件真实贡献 Provider、监听器或菜单时,仍要走对应 starter / 业务模块已有扩展点。Plugin starter 只记录“插件声明了什么”,不越过既有边界直接装载实现。
Java SPI 贡献
插件包如果不希望额外维护描述文件,可以实现 PluginContributor:
import com.google.auto.service.AutoService;
import com.springopen.starter.plugin.PluginContributor;
import com.springopen.starter.plugin.PluginDescriptor;
import java.util.List;
@AutoService(PluginContributor.class)
public class SamplePluginContributor implements PluginContributor {
@Override
public List<PluginDescriptor> descriptors() {
return List.of(PluginDescriptor.builder()
.id("sample-provider")
.name("Sample Provider")
.version("1.0.0")
.capabilities(List.of("payment-provider"))
.extensionPoints(List.of("payment.provider"))
.build());
}
}推荐使用 @AutoService 生成标准 META-INF/services/com.springopen.starter.plugin.PluginContributor,运行时由 Java ServiceLoader 发现。无法启用注解处理或需要显式接入 Spring factories 的外部包,也可以使用:
# META-INF/spring.factories
com.springopen.starter.plugin.PluginContributor=\
com.example.plugins.SamplePluginContributor默认 loader 会按“资源描述文件 → ServiceLoader 贡献 → Spring factories 贡献”的顺序合并,并按贡献者类名去重。plugin.id 必须全局唯一;重复 id、缺少 id 或贡献者执行失败时,默认遵循 spring.open.plugin.fail-on-invalid-descriptor=true 直接启动失败。关闭该配置时会跳过坏来源或保留先出现的同 id 描述。
安装升级计划
Plugin starter 不直接写文件、不内置 SQL 执行器、不加载外部 jar;它根据当前 registry 和目标描述文件生成生命周期计划,并提供显式执行入口,方便后台、安装器或运维流程在受控事务中接入真实 Runner。
| 方法 | 场景 | 计划动作 |
|---|---|---|
planInstall(targetDescriptor) | 当前 registry 中不存在目标插件 | INSTALL,返回 install-scripts、rollback-scripts 和 resources |
planInstall(targetDescriptor) | 当前 registry 已存在同名插件 | NOOP,提示插件已安装 |
planUpgrade(targetDescriptor) | 当前版本低于目标版本 | UPGRADE,返回 upgrade-scripts |
planUpgrade(targetDescriptor) | 当前版本高于目标版本 | DOWNGRADE,返回 downgrade-scripts |
planUpgrade(targetDescriptor) | 当前版本等于目标版本 | NOOP |
planUpgrade(targetDescriptor) | 当前 registry 中不存在目标插件 | INSTALL,附带“未安装,使用安装计划”的提示 |
planUninstall(id) | 当前 registry 中存在插件 | UNINSTALL,返回 uninstall-scripts 和 rollback-scripts |
planUninstall(id) | 当前 registry 中不存在插件 | NOOP |
计划中的 warnings 会包含目标插件依赖缺失和框架版本兼容提示。
执行生命周期计划时调用 PluginLifecycleExecutor.execute(plan):
NOOP或没有脚本的计划会成功返回,且不会触发任何 Runner。- 非空
migrationScripts会按声明顺序查找支持该脚本入口的PluginLifecycleScriptRunner并执行。 - 任一迁移脚本失败时,执行器会按计划中的
rollbackScripts顺序尝试补偿,并在PluginLifecycleExecutionResult.rollbackScripts中记录已成功执行的回滚脚本。 - 失败结果会结构化返回
failedScript/failureMessage;如果补偿脚本也失败,会额外返回rollbackFailedScript/rollbackFailureMessage,便于后台审计、人工重试和告警定位。 - 没有 Runner 支持某个脚本时,执行结果为失败,不会静默当作成功。
resources只作为资源入口回传,starter 不自动复制资源、不写权限菜单种子、不写审计日志;这些仍由接入方或后续业务功能簇在受控事务中处理。
Core Plugin 管理 API
spring-open-core-plugin 在检测到 PluginManager Bean 时会暴露插件管理 API,用于后台或运维页治理当前本地插件注册表。它只治理已经在 classpath、描述文件或 PluginContributor 中声明的插件,不下载远程插件,也不动态加载外部 jar。
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
GET | /api/v1/admin/plugins | plugin:view | 查询插件状态列表,包含描述信息、状态来源、启用结果、依赖检查和版本兼容矩阵 |
GET | /api/v1/admin/plugins/{id} | plugin:view | 按插件 ID 查询单个插件状态详情 |
GET | /api/v1/admin/plugins/{id}/config/schema | plugin:view | 查询插件描述符声明的配置 key,并转换为只读动态字段 Schema 元数据;不返回配置值 |
POST | /api/v1/admin/plugins/{id}/enable | plugin:state:update | 持久化启用插件,后续状态查询立即使用数据库覆盖态 |
POST | /api/v1/admin/plugins/{id}/disable | plugin:state:update | 持久化停用插件,依赖它的插件会在状态矩阵中显示阻塞 |
GET | /api/v1/admin/plugins/{id}/install-plan | plugin:view | 生成安装计划,只返回脚本、资源和风险提示,不执行 |
GET | /api/v1/admin/plugins/{id}/upgrade-plan | plugin:view | 生成升级计划,只返回脚本、资源和风险提示,不执行 |
GET | /api/v1/admin/plugins/{id}/uninstall-plan | plugin:view | 生成卸载计划,只返回脚本、资源和风险提示,不执行 |
POST | /api/v1/admin/plugins/{id}/install | plugin:lifecycle:execute | 执行安装计划并写入生命周期执行记录,operationKey 支持幂等 |
POST | /api/v1/admin/plugins/{id}/upgrade | plugin:lifecycle:execute | 执行升级计划并写入生命周期执行记录,operationKey 支持幂等 |
POST | /api/v1/admin/plugins/{id}/uninstall | plugin:lifecycle:execute | 执行卸载计划,成功后持久化为未安装 / 停用 |
GET | /api/v1/admin/plugins/executions | plugin:execution:view | 查询最近生命周期执行记录,可按插件 ID 过滤 |
返回的 state 取值与 PluginState 一致:ENABLED、DISABLED、BLOCKED、INCOMPATIBLE。
列表和详情都会返回 installed、installedVersion、stateSource、frameworkVersion、stateReason、operationKey、dependencyChecks、compatibilityChecks、dependencyIssues 和 compatibilityIssues,后台可直接展示“是否安装、状态来自哪里、依赖哪项阻塞、哪个版本模式未匹配”。
生命周期执行记录会保存动作、版本、成功状态、已执行脚本、回滚脚本、资源、提示、失败原因、操作人、原因和 operationKey。执行成功后,安装 / 升级会把插件标记为已安装并启用,卸载会把插件标记为未安装并停用。
Core Plugin 不内置 SQL Runner 或文件复制 Runner。生产环境要真正执行脚本时,应由插件包或部署侧注册 PluginLifecycleScriptRunner,并只支持受控 URI 协议或内部指令协议,避免把任意文件路径 / SQL 文本暴露给后台直接执行。
Admin 插件管理页
后台菜单内置 /plugin 插件管理页,使用 plugin:view、plugin:state:update、plugin:lifecycle:execute 和 plugin:execution:view 权限治理 Core Plugin API。
该页面支持:
- 查看已发现插件的展示名、插件 ID、版本、厂商、安装状态、启停状态、状态来源、默认启用标记、框架版本、能力、扩展点、权限码、配置 key、资源入口、依赖检查明细和版本兼容矩阵。
- 在插件详情中通过 Schema 动态字段组件展示配置 key 声明;该展示只说明插件需要哪些配置,不读取或回显运行期配置值。
- 启用 / 停用本地插件状态,并记录原因和操作 key。
- 查看安装 / 升级 / 卸载计划,提前确认迁移脚本、回滚脚本、资源入口和风险提示。
- 执行安装 / 升级 / 卸载,并查看最近生命周期执行记录、失败原因和回滚结果。
页面不下载远程插件、不加载外部 jar、不自动发布菜单 / 权限 / 静态资源;这些能力仍由对应业务模块的迁移、Service Provider 和部署流程负责。
边界
- 不让业务模块直接依赖插件实现类。
- 不把 Provider / SPI 复制一套插件专用模型。
PluginContributor只贡献元数据,不动态创建业务 Bean;真实 Provider / Bean 仍走对应 starter、生命周期 Provider 或 Spring Boot 自动装配。- 不引入动态类加载、远程市场或签名校验;本地插件包的来源可信、签名验收和交付流程由发布 / 部署 runbook 负责。
- 后台启停、安装 / 卸载操作入口和生命周期审计已经由 Core Plugin 提供;资源发布、插件专属权限菜单种子和外部安全扫描仍归插件所属模块或部署流程。