源码学习-Dubbo SPI 源码分析

图怪兽_80626f6dc1b24f4822979280bccf22c5_27460.png

背景

今天我们来聊一聊Dubbo 的 SPI

Java 的 SPI 见 https://mikeygithub.github.io/2021/07/21/yuque/coahg1/

简介

SPI : Service Porvider Interface 服务提供接口。使我们的应用程序具有可扩展的服务(微内核架构)、使用者能够添加服务提供者,而无需修改原始应用程序即可实现其适配,像JDBC、一些框架都有用到。

源码

Dubbo 的 SPI 核心加载类是 **ExtensionLoader **类

1
2
3
4
5
6
//获取接口配置的拓展
public T getExtension(String name)
//根据传入的接口返回一个自适应的实现类
public T getAdaptiveExtension()
//获取激活的拓展点列表
public List<T> getActivateExtension(URL url, String key)

1. 获取拓展

1.1 获取拓展

首先我们可以从DubboProtocol类开始看起,其获取dubbo协议就是通过spi进行加载,如下图,在获取拓展之前要优先获取它的加载器

image.png
从上图我们可以看出,2.7后的版本有较大的变化,3.0版本新增了ScopeModel模型范围来区分拓展的作用域

  • FRAMEWORK: 扩展实例在框架内使用,与所有应用程序和模块共享。
  • APPLICATION: 扩展实例在一个应用程序中使用,与应用程序的所有模块共享,不同的应用程序会创建不同的扩展实例。
  • MODULE: 扩展实例在一个模块中使用,不同的模块创建不同的扩展实例。
  • SELF: 为每个作用域创建一个实例,用于特殊的SPI扩展

image.png

1.2 获取拓展加载器

接着往下看 getExtensionLoader 获取拓展加载器方法,在获取加载器过程中主要是真的传入的接口做一些校验,设置当前加载器的作用域,优先在当前加载器管理器缓存中查找,如果找不到则向父类查找,再找不到则通过构造函数进行创建。

image.png

1.3 创建拓展器加载器

先进行判断拓展器的作用域范围是否和所注解的一致,如果不一致不做处理,如果一致就进行创建,直接通过 new 进行创建并放入缓存中。

image.png

1.4 构造拓展加载器

这一步主要是对加载器做一些初始化的工作,设置当前拓展加载加载的接口、拓展的管理器和注入器(后面包装类需要用到)、激活排序

image.png

1.5 获取拓展

现在我们再回过头来看1.1,获取了加载器后,通过加载根据name加载对应的实现,其方法就是 **getExtension **方法,其主要的功能就是通过传入的name进行获取对应的实现,优先查询缓存是否已经存在,对其创建方法进行加锁处理,如果被其他线程优先创建了则直接返回,否则执行createExtension进行创建拓展。如果传入的name==”true”则返回默认的拓展器。

image.png
Holder类的作用就是解决在数据传递过程中能改变一些不可变的对象,对目标对象加一层包装,提供get/set方法达到这种效果
image.png

1.6 创建拓展器

继续点进去,在创建拓展前需要获取拓展器的字节码,因为dubbo的SPI配置是采用key=value的方式进行配置,所以需要传入name(key)来获取他的全类名字进行实例化。

image.png
创建方法主要包含了获取当前拓展器接口所有实现字节码以及创建实例,判断当前加载的所有拓展器是否有包装类,如果有则进行注入(见1.11)
image.png

1.7 获取字节码

根据路径和接口名获取当前文件夹下拓展器配置文件。先通过当前ScopeModel进行获取类加载器,再通过类加载器去获取配置文件。

image.png

这里的加载方式和2.7版本对比做了一个修改不再强制要求在META-INF下的接口文件以key=value方式编写,也支持直接是value(全类名)的方式

image.png

1.8 缓存字节码

在获取当前拓展字节码后判断字节码是否是接口的子类,如果是则继续判断它属于那种类型:
1.如果类上带有**@Adaptive**注解的,则表示他是一个自适应的实现类(见2.1)
2.如果当前拓展器有包含拓展器接口的构造方法,则当前拓展类为包装类
3.否则按照正常加载,尝试通过name获取第一个name(如果name是多个分号隔开)作为当前字节码的激活的实现(如果当前拓展器带有@Activate注解),正常其他缓存字节码

image.png

获取到当前字节码进行类型的判断加入对应的缓存中,执行到此当前接口配置的SPI服务实现字节码已经都加载进对应的缓存中,后面就可以开始通过反射进行实例化

1
2
3
private volatile Class<?> cachedAdaptiveClass = null;//自适应字节码缓存
private Set<Class<?>> cachedWrapperClasses;//包装类字节码缓存
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();//普通拓展缓存

1.9 拓展器的实例化

