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. 自检

  1. SPI 的三件套是什么?声明文件的路径与命名规则?
  2. ServiceLoader 为什么不能热卸载?
  3. 什么情况下需要独立 ClassLoader?
  4. 设计可扩展系统的四步里,哪一步最容易被忽略?
  5. 为什么说"插件是别人的代码"决定了治理策略?

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/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。