Dubbo扩展点加载机制

22 篇文章 0 订阅
8 篇文章 0 订阅

前言

《深入理解Apache Dubbo与实战》第四章

  • 加载机制概述
  • 扩展点注解
  • ExtensionLoader的工作原原理
  • 扩展点动态编译的实现原理

一、加载机制概述

Dubbo的扩展性与恰到好处的设计模式和加载基于Dubbo SPI的加载机制密不可分
Dubbo的SPI对JAVA SPI进行了改进,又兼容Java的SPI

1.Java SPI

Java SPI 全称时 Service Provider Interface,起初是给厂家做插件开发用的
具体使用步骤如下
(1) 定义一个接口及对应的方法
(2)编写该接口的实现类
(3)在/resources/META-INF/services目录下创建一个以接口全路径的文件,如com.test.spi.PrintService
(4)文件内容为具体实现类的全路径名,如果多个,则用分行符分隔
(5)在代码中用java.util.ServiceLoader来加载具体的实现类

2.扩展点加载机制的改进

增加了扩展IOC和Aop的支持,一个扩展可以直接Setter注入其他扩展。并且Dubbo只是将配置文件中的类分成不同的缓存放在内存中,并不会全部初始化。

3.扩展点的配置规范

规范名规范说明
SPI配置文件路径META-INF/services META-INF/dubbo META-INF/dubbo/internal
文件名称全路径名
文件内容格式key=value方式,多个用换行符

4.扩展点的分类与缓存

  • class 缓存:Dubbo SPI 获取扩展类时,先从缓存中读取,如果没有,则加载配置文件
  • 实例缓存:基于性能考虑,Dubbo不仅缓存class,也会缓存实例

根据特性缓存还可以分为:
(1)普通扩展类:最基础的,配置在SPI配置文件中的扩展类实现
(2)包装扩展类:这种wrapper类没有具体实现,只是做了通用的逻辑的抽象,在构造方法中传入一个具体的扩展接口实现。
(3)自适应扩展类:一个扩展接口有多个实现类,通过URL动态参数确定
(4)其它缓存,如扩展类加载器缓存、扩展名缓存。

5.扩展点的特性

1)自动包装
当ExtensionLoader加载扩展时,如果发现这个扩展类包含其他扩展点作为构造函数的参数,则这个扩展类会被认为是Wrapper类
代码示例

public class ProtocolFilterWrapper implements Protocol {

    private final Protocol protocol;

    public ProtocolFilterWrapper(Protocol protocol) {//继承了Protocol但是构造函数又传入了一个Protocol类型的参数,框架会自动注入。
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
    }
    ...
    }

2)自动加载
如果某个扩展类是另一个扩展点类的成员属性且有Setter方法,则框架会自动注入对应扩展点示例。
3)自适应
使用@Adaptive注解,可以动态地通过URL的参数确定哪个 具体的实现类。
4)自动激活
使用@Active 注解,可以标记对应的扩展点默认被激活使用。该注解还可以通过传入不同的参数,设置在不同的条件下被自动激活。

二、扩展点注解

1.扩展点注解:@SPI

在Dubbo中,@SPI注解都是用在接口上的。它的作用是标记这个接口是个Dubbo SPI 接口,即是一个扩展点,可以有多个不同的内置或用户的定义的实现。

示例

@SPI("netty")
public interface Transport{
...
}

从上面可以看出Transport 接口使用netty作为默认实现

Dubbo 在很多地方通过getExtension(Class< T > type, String name)来获取扩展点接口的具体实现,此时会对传入的Class做校验,判读是否是接口,以及是否有@SPI做注解,两者缺一不可。

2.@Adaptive

@Adaptive注解可以标记在类,接口,方法上,但是在整个Dubbo框架中,只有几个地方使用在类级别上,如AdaptiveExtensionFactory和AdaptiveComoplier,其余都标注在接口的方法上,即方法级注解。方法级别的注解将在第一次getExtension时,会自动生成和编译一个Adaptive类,从而达到动态实现类的效果。

Adaptive的源代码如下

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    String[] value() default {};
}

在初始化Adaptive时,会先对传入的URL进行key值的匹配,第一个没匹配上则匹配第二个,以此类推。如果没有匹配到则使用@SPI注解的默认实现匹配,再失败则抛出IllegalStateException异常。

