大数据架构师分享ExtensionLoader工作原理+扩展点动态编译的实现

本文详细分析了Dubbo中ExtensionLoader的工作流程,包括其核心方法(getExtension,getAdaptiveExtension,getActivateExtension)的实现原理,以及SPI、Adaptive和Activate注解的作用。还探讨了动态编译器的种类和实现机制,如Javassist和JDK编译器的使用。
摘要由CSDN通过智能技术生成

ExtensionLoader 的工作原理

ExtensionLoader是整个扩展机制的主要逻辑类,在这个类里面实现了配置的加载、扩展类缓存、自适应对象生成等所有工作。本节将结合核心源码讲解整个ExtensionLoader的工作流程。

工作流程

ExtensionLoader 的逻辑入口可以分为 getExtension、getAdaptiveExtension、getActivateExtension三个,分别是获取普通扩展类、获取自适应扩展类、获取自动激活的扩展类。总体逻辑都是从调用这三个方法开始的,每个方法可能会有不同的重载的方法,根据不同的传入参数进行调整,如图4-1所示。

三个入口中,getActivateExtension对getExtension 的依赖比较getAdaptiveExtension则相对独立。

由4.2.3节可以知道,getActivateExtension方法只是根据不同的条件同时激活多个普通扩展类。因此,该方法中只会做一些通用的判断逻辑,如接口是否包含©Activate注解、匹配条件是否符合等。最终还是通过调用getExtension方法获得具体扩展点实现类。

getExtension (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.4节讲解。

(4) 返回对应的自适应类实例。

接下来,我们就详细讲舟军 getExtension、getAdaptiveExtension> getActivateExtension 这三个流程的实现。

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)o扩展点配置信息加载过程的源码如代码清单4-10所示。

加载完扩展点配置后,再通过反射获得所有扩展实现类并缓存起来。注意,此处仅仅是把Class加载到JVM中,但并没有做Class初始化。在加载Class文件时,会根据Class上的注解来判断扩展点类型,再根据类型分类做缓存。缓存的分类已经在4.1.4节讲过。扩展类的缓存分类如代码清单4-11所示。

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

代码清单4・12依赖注入
injectExtension(instance); <一 向扩展类注入其依赖的属性,如扩展类A又依赖了扩展类B
Set<Class<?>> wrapperclasses = cachedWrapperClasses;
if (wrapperclasses != null && !wrapperclasses.isEmpty()) {
for (Class<?> wrapperclass : wrapperclasses) { 遍历扩展点包装类,用于初始化包装类实例
instance = injectExtension((T) wrapperclass.getConstructor(type).
newlnstance(instance));找到构造方法参数类型为type (扩展类的类型)的包装类,为1其注入扩展类实例 
}
)

在injectExtension方法中可以为类注入依赖属性,它使用了 ExtensionFactory#getExtension(Class<T> type, String name)来获取对应的bean实例,这个工厂接口会在4.3.5节详细说明。

我们先来了解一下注入的实现原理。

injectExtension方法总体实现了类似Spring的IoC机制,其实现原理比较简单:首先通过反射获取类的所有方法,然后遍历以字符串set开头的方法,得到set方法的参数类型,再通过ExtensionFactory寻找参数类型相同的扩展类实例,如果找到,就设值进去,如代码清单4-13所示。

代码清单4-13注入依赖扩展类实现代码

从源码中可以知道,包装类的构造参数注入也是通过injectExtension方法实现的。

getAdaptiveExtension 的实现原理

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

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

(1) 生成package> import、类名称等头部信息。此处只会引入一个类ExtensionLoader。

为了不写其他类的import方法,其他方法调用时全部使用全路径。类名称会变为“接口名称+SAdaptive ” 的格式。例如:Transporter 接口 会生成 Transporter$Adpative。

(2) 遍历接口所有方法,获取方法的返回类型、参数类型、异常类型等。为第(3)步判断是否为空值做准备。

(3)生成参数为空校验代码,如参数是否为空的校验。如果有远程调用,还会添加Invocation参数为空的校验。

(4) 生成默认实现类名称。如果©Adaptive注解中没有设定默认值,则根据类名称生成,如YyylnvokerWrapper会被转换为yyy.invoker.wrappero生成的规则是不断找大写字母,并把它们用连接起来。得到默认实现类名称后,还需要知道这个实现是哪个扩展点的。

(5) 生成获取扩展点名称的代码。根据@Adaptive注解中配置的key值生成不同的获取代码,例如:如果是@Adaptive(nprotocoln),则会生成 ur1. getProtocol() 。

(6) 生成获取具体扩展实现类代码。最终还是通过getExtension(extName)方法获取自适应扩展类的真正实现。如果根据URL中配置的key没有找到对应的实现类,则会使用第(4)步中生成的默认实现类名称去找。

(7) 生成调用结果代码。

下面我们用Dubb。源码中自带的一个单元测试来演示代码生成过程,如代码清单4-14所示。

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

