前言
大家好,今天开始给大家分享 — Dubbo 专题之 Dubbo SPI。在前面上个章节中我们讨论了 Dubbo 服务在线测试,了解了服务测试的基本使用和其实现的原理:其核心原理是通过元数据和使用 GenericService
API 在不依赖接口 jar
包情况下发起远程调用。那本章节我们主要讨论在 Dubbo 中SPI拓展机制,那什么是SPI?以及其在我们的项目中有什么作用呢?那么我们在本章节中进行讨论。下面就让我们快速开始吧!
1. Dubbo SPI 简介
什么是 Dubbo SPI 呢?其本质是从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。下面来自官方的介绍:Dubbo 改进了 JDK 标准的 SPI 的以下问题:
- JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
- 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的
ScriptEngine
,通过 getName()
获取脚本类型的名称,但如果 RubyScriptEngine
因为所依赖的 jruby.jar
不存在,导致 RubyScriptEngine
类加载失败,这个失败原因被吃掉了,和 ruby
对应不起来,当用户执行 ruby
脚本时,会报不支持 ruby
,而不是真正失败的原因。 - 增加了对扩展点
IoC
和 AOP
的支持,一个扩展点可以直接 setter 注入其它扩展点。
我们可以简单总结:Dubbo 中 SPI 按需加载节省资源、修复了 Java SPI 因类加载类失败异常被忽略问题、增加对 IoC
和 AOP
的支持。
Dubbo SPI 支持的拓展点:
- 协议扩展
- 调用拦截扩展
- 引用监听扩展
- 暴露监听扩展
- 集群扩展
- 路由扩展
- 负载均衡扩展
- 合并结果扩展
- 注册中心扩展
- 监控中心扩展
- 扩展点加载扩展
- 动态代理扩展
- 编译器扩展
- Dubbo 配置中心扩展
- 消息派发扩展
- 线程池扩展
- 序列化扩展
- 网络传输扩展
- 信息交换扩展
- 组网扩展
- Telnet 命令扩展
- 状态检查扩展
- 容器扩展
- 缓存扩展
- 验证扩展
- 日志适配扩展
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 框架的时候非常有用,我们执行简单的拓展和配置即可实现强大的功能。下面我们列举日常工作中常使用到的场景:
- 日志打印:在服务方法调用进入打印入参日志,方法调用完成返回前打印出参日志。
- 性能监控:在方法调用进入开始计时,方法调用完成返回前记录整个调用耗费时间。
- 链路追踪:在 Dubbo RPC 调用链路中传递每个系统的调用
trace id
,通过整合其它的链路追踪系统进行链路监控。
4. 示例演示
下面我们同样使用一个获取图书列表实例进行演示,同时我们自定义一个Filter
在调用服务前后为我们输出日志。项目的结构如下:
上面的结构中我们自定义了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男 获取最新技术文章推送!
微信公众号: