1052 字
约 3 分钟
7
S04-J05 · SPI 与运行时可扩展系统设计
适用读者:想做插件化、或想知道 JDBC 驱动为什么"加个 jar 就能用"的人。
前置:S04-B06(接口)。
实测环境:OpenJDK 21.0.12.1(ServiceLoader实测,2026-11-05)。
覆盖旧文:A-407(502) A-409(474) = 976 点。
覆盖扩展:E-S04-017(ServiceLoader 与类加载隔离)。
上一篇:S04-J04 · 泛型:擦除、通配符与它的代价
下一篇:S04-J06 · IO 与 NIO。
1. 一句话结论
SPI(Service Provider Interface)= 接口在框架侧,实现在 jar 里,用 META-INF/services/<接口全限定名> 声明。ServiceLoader 在运行时扫描并实例化——这就是"加 jar 就生效"。
实测:
SPI 发现的服务: [A, B]
(META-INF/services/com.demo.Service 里写了两行:com.demo.ImplA、com.demo.ImplB)
2. 三件套:接口、实现、声明文件
spi/
├── com/demo/Service.java ← 接口(框架侧)
├── com/demo/ImplA.java ← 实现(jar 侧)
├── com/demo/ImplB.java
└── META-INF/services/
└── com.demo.Service ← 文件名 = 接口全限定名,内容 = 实现类全限定名
ServiceLoader<Service> sl = ServiceLoader.load(Service.class);
for (Service s : sl) { /* 每个实现一个实例 */ }
| 要素 | 要求 |
|---|---|
| 实现类 | 必须有无参构造器 |
| 声明文件 | 路径 META-INF/services/,文件名是接口全名,UTF-8 |
| 接口 | 与实现同一个 ClassLoader 可见(否则读不到) |
3. 类加载与隔离(E-S04-017)
| 问题 | 说明 |
|---|---|
| 不同 jar 里有同名接口 | 用独立 ClassLoader 隔离(否则类型不匹配) |
| 实现类依赖第三方库 | 需要依赖也可见(fat jar 或自建 ClassLoader) |
| 热插拔 | ServiceLoader 不提供卸载,要自己管 ClassLoader |
这是"插件化"的真实难点:不是"怎么发现实现",是"怎么隔离依赖与生命周期"。
4. 现实中的 SPI
| 框架 | 用 SPI 做什么 |
|---|---|
| JDBC | java.sql.Driver → 各数据库驱动 |
| SLF4J | 日志实现绑定(2.x 用 ServiceLoader) |
| Servlet 容器 | ServletContainerInitializer |
| Spring Boot | spring.factories(早期)/ AutoConfiguration.imports(3.x) |
| Dubbo | 自研 SPI(支持按名加载、依赖注入) |
判据:"这个扩展点会不会由第三方提供实现?" 会 → SPI;只是内部多实现 → 用依赖注入。
5. 设计一个"运行时可扩展"系统的四步(A-409)
| 步 | 内容 | 关键决策 |
|---|---|---|
| ① 定契约 | 接口尽量小、稳定 | 契约一改,所有插件失效 |
| ② 定发现机制 | SPI / 配置 / 注册中心 | SPI 零配置;配置更可控 |
| ③ 定隔离策略 | 同 ClassLoader / 独立 ClassLoader | 隔离强但复杂度高 |
| ④ 定治理 | 版本兼容、启用禁用、故障隔离 | 插件崩了不能拖垮主进程 |
第 ④ 步最容易被忽略:插件是"别人的代码",必须假设它会抛异常、会内存泄漏、会阻塞。
6. 五个误区
| 误区 | 真相 |
|---|---|
| "SPI 能热卸载" | ServiceLoader 不支持,需自管 ClassLoader |
| "实现类可以没有无参构造" | 必须有无参构造(否则加载失败) |
| "SPI 只用于 JDBC" | 日志、Servlet、Spring Boot 都在用 |
| "插件与主进程同 ClassLoader 就行" | 依赖冲突时会 ClassCastException/NoSuchMethodError |
| "加了 jar 就一定生效" | 文件名/路径/可见性任一不对都找不到 |
7. 自检
- SPI 的三件套是什么?声明文件的路径与命名规则?
ServiceLoader为什么不能热卸载?- 什么情况下需要独立 ClassLoader?
- 设计可扩展系统的四步里,哪一步最容易被忽略?
- 为什么说"插件是别人的代码"决定了治理策略?
8. 下一篇
S04-J06 · IO 与 NIO:四种 IO 模型、Buffer/Channel 与网络编程
扩展机制讲完,进阶层最后一篇讲 IO:阻塞/非阻塞/多路复用的差别,以及为什么 NIO 能撑高并发。
本篇属于系列《S04 · Java 语言核心》。旧文来源:A-407/A-409;扩展点 E-S04-017。SPI 发现结果为 OpenJDK 21 实测。
S04-J05 · SPI 与运行时可扩展系统设计
https://www.clxhxhhr.top/posts/5228/ 评论
0 条
还没有评论,先写一条吧。