
本文内容转载自 从ExtensionLoader看Dubbo插件化
前言
最近在看 Dubbo 的源码,因为 Dubbo 集成了 Spring 框架,根据 Dubbo 的 XML 配置方式(当然也可以是通过注解的方式),不难想到应该从 Dubbo 对应的 NamespaceHandler 作为分析的出发点。
因为 DubboNamespaceHandler 继承了 NamespaceHandlerSupport 抽象类,所以在 DubboNamespaceHandler 中需要实现 init() 方法的逻辑,主要就是向 Spring 容器中注册相关标签的解析器,这样在遇上对应标签后,将会调用匹配的解析器来生成对应的 bean,相关的代码如下:
1 | public class DubboNamespaceHandler extends NamespaceHandlerSupport { |
这里我最关注的就是 service 标签所对应的 ServiceBean,它实现了众多的 Spring 功能性接口,Spring 容器会在特殊的时刻调用相应的方法,而 dubbo 的服务发布,就是在 com.alibaba.dubbo.config.spring.ServiceBean#onApplicationEvent
方法中完成的,具体的过程比较复杂,其中涉及到了一个 ExtensionLoader 类,实现的功能比较多,使得自己源码分析的过程比较晕,遂去 google 了一下这个类的作用,确实找到了一篇比较好的文章,所以这里将其转载下来,方便以后回顾查看。
SPI 与 Dubbo
了解 SPI 的人知道,它只是提供一种协议,并没有提供相关插件化实施的接口,不像 OSGI 那样有一成套实施插件化 API。它只是规定在 META-INF 目录下提供接口的实现描述文件,框架本身定义接口、规范,第三方只需要将自己的实现在 META-INF 下描述清楚,那么框架就会自动加载你的实现,至于怎么加载,JDK 并没有提供相关 API,而是框架设计者需要考虑和实现的,并且在 META-INF 目录下面对规则的描述,也是需要框架设计者来规定。比如 Dubbo 的规则是在 META-INF/dubbo、META-INF/dubbo/internal 或者 META-INF/services 下面以需要实现的接口的全限定名去创建一个文件,并且在文件中以键值对的方式来规定实现类和别名的关系。下图便是 Dubbo 的 META-INF 目录的详细规则。

这里简单的介绍一下 Dubbo 的插件实现方式和规则,以及提到 SPI 是一种插件化的规范,并没有提供实施的 API,是需要框架自己去实现的。Dubbo 对这一块的实现全部都集中在类 ExtensionLoader 中,那么接下来将围绕这个类来介绍 Dubbo 插件化的实现,在介绍 Dubbo 插件化实施之前,需要知道 Dubbo 框架是以 URL 为总线的模式,即运行过程中所有的状态数据信息都可以通过 URL 来获取,比如当前系统采用什么序列化,采用什么通信,采用什么负载均衡等信息,都是通过 URL 的参数来呈现的,所以在框架运行过程中,运行到某个阶段需要相应的数据,都可以通过对应的 Key 从URL的参数列表中获取。
ExtensionLoader 的 activate 模块
ExtensionLoader 是一个单例工厂类,它对外暴露 getExtensionLoader 静态方法返回一个ExtensionLoader 实体,这个方法的入参是一个 Class 类型,这个方法的意思是返回某个接口的 ExtensionLoader。那么对于某一个接口,只会有一个 ExtensionLoader 实体。ExtensionLoader 实体对外暴露了下图中的一些接口来获取扩展实现。

上图的方法归为几类,分别是 activate extension、adaptive extension、default extension、get extension by name 以及 supported extension。activate extension 都需要传入 url 参数,这里涉及到 Activate 注解,这个注解主要用处是标注在插件接口实现类上,用来配置该扩展实现类激活条件。在 Dubbo 框架里面的 Filter 接口的各种实现类都通过 Activate 标注,用来描述该 Filter 什么时候生效。比如 MonitorFilter 通过 Activate 标注用来告诉 Dubbo 框架这个 Filter 是在服务提供端和消费端会生效的;而 TimeoutFilter 则是只在服务提供端生效,消费端是不会调用该 Filter; ValidationFilter 要激活的条件除了在消费端和服务提供端激活,它还配置了 value,这个表述另一个激活条件,上面介绍要获取 activate extension 都需要传入URL对象,那么这个 value 配置的值则表述URL必须有指定的参数才可以激活这个扩展。例如 ValidationFilter 则表示URL中必须包含参数 validation(Constants.VALIDATION_KEY 常量的值就是 validation),否则消费端和服务端都不会激活这个扩展实现,仔细的同学还会发现在 ValidationFilter 中的 Activate 注解还有一个参数 order,这是表示一种排序规则。因为一个接口的实现有多种,返回的结果是一个列表,如果不指定排序规则,那么可能列表的排序不可控,为了达到排序可控,添加了 order 属性用来控制排序,其中 order 的值越大,那么该扩展实现排序就越靠前。除了通过 order 来控制排序,还有 before 和 after 来配置当前扩展的位置,before 和 after 配置的值是扩展的别名。下边是简化的接口信息:
1 |
|
上面基本对 activate 介绍的差不多了,在 Dubbo 框架中对这个用的最多的就是 Filter 的各种实现,因为 Dubbo 的调用会经过一个过滤器链。哪些 Filter 实现类被包含在调用链中,是通过各种Filter实现类的 Activate 注解来控制的。包括上面说的排序,也可以理解为过滤器链中各个 Filter 的前后顺序。这里的顺序需要注意一个地方,即排序均是对框架本身实现的扩展进行排序,用户自定义的扩展默认是追加在列表后面。说到这里具体例子:
1 | <dubbo:reference id=”fooRef” interface=”com.foo.Foo” filter=”A,B,C”/> |
假设上面是一个有效的消费端服务引用,其中配置了一个 filter 属性,并且通过逗号隔开配置了三个过滤器 A,B,C(A,B,C 均为 Filter 实现的别名),那么对于接口 Foo 调用的过滤器链是怎么样的呢?首先 Dubbo 会加载默认的过滤器(一般消费端有三个 ConsumerContextFilter,MonitorFilter,FutureFilter),并且对这些默认的过滤器实现进行排序(ActivateComparator 实现排序逻辑),这写默认过滤器实现会在过滤器链的前面,后面紧接着的才是 A,B,C 三个自定义过滤器。
ExtensionLoader 的 adaptive 模块
上面介绍了 activate extension,下面介绍 ExtensionLoader 另一个重要模块 adaptive extension。Dubbo 框架提供的各种接口均有很多种类的实现,在引用具体实现的时候不可能通过硬编码制定引用哪个实现,这样整个框架的灵活性严重降低。所以为了能够适配一个接口的各种实现,便有了 adaptive extension 这一说。对一个接口实现的适配器 Dubbo 提供两种途径,第一种途径是手动对某个接口实现对应的适配器,第二种是 Dubbo 框架动态生成适配器类。先对第一种途径进行介绍,这种途径也最好理解,对于这种途径 Dubbo 也提供了一个注解 Adaptive,他用来标注在接口的某个实现上,表示这个实现并不是提供具体业务支持,而是作为该接口的适配器。比如 ExtensionFactory 的实现类 AdaptiveExtensionFactory 就是实现适配的功能,这个类被Adaptive进行了标注,那么在调用 ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()
的时候将会返回 AdaptiveExtensionFactory 实体,用来适配 ExtensionFactory 接口的 SPIExtensionFactory 和 SpringExtensionFactory 两种实现,在 AdaptiveExtensionFactory 将会根据运行时的状态来确定具体调用 ExtensionFactory 的哪个实现。
而第二种相对于第一种来说就隐晦一点,是 ExtensionLoader 通过分析接口配置的 adaptive 规则动态生成 adaptive 类并且加载到 ClassLoader 中,从而来实现动态适配。配置 adaptive 的规则也是通过 Adaptive 注解来设置,该注解有一个 value 属性,通过设置这个属性便可以设置该接口的 Adaptive 的规则,上面说过服务调用的所有数据均可以从 URL 获取(Dubbo 的 URL 总线模式),那么需要 Dubbo 帮我们动态生成 adaptive 的扩展接口的方法入参必须包含 URL,这样才能根据运行状态动态选择具体实现。这里列举一下 Transporter 接口中配置 adaptive 规则。
1 |
|
Transporter 接口提供了两个方法,一个是 connect(用来创建客户端连接),另一个是 bind(用来绑定服务端端口提供服务),并且这两个方法上面均通过 Adaptive 注解配置了 value 属性,bind 配置的是 server 和 transporter,connect 配置的是 client 和 transporter。那么配置这些值有什么用呢?下面看看 ExtensionLoader 根据这些生成了什么样的 adaptive 代码。
1 | import com.alibaba.dubbo.common.extension.ExtensionLoader; |
上面是 ExtensionLoader 自动生成的 Transporter$Adpative 类,并且实现了 Transporter 接口,下面我们分别看看在它 connect 和 bind 中分别做了哪些逻辑。先看看 bind 方法代码段:
1 | public com.alibaba.dubbo.remoting.Server bind( |
可以看到bind方法先对 url 参数(arg0)进行了非空判断,然后便是调用 url.getParameter 方法,首先是获取 server 参数,然后再获取 transporter 参数,最后如果两个参数均没有,extName便是netty。获取完参数之后,紧接着对 extName 进行非空判断,接下来便是获取 Transporter 的 ExtensionLoader,最后获取别名为 extName 的 Transporter 实现,并调用对应的 bind 来进行绑定服务端口操作。connect 也是类似,只是它首先是从 url 中获取 client 参数,再获取 transporter 参数,同样的如果最后两个参数都没有,那么 extName 也是 netty,最终也是依据 extName 获取对已的接口扩展实现,调用 connect 方法。
到这里或许你已经明白了 ExtensionLoader 是怎么动态生成 adaptive,上面从 url 中获取 server, client 还是 transporter 参数均是在 Transporter 接口的方法通过 Adaptive 注解配置的 value 属性所决定。其中 netty 是通过注解 SPI 制定当前接口的一种默认实现。这便是 Dubbo 通过 ExtensionLoader 动态生成 adaptive 类来动态适配接口的所有实现。
上面对 activate 和 adaptive 进行了详细的介绍,这两部分对应 ExtensionLoader 的实现分别是方法 getActivateExtension(URL url, String[] values, String group) 和createAdaptiveExtensionClassCode(),如果感兴趣,可以去查看 ExtensionLoader 源码。接下来对get extension by name 和 default extension 介绍一下,get extension by name 这个没什么好介绍的,就是通过接口实现的别名来获取某一个具体的服务。而 default extension 需要做一下详细介绍,Dubbo 的 SPI 规范除了上面说的在制定文件夹下面描述服务的实现信息之外,在被实现的接口必须标注 SPI 注解,用来告诉 Dubbo 这个接口是通过 SPI 来进行扩展实现的,否则 ExtensionLoader 则不会对这个接口创建 ExtensionLoader 实体,并且调用 ExtensionLoader.getExtensionLoader 方法会出现 IllegalArgumentException 异常。那说这些和默认扩展实现有什么关系呢?在接口上标注 SPI 注解的时候可以配置一个 value 属性用来描述这个接口的默认实现别名,例如上面 Transporter 的 @SPI(“netty”)就是指定 Transporter 默认实现是 NettyTransporter,因为 NettyTransporter 的别名是 netty。这里再对服务别名补充有点,别名是站在某一个接口的维度来区分不同实现的,所以一个接口的实现不能有相同的别名,否则 Dubbo 框架将启动失败,当然不同接口的各自实现别名可以相同。到此 ExtensionLoader 的实现原则和基本原理介绍完了,接下来我们来看看怎么基于 Dubbo 的 ExtensionLoader 来实施我们自己的插件化。
测试 ExtensionLoader 的动态加载机制
插件化的第一步是抽象一个接口,从定义了插件的规范,那么我们先创建一个 IHelloService 接口,并且标注了 SPI 注解,同时指定默认实现是别名为 default 的扩展实现。
1 |
|
那么接下来就是对这个插件接口提供不同的实现,需要我自己来实现一个适配类。适配类如下:
1 |
|
相应类 ExtentionType 枚举类:
1 | public enum ExtentionType { |
可见在 AdaptiveExtension 中将会根据 ExtensionType 分发扩展的具体实现,并触发 sayHello 方法。然后再对 IHelloService 接口提供了两种实现:
1 | public class DefaultHelloImpl implements IHelloService { |
1 | public class OtherHelloImpl implements IHelloService { |
并且在 META-INF/dubbo 下面创建了文件 com.bieber.dubbo.extension.IHelloService 其中内容如下(注意要将 META-INF 目录置于 resources 资源路径下):
1 | default=com.aprilyolies.extension.DefaultHelloImpl |
最后是一个启动类:
1 | public class App { |
总结
关于 Dubbo 插件化的内容介绍完了,其实可以把 ExtensionLoader 当作是 Spring 的 IOC 容器,只不过 IOC 容器里面做的事情是帮我们初始化和管理 bean,我们可以根据我们需要的 bean 类型或者 bean 的 id 来获取对应的 bean 实体,Dubbo 里面 ExtensionLoader 类的作用比较相似,只不过它管理的是插件,同样我们可以根据具体插件实现别名和插件接口来获取我们想要的插件实现。另一个不同点是 Spring 是通过 XML 的方式告诉 Spring 我的 bean 的实现类全路径,而 Dubbo 则是通过 SPI 的方式告诉 ExtensionLoader 具体实现类信息。如果你理解了这个,那么你就理解 ExtensionLoader 了。