SPI是什么

SPI的英文名称是Service Provider Interface,是Java 内置的服务发现机制。
在开发过程中,将问题抽象成API,可以为API提供各种实现。如果现在需要对API提供一种新的实现,我们可以不用修改原来的代码,直接生成新的Jar包,在包里提供API的新实现。通过Java的SPI机制,可以实现了框架的动态扩展,让第三方的实现能像插件一样嵌入到系统中。
Java的SPI类似于IOC的功能,将装配的控制权移到了程序之外,实现在模块装配的时候不用在程序中动态指明。所以SPI的核心思想就是解耦,这在模块化设计中尤其重要。
简单来说,它就是一种动态替换发现的机制, 举个例子来说,一个接口多个实现类,在程序运行时,我们可以动态的去选择我们想要的那个实现类。

Java SPI 源码分析

ServiceLoader.load()就是 Java SPI 入口,如图。
image.png
简单的说就是先找当前线程绑定的 ClassLoader,如果没有就用 SystemClassLoader,然后清除一下缓存,再创建一个 LazyIterator。
那现在重点就是 LazyIterator了,从上面代码可以看到调用了 hasNext() 来做实例循环,通过 next() 得到一个实例。而 LazyIterator 其实就是 Iterator 的实现类。
image.png
不管进入 if 分支还是 else 分支,重点都在我框出来的代码,接下来就重要了
image.png
可以看到这个方法其实就是在约定好的地方找到接口对应的文件,然后加载文件并且解析文件里面的内容。
再来看一下 nextService()。
image.png
所以就是通过文件里填写的全限定名加载类,并且创建其实例放入缓存之后返回实例。
整体的 Java SPI 的源码解析已经完毕。就是约定一个目录,根据接口名去那个目录找到文件,文件解析得到实现类的全限定名,然后循环加载实现类和创建其实例。
image.png

想一下 Java SPI 哪里不好

Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。
所以说 Java SPI 无法按需加载实现类。

Dubbo SPI

为什么dubbo要自己设计一套SPI?
dubbo在原来的基础上设计了以下功能

  1. 原始JDK spi不支持缓存;dubbo设计了缓存对象:spi的key与value 缓存在 cachedInstances对象里面,它是一个ConcurrentMap
  2. 原始JDK spi不支持默认值,dubbo设计默认值:@SPI(“dubbo”) 代表默认的spi对象,例如Protocol的@SPI(“dubbo”)就是 DubboProtocol,
  3. 通过 ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension()拿默认对象
  4. jdk要用for循环判断对象,dubbo设计getExtension灵活方便,动态获取spi对象,例如 ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(spi的key)来提取对象
  5. 原始JDK spi不支持 AOP功能,dubbo设计增加了AOP功能,在cachedWrapperClasses,在原始spi类,包装了XxxxFilterWrapper XxxxListenerWrapper
  6. 原始JDK spi不支持 IOC功能,dubbo设计增加了IOC,通过构造函数注入,代码为:wrapperClass.getConstructor(type).newInstance(instance),

因此 Dubbo 就自己实现了一个 SPI,让我们想一下按需加载的话首先得给个名字,通过名字去文件里面找到对应的实现类全限定名然后加载实例化即可。
Dubbo 就是这样设计的,配置文件里面存放的是键值对,我截一个 Cluster 的配置。
dubbo SPI - 图6
并且 Dubbo SPI 除了可以按需加载实现类之外,增加了 IOC 和 AOP 的特性,还有个自适应扩展机制。
我们先来看一下 Dubbo 对配置文件目录的约定,不同于 Java SPI ,Dubbo 分为了三类目录。

  • META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。
  • META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件。
  • META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件。

    Dubbo SPI 简单实例

    用法很是简单,我就拿官网上的例子来展示一下。
    首先在 META-INF/dubbo 目录下按接口全限定名建立一个文件,内容如下:
    1. optimusPrime = org.apache.spi.OptimusPrime
    2. bumblebee = org.apache.spi.Bumblebee
    然后在接口上标注@SPI 注解,以表明它要用SPI机制,类似下面这个图(我就是拿 Cluster 的图举个例子,和这个示例代码定义的接口不一样)。
    image.png
    接着通过下面的示例代码即可加载指定的实现类。
    image.png
    再来看一下运行的结果。
    image.png

    Dubbo 源码分析

    此次分析的源码版本是 2.6.4

相信通过上面的描述大家已经对 Dubbo SPI 已经有了一定的认识,接下来我们来看看它的实现。
从上面的示例代码我们知道 ExtensionLoader 好像就是重点,它是类似 Java SPI 中 ServiceLoader 的存在。
我们可以看到大致流程就是先通过接口类找到一个 ExtensionLoader ,然后再通过 ExtensionLoader.getExtension(name) 得到指定名字的实现类实例。
我们就先看下 getExtensionLoader() 做了什么。
image.png
很简单,做了一些判断然后从缓存里面找是否已经存在这个类型的 ExtensionLoader ,如果没有就新建一个塞入缓存。最后返回接口类对应的 ExtensionLoader 。
我们再来看一下 getExtension() 方法,从现象我们可以知道这个方法就是从类对应的 ExtensionLoader 中通过名字找到实例化完的实现类。
image.png
可以看到重点就是 createExtension(),我们再来看下这个方法干了啥。
image.png
整体逻辑很清晰,先找实现类,判断缓存是否有实例,没有就反射建个实例,然后执行 set 方法依赖注入。如果有找到包装类的话,再包一层。
到这步为止我先画个图,大家理一理,还是很简单的。
image.png
那么问题来了 getExtensionClasses() 是怎么找的呢?injectExtension() 如何注入的呢(其实我已经说了set方法注入)?为什么需要包装类呢?

