前言

大家好,今天开始给大家分享 — Dubbo 专题之 Dubbo SPI。在前面上个章节中我们讨论了 Dubbo 服务在线测试,了解了服务测试的基本使用和其实现的原理:其核心原理是通过元数据和使用 ​​GenericService​​​ API 在不依赖接口 ​​jar​​ 包情况下发起远程调用。那本章节我们主要讨论在 Dubbo 中SPI拓展机制,那什么是SPI?以及其在我们的项目中有什么作用呢?那么我们在本章节中进行讨论。下面就让我们快速开始吧!

1. Dubbo SPI 简介

什么是 Dubbo SPI 呢?其本质是从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。下面来自官方的介绍:Dubbo 改进了 JDK 标准的 SPI 的以下问题:

  1. JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  2. 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ​​ScriptEngine​​,通过 ​​getName()​​ 获取脚本类型的名称,但如果 ​​RubyScriptEngine​​ 因为所依赖的 ​​jruby.jar​​不存在,导致 ​​RubyScriptEngine​​ 类加载失败,这个失败原因被吃掉了,和 ​​ruby​​ 对应不起来,当用户执行 ​​ruby​​ 脚本时,会报不支持 ​​ruby​​,而不是真正失败的原因。
  3. 增加了对扩展点 ​​IoC​​ 和 ​​AOP​​ 的支持,一个扩展点可以直接 setter 注入其它扩展点。

我们可以简单总结:Dubbo 中 SPI 按需加载节省资源、修复了 Java SPI 因类加载类失败异常被忽略问题、增加对 ​​IoC​​​ 和 ​​AOP​​ 的支持。

Dubbo SPI 支持的拓展点:

  1. ​协议扩展​
  2. ​调用拦截扩展​
  3. ​引用监听扩展​
  4. ​暴露监听扩展​
  5. ​集群扩展​
  6. ​路由扩展​
  7. ​负载均衡扩展​
  8. ​合并结果扩展​
  9. ​注册中心扩展​
  10. ​监控中心扩展​
  11. ​扩展点加载扩展​
  12. ​动态代理扩展​
  13. ​编译器扩展​
  14. ​Dubbo 配置中心扩展​
  15. ​消息派发扩展​
  16. ​线程池扩展​
  17. ​序列化扩展​
  18. ​网络传输扩展​
  19. ​信息交换扩展​
  20. ​组网扩展​
  21. ​Telnet 命令扩展​
  22. ​状态检查扩展​
  23. ​容器扩展​
  24. ​缓存扩展​
  25. ​验证扩展​
  26. ​日志适配扩展​

2. 使用方式

在 Dubbo 中有三个路径来存放这些拓展配置:​​META-INF/dubbo​​​、​​META-INF/dubbo/internal​​​、​​META-INF/services/​​​第二个目录是用来存放 Dubbo 内部的 SPI 拓展使用,第一个和第三个目录是我们可以使用的目录。拓展文件内容为:​​配置名=扩展实现类全限定名​​,多个实现类用换行符分隔,文件名为类全限定名。例如:

|- resources

|- META-INF

|- dubbo

| - org.apache.dubbo.rpc.Filter

| - custom=com.muke.dubbocourse.spi.custom.CustomFilter

Tips:​META-INF/services/​​是 JDK 提供的 SPI 路径。


3. 使用场景

Dubbo 的拓展点是 Dubbo 成为最热门的 RPC 框架原因之一,它把灵活性、可拓展性发挥到了极致。在我们定制 Dubbo 框架的时候非常有用,我们执行简单的拓展和配置即可实现强大的功能。下面我们列举日常工作中常使用到的场景:

  1. 日志打印:在服务方法调用进入打印入参日志,方法调用完成返回前打印出参日志。
  2. 性能监控:在方法调用进入开始计时,方法调用完成返回前记录整个调用耗费时间。
  3. 链路追踪:在 Dubbo RPC 调用链路中传递每个系统的调用​​trace id​​,通过整合其它的链路追踪系统进行链路监控。

4. 示例演示

下面我们同样使用一个获取图书列表实例进行演示,同时我们自定义一个​​Filter​​在调用服务前后为我们输出日志。项目的结构如下:

Dubbo SPI_apache

上面的结构中我们自定义了​​CustomFilter​​代码如下:

/**
* @author <a href="http://youngitman.tech">青年IT男</a>
* @version v1.0.0
* @className CustomFilter
* @description 自定义过滤器
* @JunitTest: {@link }
* @date 2020-12-06 14:28
**/
public class CustomFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

System.out.println("自定义过滤器执行前");

Result result = invoker.invoke(invocation);

System.out.println("自定义过滤器执行后");

return result;
}
}

我们实现Filter并且在调用​​Invoker​​​前后打印日志输出。下面我们看看服务提供端​​dubbo-provider-xml.xml​​配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

<dubbo:protocol port="20880" />

<!--指定 filetr key = custom -->
<dubbo:provider filter="custom"/>

<dubbo:application name="demo-provider" metadata-type="remote"/>

<dubbo:registry address="zookeeper://127.0.0.1:2181"/>

