Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?

#中间件#面试八股

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。
  • 扩展实现必须无状态,因为它默认是全局单例。
笔记加载中…