一探究竟Dubbo可扩展中最最最最核心重要的SPI机制

209 阅读4分钟

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

jdk实现:

四部曲

  1. 定义接口

  2. 实现接口实现类

  3. 在META-INF/services/中定义文件:文件名为接口全路径,内容为实现类全路径,换行区分。多个接口的扩展配置为多个文件。

  4. 触发类调用

    ServiceLoader.load(Class<S> service)
    

jdk的spi大致就是委托ClassLoader去按META-INF/services/(接口全类名)加载各个实现类,之后反射实例化实现类存到map中,并将实例返回客户端。

spring实现

四部曲

  1. 定义接口

  2. 实现接口实现类

  3. 在META-INF/services/中定义文件:文件名为spring.factories,内容为接口全路径=实现类全路径,逗号隔开。接口的配置实现若为多个,都放在这个文件中。

  4. 触发类调用

    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加载扩展的整个流程

dubbo.apache.org/imgs/v3/con…

主要步骤为 4 个:

  • 读取并解析配置文件
  • 缓存所有扩展实现
  • 基于用户执行的扩展名,实例化对应的扩展实现
  • 进行扩展实例属性的 IOC 注入以及实例化扩展的包装类,实现 AOP 特性

四部曲

  1. 定义接口,注意类上的@SPI注解

  2. 实现接口实现类

  3. 在META-INF/dubbo/中定义文件:文件名为接口全路径,内容为别名=实现类全路径。接口的配置实现若为多个,都放在这个文件中。

  4. 触发类调用

    ExtensionLoader.getExtensionLoader(Class<T> type)
    

对比

JDK SPIDUBBO SPISpring SPI
文件方式每个扩展点单独一个文件每个扩展点单独一个文件所有的扩展点在一个文件
获取某个固定的实现不支持,只能按顺序获取所有实现有“别名”的概念,可以通过名称获取扩展点的某个固定实现,配合Dubbo SPI的注解很方便不支持,只能按顺序获取所有实现。但由于Spring Boot ClassLoader会优先加载用户代码中的文件,所以可以保证用户自定义的spring.factoires文件在第一个,通过获取第一个factory的方式就可以固定获取自定义的扩展
其他支持Dubbo内部的依赖注入,通过目录来区分Dubbo 内置SPI和外部SPI,优先加载内部,保证内部的优先级最高
文档完整度文章 & 三方资料足够丰富文档 & 三方资料足够丰富文档不够丰富,但由于功能少,使用非常简单
IDE支持IDEA 完美支持,有语法提示

Dubbo的SPI解析

ExtensionLoader