<bean id="bookFacade" class="com.muke.dubbocourse.spi.provider.BookFacadeImpl"/>

<!--暴露服务为Dubbo服务-->
<dubbo:service interface="com.muke.dubbocourse.common.api.BookFacade" ref="bookFacade" />

</beans>

上面的配置中我们配置​​<dubbo:provider filter="custom"/>​​​指定使用​​custom​​​自定义过滤器。接下来我们在​​resources​​​->​​META-INF.dubbo​​​目录下新建​​org.apache.dubbo.rpc.Filter​​文件配置内容如下:

custom=com.muke.dubbocourse.spi.custom.CustomFilter

其中​​custom​​​为我们的拓展​​key​​与我们在 XML 中配置保持一致。

5. 原理分析

Dubbo 中的 SPI 拓展加载使用 ​​ExtensionLoader​​​下面我们简单的通过源码来分析下。首先入口为静态函数​​org.apache.dubbo.common.extension.ExtensionLoader#ExtensionLoader​​代码如下:

private ExtensionLoader(Class<?> type) {
//加载的拓展类类型
this.type = type;
//容器工厂,如果不是加载ExtensionFactory对象先执行ExtensionFactory加载再执行 getAdaptiveExtension
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

上面的方法很简单就是获得​​ExtensionLoader​​​对象,值得注意的是这里是一个层层递归的调用直到加载类型为ExtensionFactory时终止。接下来我们看看​​getAdaptiveExtension​​代码:

public T getAdaptiveExtension() {
//缓存获取
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
//...
//加锁判断
synchronized (cachedAdaptiveInstance) {
//再次获取 双重锁检测
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
//创建拓展实例
instance = createAdaptiveExtension();
//进行缓存
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
//...
}
}
}
}
return (T) instance;
}

我们解析看看​​createAdaptiveExtension​​方法是怎样创建实例:

private T createAdaptiveExtension() {
try {
//首先创建拓展实例,然后注入依赖
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
}
}

​getAdaptiveExtensionClass​​方法代码如下:

private Class<?> getAdaptiveExtensionClass() {
//获取拓展类
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
//动态生成Class
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

我们主要分析​​getExtensionClasses​​核心代码如下:

/***
*
* 获取所有的拓展Class
*
* @author liyong
* @date 20:18 2020-02-27
* @param
* @exception
* @return java.util.Map<java.lang.String,java.lang.Class<?>>
**/
private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
//开始从资源路径加载Class
classes = loadExtensionClasses();
//设置缓存
cachedClasses.set(classes);
}
}
}
return classes;
}

​loadExtensionClasses​​代码如下:

/**
*
* CLASS_PATH=org.apache.dubbo.common.extension.ExtensionFactory
*
* 1.META-INF/dubbo/internal/${CLASS_PATH} Dubbo内部使用路径
* 2.META-INF/dubbo/${CLASS_PATH} 用户自定义扩展路径
* 3.META-INF/services/{CLASS_PATH} JdkSPI路径
*
* synchronized in getExtensionClasses
* */
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);
//兼容处理 由于dubbo捐献给apache
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;
}

接下来我们看到真正进行资源加载的方法​​loadDirectory​​:

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) {
String fileName = dir + type;
try {
Enumeration<java.net.URL> urls = null;
ClassLoader classLoader = findClassLoader();

// try to load from ExtensionLoader's ClassLoader first
if (extensionLoaderClassLoaderFirst) {
ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
//这里首先使用ExtensionLoader的类加载器,有可能是用户自定义加载
if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
urls = extensionLoaderClassLoader.getResources(fileName);
}
}

if(urls == null || !urls.hasMoreElements()) {
if (classLoader != null) {//使用AppClassLoader加载
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("Exception occurred when loading extension class (interface: " +
type + ", description file: " + fileName + ").", t);
}
}

我们看看资源内容的加载逻辑方法​​loadResource​​核心代码如下:

//加载文件值转换为Class到Map
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
String line;
//读取一行数据
while ((line = reader.readLine()) != null) {
final int ci = line.indexOf('#');//去掉注释
if (ci >= 0) {
line = line.substring(0, ci);
}
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
//加载Class
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
//...
}
}
}
}
} catch (Throwable t) {
//...
}
}

上面的方法通过循环加载每一行数据,同时解析出​​=​​​后面的路径进行​​Class​​的装载。由此循环加载自定资源路径下面的所有通过配置文件配置的类。

6. 小结

在本小节中我们主要学习了 Dubbo 中 SPI,首先我们知道 Dubbo SPI 其本质是从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来,同时解决了 Java 中 SPI 的一些缺陷。我们也通过简单的使用案例来介绍我们日常工作中怎样去拓展,以及从源码的角度去解析 SPI 的加载原理其核心入口类为ExtensionLoader`。

本节课程的重点如下:

  • 理解 Dubbo 中 SPI
  • 了解 Dubbo SPI 与 Java SPI 区别
  • 了解 Dubbo 怎样使用 SPI 进行拓展
  • 了解 SPI 实现原理

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。关注公众号:青年IT男 获取最新技术文章推送!

微信公众号:

Dubbo SPI_加载_02