Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?
结论先行:JDK SPI 是“按接口找实现、一次全加载”,Dubbo SPI 是“按名字找实现、按需加载”,并额外提供了默认实现、自适应扩展、激活扩展、包装增强与依赖注入。前者解决“能不能扩展”,后者解决“扩展怎么被治理”。
一、JDK SPI 的做法与三个痛点
JDK 的 ServiceLoader 约定:在 META-INF/services/ 下放一个以接口全限定名命名的文件,内容是实现类全限定名,一行一个。
ServiceLoader<Codec> loader = ServiceLoader.load(Codec.class);
for (Codec codec : loader) { // 遍历即实例化全部实现
System.out.println(codec.name());
}
| 痛点 | 表现 |
|---|---|
| 全量加载 | 遍历一次就把所有实现 new 出来,哪怕只用其中一个 |
| 无法按名获取 | 只能遍历后自己判断,拿不到“我要 json 这个实现” |
| 无扩展能力 | 不支持依赖注入、不支持包装类做 AOP、失败时异常信息弱 |
二、Dubbo SPI 的约定
配置文件放在 META-INF/dubbo/(兼容 META-INF/dubbo/internal/ 与 META-INF/services/),格式是 名字=实现类:
# META-INF/dubbo/com.demo.Codec
json=com.demo.JsonCodec
proto=com.demo.ProtoCodec
@SPI("json") // 默认实现名
public interface Codec {
String name();
byte[] encode(Object obj);
}
获取扩展:
ExtensionLoader<Codec> loader = ExtensionLoader.getExtensionLoader(Codec.class);
Codec codec = loader.getExtension("proto"); // 只实例化 proto,其余不加载
Codec def = loader.getDefaultExtension(); // 取 @SPI 声明的默认值
| 能力 | 注解 / 机制 | 作用 |
|---|---|---|
| 默认实现 | @SPI("json") | 未指定名字时用哪个 |
| 按需加载 | getExtension(name) | 只初始化被用到的实现,并缓存 |
| 自适应扩展 | @Adaptive | 运行时根据 URL 参数选择实现 |
| 自动激活 | @Activate | 按条件批量启用一组扩展,如 Filter 链 |
| 包装增强 | 构造器入参为接口本身 | 自动套一层 Wrapper,实现 AOP |
| 依赖注入 | injectExtension | 扩展里的 setter 可注入其他扩展 |
三、自适应扩展是关键差异
@Adaptive 让框架在“不知道用哪个实现”时延迟决策。比如协议层要按 URL 里的 protocol=dubbo 选择实现:
@SPI("dubbo")
public interface Protocol {
@Adaptive("protocol") // 从 URL 的 protocol 参数取值
Exporter<?> export(Invoker<?> invoker, URL url);
}
框架为该接口生成一个动态类,运行时读 url.getParameter("protocol") 再转发给真实实现。JDK SPI 完全没有对应能力,只能靠业务代码自己写 if-else。
常见追问
- 追问:Dubbo SPI 会不会有线程安全问题?
ExtensionLoader内部用 ConcurrentHashMap 缓存已创建的实例,扩展默认是单例,所以扩展实现本身必须无状态。 - 追问:Wrapper 类和普通实现怎么区分?构造器参数是接口类型的类被视为 Wrapper,会被自动包装到真实实现外层,
ProtocolFilterWrapper、ProtocolListenerWrapper都是这个套路。 - 追问:能同时用 JDK SPI 吗?可以,Dubbo 也会读
META-INF/services/,但拿不到名字与默认值,能力退化。
小结
- JDK SPI:能发现,不能选择;一次全加载,无增强。
- Dubbo SPI:
名字=类的 KV 约定 + 按需单例 + 四种注解增强。 @SPI定默认、@Adaptive定运行期决策、@Activate定批量启用、Wrapper 定 AOP。- 扩展实现必须无状态,因为它默认是全局单例。