基于Dubbo SPI 的加载机制,让整个框架的接口和具体实现完全解耦,从而奠定了整个框架良好的可扩展性。Dubbo SPI 并没有直接使用 Java SPI,而是在其基础上做了相应的改进,形成了一套自己的规范和特性。下面我们从最基础的 Java SPI 介绍一下 SPI 的思想。
Java SPI
SPI (Service Provider Interface)最初是提供给厂商做插件开发的。Java SPI 使用了策略模式,一个接口会有多个实现。我们只生命接口,具体的实现并不在程序中直接确定,而是由程序之外的配置控制,用于最终的装配。一个简单的实现步骤如下:
1. 定义一个接口及方法
2. 编写该接口的一个实现
3. 在META-INF/services/ 目录下创建一个接口全路径命名的文件,如 com.moguhu.spi.HelloService
4. 文件内容为接口的具体实现类的全路径,每个实现类单独一行
5. 在代码中通过 java.util.ServiceLoader 来加载具体的实现
下面我们看下具体的代码实现
下面我们看下获取实现类的工具方法 SpiServiceHelper 的代码实现:
Dubbo SPI 规范
Dubbo SPI 和 Java SPI 类似,需要在 META-INF/dubbo/ 目录下放置对应的 SPI 配置文件,文件名为接口的全路径名。如下所示:
这里面需要注意的是,除了资源路径与Java SPI 不同之外,文件内容中也多了个 key,如上图中的dubbo、hessian2、fastjson 等。这里与dubbo 最开始 2.0.x 版本中的 SPI 也有所不同(原先@SPI 叫做 @Extension),原先每个SPI 的实现的名称是写在每个实现类上的。接口和注解都需要有 @Extension 注解,实现类的注解上面会写这个SPI 实现的名字。而最新的扩展则是只需要在接口上加上 @SPI,然后配置文件上带有对应 SPI 实现类的名称,从一定程度上做了简化。
扩展点的分类与缓存
Dubbo SPI 是按需加载的,可以分为 Class 缓存、实例缓存。这两种缓存又能根据扩展类的种类分为普通扩展类、包装扩展类(Wrapper)、自适应扩展类(Adaptive)等。
Class 缓存:Dubbo SPI 获取扩展类时,首先会从缓存中获取。如果缓存中不存在,则会加载配置文件,将Class 缓存加载到内存中,类似于懒加载。
实例缓存:ExtensionLoader 中不仅会缓存Class,也会缓存Class实例化后的对象。整体原则就是会从缓存中优先获取,缓存中没有的再使用 Class.forName() 将类加载到内存中。
被缓存的Class 和对象实例可以分为以下几类:
1. 普通扩展类,最基础的,配置在SPI 配置文件中的扩展类实现
2. 包装扩展类,包装类通常不会直接承载具体的实现,它是对ExtensionLoader加载的策略类的包装,通过这个包装可以实现Filter、Listener 等效果。如 Protocol 的包装类 ProtocolFilterWrapper、ProtocolListenerWrapper
3. 自适应扩展类,可以理解为对不同策略类的包装,这个类是由Dubbo 动态生成的,里面的内容大概可以理解为就是一个策略的组装,最终的业务调用还是由各个策略类实现。不同策略的选择通常是由 URL 参数决定的。
4. 其他缓存,如扩展类加载器缓存、扩展名缓存等。
扩展点提供的功能
Dubbo SPI 一共包含 4 中特性,包含:自动包装(Wrapper)、自动装载(Autowire)、自适应(Adaptive)、自动激活(Active),下面分别介绍一下这4类特性。其中 Wrapper、Autowire、Adaptive 这3 个功能,在 Dubbo 开源的第一版(2.0.7)中就已经提供,后续只是简单的加了一个 Active 的功能。
自动包装
在ExtensionLoader 加载扩展时,如果发现这个类有1个参数并且参数为ExtensionLoader 中的T 类型时,就会自动认为是 wrappers(老版本命名叫做 autoproxies),也就是表示对真正的扩展包了一层。
从上面可以看出,ProtocolListenerWrapper 有个1个参数且入参为 Protocol 的构造方法,此时ProtocolListenerWrapper j就是一个包装类,当获取一个Protocol的扩展时,如:DubboProtocol,此时就会为 DubboProtocol 自动作为入参,被包装为ProtocolListenerWrapper 的类型。因为 ProtocolListenerWrapper 也是一个Protocol 类型,此时ExtensionLoader 可以返回此包装类给上游使用。包装类主要是用于增强 扩展类的功能,比如 Protocol 的包装类有 2个:ProtocolListenerWrapper 和 ProtocolFilterWrapper ,分别为 Protocol 增加了过滤器和 监听器,以扩展其功能。
自动装载
自动装载类似于Spring 中的 Autowire,也就是自动装配。当一个扩展类是另外一个扩展类的成员变量时,并且有 setter 方法,此时ExtensionLoader 就会自动new 一个扩展类,自动注入。通常被注入的会是一个接口类型,如 Protocol,则会被注入为 Protocol$Adaptive,也就是一个自适应类型。
自适应
自适应的扩展可以认为是策略模式中,组装策略的那部分代码,而策略本身是由 URL 参数中的参数决定的。下面我们看下一个 Adaptive 的例子。
首先我们提供一个扩展接口 SimpleExt,其中有3个方法,echo()、yell() 允许做自适应,而 bang() 不允许做自适应。
经过ExtensionLoader 创建出来的自适应扩展代码,大概如下所示,也就是说具体通过哪个策略,是通过 URL 中对应的参数决定的:
自动激活
自动激活是ExtensionLoader 后续新增的功能,通过 @Adaptive 注解实现。可以标记对应的扩展点是否可以被激活使用,目前主要用于Filter 链构建前的,可用Filter列表获取。因为是后续新增的功能,笔者对比了Dubbo 2.6.0 和 2.0.7 两个版本里面Filter 列表获取的差异,老的里面是通过配置写死的 Filter 列表顺序,而新的顺序则是通过 @Adaptive 在各个扩展实现类中标注的,此时它可以标注分组(Filter 链可以用在服务端/客户端),也可以标注分组内的顺序。此时依赖 @Active 注解就可以构建出原来写死在代码里的 Filter 顺序列表了。
参考:《深入理解Apache Dubbo 与实战》、Dubbo 2.6.0 源代码、Dubbo 2.0.7 源代码