Dubbo SPI
一、SPI
SPI,英文全称是 Service Provider Interface,按每个单词翻译就是:服务提供接口。
这里的“服务”泛指任何一个可以提供服务的功能、模块、应用或系统,这些“服务”在设计接口或规范体系时,往往会预留一些比较关键的口子或者扩展点,让调用方按照既定的规范去自由发挥实现,而这些所谓的“比较关键的口子或者扩展点”,就叫“服务”提供的“接口”。
二、实现SPI
image.png在 Web 应用成功启动的时刻,预加载 Dubbo 框架的一些资源。
为了讲究通用性,开源团队也只会提供一个口子,定义一种规范约束,给上层开发人员实现该口子做一些定制化逻辑,一般会这样做:
image.png
///////////////////////////////////////////////////
// web-fw.jar 插件的启动类,在“应用成功启动”时刻提供一个扩展口子
///////////////////////////////////////////////////
public class WebFwBootApplication {
// web-fw.jar 插件的启动入口
public static void run(Class<?> primarySource, String... args) {
// 开始启动中,此处省略若干行代码...
// 环境已准备好,此处省略若干行代码...
// 上下文已实例化,此处省略若干行代码...
// 上下文已准备好,此处省略若干行代码...
// 应用成功启动
onCompleted();
// 应用已准备好,此处省略若干行代码...
}
// 应用成功启动时刻,提供一个扩展口子
private static void onCompleted() {
// 加载 ApplicationStartedListener 接口的所有实现类
ServiceLoader<ApplicationStartedListener> loader =
ServiceLoader.load(ApplicationStartedListener.class);
// 遍历 ApplicationStartedListener 接口的所有实现类,并调用里面的 onCompleted 方法
Iterator<ApplicationStartedListener> it = loader.iterator();
while (it.hasNext()){
// 获取其中的一个实例,并调用 onCompleted 方法
ApplicationStartedListener instance = it.next();
instance.onCompleted();
}
}
}
///////////////////////////////////////////////////
// web-fw.jar 插件的“应用启动成功的监听器接口”,定制一种接口规范
///////////////////////////////////////////////////
public interface ApplicationStartedListener {
// 触发完成的方法
void onCompleted();
}
///////////////////////////////////////////////////
// app-web 后台应用的启动类代码
///////////////////////////////////////////////////
public class Dubbo14JdkSpiApplication {
public static void main(String[] args) {
// 模拟 app-web 调用 web-fw 框架启动整个后台应用
WebFwBootApplication.run(Dubbo14JdkSpiApplication.class, args);
}
}
///////////////////////////////////////////////////
// app-web 后台应用的资源目录文件
// 路径为:/META-INF/services/com.hmilyylimh.cloud.jdk.spi.ApplicationStartedListener
///////////////////////////////////////////////////
com.hmilyylimh.cloud.jdk.spi.PreloadDubboResourcesListener
代码中定义了一个应用启动成功的监听器接口(ApplicationStartedListener),接着 app-web 自定义一个预加载 Dubbo 资源监听器(PreloadDubboResourcesListener)来实现该接口。
在插件应用成功启动的时刻,会寻找 ApplicationStartedListener 接口的所有实现类,并将所有实现类全部执行一遍,这样,插件既提供了一种口子的规范约束,又能满足业务诉求在应用成功启动时刻做一些事情。
其实插件在指定标准接口规范的这件事情上,就是 SPI 的思想体现,只不过是 JDK 通过 ServiceLoader 实现了这套思想,也就是我们耳熟能详的 JDK SPI 机制。
三、JDK SPI
ServiceLoader 大致的核心代码流程:
image.png
- 第一块,将接口传入到 ServiceLoader.load 方法后,得到了一个内部类的迭代器。
- 第二块,通过调用迭代器的 hasNext 方法,去读取“/META-INF/services/ 接口类路径”这个资源文件内容,并逐行解析出所有实现类的类路径。
- 第三块,将所有实现类的类路径通过“Class.forName”反射方式进行实例化对象。
使用 ServiceLoader 的 load 方法执行多次时,会不断创建新的实例对象。
public static void main(String[] args) {
// 模拟进行 3 次调用 load 方法并传入同一个接口
for (int i = 0; i < 3; i++) {
// 加载 ApplicationStartedListener 接口的所有实现类
ServiceLoader<ApplicationStartedListener> loader
= ServiceLoader.load(ApplicationStartedListener.class);
// 遍历 ApplicationStartedListener 接口的所有实现类,并调用里面的 onCompleted 方法
Iterator<ApplicationStartedListener> it = loader.iterator();
while (it.hasNext()){
// 获取其中的一个实例,并调用 onCompleted 方法
ApplicationStartedListener instance = it.next();
instance.onCompleted();
}
}
}
调用 3 次 ServiceLoader 的 load 方法,并且每一次传入的都是同一个接口,运行编写好的代码,打印出如下信息:
预加载 Dubbo 框架的一些资源, com.hmilyylimh.cloud.jdk.spi.PreloadDubboResourcesListener@300ffa5d
预加载 Dubbo 框架的一些资源, com.hmilyylimh.cloud.jdk.spi.PreloadDubboResourcesListener@1f17ae12
预加载 Dubbo 框架的一些资源, com.hmilyylimh.cloud.jdk.spi.PreloadDubboResourcesListener@4d405ef7
每次调用 load 方法传入同一个接口的话,打印出来的引用地址都不一样,说明创建出了多个实例对象。
JDK SPI 的问题
- 使用 load 方法频率高,容易影响 IO 吞吐和内存消耗。
- 使用 load 方法想要获取指定实现类,需要自己进行遍历并编写各种比较代码。
解决:
- 有方法被大量调用,我们的尝试是看看是否可以缓存起来。有 N 次调用,如果第一次通过读取文件、解析文件、反射实例化拿到接口的所有实现类并缓存起来,后面 N - 1 次就可以直接从缓存读取,大大降低了各种耗时的操作,性能有质的提升。
- 每次需要遍历找到想要的实现类,可以以空间换时间,叠加哈希算法进行快速寻址查找。
增加缓存,来降低磁盘 IO 访问以及减少对象的生成;使用 Map 的 hash 查找,来提升检索指定实现类的性能。
四、Dubbo SPI
Dubbo 也定义出了自己的一套 SPI 机制逻辑,既要通过 O(1) 的时间复杂度来获取指定的实例对象,还要控制缓存创建出来的对象,做到按需加载获取指定实现类,并不会像 JDK SPI 那样一次性实例化所有实现类。
Dubbo 设计出了一个 ExtensionLoader 类,实现了 SPI 思想,也被称为 Dubbo SPI 机制。
///////////////////////////////////////////////////
// Dubbo SPI 的测试启动类
///////////////////////////////////////////////////
public class Dubbo14DubboSpiApplication {
public static void main(String[] args) {
ApplicationModel applicationModel = ApplicationModel.defaultModel();
// 通过 Protocol 获取指定像 ServiceLoader 一样的加载器
ExtensionLoader<IDemoSpi> extensionLoader = applicationModel.getExtensionLoader(IDemoSpi.class);
// 通过指定的名称从加载器中获取指定的实现类
IDemoSpi customSpi = extensionLoader.getExtension("customSpi");
System.out.println(customSpi + ", " + customSpi.getDefaultPort());
// 再次通过指定的名称从加载器中获取指定的实现类,看看打印的引用是否创建了新对象
IDemoSpi customSpi2 = extensionLoader.getExtension("customSpi");
System.out.println(customSpi2 + ", " + customSpi2.getDefaultPort());
}
}
///////////////////////////////////////////////////
// 定义 IDemoSpi 接口并添加上了 @SPI 注解,
// 其实也是在定义一种 SPI 思想的规范
///////////////////////////////////////////////////
@SPI
public interface IDemoSpi {
int getDefaultPort();
}
///////////////////////////////////////////////////
// 自定义一个 CustomSpi 类来实现 IDemoSpi 接口
// 该 IDemoSpi 接口被添加上了 @SPI 注解,
// 其实也是在定义一种 SPI 思想的规范
///////////////////////////////////////////////////
public class CustomSpi implements IDemoSpi {
@Override
public int getDefaultPort() {
return 8888;
}
}
///////////////////////////////////////////////////
// 资源目录文件
// 路径为:/META-INF/dubbo/internal/com.hmilyylimh.cloud.dubbo.spi.IDemoSpi
///////////////////////////////////////////////////
customSpi=com.hmilyylimh.cloud.dubbo.spi.CustomSpi
打印结果:
com.hmilyylimh.cloud.dubbo.spi.CustomSpi@143640d5, 8888
com.hmilyylimh.cloud.dubbo.spi.CustomSpi@143640d5, 8888
步骤:
- 第一,定义一个 IDemoSpi 接口,并在该接口上添加 @SPI 注解。
- 第二,定义一个 CustomSpi 实现类来实现该接口,然后通过 ExtensionLoader 的 getExtension 方法传入指定别名来获取具体的实现类。
- 最后,在“/META-INF/services/com.hmilyylimh.cloud.dubbo.spi.IDemoSpi”这个资源文件中,添加实现类的类路径,并为类路径取一个别名(customSpi)。
通过别名去获取指定的实现类时,打印的实例对象的引用是同一个,说明 Dubbo 框架做了缓存处理。而且整个操作,只通过一个简单的别名,就能从 ExtensionLoader 中拿到指定实现类,简单方便。
SPI 的思想:
主要是通过定制底层规范接口,在不同的业务场景,封装底层逻辑不变性,提供扩展点给到上层应用做不同的自定义开发实现,既可以用来提供框架扩展,也可以用来替换组件。
极客时间《Dubbo 源码剖析与实战》学习笔记Day17 - http://gk.link/a/11VBp