Dubbo SPI

一、什么是SPI机制?

SPI (Service Provider Interface),主要用于扩展的作用。

举个例子来说,假如有一个框架有一个接口,他有自己默认的实现类,但是在代码运行的过程中,你不想用他的实现类或者想扩展一下他的实现类的功能,但是此时你又不能修改别人的源码,那么此时该怎么办?这时spi机制就有了用武之地。一般框架的作者在设计这种接口的时候不会直接去new这个接口的实现类,而是在Classpath路径底下将这个接口的实现类按作者约定的格式写在一个配置文件上,然后在运行的过程中通过java提供的api,从所有jar包中读取所有的这个指定文件中的内容,获取到实现类,用这个实现类,这样,如果你想自己替换原有的框架的实现,你就可以按照作者规定的方式配置实现,这样就能使用你自己写的实现类了。

spi机制其实体现了设计思想中的解耦思想,方便开发者对框架功能进行扩展。

二、Spring中的SPI - SpringFactoriesLoader

相信spring大家都不陌生,在spring扩展也是依赖spi机制完成的,只不过spring对于扩展文件约定在Classpath 路径下的 META-INF 目录底下,所有的文件名都是叫 spring.factories,文件里的内容是一个以一个个键值对的方式存储的,键为类的全限定名,值也为类的全限定名,如果有多个值,可以用逗号分割,有一点得注意的是,键和值本身约定并没有类与类之间的依赖关系(当然也可以有,得看使用场景的约定),也就是说键值可以没有任何关联,键仅仅是一种标识。

代表一种场景,最常见的自动装配的注解,**@EnableAutoConfiguration**,也就是代表自动装配的场景,当你需要你的类被自动装配,就可以以这个注解的权限定名键,你的类为名,这样springboot在进行自动装配的时候,就会拿这个键,找到你写的实现类来完成自动装配。

SpringBoot自动装配的入口:

1
2
3
4
5
6
7
8
9
public class AutoConfigurationImportSelector {
//selectImports
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//SpringBoot自动配置的入口方法
AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);

return autoConfigurationEntry.getConfigurations();
}
}

SpringBoot自动装配的核心原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {

//1.获取 annotationMetadata 的注解 @EnableAutoConfiguration 的属性
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);

//2.从资源类 spring.factories 中获取 EnableAutoConfiguration 对应的所有类
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);

//3.通过在注解 @EnableAutoConfiguration 设置exclude属性,可以排除指定的配置类
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);

//4.根据注解 @Conditional 来判断是否需要排除某些配置类
configurations = this.getConfigurationClassFilter().filter(configurations);

//5.触发 AutoConfiguration 导入的相关事件
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}

在这里我们可以看到,MybatisPlus也是按照Spring的规则来进行配置的,通过spring.factories中的键值org.springframework.boot.autoconfigure.EnableAutoConfiguration,以导入MybatisPlusAutoConfiguration完成SpringBoot与MybatisPlus的整合。

三、Dubbo中的SPI - ExtensionLoader

ExtensionLoader是dubbo的spi机制所实现的类,通过这个类来加载接口所有实现类,获取实现类的对象。同时每一个接口都会有一个自己的ExtensionLoader。

1、Dubbo的配置文件约束

dubbo会从四个目录读取文件META-INF/dubbo/internal/ 、META-INF/dubbo/ 、META-INF/services/、META-INF/dubbo/external/,文件名为接口的全限定名,内容为键值对,键为短名称(可以理解为spring中的对象的名称),值为实现类。

image-20230613112117030

2、@SPI注解的约束

dubbo中所有的扩展接口,都需要在接口上加@SPI注解,不然在创建ExtensionLoader的时候,会报错。

image-20230613112117030

顺便说说ExtensionDirector的作用,在3.0.3以前的版本,是没有这个类的,但是在之后的版本为了实现一些新的特性,就抽象出来了这个类,通过这个类来获取每个接口对应的ExtensionLoader。

3、Dubbo中类的加载

先说各种特性之前,先说一下这些实现类是如何加载的,类的加载是非常重要的一个环节,与后面的spi特性有重要的关系。

Dubbo中类加载依赖于ExtensionLoader类,默认是先调用getExtensionClasses这个方法的。

image-20230613112117030

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

这里也是先检查缓存,若缓存未命中,则通过 synchronized 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类。下面分析 loadExtensionClasses 方法的逻辑。

image-20230613112117030

loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。

下面研究下 loadDirectory方法:

image-20230613112117030

loadDirectory 通过 loadDirectoryInternal 方法加载资源。我们继续跟下去,在 loadDirectoryInternal 方法中最终通过调用 loadFromClass 方法,然后去加载在 loadDirectoryInternal 方法中用 classLoader 获取到的资源链接,我们再跟进 loadResource 方法

image-20230613112117030

下面是 loadResource 的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceURL, boolean overridden, String[] includedPackages, String[] excludedPackages, String[] onlyExtensionClassLoaderPackages) {
try {
List<String> newContentList = this.getResourceContent(resourceURL);
Iterator var10 = newContentList.iterator();

while(var10.hasNext()) {
//按行读取配置文件
String line = (String)var10.next();

try {
String name = null;
int i = line.indexOf(61);
String clazz;
if (i > 0) {
name = line.substring(0, i).trim();
clazz = line.substring(i + 1).trim();
} else {
clazz = line;
}

if (StringUtils.isNotEmpty(clazz) && !this.isExcluded(clazz, excludedPackages) && this.isIncluded(clazz, includedPackages) && !this.isExcludedByClassLoader(clazz, classLoader, onlyExtensionClassLoaderPackages)) {
this.loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
}
} catch (Throwable var14) {
IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + this.type + ", class line: " + line + ") in " + resourceURL + ", cause: " + var14.getMessage(), var14);
this.exceptions.put(line, e);
}
}
} catch (Throwable var15) {
logger.error("0-15", "", "", "Exception occurred when loading extension class (interface: " + this.type + ", class file: " + resourceURL + ") in " + resourceURL, var15);
}

}

loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。

Dubbo加载扩展的整个流程如下:

extension-load