(一)Dubbo源码解析:增强SPI

简介: (一)Dubbo源码解析:增强SPI

〇、前言

在Dubbo的架构设计中,如何可以通过“类插拔”的方式,对其功能进行灵活的扩展或者削弱,那么,SPI起到了极其关键的作用。本篇文章作为分析Dubbo源码的第一篇文章,我们先暂时放下“服务注册发布流程”、“服务启动流程”、“请求处理流程”……这些功能代码的探索,我们先从最基本的一个问题着手,即:Dubbo的增强SPI是如何实现的,只有搞懂了这个问题,我们后续再看其他功能代码的时候,才会更加游刃有余、畅快无阻~

一、整体时序图

在介绍具体源码详情之前,先将动态获得AdaptiveExtensionExtension的整体时序图给大家展示出来,通过下图,大家会对其处理过程有一个大致的了解,时序图如下所示:

二、源码详解

首先,作为源码解析的入口,我们来看一下Provider端如何通过调用dubbo的API方式来使用dubbo注册自己的服务的,代码如下所示:

在上图的代码中,我们可以看到,当我们获得了ServiceConfig实例对象之后,通过一系列的赋值操作,最终通过调用它的export()方法,就实现了服务接口的注册/暴露操作了;那么,我们第一个需要关心的点就是上图红框部分,即:通过new ServiceConfig()创建ServiceConfig实例。在创建的过程中,首先会执行静态全局变量的初始化操作,即:下图红框的变量创建代码,而这部分就是增强SPI代码部分

2.1> ExtensionLoader.getExtensionLoader(Protocol.class)

首先,我们来解析一下ExtensionLoader类的getExtensionLoader(Class<T> type)方法,该方法的入参type一定要满足非空接口类型并且使用@SPI注解。那么最初EXTENSION_INSTANCES是空的ConcurrentHashMap。所以,需要创建ExtensionLoader并缓存到EXTENSION_INSTANCES中。                

由于入参的type=Protocol.class,所以我们再来看一下new ExtensionLoader(Protocol.class)构造器方法,在其构造方法的内部,我们还需要针对objectFactory进行赋值操作,即:需要调用ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()来获取ExtensionFactory的适配器扩展实例

当我们调用getExtensionLoader(ExtensionFactory.class)时,EXTENSION_INSTANCES中依然为空。所以,会以type=ExtensionFactory.class为入参再次调用ExtensionLoader的构造方法,那么此时入参的type等于ExtensionFactory.class,满足type == ExtensionFactory.class ? null:...,所以objectFactory=null

2.2> getAdaptiveExtension()从缓存中获取适配器扩展实例

由于在ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()内部逻辑中,调用了ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension。所以,此处调用的getAdaptiveExtension()type=ExtensionFactory.class的方法。

在该方法内部,通过Double Check的方式对instance是否为null进行了双验证,如果依然为空,我们就可以通过createAdaptiveExtension()方法来创建适配器的扩展对象了。代码如下红框所示:

2.3> createAdaptiveExtension()用于创建适配器扩展实例

createAdaptiveExtension()方法中,代码比较简单,就一行代码,但是却做了两件大事:

事件1】通过getAdaptiveExtensionClass()来获得适配器扩展类,并通过newInstance方法创建实例对象;

事件2】通过injectExtension(...)方法,对扩展点之间的依赖执行自动注入操作。

这两个事件,请见下图中两个红框所示:

2.3.1> getAdaptiveExtensionClass()

该方法的主要作用是:获得适配器扩展类Class对象。方法内部涉及两部分内容:

首先加载并缓存拓展类,如果找到了cachedAdaptiveClass,则进行返回。

其次】如果没找到,则通过代码来组装源码,并通过Compiler编译生成Class;

上面介绍的处理步骤,请见下图中三个红框所示:

2.3.1.1> getExtensionClasses()加载指定文件名的SPI接口类

如果缓存cachedClasses中已存在,则返回。如果不存在,调用loadExtensionClasses()方法获得加载拓展类,并缓存到cachedClasses

我们来看一下loadExtensionClasses()方法如何进行类加载的

在方法cacheDefaultExtensionName()中,会通过3步骤,针对给@SPI配置了value值缓存到cachedDefaultName中。

步骤1】获得type类上的@SPI注解defaultAnnotation

步骤2】如果defaultAnnotation不为空的话,则获得注解配置的value值;

步骤3】将value值缓存到cachedDefaultName中,供后续使用;

上面介绍的处理步骤,请见下图中三个红框所示:

看完cacheDefaultExtensionName方法后,我们再将视野转移到loadDirectory()方法上,该方法是用于加载fileName文件,并解析其中所配置的内容的,由于在SPI的配置文件中,都是以key和value配置的,所以,最终也会将其读取到内存中:

loadResource()方法用于解析fileName文件中的内容,该方法内主要是去取每行配置,然后通过配置文件中的等号(“=”)来分割出key和value,即:

key】等号的左侧;

value】等号的右侧;

分割出来后,再通过loadClass来加载value中所配置的Class名称列表,代码如下所示:

loadClass()用于解析并且缓存cachedAdaptiveClasscachedWrapperClassescachedActivatescachedNamesextensionClasses,具体逻辑如下所示:

步骤1】如果入参clazz使用了@Adaptive注解,则将这个clazz缓存到cachedAdaptiveClass中;

步骤2】如果入参clazz是一个Wrapper类(即:存在入参为clazz的构造方法),则将这个clazz缓存到cachedWrapperClasses中;

步骤3】通过逗号分割value值,即:获得配置文件中的扩展类Class名称数组names

步骤4】将names中的类,使用了@Activate注解的类,都缓存到cachedActivates中;

步骤5】遍历names数组,将name缓存到cachedNames中;

步骤6】将name和clazz缓存到extensionClasses中;

此处大家需要注意的是,只需要将被缓存的这些缓存名称有个印象即可,先不用着急缓存后要去做什么事情,到后面的解析部分,面纱也会慢慢的撤下,上面步骤相关代码,如下红框所示:

2.3.1.2> createAdaptiveExtensionClass()

分析完上面的代码之后,我们再来回到2.3.1章节的代码部分,如下所示:

我们已经分析完getExtensionClasses()方法了,下面假设如果SPI没有配置Adaptive类,即:cachedAdaptiveClass等于null,则会执行createAdaptiveExtensionClass()方法来通过程序拼装Adaptive源码,然后默认通过JavassistCompiler将适配器源码编译为Class对象。下面我们就来看一下createAdaptiveExtensionClass()方法的具体处理流程。

在该方法中,首先通过调用generate()方法,来获得java源代码(String code),这个就是我们前文提过的——通过程序拼装Adaptive源码,具体拼装过程如下所述:

public String generate() {
    if (!hasAdaptiveMethod()) {
        throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
    }
    StringBuilder code = new StringBuilder();
    code.append(generatePackageInfo()); //【拼装包路径】"package %s;\n"
    code.append(generateImports()); //【拼装类引用】"import %s;\n"
    code.append(generateClassDeclaration()); //【拼装类声明】"public class %s$Adaptive implements %s {\n"
    Method[] methods = type.getMethods();
    for (Method method : methods) {
        code.append(generateMethod(method)); //【拼装方法】"public %s %s(%s) %s {\n%s}\n"
    }
    code.append('}');
    return code.toString(); // 返回拼装后的java源代码
}

获得了拼装好的java源代码code之后,通过AdaptiveCompiler来对java源代码进行编译,生成Class类型的实例对象,如下所示:

我们可以看到,获得Compiler也是通过getAdaptiveExtension()方法获得的,如下所示:

Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();

但是由于Compiler配置了适配器类AdaptiveCompiler(见下图),所以,它不需要通过拼装源码的方式获得适配器类。而是直接返回AdaptiveCompiler类。

2.3.1.3> AdaptiveCompiler.compile()

接下来,我们来看一下,调用了AdaptiveCompilercompile方法的处理流程,其中主要是执行两个步骤:

步骤1】首先,通过Dubbo的增强SPI,获得默认扩展compiler实例对象;

步骤2】然后调用compiler的compile(...)方法对java源代码code进行编译操作;

代码如下所示:

由于@SPI指定value=“javassist”,所以在执行cacheDefaultExtensionName()方法的时候,cachedDefaultName会被赋值为“javassist”。然后通过name=“javassist”找到缓存过的扩展类loader为JavassistCompiler,那么我们调用的就是JavassistCompilergetDefaultExtension()方法了,如下所示:

2.3.1.4> getExtension(...)

getExtension(...)方法中,首先试图去holder中查询是否之前已经创建好了入参name的实例对象。由于我们是第一次运行这个方法,所以自然而然我们从holder中获得的就是null对象了,那么此时我们需要做两个事情:

步骤1】创建入参name的扩展类实例对象instance

步骤2】将instance维护到holder中,这样如果再想要获得该实例,就可以直接从holder中获取到了。

那么其中比价复杂的就是负责创建instance实例对象的方法createExtension(name)了,如下图红框所示:

通过name=“javassist”从cachedClasses中加载到对应的clazz=org.apache.dubbo.common.compiler.support.JavassistCompiler,因为JavassistCompiler并没有缓存到EXTENSION_INSTANCES中,所以需要调用clazz.newInstance()来创建实例,并缓存到EXTENSION_INSTANCES中去,如下所示:

如上所述,我们就介绍完JavassistCompiler实例的构造过程了所以,我们再将视野拉回来,看一下compiler.compile(code, classLoader)这段代码,其中,compiler实例对象,其实就是JavassistCompiler实例,如下所示

那么在compile(...)方法中,实际调用的是其父类AbstractCompiler类的compile(...)方法,该方法虽然代码看似很多,但是核心代码其实知识在通过Class.forName来生成Class实例对象这行代码上。如下红框所示:

2.3.2> newInstance()

为什么此处要单单把newInstance()拿出来讲呢?其实醉翁之意不在酒,还记得我们在2.1章节介绍过关于ExtensionFactoy获取AdaptiveExtension的代码吗?为了便于大家回忆,如下红框所示:

那么,当我们针对ExtensionFactoy来调用newInstance()方法时,会执行AdaptiveExtensionFactory的构造方法,该方法内部获得了关于ExtensionFactory类型的的扩展类加载类loader,然后通过调用getSupportedExtensions()方法,获得了“spi”和“spring”。后续我们就可以以spispring为key,将org.apache.dubbo.common.extension.ExtensionFactory文件中配置的SpiExtensionFactorySpringExtensionFactory创建出扩展工厂实例来:

然后,将SpiExtensionFactorySpringExtensionFactory实例对象保存到factories中,用于后续调用ExtensionFactory.getExtension(...)方法的时候,通过遍历factories,再调用factories.getExtension(type, name)来获得对应的扩展类,代码如下所示:

2.3.3> injectExtension()

通过该方法我们就可以实现扩展类的注入操作了。代码量其实不多,主要逻辑就是通过遍历instance实例的每一个setter方法,过滤掉“不符合”的方法。如果setter方法的入参是一个扩展类,那么就通过objectFactory.getExtension(pt, property)方法获得扩展类对象,并通过反射注入到相应的方法中去,代码&注释如下图所示:

今天的文章内容就这些了:

写作不易,笔者几个小时甚至数天完成的一篇文章,只愿换来您几秒钟的 点赞 & 分享

更多技术干货,欢迎大家关注公众号“爪哇缪斯” ~ \(^o^)/ ~ 「干货分享,每天更新」

相关文章
|
9月前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
903 29
|
9月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
380 4
|
9月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
9月前
|
存储 前端开发 JavaScript
在线教育网课系统源码开发指南:功能设计与技术实现深度解析
在线教育网课系统是近年来发展迅猛的教育形式的核心载体,具备用户管理、课程管理、教学互动、学习评估等功能。本文从功能和技术两方面解析其源码开发,涵盖前端(HTML5、CSS3、JavaScript等)、后端(Java、Python等)、流媒体及云计算技术,并强调安全性、稳定性和用户体验的重要性。
|
9月前
|
负载均衡 JavaScript 前端开发
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
364 2
|
9月前
|
移动开发 前端开发 JavaScript
从入门到精通:H5游戏源码开发技术全解析与未来趋势洞察
H5游戏凭借其跨平台、易传播和开发成本低的优势,近年来发展迅猛。接下来,让我们深入了解 H5 游戏源码开发的技术教程以及未来的发展趋势。
|
10月前
|
机器学习/深度学习 自然语言处理 算法
生成式 AI 大语言模型(LLMs)核心算法及源码解析:预训练篇
生成式 AI 大语言模型(LLMs)核心算法及源码解析:预训练篇
2635 1
|
12月前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
12月前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析

热门文章

最新文章

推荐镜像

更多
  • DNS