一. SPI定义

  • SPI 全称为 Service Provider Interface,一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如mysql-connector包、spring-web包等,Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。
  • 面向的对象的设计里,模块之间是基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候,不在模块里面写死代码,这就需要一种服务发现机制。java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到代码之外(配置文件中)。
  • jdk的spi的具体约定如下 :当服务的提供者(provider),提供了一个接口多种实现时,一般会在jar包的META-INF/services/目录下,创建该接口的同名(相对路径加文件名称)文件。该文件里面的内容就是该服务接口的具体实现类的名称。而当外部加载这个模块的时候,就能通过该jar包META-INF/services/里的配置文件得到具体的实现类名,并加载实例化,完成模块的装配。

二. dubbo为啥不使用jdk的spi

  1. jdk的spi会一次性加载所有的接口实现,如果有扩展实现初始化很耗时,没有使用的实现也会加载,耗费资源
  2. jdk的spi没有对IOC和AOP的支持,dubbo增强的spi增加了对扩展点IOC和APO的支持,一个扩展点可以直接setter注入其他扩展点
  3. 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因

三. dubbo SPI的实现

  • dubbo使用spi的目的就是获取一个实现类的实现对象,获取途径:org.apache.dubbo.common.extension.ExtensionLoader#getExtension(java.lang.String)
  • dubbo的spi实现路径:a):org.apache.dubbo.common.extension.ExtensionLoader#getExtensionLoader这个就是给入参Class接口创建一个ExtensionLoader并缓存到map里面;b):org.apache.dubbo.common.extension.ExtensionLoader#getAdaptiveExtension这个是获取一个扩展装饰类的对象,这个类有个规则是:如果他没有一个@Adaptive注解,就动态创建一个装饰类,比如Protocol$Adaptive对象;c):org.apache.dubbo.common.extension.ExtensionLoader#getExtension(java.lang.String)这个就是获取或者创建一个对象

下面通过代码讲这三个方式的源码,对应下面的1、2、3小节