getExtensionClasses

这个方法进去也是先去缓存中找,如果缓存是空的,那么调用 loadExtensionClasses,我们就来看下这个方法。
image.png
loadDirectory里面就是根据类名和指定的目录,找到文件先获取所有的资源,然后一个一个去加载类,然后再通过loadClass去做一下缓存操作。
image.png
可以看到,loadClass 之前已经加载了类,loadClass 只是根据类上面的情况做不同的缓存。分别有 AdaptiveWrapperClass 和普通类这三种,普通类又将Activate记录了一下。至此对于普通的类来说整个 SPI 过程完结了。
image.png
接下来我们分别看不是普通类的几种东西是干啥用的。

Adaptive 注解 - 自适应扩展

在进入这个注解分析之前,我们需要知道 Dubbo 的自适应扩展机制。
我们先来看一个场景,首先我们根据配置来进行 SPI 扩展的加载,但是我不想在启动的时候让扩展被加载,我想根据请求时候的参数来动态选择对应的扩展。
怎么做呢?
Dubbo 通过一个代理机制实现了自适应扩展,简单的说就是为你想扩展的接口生成一个代理类,可以通过JDK 或者 javassist 编译你生成的代理类代码,然后通过反射创建实例。
这个实例里面的实现会根据本来方法的请求参数得知需要的扩展类,然后通过 ExtensionLoader.getExtensionLoader(type.class).getExtension(从参数得来的name),来获取真正的实例来调用。
我从官网搞了个例子,大家来看下。
image.png
现在大家应该对自适应扩展有了一定的认识了,我们再来看下源码,到底怎么做的。
image.png
这个注解就是自适应扩展相关的注解,可以修饰类和方法上,在修饰类的时候不会生成代理类,因为这个类就是代理类,修饰在方法上的时候会生成代理类。

Adaptive 注解在类上

比如这个 ExtensionFactory 有三个实现类,其中一个实现类就被标注了 Adaptive 注解。
image.png
image.png
在 ExtensionLoader 构造的时候就会去通过getAdaptiveExtension 获取指定的扩展类的 ExtensionFactory。
image.png
我们再来看下 AdaptiveExtensionFactory 的实现。
image.png
可以看到先缓存了所有实现类,然后在获取的时候通过遍历找到对应的 Extension。
我们再来深入分析一波 getAdaptiveExtension 里面到底干了什么。
image.png
到这里其实已经和上文分析的 getExtensionClasses中loadClass 对 Adaptive 特殊缓存相呼应上了。
image.png

Adaptive 注解在方法上

注解在方法上则需要动态拼接代码,然后动态生成类,我们以 Protocol 为例子来看一下。
image.png
Protocol 没有实现类注释了 Adaptive ,但是接口上有两个方法注解了 Adaptive ,有两个方法没有。
因此它走的逻辑应该应该是 createAdaptiveExtensionClass
image.png
具体在里面如何生成代码的我就不再深入了,有兴趣的自己去看吧,我就把成品解析一下,就差不多了。
image.png
我美化一下给大家看看。
image.png
可以看到会生成包,也会生成 import 语句,类名就是接口加个$Adaptive,并且实现这接口,没有标记 Adaptive 注解的方法调用的话直接抛错。
我们再来看一下标注了注解的方法,我就拿 export 举例。
image.png
就像我前面说的那样,根据请求的参数,即 URL 得到具体要调用的实现类名,然后再调用 getExtension 获取。
整个自适应扩展流程如下。
image.png

WrapperClass - AOP

包装类是因为一个扩展接口可能有多个扩展实现类,而这些扩展实现类会有一个相同的或者公共的逻辑,如果每个实现类都写一遍代码就重复了,并且比较不好维护。
因此就搞了个包装类,Dubbo 里帮你自动包装,只需要某个扩展类的构造函数只有一个参数,并且是扩展接口类型,就会被判定为包装类,然后记录下来,用来包装别的实现类。
image.png
简单又巧妙,这就是 AOP 了。

injectExtension - IOC

直接看代码,很简单,就是查找 set 方法,根据参数找到依赖对象则注入。
image.png
这就是 IOC。

Activate 注解

拿 Filter 举例,Filter 有很多实现类,在某些场景下需要其中的几个实现类,而某些场景下需要另外几个,而 Activate 注解就是标记这个用的。
它有三个属性,group 表示修饰在哪个端,是 provider 还是 consumer,value 表示在 URL参数中出现才会被激活,order 表示实现类的顺序。

总结

先放个上述过程完整的图。
image.png
然后我们再来总结一下,今天丙先带大家了解了下什么是 SPI,写了个简单示例,并且进行了 Java SPI 源码分析。
得知了 Java SPI 会一次加载和实例化所有的实现类。
而 Dubbo SPI 则自己实现了 SPI,可以通过名字实例化指定的实现类,并且实现了 IOC 、AOP 与 自适应扩展 SPI 。