|
| 1 | +# EasyPostman 插件 Runtime 架构说明 |
| 2 | + |
| 3 | +这份文档聚焦 `easy-postman-plugin-runtime` 在当前重构后的实现结构。 |
| 4 | + |
| 5 | +它不重复讲“怎么开发插件”或“怎么发布插件”,而是回答下面几个问题: |
| 6 | + |
| 7 | +- runtime 现在分成了哪些职责块 |
| 8 | +- 插件从安装到加载的真实链路是什么 |
| 9 | +- 宿主如何消费插件贡献 |
| 10 | +- 当前冲突策略和测试覆盖到了哪里 |
| 11 | +- 后面继续演进时,优先改哪里 |
| 12 | + |
| 13 | +相关总览文档见: |
| 14 | + |
| 15 | +- [PLUGINS_zh.md](./PLUGINS_zh.md) |
| 16 | + |
| 17 | +## 1. 当前结构 |
| 18 | + |
| 19 | +当前 runtime 相关的主类可以按职责理解为: |
| 20 | + |
| 21 | +```text |
| 22 | +easy-postman-plugin-runtime |
| 23 | +├── PluginRuntime |
| 24 | +├── PluginScanner |
| 25 | +├── PluginCandidateResolver |
| 26 | +├── PluginLoader |
| 27 | +├── PluginRegistry |
| 28 | +├── PluginStateStore |
| 29 | +├── PluginSettingsStore |
| 30 | +├── PluginRuntimePaths |
| 31 | +├── RuntimeVersionResolver |
| 32 | +├── PluginCompatibility |
| 33 | +├── PluginFileInfo |
| 34 | +└── PluginVersionComparator |
| 35 | +``` |
| 36 | + |
| 37 | +### 1.1 每个类负责什么 |
| 38 | + |
| 39 | +- `PluginRuntime` |
| 40 | + - 运行时总编排入口 |
| 41 | + - 协调启动、关闭、候选选择、生命周期收尾 |
| 42 | + - 对外暴露 `initialize()`、`shutdown()`、`getInstalledPlugins()` 等静态入口 |
| 43 | +- `PluginScanner` |
| 44 | + - 扫描插件目录 |
| 45 | + - 读取插件 jar 内的 descriptor |
| 46 | + - 生成磁盘层的 `PluginFileInfo` |
| 47 | +- `PluginCandidateResolver` |
| 48 | + - 从扫描结果里选出真正应该加载的插件 |
| 49 | + - 处理禁用、待卸载、兼容性过滤、同 `plugin.id` 多版本择优 |
| 50 | +- `PluginLoader` |
| 51 | + - 创建 `URLClassLoader` |
| 52 | + - 反射实例化插件入口类 |
| 53 | + - 调用 `onLoad()` / `onStart()` / `onStop()` |
| 54 | + - 把 `PluginContext` 注册动作接到 `PluginRegistry` |
| 55 | +- `PluginRegistry` |
| 56 | + - 保存插件注册出来的脚本 API、服务、Toolbox、补全、Snippet |
| 57 | + - 宿主运行时从这里消费插件能力 |
| 58 | +- `PluginStateStore` |
| 59 | + - 维护禁用插件和待卸载插件状态 |
| 60 | +- `PluginSettingsStore` |
| 61 | + - 底层 JSON 持久化 |
| 62 | +- `PluginRuntimePaths` |
| 63 | + - 统一运行时数据目录、安装目录、包缓存目录 |
| 64 | +- `RuntimeVersionResolver` |
| 65 | + - 解析 app version 和 plugin platform version |
| 66 | + - 打包态优先读资源,开发态回退读 `pom.xml` |
| 67 | + |
| 68 | +## 2. 真实加载链路 |
| 69 | + |
| 70 | +当前插件加载链路可以简化成: |
| 71 | + |
| 72 | +```text |
| 73 | +PluginManager / 本地放入 jar |
| 74 | + -> plugins/installed 和 plugins/packages |
| 75 | + -> StartupCoordinator.prepareMainFrame() |
| 76 | + -> PluginRuntime.initialize() |
| 77 | + -> cleanupPendingUninstallPlugins() |
| 78 | + -> PluginScanner.resolvePluginDirs() |
| 79 | + -> PluginScanner.listPluginsFromDirectory() |
| 80 | + -> PluginCandidateResolver.resolveLoadCandidates() |
| 81 | + -> PluginLoader.loadPluginJar() |
| 82 | + -> plugin.onLoad(context) |
| 83 | + -> PluginRegistry 收集贡献 |
| 84 | + -> PluginLoader.startPlugins() |
| 85 | + -> 宿主 UI / 脚本 / bridge 层消费贡献 |
| 86 | +``` |
| 87 | + |
| 88 | +这里有几个关键设计点: |
| 89 | + |
| 90 | +- 安装和加载分离 |
| 91 | + - `plugin-manager` 负责落盘和校验 |
| 92 | + - `runtime` 只负责“当前进程应该加载谁” |
| 93 | +- 扫描和选择分离 |
| 94 | + - `PluginScanner` 负责“看见什么” |
| 95 | + - `PluginCandidateResolver` 负责“最终选谁” |
| 96 | +- 注册和消费分离 |
| 97 | + - 插件只在 `onLoad()` 里声明能力 |
| 98 | + - 宿主稍后再从 `PluginRegistry` 统一读取 |
| 99 | + |
| 100 | +## 3. 宿主消费链路 |
| 101 | + |
| 102 | +宿主 app 层已经不再四处直连 `PluginRuntime.getRegistry()`,而是统一收口到: |
| 103 | + |
| 104 | +- [PluginAccess.java](../easy-postman-app/src/main/java/com/laker/postman/plugin/bridge/PluginAccess.java) |
| 105 | + |
| 106 | +当前消费关系大致是: |
| 107 | + |
| 108 | +```text |
| 109 | +PluginRegistry |
| 110 | + -> PluginAccess |
| 111 | + -> PostmanApiContext.createScriptApis() |
| 112 | + -> ToolboxPanel.getToolboxContributions() |
| 113 | + -> ScriptSnippetManager.getScriptCompletionContributors() |
| 114 | + -> SnippetDialog.getSnippetDefinitions() |
| 115 | + -> ClientCertificatePluginServices.getService(...) |
| 116 | +``` |
| 117 | + |
| 118 | +这样做的价值是: |
| 119 | + |
| 120 | +- app 层不用到处依赖 `PluginRuntime` |
| 121 | +- 未来如果 registry 或 runtime 实现变化,app 改动面会小很多 |
| 122 | + |
| 123 | +## 4. 插件入口模式 |
| 124 | + |
| 125 | +插件入口类当前建议只做“能力声明”,不做复杂业务。 |
| 126 | + |
| 127 | +官方插件已经开始统一走: |
| 128 | + |
| 129 | +- `PluginContributionSupport` |
| 130 | +- `PluginAccess` |
| 131 | +- `RedisI18n` / `KafkaI18n` |
| 132 | + |
| 133 | +例如 `KafkaPlugin` / `RedisPlugin` 的 `onLoad()` 现在更接近: |
| 134 | + |
| 135 | +```text |
| 136 | +registerScriptApi |
| 137 | +registerToolbox |
| 138 | +registerScriptCompletionContributor |
| 139 | +registerExampleSnippet |
| 140 | +``` |
| 141 | + |
| 142 | +这比原来直接在入口类里手写大量 `ToolboxContribution` / `ShorthandCompletion` / `SnippetDefinition` 更容易 review。 |
| 143 | + |
| 144 | +## 5. 当前冲突策略 |
| 145 | + |
| 146 | +### 5.1 Script API alias / service type |
| 147 | + |
| 148 | +`PluginRegistry` 目前对两类冲突做了基础保护: |
| 149 | + |
| 150 | +- 脚本 API alias 冲突 |
| 151 | +- service type 冲突 |
| 152 | + |
| 153 | +当前策略是: |
| 154 | + |
| 155 | +- 保持兼容:后注册覆盖前注册 |
| 156 | +- 不再静默:会打 `warn` 日志 |
| 157 | +- 日志会带 owner 信息,指出“哪个插件覆盖了哪个插件” |
| 158 | + |
| 159 | +也就是说,现在 registry 已经会记录: |
| 160 | + |
| 161 | +- 某个 script API alias 属于哪个 pluginId |
| 162 | +- 某个 service type 属于哪个 pluginId |
| 163 | + |
| 164 | +这一步还不是最终方案,但已经足够让冲突可观测。 |
| 165 | + |
| 166 | +### 5.2 还没做的部分 |
| 167 | + |
| 168 | +下面这些冲突目前还没有统一治理: |
| 169 | + |
| 170 | +- `ToolboxContribution` 的重复 `id` |
| 171 | +- `SnippetDefinition` 的重复标题或语义冲突 |
| 172 | +- `ScriptCompletionContributor` 的重复补全项 |
| 173 | + |
| 174 | +这些可以后续再做。 |
| 175 | + |
| 176 | +## 6. 当前测试覆盖 |
| 177 | + |
| 178 | +runtime 测试目前主要在: |
| 179 | + |
| 180 | +- [PluginRuntimeTest.java](../easy-postman-plugin-runtime/src/test/java/com/laker/postman/plugin/runtime/PluginRuntimeTest.java) |
| 181 | +- [PluginRegistryTest.java](../easy-postman-plugin-runtime/src/test/java/com/laker/postman/plugin/runtime/PluginRegistryTest.java) |
| 182 | + |
| 183 | +已经覆盖的关键行为包括: |
| 184 | + |
| 185 | +- 重新启用插件时会清理 pending uninstall |
| 186 | +- 自定义数据目录会生效 |
| 187 | +- plugin platform version 独立于 app version |
| 188 | +- 不兼容平台版本会被拒绝 |
| 189 | +- pending uninstall 文件清理 |
| 190 | +- 同一 `plugin.id` 多版本时只加载最高版本 |
| 191 | +- 最新版本不兼容时,回退到次新兼容版本 |
| 192 | +- 某个插件加载失败时,其他插件继续加载 |
| 193 | +- `shutdown()` 会调用插件 `onStop()` |
| 194 | +- registry 对 alias/type 的重复注册遵循“后注册覆盖前注册” |
| 195 | +- registry 会保留最新注册者的 owner 信息 |
| 196 | + |
| 197 | +## 7. 当前仍然存在的限制 |
| 198 | + |
| 199 | +这轮重构之后,结构已经清楚很多,但 runtime 还不是终点版本。 |
| 200 | + |
| 201 | +目前仍然存在这些限制: |
| 202 | + |
| 203 | +- `PluginRuntime` 仍然是静态全局入口 |
| 204 | + - 测试虽然可控,但还不是真正的实例化 runtime |
| 205 | +- registry 对 UI 贡献没有 owner 级别追踪 |
| 206 | + - 暂时只追踪了 script API 和 service |
| 207 | +- 不支持真正的热卸载 |
| 208 | + - 当前启用/禁用/卸载仍以“下次启动生效”思路为主 |
| 209 | +- `PluginSettingsStore` 还是通用 key-value JSON 包装 |
| 210 | + - 还没有 typed state model |
| 211 | + |
| 212 | +## 8. 后续演进建议 |
| 213 | + |
| 214 | +如果继续演进,建议优先级如下: |
| 215 | + |
| 216 | +1. 给 `ToolboxContribution` / `SnippetDefinition` / completion 也补 owner 追踪 |
| 217 | +2. 把 `PluginRuntime` 从静态入口再包一层实例化 facade |
| 218 | +3. 给 `PluginStateStore` 引入 typed state model,而不只是字符串集合 |
| 219 | +4. 评估是否支持无需重启的热启停 |
| 220 | +5. 把当前冲突策略整理成对插件开发者可见的契约文档 |
| 221 | + |
| 222 | +## 9. 阅读顺序建议 |
| 223 | + |
| 224 | +如果要顺着代码理解现在这套 runtime,推荐顺序是: |
| 225 | + |
| 226 | +1. [PluginRuntime.java](../easy-postman-plugin-runtime/src/main/java/com/laker/postman/plugin/runtime/PluginRuntime.java) |
| 227 | +2. [PluginScanner.java](../easy-postman-plugin-runtime/src/main/java/com/laker/postman/plugin/runtime/PluginScanner.java) |
| 228 | +3. [PluginCandidateResolver.java](../easy-postman-plugin-runtime/src/main/java/com/laker/postman/plugin/runtime/PluginCandidateResolver.java) |
| 229 | +4. [PluginLoader.java](../easy-postman-plugin-runtime/src/main/java/com/laker/postman/plugin/runtime/PluginLoader.java) |
| 230 | +5. [PluginRegistry.java](../easy-postman-plugin-runtime/src/main/java/com/laker/postman/plugin/runtime/PluginRegistry.java) |
| 231 | +6. [PluginRuntimeTest.java](../easy-postman-plugin-runtime/src/test/java/com/laker/postman/plugin/runtime/PluginRuntimeTest.java) |
| 232 | +7. [PluginRegistryTest.java](../easy-postman-plugin-runtime/src/test/java/com/laker/postman/plugin/runtime/PluginRegistryTest.java) |
0 commit comments