如果包装类 ( Wrapper ) 没有用 Adaptive 指定 key 值 , 则 Dubbo 会自动把接口名称根据驼峰大小写分开 , 并用符号连接起来 , 以此来作为默认实现类的名称 , 如 org.apache.dubbo.xxx.YyylnvokerWpapper 中的YyylnvokerWrapper 会被转换为 yyy.invoker.wrapper

3.@Activate

@Activate 可以标记在类 、 接口 、 枚举类和方法上 。 主要使用在有多个扩展点实现 、 需要根据不同条件被激活的场景中 , 如 Filter 需要多个同时激活 , 因为每个 Filter 实现的是不同的功能 。
@Activate 可传入的参数很多 , 如表所示

参数名效果
String[] group()Url 中的分组如果匹配则激活,则可以设置多个
String[] value()查找URL中如果含有Key值,则会激活
String[] before()填写扩展列表,表示哪些扩展点要在本扩展点之前
String[] after()表示哪些在本扩展点之后
int order()整形,直接的排序信息

三、ExtensionLoader的工作原理

ExtensionLoader 是整个扩展机制的主要逻辑类 , 在这个类里面卖现了配置的加载 、 扩展类缓存 、 自适应对象生成等所有工作 。

1.工作流程

ExtensionLoader 的逻辑入口可以分为 getExtension 、 getAdaptiveExtension 、getActivateExtension 三个 , 分别是获取普通扩展类 、 获取自适应扩展类 、 获取自动激活的扩展类 。 总体逻辑都是从调用这三个方法开始的 , 每个方法可能会有不同的重载的方法 , 根据不同的传入参数进行调整。
get Extension (String name) 是整个扩展加载器中最核心的方法 , 实现了一个完整的普通扩展类加载过程 。 加载过程中的每一步 , 都会先检查缓存中是否己经存在所需的数据 , 如果存在则直接从缓存中读取 , 没有则重新加载 。 这个方法每次只会根据名称返回一个扩展点实现类 。

初始化的过程可以分为 4 步 :

(1) 框架读取 SPI 对应路径下的配置文件 , 并根据配置加载所有扩展类并缓存(不初始化) 。
(2) 根据传入的名称初始化对应的扩展类 。
(3) 尝试查找符合条件的包装类 : 包含扩展点的 setter 方法 , 例如 setProtocol(Protocolprotocol) 方法会自动注入 protocol 扩展点实现 ; 包含与扩展点类型相同的构造函数 , 为其注入扩展类实例 , 例如本次初始化了一个 Class A, 初始化完成后 , 会寻找构造参数中需要 Class A 的包装类 ( Wrapper), 然后注入 Class A 实例 , 并初始化这个包装类 。
(4) 返回对应的扩展类实例 。

getAdaptiveExtension 也相对独立 , 只有加载配置信息部分与 getExtension 共用了同一个方法 。 和获取普通扩展类一样 , 框架会先检查缓存中是否有已经初始化化好的 Adaptive 实例 ,没有则调用 createAdaptiveExtension 重新初始化 。 初始化过程分为 4 步 :

(1) 和 getExtension 一样先加载配置文件 。
(2) 生成自适应类的代码字符串 。
(3) 获取类加载器和编译器 , 并用编译器编译刚才生成的代码字符串 。 Dubbo 一共有三种类型的编译器实现 。
(4) 返回对应的自适应类实例 。

2.getExtension的实现原理

getExtension 的主要流程前面已经讲过了 , 本节主要会讲解每一步的实现原理 。

当调用 getExtension(String name) 方法时 , 会先检查缓存中是否有现成的数据 , 没有则调用 createExtension 开始创建 。 这里有个特殊点 , 如果 getExtension 传入的 name 是 true,则加载并返回默认扩展类 。

在调用 createExtension 开始创建的过程中 , 也会先检查缓存中是否有配置信息 , 如果不存在扩展类 , 则会从 META-INF/services/> META-INF/dubbo/ 、 META-INF/dubbo/internal/ 这几个路径中读取所有的配置文件 , 通过 I/O 读取字符流 , 然后通过解析字符串 , 得到配置文件中对应的扩展点实现类的全称(如 com.alibaba.dubbo.common.extensionloader.activate.impl.GroupActivateExtImpl)