如果一个接口上既有@SPI(”impl”)注解,方法上又有@Adaptive(”impl2”)注解,那么会以哪个key作为默认实现呢?由上面动态生成的SAdaptive类可以得知,最终动态生成的实现方法会是url.getParameter(nimpl2n, "impl”),即优先通过©Adaptive注解传入的key去查找扩展实现类;

如果没找到,则通过@SPI注解中的key去查找;如果@SPI注解中没有默认值,则把类名转化为key,再去查找。

getActivateExtension 的实现原理

接下来,我们讲解图4-1中的旧Activate的实现原理,先从它的入口方法说起。

getActivateExtension(URL url. String key. String group)方法可以获取所有自动激活扩展点。参数分别是URL.URL中指定的key(多个则用逗号隔开)和URL中指定的组信息(group)。

其实现逻辑非常简单,当调用该方法时,主线流程分为4步:

(1) 检查缓存,如果缓存中没有,则初始化所有扩展类实现的集合。

(2) 遍历整个©Activate注解集合,根据传入URL匹配条件(匹配group> name等),得到所有符合激活条件的扩展类实现。然后根据@入"浦3七。中配置的before、after、order等参数进行排序,这些参数在4.2.3节中已经介绍过。

(3) 遍历所有用户自定义扩展类名称,根据用户URL配置的顺序,调整扩展点激活顺序(遵循用户在 URL 中配置的顺序,例如 URL 为 test ://localhost/test?ext=orderlJdefault,则扩展点ext的激活顺序会遵循先orderl再default,其中default代表所有有@Activate注解的扩展点)。

(4) 返回所有自动激活类集合。

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

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

则该扩展点也不会被自动激活。例如:-xxxx,表示名字为xxxx的扩展点不会被激活。

ExtensionFactory 的实现原理

经过前面的介绍,我们可以知道ExtensionLoader类是整个SPI的核心。但是,ExtensionLoader类本身又是如何被创建的呢?

我们知道RegistryFactory工厂类通ii@Adaptive(("protocol"})注解动态查找注册中心实现,根据URL中的protocol参数动态选择对应的注册中心工厂,并初始化具体的注册中心客户端。而实现这个特性的ExtensionLoader类,本身又是通过工厂方法ExtensionFactory创建的,并且这个工厂接口上也有SPI注解,还有多个实现。具体见代码清单4-15。

代码清单4-15 ExtensionFactory工厂接口
@SPI
public interface ExtensionFactory {
<T> T getExtension(Class<T> type. String name);

既然工厂接口有多个实现,那么是怎么确定使用哪个工厂实现的呢?我们可以看到AdaptiveExtensionFactory 这个实现类工厂上W@Adaptive 注解。因此,AdaptiveExtensionFactory会作为一开始的默认实现。工厂类之间的关系如图4.2所示。

可以看到,除了 AdaptiveExtensionFactory,还有 SpiExtensionFactory 和 SpringExtensionFactory两个工厂。也就是说,我们除了可以从Dubbo SPI管理的容器中获取扩展点实例,还可以从Spring容器中获取。

那么Dubbo和Spring容器之间是如何打通的呢?我们先来看SpringExtensionFactory的实现,该工厂提供了保存Spring上下文的静态方法,可以把Spring上下文保存到Set集合中。

当调用getExtension获取扩展类时,会遍历Set集合中所有的Spring上下文,先根据名字依次从每个Spring容器中进行匹配,如果根据名字没匹配到,则根据类型去匹配,如果还没匹配到则返回null,如代码清单4-16所示。

那么Spring的上下文又是在什么时候被保存起来的呢?我们可以通过代码搜索得知,在ReferenceBean和ServiceBean中会调用静态方法保存Spring上下文,即一个服务被发布或被引用的时候,对应的Spring ±下文会被保存下来。

我们再看一下SpiExtensionFactory,主要就是获取扩展点接口对应的Adaptive实现类。例如:某个扩展点实现类 ClassA 上有@Adaptive 注解,则调用 SpiExtensionFactory#getExtension会直接返回ClassA实例,如代码清单4-17所示。

经过一番流转,最终还是回到了默认实现AdaptiveExtensionFactory上.,因为该工厂上有©Adaptive注解。这个默认工厂在构造方法中就获取了所有扩展类工厂并缓存起来,包括SpiExtensionFactory 和 SpringExtensionFactory0 AdaptiveExtensionFactory 构造方法如代码清单4-18所示。

被AdaptiveExtensionFactory缓存的工厂会通过TreeSet进行排序,SPI排在前面,Spring排在后面。当调用getExtension方法时,会遍历所有的工厂,先从SPI容器中获取扩展类;如果没找到,则再从Spring容器中查找。我们可以理解为,AdaptiveExtensionFactory持有了所有的具体工厂实现,它的getExtension方法中只是遍历了它持有的所有工厂,最终还是调用SPI或Spring工厂实现的getExtension方法。getExtension方法如代码清单4-19所示。

扩展点动态编译的实现

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

总体结构

Dubbo中有三种代码编译器,分别是JDK编译器、Javassist编译器和AdaptiveCompiler编译器。这几种编译器都实现了 Compiler接口,编译器类之间的关系如图4.3所示。

从图4-3中可以看®J,Compiler接口上含有一个SPI注解,注解的默认值是@SPI(”javassist”),很明显,Javassist编译器将作为默认编译器。如果用户想改变默认编译器,则可以通过<dubbo:application compiler="jdk" />标签进行配置。

AdaptiveCompiler上面有^Adaptive注解,说明AdaptiveCompiler会固定为默认实现,这个Compiler的主要作用和AdaptiveExtensionFactory相似,就是为了管理其他Compiler,如代码清单4-20所示。

AdaptiveCompiler#setDefaultCompiler 方法会在 ApplicationConfig 中被调用,也就是 Dubbo在启动时,会解析配置中的<dubbo:application compiler="jdk" />标签,获取设置的值,初始化对应的编译器。如果没有标签设置,贝U使用@SPI(Hjavassistn)中的设置,即3avassistCompilero

然后看一下AbstpactCompiler,它是一个抽象类,无法实例化,但在里面封装了通用的模板逻辑。还定义了一个抽象方法decompile ,留给子类来实现具体的编译逻辑。

OavassistCompiler和DdkCompiler都实现了这个抽象方法。

Abstractcompiler的主要抽象逻辑如下:

(1) 通过正则匹配出包路径、类名,再根据包路径、类名拼接出全路径类名。

(2) 尝试通过Class.forName加载该类并返回,防止重复编译。如果类加载器中没有这个类,则进入第3步。

(3) 调用doCompile方法进行编译。这个抽象方法由子类实现。

下面将介绍两种编译器的具体实现。

Javassist动态代码编译

Java中动态生成Class的方式有很多,可以直接基于字节码的方式生成,常见的工具库有CGLIB、ASM> Javassist等。而自适应扩展点使用了生成字符串代码再编译为Class的方式。

在讲解Dubbo中Javassist动态代码编译之前,我们先看一个Javassist生成一个“Hello World”的例子,这样对理解后续实现原理很有帮助,如代码清单4.21所示。

看完Javassist使用示例,其实Dubbo中DavassistCompiler的实现原理也很清晰了。由于我们之前已经生成了代码字符串,因此在JavassistCompiler中,就是不断通过正则表达式匹配不同部位的代码,然后调用Javassist库中的API生成不同部位的代码,最后得到一个完整的Class对象。具体步骤如下:

(1) 初始化Javassist,设置默认参数,如设置当前的classpatho

(2) 通过正则匹配出所有import的包,并使用Javassist添加importo

(3) 通过正则匹配出所有extends的包,创建Class对象,并使用Javassist添加extendso

(4) 通过正则匹配出所有implements包,并使用Javassist添加implementso

(5) 通过正则匹配出类里面所有内容,即得到{}中的内容,再通过正则匹配出所有方法,并使用Javassist添加类方法。

(6) 生成Class对象。

JavassistCompiler继承了抽象类Abstractcompiler,需要实现父类定义的一个抽象方法doCompileo以上步骤就是整个doCompile方法在3avassistCompiler中的实现。

JDK动态代码编译

DdkCompiler是Dubbo编译器的另一种实现,使用了 JDK自带的编译器,原生JDK编译器包位于 javax. tools下。主要使用了三个东西:JavaFileObject 接口、ForwardingJavaFileManager 接口、
DavaCompiler.CompilationTask 方法。整个动态编译过程可以简单地总结为:首先初始化一个JavaFileObject对象,并把代码字符串作为参数传入构造方法,然后调用JavaCompiler.CompilationTask方法编译出具体的类。JavaFileManager负责管理类文件的输入/输出位置。以下是每个接口/方法的简要介绍:

(1) JavaFileObject接口。字符串代码会被包装成一个文件对象,并提供获取二进制流的接口。Dubbo框架中的JavaFileObjectlmpl类可以看作该接口一种扩展实现,构造方法中需要传入生成好的字符串代码,此文件对象的输入和输出都是ByteArray流。由于SimpleDavaFileObject> DavaFileObject之间的关系属于JDK中的知识,因此在本章不深入讲解,有兴趣的读者可以自行查看JDK源码。

(2) DavaFileManager接口。主要管理文件的读取和输出位置。JDK中没有可以直接使用的实现类,唯一的实现类ForwardingDavaFileManager构造器又是protect类型。因此Dubbo中定制化实现了一个DavaFileManagerlmpl类,并通过一个自定义类加载器ClassLoaderlmpl完成资源的加载。

(3)
DavaCompiler.CompilationTask 把 DavaFileObject 对象编译成具体的类。

小结

本章的内容比较多,首先介绍了 Dubbo SPI的一些概要信息,包括与JavaSPI的区别>DubboSPI的新特性、配置规范和内部缓存等。其次介绍了 Dubbo SPI中最重要的三个注解:@SPI、@Adaptive、@Activate,讲解了这几个注解的作用及实现原理。然后结合ExtensionLoader类的源码介绍了整个Dubbo SPI中最关键的三个入口: getExtension、getAdaptiveExtension>getActivateExtension,并讲解了创建 ExtensionLoader 的工厂(ExtensionFactory)的工作原理。最后还讲解了自适应机制中动态编译的实现原理。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值