回到第1.6步骤中可以看到,获取拓展类的字节码后进行调用createExtensionInstance(clazz)实例化拓展,并加入缓存中,再进行前置和后置的处理,对于需要注入的属性进行注入处理。

image.png

真正创建实例的这个方法,通过反射获取当前字节码的所有构造器,查找到匹配的那个构造函数,如果没有构造器则使用默认的构造器进行反射实例化返回实例。

image.png
当前当前拓展器实例化完成返回后,再进行前置和后置处理是默认设置当前实例的 scope model
image.png

1.10 拓展注入

该功能类似Spring 的IOC 通过反射获取当前实例的所有set方法,判断是否是拓展点实例的set方法,如果是通过注入器反射获取实例,进行注入所需实例到当前实例中(和Spring中的IOC类似)。

image.png

1.11 包装类处理

包装类就是当前类存在以拓展器为入参的构造函数的类,其实回到1.7加载字节码中就已经有了区分,如果是包装类字节码则会加入cachedWrapperClasses缓存中,此时通过对包装类进行排序,实例化且注入所依赖的拓展点。

image.png

1.12 初始化拓展生命周期

初始化拓展器生命周期(如果当前拓展器继承了Lifecycle接口)

1
2
3
4
5
6
7

private void initExtension(T instance) {
if (instance instanceof Lifecycle) {
Lifecycle lifecycle = (Lifecycle) instance;
lifecycle.initialize();
}
}

1.13 返回拓展器

至此拓展器的加载实例化已经完成。

2.自适应的实现类

@Adaptive称为自适应扩展点注解

在实际应用场景中,一个扩展接口往往会有多种实现类,因为Dubbo是基于URL驱动,所以在运行时,通过传入URL中的某些参数来动态控制具体实现,这便是Dubbo的扩展点自适应特性。

在Dubbo中,@Adaptive一般用来修饰类和接口方法,在整个Dubbo框架中,只有少数几个地方使用在类级别上,如AdaptiveExtensionFactory和AdaptiveCompiler,其余都标注在方法上。如果用在接口的子类上,则表示Adaptive机制的实现会按照该子类的方式进行自定义实现;如果用在方法上,则表示Dubbo会为该接口自动生成一个子类,并且按照一定的格式重写该方法,而其余没有标注@Adaptive注解的方法将会默认抛出异常。

在通过URL对象获取参数时,参数key获取的对应规则是,首先会从@Adaptive注解的参数值中获取,如果该注解没有指定参数名,那么就会默认将目标接口的类名转换为点分形式作为参数名,比如SPI接口为SimpleSpiDemo转换为点分形式就是simple.spi.demo。

2.1获取自适应实现类

获取一个Adaptive类的class对象,优先在缓存中查找,如果不存在则创建一个,该方法会保证一定存在一个该class对象

image.png

2.2 创建自适应实现类

加锁创建保证不会存在多个线程创建的情况,和1中的获取拓展器一样需要先获取相关的字节码文件,不过自适应拓展是通过动态的创建字节码文件来进行实例化的,通过提供的代码生成器(规则)进行生成字节码

image.png

回到上面的1.8缓存字节码步骤中,如果是类上带有@Adaptive的拓展器其在加载的时候已经缓存,到1249行已经返回对应的字节码无需再进行创建。否则是方法上带有@Adaptive则进行动态生成。

2.3 生成代码

进入到代码生成器的generate方法,如果字节码的所有方法上至少有一个方法带有@Adaptive注解才会进行正常生成代码,否则生成的方法是抛出UnsupportedOperationException异常。

image.png
通过获取当前字节码的所有方法进行遍历调用生成方法代码

1
code.append(generateMethod(method));//生成代码并追加到当前动态构建的类

image.png
比较核心的是生成方法的方法体,需要先判断当前方法上是否存在@Adaptive修饰,检查URL在该方法的形参索引位置,因为需要根据URL的值来返回对应的拓展实现,如果形参列表存在URL参数,需要添加URL空值检查。如果不存在尝试通过从方法形参获取URL的get方法获取(包装在参数里面)如果还查找不到则抛出异常
image.png
参考例子

1
2
3
4
5
6
7
8
9
10
11
package com.example.demo.spi;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;

@SPI("animal")//必须以@SPI注解标记
public interface Animal {
@Adaptive
void greet(URL url);
}

拓展1

1
2
3
4
5
6
7
8
9
10
11
package com.example.demo.spi;

import org.apache.dubbo.common.URL;

public class Cat implements Animal {
@Override
public void greet(URL url) {
System.out.println("miao miao miao ~");
}
}

拓展2

1
2
3
4
5
6
7
8
9
10
11
package com.example.demo.spi;

import org.apache.dubbo.common.URL;

public class Dog implements Animal {
@Override
public void greet(URL url) {
System.out.println("wang wang wang ~");
}
}

SPI配置实现文件

