Dubbo源码解析(2)SPI原理

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

本篇文章是Dubbo源码解析系列文章的第二篇,本系列文章分析基于Dubbo官方代码2.7.5-release版本,本篇文章主要分析Dubbo中是如何使用SPI机制来加载扩展类的

在阅读本文之前,你需要阅读如下文章:

  1. Dubbo源码解析(1)概览

SPI术语

什么是SPI

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能

Dubbo中的SPI约定

首先我们需要明确一点,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强

我们先来看一下其中涉及到一些名词

扩展点

Dubbo 中被 @SPI 注解的 Interface 为一个扩展点

扩展点约定

在 META-INF/services/* 、META-INF/dubbo/、 META-INF/dubbo/internal/, 这些路径下定义的文件名称为扩展点接口的全类名 , 文件中以键值对的方式配置扩展点的扩展实现。例如文件 META-INF/dubbo/internal/org.apache.dubbo.common.extension.ext1.SimpleExt 中定义的扩展 :

impl1=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl1
impl2=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl2
impl3=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl3

这个文件代表了什么呢?org.apache.dubbo.common.extension.ext1.SimpleExt这个接口有三个可选的实现类:SimpleExtImpl1SimpleExtImpl2SimpleExtImpl3

默认适应扩展

如果我们把@SPI("impl1")注解放到SimpleExt这个接口上,就代表着这个接口的缺省实现就是文件中impl1属性所对应的那个实现类SimpleExtImpl1

除了这种方式,如果SimpleExtImpl1类上被标注了注解@Adaptive

它同样可以被代表接口的默认实现

条件适应扩展

@Activate 注解在扩展点的实现类上 ,表示了一个扩展类被获取到的的条件,符合条件就被获取,不符合条件就不获取 ,过滤条件包含group、 value

方法适应扩展

刚才说到@Adaptive注解在注解在类上,这个类就是缺省的适配扩展,除了这个用法,这个注解还可以标注在方法上。
当这个注解标注在方法上时,
dubbo会根据在运行时通过传入的 URL 类型的参数或者内部含有获取 URL 方法的参数 ,从URL中获取到要使用的扩展类的名称 ,再去根据名称加载对应的扩展实例 ,用这个扩展实例对象调用相同的方法

源码解析

注意,本文不会分析@Activate@Adaptive相关的内容,大家看完本文可以自行研究

示例代码

源码分析使用的示例代码在dubbo-common项目中的ExtensionLoaderTest类的test_getExtension方法

   @Test
public void test_getExtension() throws Exception {
assertTrue(getExtensionLoader(SimpleExt.class).getExtension("impl1") instanceof SimpleExtImpl1);
assertTrue(getExtensionLoader(SimpleExt.class).getExtension("impl2") instanceof SimpleExtImpl2);
}

不去分析源码我们可以大概猜一下这两行代码,大致就是加载SimpleExt接口的两个实现类,这个接口上方提到过,我们先看下这个接口的内容

@SPI("impl1")
public interface SimpleExt {
// @Adaptive example, do not specify a explicit key.
@Adaptive
String echo(URL url, String s);

@Adaptive({"key1", "key2"})
String yell(URL url, String s);

// no @Adaptive
String bang(URL url, int i);
}

ExtensionLoader初始化

现在我们开始分析源码

我们首先来看下getExtensionLoader方法

    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type +
") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}

ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}

逻辑比较简单

  1. 判断接口的合法性,传入的接口的类型是否为空,是否是个接口,是否包含SPI注解

  2. 从缓存中获取与拓展类对应的ExtensionLoader对象,若缓存未命中,则创建一个新的实例

SPI核心

ExtensionLoader对象初始化完毕后就可以执行getExtension方法了,这个方法的参数是这个接口的实现类在配置文件中的属性名

public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) {
// 获取默认的拓展实现类
return getDefaultExtension();
}
// Holder,顾名思义,用于持有目标对象
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
// 双重检查
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 创建拓展实例
instance = createExtension(name);
// 设置实例到 holder 中
holder.set(instance);
}
}
}
return (T) instance;
}

上面代码的逻辑比较简单,使用了一个双重检查锁的模式,主要关注创建拓展对象的过程

   private T createExtension(String name) {
// 从配置文件中加载所有的拓展类,可得到“配置项属性”到“配置类”的映射关系表
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 通过反射创建实例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 向实例中注入依赖
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
// 循环创建 Wrapper 实例
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}

createExtension方法的逻辑如下:

  1. 通过 getExtensionClasses 获取所有的拓展类

  2. 通过反射创建拓展对象

  3. 向拓展对象中注入依赖

  4. 将拓展对象包裹在相应的 Wrapper 对象中

以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现,我们主要关注下第一步

获取所有的拓展类

我们在通过名称获取拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可。相关过程的代码如下:

    private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}

又一个双重检查锁,重点关注 loadExtensionClasses方法吧:

    private Map<String, Class<?>> loadExtensionClasses() {
cacheDefaultExtensionName();

Map<String, Class<?>> extensionClasses = new HashMap<>();
// internal extension load from ExtensionLoader's ClassLoader first
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"), true);

loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;
}
保存默认Extension

首先关注下第一行的cacheDefaultExtensionName

    // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
// 对 SPI 注解内容进行切分
String[] names = NAME_SEPARATOR.split(value);
// 检测 SPI 注解内容是否合法,不合法则抛出异常
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension...");
}

// 设置默认名称,参考 getDefaultExtension 方法
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}

回忆下SimpleExt接接口,这里校验的就是这个类的@SPI注解上的信息是否合法以及获取的@SPI注解的默认值impl1,这个值会赋值给cachedDefaultName属性,这个属性主要是获取接口默认实现的方法会用到

加载配置文件

接下来就是一堆loadDirectory方法,这6行代码里面一共加载了三个文件夹下的内容,而用了6行代码的原因是Dubbo贡献给apache之前使用的包名是以com.alibaba开头的,这里是为了兼容贡献之前的版本

现在我们来看下loadDirectory方法

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
// fileName = 文件夹路径 + type 全限定名
String fileName = dir + type.getName();
try {
Enumeration<java.net.URL> urls;
ClassLoader classLoader = findClassLoader();
// 根据文件名加载所有的同名文件
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
if (urls != null) {
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
// 加载资源
loadResource(extensionClasses, classLoader, resourceURL);
}
}
} catch (Throwable t) {
logger.error("...");
}
}

loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源,加载到具体的资源会通过反射加载类,具体逻辑我就不详细展示了,最终就是加载的下图的这个类
Dubbo源码解析(2)SPI原理

接着回到最初的createExtension方法,此时已经知道了需要加载的SimpleExt这个接口的实现类为SimpleExtImpl1,下面就是通过反射实例化这个类

这样下来通过整个SPI机制来加载扩展类的流程就梳理完毕了



往期好文

阅读源码的小技巧 

SpringBoot源码解析系列汇总

Spring源码解析系列汇总

Eureka源码解析系列汇总

SpringCloud服务调用源码解析汇总


Dubbo源码解析(2)SPI原理

好文章就该:收藏、转发、在看!!!

原文始发于微信公众号(Java学习录):Dubbo源码解析(2)SPI原理