前言
大家好,今天开始给大家分享 — 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 路径。
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script><ins class="adsbygoogle"style="display:block; text-align:center;"data-ad-layout="in-article"data-ad-format="fluid"data-ad-client="ca-pub-4279907681900931"data-ad-slot="6812672741"></ins><script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
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`。
本节课程的重点如下:
作者
个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。关注公众号:青年 IT 男 获取最新技术文章推送!
博客地址: http://youngitman.tech
微信公众号:
评论