一、什么是SPI

Service Provider Interface 一个接口多种实现,通过配置确定使用哪个实现,是JDK内置的一种服务提供发现机制;而Dubbo的SPI并不是基于Java的SPI实现的,而是自己按照SPI的功能实现了一遍,并添加了拓展点:IOC和AOP的支持。

二、Java SPI

定义

SPI是一种动态替换发现的机制, 比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。我们经常遇到的就是java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。
Java SPI 和 Dubbo SPI - 图1

使用方法

使用Java SPI为某个接口实现动态替换时,我们需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。下面以一个对象序列化接口为例:
首先是接口:

  1. public interface ObjectSerializer {
  2. byte[] serialize(Object obj) throws ObjectSerializerException;
  3. <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException;
  4. String getSchemeName();
  5. }

实现类:

  1. public class KryoSerializer implements ObjectSerializer {
  2. @Override
  3. public byte[] serialize(Object obj) throws ObjectSerializerException {
  4. byte[] bytes;
  5. ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
  6. try {
  7. //获取kryo对象
  8. Kryo kryo = new Kryo();
  9. Output output = new Output(outputStream);
  10. kryo.writeObject(output, obj);
  11. bytes = output.toBytes();
  12. output.flush();
  13. } catch (Exception ex) {
  14. throw new ObjectSerializerException("kryo serialize error" + ex.getMessage());
  15. } finally {
  16. try {
  17. outputStream.flush();
  18. outputStream.close();
  19. } catch (IOException e) {
  20. }
  21. }
  22. return bytes;
  23. }
  24. @Override
  25. public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
  26. T object;
  27. try (ByteArrayInputStream inputStream = new ByteArrayInputStream(param)) {
  28. Kryo kryo = new Kryo();
  29. Input input = new Input(inputStream);
  30. object = kryo.readObject(input, clazz);
  31. input.close();
  32. } catch (Exception e) {
  33. throw new ObjectSerializerException("kryo deSerialize error" + e.getMessage());
  34. }
  35. return object;
  36. }
  37. @Override
  38. public String getSchemeName() {
  39. return "kryoSerializer";
  40. }
  41. }

文件名:com.blueskykong.javaspi.serializer.ObjectSerializer
文件内容:

  1. com.blueskykong.javaspi.serializer.KryoSerializer

使用到序列化接口的Service类:

@Service
public class SerializerService {


    public ObjectSerializer getObjectSerializer() {
        ServiceLoader<ObjectSerializer> serializers = ServiceLoader.load(ObjectSerializer.class);

        final Optional<ObjectSerializer> serializer = StreamSupport.stream(serializers.spliterator(), false)
                .findFirst();

        return serializer.orElse(new JavaSerializer());
    }
}

测试类:

    @Autowired
    private SerializerService serializerService;

    @Test
    public void serializerTest() throws ObjectSerializerException {
        ObjectSerializer objectSerializer = serializerService.getObjectSerializer();
        System.out.println(objectSerializer.getSchemeName());
        byte[] arrays = objectSerializer.serialize(Arrays.asList("1", "2", "3"));
        ArrayList list = objectSerializer.deSerialize(arrays, ArrayList.class);
        Assert.assertArrayEquals(Arrays.asList("1", "2", "3").toArray(), list.toArray());
    }

输出结果:kryoSerializer

Java SPI 源码分析

之前的文章我也提到了 Dubbo 并没有用 Java 实现的 SPI,而是自定义 SPI,那肯定是 Java SPI 有什么不方便的地方或者劣势。
因此先深入了解一下 Java SPI,这样才能知道哪里不好,进而再和 Dubbo SPI 进行对比的时候会更加的清晰其优势。
已经给大家做了注释,并且逻辑也不难的,想要变强源码不可或缺。为了让大家更好的理解,在源码分析完了之后还会画个图,帮大家再理一下思路。
从上面我的示例中可以看到ServiceLoader.load()其实就是 Java SPI 入口,我们来看看到底做了什么操作。
Java SPI 和 Dubbo SPI - 图2
我用一句话概括一下,简单的说就是先找当前线程绑定的 ClassLoader,如果没有就用 SystemClassLoader,然后清除一下缓存,再创建一个 LazyIterator。
那现在重点就是 LazyIterator了,从上面代码可以看到我们调用了 hasNext() 来做实例循环,通过 next() 得到一个实例。而 LazyIterator 其实就是 Iterator 的实现类。我们来看看它到底干了啥。
Java SPI 和 Dubbo SPI - 图3
不管进入 if 分支还是 else 分支,重点都在我框出来的代码,接下来就进入重要时刻了!
Java SPI 和 Dubbo SPI - 图4
可以看到这个方法其实就是在约定好的地方找到接口对应的文件,然后加载文件并且解析文件里面的内容。
我们再来看一下 nextService()。
Java SPI 和 Dubbo SPI - 图5
所以就是通过文件里填写的全限定名加载类,并且创建其实例放入缓存之后返回实例。
整体的 Java SPI 的源码解析已经完毕,是不是很简单?就是约定一个目录,根据接口名去那个目录找到文件,文件解析得到实现类的全限定名,然后循环加载实现类和创建其实例。
我再用一张图来带大家过一遍。
Java SPI 和 Dubbo SPI - 图6

Java SPI的劣势

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

三、Dubbo SPI

Dubbo 实现了一个 SPI,让我们想一下按需加载的话首先你得给个名字,通过名字去文件里面找到对应的实现类全限定名然后加载实例化即可。
Dubbo 就是这样设计的,配置文件里面存放的是键值对,我截一个 Cluster 的配置。
Java SPI 和 Dubbo SPI - 图7
并且 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 目录下按接口全限定名建立一个文件,内容如下:
    optimusPrime = org.apache.spi.OptimusPrime
    bumblebee = org.apache.spi.Bumblebee
    复制代码
    
    然后在接口上标注@SPI 注解,以表明它要用SPI机制,类似下面这个图(我就是拿 Cluster 的图举个例子,和这个示例代码定义的接口不一样)。
    Java SPI 和 Dubbo SPI - 图8
    接着通过下面的示例代码即可加载指定的实现类。
    Java SPI 和 Dubbo SPI - 图9
    再来看一下运行的结果。
    Java SPI 和 Dubbo SPI - 图10

    Dubbo 源码分析

    此次分析的源码版本是 2.6.5

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

getExtensionClasses

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

Adaptive 注解 - 自适应扩展

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

Adaptive 注解在类上

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

Adaptive 注解在方法上

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

WrapperClass - AOP

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

injectExtension - IOC

直接看代码,很简单,就是查找 set 方法,根据参数找到依赖对象则注入。
Java SPI 和 Dubbo SPI - 图33
这就是 IOC。

Activate 注解

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

总结

先放个上述过程完整的图。
Java SPI 和 Dubbo SPI - 图34