加载完扩展点配置后 , 再通过反射获得所有扩展实现类并缓存起来 。 注意 , 此处仅仅是把 Class 加载到 JVM 中 , 但并没有做 Class 初始化 。 在加载 Class 文件时 , 会根据 Class 的注解来判断扩展点类型 , 再根据类型分类做缓存 。

最后 , 根据传入的 name 找到对应的类并通过 Class.forName 方法进行初始化 ,并为其注入依赖的其他扩展类(自动加载特性) 。 当扩展类初始化后 , 会检查一次包装扩展类 Set<Class<?>>wrapperclasses, 查找包含与扩展点类型相同的构造函数 , 为其注入刚初始化的扩展类 。

3.getAdaptiveExtension 的实现原理

由之前的流程我们可以知道 , 在 getAdaptiveExtension () 方法中 , 会为扩展点接口自动生成实现类字符串 , 实现类主要包含以下逻辑 : 为接口中每个有 @Adaptive 注解的方法生成默认实现(没有注解的方法则生成空实现) , 每个默认实现都会从 URL 中提取 Adaptive 参数值 , 并以此为依据动态加载扩展点 。 然后 , 框架会使用不同的编译器 , 把实现类字符串编译为自适应类并返回 。 本节主要讲解字符串代码生成的实现原理 。

生成代码的逻辑主要分为 7 步 , 具体步骤如下 :

(1) 生成 package 、 import 、 类名称等头部信息 。 此处只会引入一个类 ExtensionLoader 。为了不写其他类的 import 方法 , 其他方法调用时全部使用全路径 。 类名称会变为 “ 接口名称 + $ Adaptive ” 的格式 。 例如 : Transporter 接口 会生成Transporter$Adpative 。
(2) 遍历接口所有方法 , 获取方法的返回类型 、 参数类型 、 异常类型等 。 为第 (3) 步判断是否为空值做准备 。
(3) 生成参数为空校验代码 , 如参数是否为空的校验 。 如果有远程调用 , 还会添加 Invocation参数为空的校验 。
(4) 生成默认实现类名称 。 如果 @Adaptive 注解中没有设定默认值 , 则根据类名称生成 ,如 YyylnvokerWrapper 会被转换为 yyy.invoker.wrapper 生成的规则是不断找大写字母 , 并把它们用连接起来 。 得到默认实现类名称后 , 还需要知道这个实现是哪个扩展点的 。
(5) 生成获取扩展点名称的代码 。 根据 @Adaptive 注解中配置的 key 值生成不同的获取代码 , 例如 : 如果是@Adaptive( n protocol n ) , 则会生成 url . getProtocol ()
(6) 生成获取具体扩展实现类代码 。 最终还是通过 getExtension(extName) 方法获取自适应扩展类的真正实现 。 如果根据 URL 中配置的 key 没有找到对应的实现类 , 则会使用第 (4)步中生成的默认实现类名称去找 。
( 7) 生成调用结果代码 。

生成完代码之后就要对代码进行编译 , 生成一个新的 Classo Dubbo 中的编译器也是一个自适应接口 , 但 @Adaptive 注解是加在实现类 AdaptiveCompiler 上的 。 这样一来 AdaptiveCompiler就会作为该自适应类的默认实现 , 不需要再做代码生成和编译就可以使用了 。

4.getActivateExtension的实现原理

接下来 , 我们讲解旧 Activate 的实现原理 , 先从它的入口方法说起 。getActivateExtension(URL url. String key. String group) 方法可以获取所有自动激活扩展点 。 参数分别是 URL.URL 中指定的 key (多个则用逗号隔开)和 URL 中指定的组信息 ( group ) 其实现逻辑非常简单 , 当调用该方法时 , 主线流程分为 4 步 :

