SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。
jdk实现:
四部曲
-
定义接口
-
实现接口实现类
-
在META-INF/services/中定义文件:文件名为接口全路径,内容为实现类全路径,换行区分。多个接口的扩展配置为多个文件。
-
触发类调用
ServiceLoader.load(Class<S> service)
jdk的spi大致就是委托ClassLoader去按META-INF/services/(接口全类名)加载各个实现类,之后反射实例化实现类存到map中,并将实例返回客户端。
spring实现
四部曲
-
定义接口
-
实现接口实现类
-
在META-INF/services/中定义文件:文件名为spring.factories,内容为接口全路径=实现类全路径,逗号隔开。接口的配置实现若为多个,都放在这个文件中。
-
触发类调用
SpringFactoriesLoader.loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader);
详细说了jdk的spi这个就不细说了,也是通过ClassLoader去META-INF/spring.factories加载class,然后反射实例化返回,说说应用吧,像SpringBoot用这种方式去加载一些自动配置类,即引入xx-starter就能够自动向spring容器中注入许多配置好的组件。 [spring boot的自动配置]
dubbo实现
Dubbo就通过SPI机制加载所有的组件。 Dubbo 中的扩展能力是从 JDK 标准的 SPI 扩展点发现机制加强而来,它改进了 JDK 标准的 SPI 以下问题:
- JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
- 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。 在Dubbo中,SPI是一个非常重要的模块。基于SPI,我们很容易对Dubbo进行扩展。
Dubbo加载扩展的整个流程
主要步骤为 4 个:
- 读取并解析配置文件
- 缓存所有扩展实现
- 基于用户执行的扩展名,实例化对应的扩展实现
- 进行扩展实例属性的 IOC 注入以及实例化扩展的包装类,实现 AOP 特性
四部曲
-
定义接口,注意类上的@SPI注解
-
实现接口实现类
-
在META-INF/dubbo/中定义文件:文件名为接口全路径,内容为别名=实现类全路径。接口的配置实现若为多个,都放在这个文件中。
-
触发类调用
ExtensionLoader.getExtensionLoader(Class<T> type)
对比
| JDK SPI | DUBBO SPI | Spring SPI | |
|---|---|---|---|
| 文件方式 | 每个扩展点单独一个文件 | 每个扩展点单独一个文件 | 所有的扩展点在一个文件 |
| 获取某个固定的实现 | 不支持,只能按顺序获取所有实现 | 有“别名”的概念,可以通过名称获取扩展点的某个固定实现,配合Dubbo SPI的注解很方便 | 不支持,只能按顺序获取所有实现。但由于Spring Boot ClassLoader会优先加载用户代码中的文件,所以可以保证用户自定义的spring.factoires文件在第一个,通过获取第一个factory的方式就可以固定获取自定义的扩展 |
| 其他 | 无 | 支持Dubbo内部的依赖注入,通过目录来区分Dubbo 内置SPI和外部SPI,优先加载内部,保证内部的优先级最高 | 无 |
| 文档完整度 | 文章 & 三方资料足够丰富 | 文档 & 三方资料足够丰富 | 文档不够丰富,但由于功能少,使用非常简单 |
| IDE支持 | 无 | 无 | IDEA 完美支持,有语法提示 |