四. 代码举例

  1. dubbo代码开始是在:


    上面三个图可以看出实际是创建了ExtensionLoader并加入到了org.apache.dubbo.common.extension.ExtensionLoader#EXTENSION_LOADERS缓存起来
  2. 那么实际重点是在:org.apache.dubbo.common.extension.ExtensionLoader#getAdaptiveExtension 进入这个org.apache.dubbo.common.extension.ExtensionLoader#createAdaptiveExtension方法一步一步跟进:
    org.apache.dubbo.common.extension.ExtensionLoader#getAdaptiveExtensionClass
    org.apache.dubbo.common.extension.ExtensionLoader#getExtensionClasses
    org.apache.dubbo.common.extension.ExtensionLoader#loadExtensionClasses
    org.apache.dubbo.common.extension.ExtensionLoader#loadDirectory(java.util.Map<java.lang.String,java.lang.Class<?>>, java.lang.String, java.lang.String, boolean, boolean, java.lang.String...)
    org.apache.dubbo.common.extension.ExtensionLoader#loadResource
    从代码可以看到SPI发现接口实现实际是通过:得到配置文件的全路径,然后通过ClassLoader获取到Enumeration<java.net.URL> urls,通过java.net.URL.openStream()加载class
    然后继续看上面代码过程,从这些代码跟进中会发现其实他是把SPI中的实现全部加载进了缓存中。
    有注解:org.apache.dubbo.common.extension.Adaptive的赋值到了org.apache.dubbo.common.extension.ExtensionLoader#cachedAdaptiveClass 否则:构造函数入参是传入参数clazz的set到了:org.apache.dubbo.common.extension.ExtensionLoader#cachedWrapperClasses,这个cachedWrapperClasses里面存放的就是几个Wrapper类,这个是后面SPI的AOP使用的
    其余的根据条件加入各自缓存容器里
    代码截图:

    这里就是dubbo的SPI基本用法,dubbo中的接口实现很多都是使用SPI的方式加载的
    同时上面图这里也有一个细节,通过代码可以看到org.apache.dubbo.common.extension.ExtensionLoader#getExtensionClasses方法里面会把(上面写到了)@Adaptive注解的赋值到cachedAdaptiveClass,那么如果根据判断clazz.isAnnotationPresent(Adaptive.class)没有@Adaptive注解则这个cachedAdaptiveClass为空,那么就会进入org.apache.dubbo.common.extension.ExtensionLoader#createAdaptiveExtensionClass,这个方法的作用就是动态生成一个Adaptive代理类,具体生成代码在:org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generate,生成代码之后会动态编译这个生成的代码,并返回一个编译后的Class,这里主要是动态编译相关内容,在我的后面一篇中专门讲了这个:dubbo的动态编译javassist,大家可以去看看
    所以根据上面这代码可以得出一个结论:org.apache.dubbo.common.extension.ExtensionLoader#getAdaptiveExtension这个方法会获取一个扩展类的对象,如果这个类有@Adaptive注解,那么通过类编码的内容获取到,如果没有@Adaptive注解,那么就动态生成一个代理类,所以总结起来就是:如果@Adaptive注解在类上,那么这个类就是一个装饰类,如果注解在方法上就是一个自动生成的动态代理类
    例如:

    回到:

    可以看到在getAdaptiveExtensionClass()获取到Class之后调用了java.lang.Class#newInstance创建实例,然后进入了org.apache.dubbo.common.extension.ExtensionLoader#injectExtension,这个方法其实是dubbo的IOC实现,这个在下面讲
  3. 上面讲了org.apache.dubbo.common.extension.ExtensionLoader#getAdaptiveExtension,最后讲到了org.apache.dubbo.common.extension.ExtensionLoader#injectExtension这里停下来了,这里我们从org.apache.dubbo.common.extension.ExtensionLoader#getExtension(java.lang.String)方法切入查看,举例:ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("injvm") 从这图中可以看出,根究传入的name,先从缓存中获取Holder,如果Holder实例为空那么创建一个进入:org.apache.dubbo.common.extension.ExtensionLoader#createExtension 这个方法可以看到刚进来就是getExtensionClasses(),这个就是上面我们已经看过的加载所有SPI实现的方法,然后根据获取的Class又是一套缓存获取没有就创建,然后把创建的实例带着进入:org.apache.dubbo.common.extension.ExtensionLoader#injectExtension,这个方法就是上面我们停下来的地方,这里我们接着分析这个方法
    这个方法进来就是循环这个实例的所有方法,看方法是不是set方法("set"开头、public、并且只有一个入参)、是不是标注有@DisableInject注解、参数是不是基本数据类型的,如果满足其中一个就不循环直接下一个,重点看这个方法:objectFactory.getExtension(pt, property),这个objectFactory其实是org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory,他里面有个属性:org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory#factories是个List,这个List里面其实就是:org.apache.dubbo.common.extension.factory.SpiExtensionFactoryorg.apache.dubbo.config.spring.extension.SpringExtensionFactory,通过org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory#getExtension方法分别从SPI和Spring里面获取实例
    这里我们先看SpiExtensionFactory的时候执行的代码
    这段代码其实就是我们之前分析的ExtensionLoader.getExtensionLoader(type)org.apache.dubbo.common.extension.ExtensionLoader#getAdaptiveExtension,从SPI里面获取实例对象,这里就不再重新说一遍了。接着再看SpringExtensionFactory
    其实就是从spring容器ApplicationContext中获取实例对象,当然如果这个实例标注有@SPI注解,那么不从spring中获取,应该从SPI中获取。到这里就分析完了获取实例的部分了,然后回到
    获取到实例之后因为这里是set方法,通过method.invoke(instance, object)进行注入,这里就是dubbo的IOC依赖注入,接下来继续回到org.apache.dubbo.common.extension.ExtensionLoader#createExtension中来,接着往下走
    if(wrap)这个进入之后,先是找到实现这个SPI接口的所有wrapper类,对其根据org.apache.dubbo.common.extension.support.WrapperComparator#compare规则进行排序并反转顺序,那么这个org.apache.dubbo.common.extension.ExtensionLoader#cachedWrapperClasses里面存放的是什么呢,其实上面在分析SPI根据配置文件加载对象Class的时候看到这个了org.apache.dubbo.common.extension.ExtensionLoader#loadClass这个方法,
    意思是构造函数入参是这个实例Class的时候,就认为他是个wrapper类,比如

    那回到org.apache.dubbo.common.extension.ExtensionLoader#createExtension方法中,接着讲在拿到warpper类之后,会执行:injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance))这行代码,这行代码的意思是获取这个warpper类的构造器,并创建实例,然后继续调用上面我们刚分析过的dubbo的IOC方法org.apache.dubbo.common.extension.ExtensionLoader#injectExtension把这个wrapper实例进行依赖注入,其实就是AOP,加入了过滤和监听事件(这几个Wrapper类,后面在dubbo服务发布这篇里面有提到这几个),之后就是经过这行代码再把创建的实例return出去
    到这里就返回了一个包装过的对象实例,这就是dubbo的SPI通过getExtension方法获取实例的过程