(1) 检查缓存 , 如果缓存中没有 , 则初始化所有扩展类实现的集合 。
(2) 遍历整个 @Activate 注解集合 , 根据传入 URL 匹配条件(匹配 group > name 等) , 得到所有符合激活条件的扩展类实现 。 然后根据 @Activate 中配置的 before 、 after 、 order 等参数进行排序 , 这些参数在 2.3 节中已经介绍过 。
(3) 遍历所有用户自定义扩展类名称 , 根据用户 URL 配置的顺序 , 调整扩展点激活顺序(遵循用户在 URL 中配置的顺序 , 例如 URL 为 test://localhost/test?ext=order,default, 则扩展点 ext 的激活顺序会遵循先 order 再 default, 其中 default 代表所有有@ Activate 注解的扩展点) 。
(4) 返回所有自动激活类集合 。

获取 Activate 扩展类实现 , 也是通过 getExtension 得到的 。 因此 , 可以认为 getExtension是其他两种 Extension 的基石 。

此处有一点需要注意 , 如果 URL 的参数中传入了 -default, 则所有的默认 @Activate 都不会被激活 , 只有 URL 参数中指定的扩展点会被激活 。 如果传入了符号开头的扩展点名 ,则该扩展点也不会被自动激活 。 例如 : -xxxx, 表示名字为 xxxx 的扩展点不会被激活 。

四、扩展点动态编译的实现

Dubbo SPI 的自适应特性让整个框架非常灵活 , 而动态编译又是自适应特性的基础 , 因为动态生成的自适应类只是字符串 ,需要通过编译才能得到真正的 Class 虽然我们可以使用反射来动态代理一个类 , 但是在性能上和直接编译好的 Class 会有一定差距 。 Dubbo SPI 通过代码的动态生成 , 并配合动态编译器 , 灵活地在原始类基础上创建新的自适应类 。 本节将介绍DubboSPI 动态编译器的种类及对应的现实原理

Dubbo 中有三种代码编译器 , 分别是 JDK 编译器 、 Javassist 编译器和 AdaptiveCompiler 编译器 。 这几种编译器都实现了Compiler 接口 。

Abstractcompiler 的主要抽象逻辑如下 :

(1) 通过正则匹配出包路径 、 类名 , 再根据包路径 、 类名拼接出全路径类名 。
(2) 尝试通过 Class.forName 加载该类并返回 , 防止重复编译 。 如果类加载器中没有这个类 , 则进入第 3 步 。
(3) 调用 doCompile 方法进行编译 。 这个抽象方法由子类实现 。

1.Javaassit 动态代码编译

具体步骤如下 :

(1) 初始化 Javassist, 设置默认参数 , 如设置当前的 classpath
(2) 通过正则匹配出所有 import 的包 , 并使用 Javassist 添加 import
(3) 通过正则匹配出所有 extends 的包 , 创建 Class 对象 , 并使用 Javassist 添加 extends
(4) 通过正则匹配出所有 implements 包 , 并使用 Javassist 添加 implements
( 5) 通过正则匹配出类里面所有内容 , 即得到 { } 中的内容 , 再通过正则匹配出所有方法,并使用 Javassist 添加类方法 。
(6) 生成 Class 对象 。

2.JDK动态代码的编译

dkCompiler 是 Dubbo 编译器的另一种实现 , 使用了 JDK 自带的编译器 , 原生 JDK 编译器包位于 javax. tools 下 。 主要使用了三个东西 : JavaFileObject 接口 、ForwardingJavaFileManager 接口 、 DavaCompiler.CompilationTask 方法 。

整个动态编译过程可以简单地总结为 : 首先初始化一个 JavaFileObject 对象 , 并把代码字符串作为参数传入构造方法 , 然后调用 JavaCompiler.CompilationTask 方法编译出具体的类 。 JavaFileManager 负责管理类文件的输入 / 输出位置 。

以下是每个接口 / 方法的简要介绍 :
(1) JavaFileObject 接口 。 字符串代码会被包装成一个文件对象 , 并提供获取二进制流的接口 。 Dubbo 框架中的 JavaFileObjectlmpl 类可以看作该接口一种扩展实现 , 构造方法中需要传入生成好的字符串代码 , 此文件对象的输入和输出都是 ByteArray 流 。
(2) DavaFileManager 接口 。 主要管理文件的读取和输出位置 。 JDK 中没有可以直接使用的实现类 , 唯一的实现类 ForwardingDavaFileManager 构造器又是 protect 类型 。 因此 Dubbo 中定制化实现了一个 DavaFileManagerlmpl 类 , 并通过一个自定义类加载器 ClassLoaderlmpl 完成资源的加载 。
(3) DavaCompiler.CompilationTask 把 DavaFileObject 对象编译成具体的类

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值