Dubbo 源码解析之 SPI(一):扩展类的加载过程(下)

阅读数:1 2020 年 2 月 6 日 10:32

Dubbo源码解析之SPI(一):扩展类的加载过程(下)

接上文

二、Dubbo SPI

2.1 Dubbo SPI 的小栗子

老习惯,在拆解源码之前,先来个栗子。此处示例是在前文例子的基础上稍做了些修改。

1)定义一个接口

修改接口,加上了 Dubbo 的 @SPI 注解。

@SPI
public interface Operation {
        int operate(int num1, int num2);
}

2)写两个简单的实现

沿用之前的两个实现类。

3)添加一个配置文件

新增配置文件放在 dubbo 目录下。

目录结构

图片 11.png

文件内容

division=com.api.impl.DivisionOperation
plus=com.api.impl.PlusOperation

4)测试程序

public class DubboSpiTest {
    @Test
    public void testOperation() throws Exception {
          ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Operation.class);
          Operation division = loader.getExtension("division");
          System.out.println("result: " + division.operate(1, 2));
    } 
}

5)测试结果

run division operation
result:0

2.2 Dubbo SPI 源码

上面的测试例子也很简单,和 JDK 原生的 SPI 对比来看,Dubbo 的 SPI 可以根据配置的 kv 值来获取。在没有拆解源码之前,考虑一下如何实现。

我可能会用双层 Map 来实现缓存:第一层的 key 为接口的 class 对象,value 为一个 map;第二层的 key 为扩展名 (配置文件中的 key),value 为实现类的 class。实现懒加载的方式,当运行方法的时候创建空 map。在真正获取时先从缓存中查找具体实现类的 class 对象,找得到就直接返回、找不到就根据配置文件加载并缓存。

Dubbo 又是如何实现的呢?

2.2.1 getExtensionLoader 方法

首先来拆解 getExtensionLoader 方法。

图片 15.png

这是一个静态的工厂方法,要求传入的类型必须为接口并且有 SPI 的注解,用 map 做了个缓存,key 为接口的 class 对象,而 value 是 ExtensionLoader 对象。

2.2.2 getExtension 方法

再来拆解 ExtensionLoader 的 getExtension 方法。

图片 16.png

这段代码也不复杂,如果传入的参数为’true’,则返回默认的扩展类实例;否则,从缓存中获取实例,如果有就从缓存中获取,没有的话就新建。用 map 做缓存,缓存了 holder 对象,而 holder 对象中存放扩展类。用 volatile 关键字和双重检查来应对多线程创建问题,这也是单例模式的常用写法。

2.2.3 createExtension 方法

重点分析 createExtension 方法。

图片 17.png

这段代码由几部分组成:

  • 根据传入的扩展名获取对应的 class。

  • 根据 class 去缓存中获取实例,如果没有的话,通过反射创建对象并放入缓存。

  • 依赖注入, 完成对象实例的初始化。

  • 创建 wrapper 对象。也就是说,此处返回的对象不一定是具体的实现类,可能是包装的对象。

第二个没啥好说的,我们重点来分析一下 1、3、4 三个部分。

1)getExtensionClasses 方法

图片 18.png

老套路,从缓存获取,没有的话创建并加入缓存。这里缓存的是一个扩展名和 class 的关系。这个扩展名就是在配置文件中的 key。创建之前,先缓存了一下接口的限定名。加载配置文件的路径是以下这几个。

图片 19.png

2)loadDirectory 方法

图片 20.png

获取配置文件路径,获取 classLoader,并使用 loadResource 方法做进一步处理。

3)loadResource 方法

图片 21.png

图片 22.png

loadResource 加载了配置文件,并解析了配置文件中的内容。loadClass 方法操作了不同的缓存。

首先判断是否有 Adaptive 注解,有的话缓存到 cacheAdaptiveClass(缓存结构为 class);然后判断是否 wrapperclasses,是的话缓存到 cacheWrapperClass 中 (缓存结构为 Set);如果以上都不是,这个类就是个普通的类,存储 class 和名称的映射关系到 cacheNames 里 (缓存结构为 Map)。

基本上 getExtensionClasses 方法就分析完了,可以看出来,其实并不是很复杂。

2.2.4 IOC

1)injectExtension 方法

图片 23.png

这个方法实现了依赖注入,即 IOC。首先通过反射获取到实例的方法;然后遍历,获取 setter 方法;接着从 objectFactory 中获取依赖对象;最后通过反射调用 setter 方法注入依赖。

objectFactory 的变量类型为 AdaptiveExtensionFactory。

2)AdaptiveExtensionFactory

图片 24.png

这个类里面有个 ExtensionFactory 的列表,用来存储其他类型的 ExtensionFactory。Dubbo 提供了两种 ExtensionFactory,一种是 SpiExtensionFactory, 用于创建自适应的扩展;另一种是 SpringExtesionFactory, 用于从 Spring 的 IOC 容器中获取扩展。配置文件一个在 dubbo-common 模块,一个在 dubbo-config 模块。

配置文件

图片 25.png

图片 26.png

SpiExtensionFactory 中的 Spi 方式前面已经解析过了。

图片 27.png

SpringExtesionFactory 是从 ApplicationContext 中获取对应的实例。先根据名称查找,找不到的话,再根据类型查找。

图片 28.png

依赖注入的部分也拆解完毕,看看这次拆解的最后一部分代码。

2.2.5 AOP

创建 wrapper 对象的部分,wrapper 对象是从哪里来的呢?还记得之前拆解的第一步么,loadClass 方法中有几个缓存,其中 wrapperclasses 就是缓存这些 wrapper 的 class。

图片 29.png

从代码中可以看出,只要构造方法里有且只有唯一参数,同时此参数为当前传入的接口类型,即为 wrapper class。

图片 30.png

此处循环创建 wrapper 实例,首先将 instance 做为构造函数的参数,通过反射来创建 wrapper 对象,然后再向 wrapper 中注入依赖。

看到这里,可能会有人有疑问:为什么要创建一个 wrapper 对象?其实很简单,系统要在真正调用的前后干点别的事呗。这个就有点类似于 spring 的 aop 了。

三、总结

本文简单介绍了 JDK 的 SPI 和 Dubbo 的 SPI 用法,分析了 JDK 的 SPI 源码和 Dubbo 的 SPI 源码。在拆解的过程中可以看出,Dubbo 的源码还是很值得一读的。在实现方面考虑得很周全,不仅有对多线程的处理、多层缓存,也有 IOC、AOP 的过程。不过,Dubbo 的 SPI 就这么简单么?当然不是,这篇只拆解了扩展类的加载过程,Dubbo 的 SPI 中还有个很复杂的扩展点 - 自适应机制。欲知后事如何,请听下回分解~~

评论

发布