4

源码学习-Dubbo SPI 源码分析

 2 years ago
source link: https://mikeygithub.github.io/2022/04/19/yuque/carc8e/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

今天我们来聊一聊 Dubbo 的 SPI

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

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

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

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

1. 获取拓展

1.1 获取拓展

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

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

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

1.2 获取拓展加载器

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

1.3 创建拓展器加载器

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

1.4 构造拓展加载器

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

1.5 获取拓展

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

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

1.6 创建拓展器

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

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

1.7 获取字节码

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

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

1.8 缓存字节码

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

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

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

1.9 拓展器的实例化

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

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

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

1.10 拓展注入

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

1.11 包装类处理

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

1.12 初始化拓展生命周期

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


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 对象

2.2 创建自适应实现类

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

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

2.3 生成代码

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

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

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

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

参考例子

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);
}
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 ~");
}
}

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 配置实现文件

cat=com.example.demo.spi.Cat
dog=com.example.demo.spi.Dog
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);
}
}

动态生成的代码

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 编译代码

2.5 初始化自适应拓展

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

2.6 返回自适应拓展

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


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 则进行加入当前注解的缓存中去(注意是注解)

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);
}
}
}

3.2 加载激活拓展

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

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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK