【Dubbo】扩展点加载机制
主要内容 :
- 加载机制概述 ;
- 扩展点注解 ;
- ExtensionLoader 的工作原理 ;
- 扩展点动态编译的实现原理 。
介绍整个加载机制中最核心的 ExtensionLoader 的工作流程及实现原理 。 最后介绍扩展中使用的类动态编译的实现原理 。 然后介绍整个加载机制中最核心的 ExtensionLoader 的工作流程及实现原理 。 最后介绍扩展中使用的类动态编译的实现原理 。
首先介绍现有 Dubbo 加载机制的概况 , 包括 Dubbo 所做的改进及部分特性 。 其次介绍加载机制中已经存在的一些关键注解 , 如 @SPI 、 @Adaptive 、@Activate 。
1 加载机制概述
Dubbo 良好的扩展性与两个方面是密不可分的 , 一是整个框架中针对不同的场景 , 恰到好处地使用了各种设计模式 , 二就是本章要介绍的加载机制 。 基于 Dubbo SPI 加载机制 , 让整个框架的接口和具体实现完全解耦 , 从而奠定了整个框架良好可扩展性的基础
Dubbo 定义了良好框架结构 , 它默认提供了很多可以直接使用的扩展点 。 Dubbo 几乎所有的功能组件都是基于扩展机制 (SPI) 实现的 。Dubbo SPI 没有直接使用 Java SPI 而是在它的思想上又做了一定的改进 , 形成了一套自己的配置规范和特性 。 同时 , Dubbo SPI 又兼容 Java SPL 服务在启动的时候 , Dubbo 就会查找这些扩展点的所有实现
1.1 Java SPI
在讲解 Dubbo SPI 之前 , 我们先了解一下 Java SPI 是怎么使用的 。 SPI 的全称是 Service Provider Interface, 起初是提供给厂商做插件开发的 。 关于 Java SPI 的详细定义和解释 , 可以参见此处Java SPI 使用了策略模式 , 一个接口多种实现 。 我们只声明接口 , 具体的实现并不在程序中直接确定 , 而是由程序之外的配置掌控 , 用于具体实现的装配 。 具体步骤如下 :
- (1) 定义一个接口及对应的方法 。
- (2) 编写该接口的一个实现类 。
- (3) 在 META-INF/services/ 目录下 , 创建一个以接口全路径命名的文件 , 如 com.test.spi.PrintService
- (4) 文件内容为具体实现类的全路径名 , 如果有多个 , 则用分行符分隔 。
- (5) 在代码中通过 java . util . ServiceLoader 来加载具体的实现类 。
如此一来 , PrintService 的具体实现就可以由文件 com . test . spi . Printservice 中配置的实现类来确定了 。
项目结构如下
Java SPI 示例代码
从上面的代码清单中可以看到 , main 方法里通过 java . util . ServiceLoader 可以获取所有的接口实现 , 具体调用哪个实现 , 可以通过用户定制的规则来决定 。
1.2 扩展点加载机制的改进
与 Java SPI 相比 , Dubbo SPI 做了一定的改进和优化 , 官方文档中有这么一段 :
- JDK 标准的 SPI 会一次性实例化扩展点所有实现 , 如果有扩展实现则初始化很耗时 , 如果没用上也加载 , 则浪费资源
- 如果扩展加载失败 , 则连扩展的名称都取不到了 。 比如 JDK 标准的 ScriptEngine, 通过getName () 获取脚本类型的名称 , 如果 RubyScriptEngine 因为所依赖的 jruby .jar 不存在 , 导致RubyScriptEngine 类加载失败 , 这个失败原因被 “ 吃掉 ” 了 , 和 Ruby 对应不起来 , 当用户执行 Ruby 脚本时 , 会报不支持 Ruby, 而不是真正失败的原因 。
- 增加了对扩展 IoC 和 AOP 的支持 , 一个扩展可以直接 setter 注入其他扩展 。 java.util.ServiceLoader 会一次把 Printservice接口下的所有实现类全部初始化 , 用户直接调用即可 。Dubbo SPI 只是加载配置文件中的类 ,并分成不同的种类缓存在内存中 , 而不会立即全部初始化 , 在性能上有更好的表现 。 具体的实现原理会在后面讲解 , 此处演示一个使用示例 。 我们把代码清单中的 Printservice改造成 Dubbo SPI 的形式 。
PrintService 接口的 Dubbo SPI 改造
Dubbo SPI 在扩展加载失败的时候会先抛出真实异常并打印日志 。 扩展点在被动加载的时候 , 即使有部分扩展加载失败也不会影响其他扩展点和整个框架的使用 。
Dubbo SPI 自己实现了 IoC 和 AOP 机制 。 一个扩展点可以通过 setter 方法直接注入其他扩展的方法 , T injectExtension(T instance) 方法实现了这个功能 , 后面会专门讲解 。
另外 , Dubbo 支持包装扩展类 , 推荐把通用的抽象逻辑放到包装类中 , 用于实现扩展点的 AOP 特性 。 举个例子 , 我们可以看到 ProtocolFilterWrapper 包装扩展了 DubboProtocol 类 , 一些通用的判断逻辑全部放在了 ProtocolFilterWrapper 类的 export 方法中 , 但最终会调用DubboProtocol#export方法 。 这和 Spring 的动态代理思想一样 , 在被代理类的前后插入自己的逻辑进行增强 , 最终调用被代理类 。 下面是 ProtocolFilterWrapper#export 方法 , 如代码清单所示 。
包装类代码示例
1.3 扩展点的配置规范
Dubbo SPI 和 Java SPI 类似 , 需要在 META-INF/dubbo/ 下放置对应的 SPI 配置文件 , 文件名称需要命名为接口的全路径名 。 配置文件的内容为 key= 扩展点实现类全路径名 , 如果有多个实现类则使用换行符分隔 。 其中 , key 会作为 DubboSPI 注解中的传入参数 。 另外 , Dubbo SPI 还兼容了 Java SPI 的配置路径和内容配置方式 。 在 Dubbo 启动的时候 , 会默认扫这三个目录下的配置文件 : META-INF/services/ 、 META-INF/dubbo/ 、 META-INF/dubbo/internal/, 如表 4-1 所示
1.4 扩展点的分类与缓存
Dubbo SPI 可以分为 Class 缓存 、 实例缓存 。 这两种缓存又能根据扩展类的种类分为普通扩展类 、 包装扩展类 ( Wrapper 类 ) 、 自适应扩展类 ( Adaptive 类 ) 等 。
- Class 缓存 : Dubbo SPI 获取扩展类时 , 会先从缓存中读取 。 如果缓存中不存在 , 则加载配置文件 , 根据配置把 Class 缓存到内存中 , 并不会直接全部初始化 。
实例缓存 : 基于性能考虑 , Dubbo 框架中不仅缓存 Class, 也会缓存 Class 实例化后的对象 。 每次获取的时候 , 会先从缓存中读取 , 如果缓存中读不到 , 则重新加载并缓存起来 。 这也是为什么 Dubbo SPI 相对 Java SPI 性能上有优势的原因 , 因为 Dubbo SPI缓存的 Class 并不会全部实例化 , 而是按需实例化并缓存 , 因此性能更好 。被缓存的 Class 和对象实例可以根据不同的特性分为不同的类别 :
- ( 1 ) 普通扩展类 。 最基础的 , 配置在 SPI 配置文件中的扩展类实现 。
- ( 2 ) 包装扩展类 。 这种 Wrapper 类没有具体的实现 , 只是做了通用逻辑的抽象 , 并且需要在构造方法中传入一个具体的扩展接口的实现 。 属于 Dubbo 的自动包装特性 , 该特1.5节中详细介绍 。
- ( 3 ) 自适应扩展类 。 一个扩展接口会有多种实现类 , 具体使用哪个实现类可以不写死在配置或代码中 , 在运行时 , 通过传入 URL 中的某些参数动态来确定 。 这属于扩展点的自适应特性 ,使用的 @Adaptive 注解也会在 1.5 节中详细介绍 。
- ( 4 ) 其他缓存 , 如扩展类加载器缓存 、 扩展名缓存等 。
扩展类缓存如表所示
1.5 扩展点的特性
从 Dubbo 官方文档中可以知道 , 扩展类一共包含四种特性 : 自动包装 、 自动加载 、 自适应和自动激活 。
自动包装
自动包装是在 1.4 节中提到的一种被缓存的扩展类 , ExtensionLoader 在加载扩展时 , 如果发现这个扩展类包含其他扩展点作为构造函数的参数 , 则这个扩展类就会被认为是 Wrapper 类 。Wrapper 类示例代码
ProtocolFilterWrapper 虽然继承了 Protocol 接口 , 但是其构造函数中又注入了一个Protocol 类型的参数 。 因此 ProtocolFilterWrapper 会被认定为 Wrapper 类 。 这是一种装饰器模式 , 把通用的抽象逻辑进行封装或对子类进行增强 , 让子类可以更加专注具体的实现 。- 自动加载
除了在构造函数中传入其他扩展实例 , 我们还经常使用 setter 方法设置属性值 。 如果某个扩展类是另外一个扩展点类的成员属性 , 并且拥有 setter 方法 , 那么框架也会自动注入对应的扩展点实例 。 ExtensionLoader 在执行扩展点初始化的时候 , 会自动通过 setter 方法注入对应的实现类 。 这里存在一个问题 , 如果扩展类属性是一个接口 , 它有多种实现 , 那么具体注入哪一
个呢 ? 这就涉及第三个特性 一一 自适应 。 自适应
在 Dubbo SPI 中 , 我们使用@入( 14? 什作注解 , 可以动态地通过 URL 中的参数来确定要使用
哪个具体的实现类 。 从而解决自动加载中的实例注入问题 。©Adaptive 注解使用示例
@Adaptive 传入了两个 Constants 中的参数 , 它们的值分别是 “server ” 和 “ transporter ” 。 当外部调用 Transporter#bind 方法时 , 会动态从传入的参数 “ URL ” 中提取 key 参数 “ server ”的 value 值 , 如果能匹配上某个扩展实现类则直接使用对应的实现类 ; 如果未匹配上 , 则继续通过第二个 key 参数 “ transporter ” 提取 value 值 。 如果都没匹配上 , 则抛出异常 。 也就是说 ,如果 ©Adaptive 中传入了多个参数 , 则依次进行实现类的匹配 , 直到最后抛出异常 。这种动态寻找实现类的方式比较灵活 , 但只能激活一个具体的实现类 , 如果需要多个实现类同时被激活 , 如 Filter 可以同时有多个过滤器;或者根据不同的条件 , 同时激活多个实现类 ,如何实现 ? 这就涉及最后一个特性 一一 自动激活 。- 自动激活
使用 @Activate 注解 , 可以标记对应的扩展点默认被激活启用 。 该注解还可以通过传入不同的参数 , 设置扩展点在不同的条件下被自动激活 。 主要的使用场景是某个扩展点的多个实现类需要同时启用(比如 Filter 扩展点) 。 在 2 节中会详细介绍以上几种注解 。
2 扩展点注解
2.1 扩展点注解 : @SPI
@SPI 注解可以使用在类 、 接口和枚举类上 , Dubbo 框架中都是使用在接口上 。 它的主要作用就是标记这个接口是一个 Dubbo SPI 接口 , 即是一个扩展点 , 可以有多个不同的内置或用户定义的实现 。 运行时需要通过配置找到具体的实现类 。
@SPI 注解的源码
我们可以看到 SPI 注解有一个 value 属性 , 通过这个属性 , 我们可以传入不同的参数来设置这个接口的默认实现类 。 例如 , 我们可以看到 Transporter 接口使用 Netty 作为默认实现
SPI 默认实现示例代码
Dubbo 中很多地方通过 get Extension (Class type. String name) 来获取扩展点接口的具体实现 , 此时会对传入的 Class 做校验 , 判断是否是接口 , 以及是否有 @SPI 注解 , 两者缺一不可
2.2 扩展点自适应注解 : @Adaptive
@Adaptive 注解可以标记在类 、 接口 、 枚举类和方法上 , 但是在整个 Dubbo 框架中 , 只有几个地方使用在类级别上 , 如 AdaptiveExtensionFactory 和 AdaptiveCompiler, 其余都标注在方法上 。 如果标注在接口的方法上 , 即方法级别注解 , 则可以通过参数动态获得实现类 , 这一点已经在 1.5 节的自适应特性上说明 。 方法级别注解在第一次 getExtension 时 , 会自动生成和编译一个动态的 Adaptive 类 , 从而达到动态实现类的效果 。
例如 : Transporter 接口在 bind 和 connect 两个方法上添加了 ©Adaptive 注解 , Dubbo 在初始化扩展点时 , 会生成一个 Transporter$Adaptive 类 , 里面会实现这两个方法 , 方法里会有一些抽象的通用逻辑 , 通过 @Adaptive 中传入的参数 , 找到并调用真正的实现类 。 熟悉装饰器模式的读者会很容易理解这部分的逻辑 。 具体实现原理会在 4 节讲解
下面是自动生成的 Transporter$Adaptive#bind 实现代码 已经省略了无关代码 。
实现代码
我们可以从生成的源码中看到 , 自动生成的代码中实现了很多通用功能 , 最终会调用真正的接口实现 。当该注解放在实现类上 , 则整个实现类会直接作为默认实现 , 不再自动生成代码清单中的代码 。
在扩展点接口的多个实现里 , 只能有一个实现上可以加 @Adaptive 注解 。 如果多个实现类都有该注解 , 则会抛出异常 :More than 1 adaptive class found
Adaptive 注解的源代码
该注解也可以传入 value 参数 , 是一个数组 。 我们在代码清单中可以看到 , Adaptive 可以传入多个 key 值 , 在初始化 Adaptive 注解的接口时 , 会先对传入的 URL 进行 key 值匹配 , 第一个 key 没匹配上则匹配第二个 , 以此类推 。 直到所有的 key 匹配完毕 , 如果还没有匹配到 ,则会使用 “ 驼峰规则 ” 匹配 , 如果也没匹配到 , 则会抛出 IllegalStateException 异常 。
什么是”驼峰规则 ” 呢 ? 如果包装类 ( Wrapper ) 没有用 Adaptive 指定 key 值 , 则 Dubbo 会自动把接口名称根据驼峰大小写分开 , 并用符号连接起来 , 以此来作为默认实现类的名称 , 如 org.apache.dubbo.xxx.YyylnvokerWpapper 中的YyylnvokerWrapper 会被转换为 yyy.invoker.wrapper
最后 , 为什么有些实现类上会标注 ©Adaptive 呢 ? 放在实现类上 , 主要是为了直接固定对应的实现而不需要动态生成代码实现 , 就像策略模式直接确定实现类 。 在代码中的实现方式是 :ExtensionLoader 中会缓存两个与 @Adaptive 有关的对象 , 一个缓存在 cachedAdaptiveClass 中 ,即 Adaptive 具体实现类的 Class 类型 ; 另外一个缓存在 cachedAdaptivelnstance 中 , 即 Class的具体实例化对象 。 在扩展点初始化时 , 如果发现实现类有 @Adaptive 注解 , 则直接赋值给cachedAdaptiveClass , 后续实例化类的时候 , 就不会再动态生成代码 , 直接实例化cachedAdaptiveClass, 并把实例缓存到 cachedAdaptivelnstance 中 。 如果注解在接口方法上 ,则会根据参数 , 动态获得扩展点的实现 , 会生成 Adaptive 类 , 再缓存到cachedAdaptivelnstance 中 。
2.3 扩展点自动激活注解 : @Activate
@Activate 可以标记在类 、 接口 、 枚举类和方法上 。 主要使用在有多个扩展点实现 、 需要根据不同条件被激活的场景中 , 如 Filter 需要多个同时激活 , 因为每个 Filter 实现的是不同的功能 。
@Activate 可传入的参数很多 , 如表所示
接下一篇 【Dubbo】Dubbo 扩展点加载原理
还没有评论,来说两句吧...