1
2
cat=com.example.demo.spi.Cat
dog=com.example.demo.spi.Dog

调查测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.example.demo.spi;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.model.ApplicationModel;

public class Test {
public static void main(String[] args) {
// 首先创建一个模拟用的URL对象
URL url = URL.valueOf("dubbo://192.168.0.101:20880?animal=dog");
// 通过ExtensionLoader获取
Animal adaptiveExtension = ApplicationModel.defaultModel().getDefaultModule().getExtensionLoader(Animal.class).getAdaptiveExtension();
// 使用该FruitGranter调用其"自适应标注的"方法,获取调用结果
adaptiveExtension.greet(url);
}
}

动态生成的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.example.demo.spi;
import org.apache.dubbo.rpc.model.ScopeModel;
import org.apache.dubbo.rpc.model.ScopeModelUtil;

public class Animal$Adaptive implements com.example.demo.spi.Animal {
public void greet(org.apache.dubbo.common.URL arg0) {
//检查参数
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getParameter("animal", "animal");
//空检查
if(extName == null) throw new IllegalStateException("Failed to get extension (com.example.demo.spi.Animal) name from url (" + url.toString() + ") use keys([animal])");
ScopeModel scopeModel = ScopeModelUtil.getOrDefault(url.getScopeModel(), com.example.demo.spi.Animal.class);
//根据extName动态获取
com.example.demo.spi.Animal extension = (com.example.demo.spi.Animal)scopeModel.getExtensionLoader(com.example.demo.spi.Animal.class).getExtension(extName);
extension.greet(arg0);
}
}

2.4 编译代码

image.png

2.5 初始化自适应拓展

image.png
Dubbo中提供的编译器 共三种 分别是JDK编译器、javassist编译器、dubbo提供的adaptive编译器
image.png

2.6 返回自适应拓展

初始化拓展器生命周期(如果当前拓展器继承了Lifecycle接口)

1
2
3
4
5
6
7

private void initExtension(T instance) {
if (instance instanceof Lifecycle) {
Lifecycle lifecycle = (Lifecycle) instance;
lifecycle.initialize();
}
}

3.获取激活的拓展点

对于集合类扩展点,比如:Filter, InvokerListener, ExportListener, TelnetHandler, StatusChecker等, 可以同时加载多个实现,此时,可以用自动激活来简化配置。在激活拓展器注解@Activate中可配置顺序,在获取拓展时会根据红黑树来排序

3.1 获取激活的拓展字节码

回到1.8中可以看到如果加载 的字节码不是自适应也不是包装类则正常进行缓存,同时尝试设置为激活类,如果当前字节码的类上带有@Activate则进行加入当前注解的缓存中去(注意是注解)

1
2
3
4
5
6
7
8
9
10
11
12
private void cacheActivateClass(Class<?> clazz, String name) {
Activate activate = clazz.getAnnotation(Activate.class);//如果当前字节码的类上带有@Activate则进行加入激活字节码的缓存中去
if (activate != null) {
cachedActivates.put(name, activate);
} else {
// support com.alibaba.dubbo.common.extension.Activate
com.alibaba.dubbo.common.extension.Activate oldActivate = clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
if (oldActivate != null) {
cachedActivates.put(name, oldActivate);
}
}
}

image.png

3.2 加载激活拓展

通过遍历所有缓存激活组字节码查找出匹配的group,如果匹配则根据name加载对应拓展

image.png

3.3 返回激活的列表

返回基于kv键值对的拓展列表

总结

总体流程

版本变化

dubbo2.7 版本后的SPI加载方式有一些变化,笔者主要是对比了Dubbo 3.0 得出以下几点
1.dubbo2.7版本之前不支持作用域模型,dubbo3.0后支持 FrameworkModel 、ApplicationModel、 ModuleModel 三种
2.dubbo3.0后支持key=value格式配置,也支持不使用key方式(直接是全类名)
3.dubbo3.0后添加加载策略的概念实现(LoadingStrategy),而不是在代码写死几种加载的类型
4.dubbo2.7.5后添加了Lifecycle生命周期,前置后置处理器

JDK SPI 和 Dubbo SPI 异同

  1. Java SPI 会一次性全部加载所有的拓展实现,而Dubbo 可以实现按需加载基于kv方式,设置了很多缓存,支持动态加载实现。
  2. Dubbo SPI 基于URL驱动,支持获取自适应拓展、激活拓展列表等。
  3. Dubbo SPI 支持IOC方式注入、自动注入包装类。

资料

https://gitee.com/apache/dubbo
https://mikeygithub.github.io/2021/07/21/yuque/coahg1/


源码学习-Dubbo SPI 源码分析
https://mikeygithub.github.io/2022/04/17/yuque/源码学习-Dubbo SPI 源码分析/
作者
Mikey
发布于
2022年4月17日
许可协议