Apache Dubbo 可扩展机制分析
背景介绍
在上一篇文章 Java SPI 使用及原理分析 中我们通过对一些著名的 Java
开源框架可扩展机制的实现原理分析,引出了 Java
SPI
机制,同时通过一个小的实战案例演示了 Java
SPI
机制的使用方式,并结合 JDK 源码,对 Java
SPI
实现原理进行了分析,最后还总结了使用原生的 Java
SPI
机制可能存在的一些不足;这一篇文章中再结合 Apache Dubbo
开源框架,来分析一下 Java
SPI
在实践场景中的应用,我们知道 Java SPI 机制还是存在一些不足的,那这些不足在 Apache Dubbo
框架又是如何解决的呢?
PS. 这一篇文章的内容并非原创,而是来源于 Apache Dubbo 官网的博客文章,文章的版权属于 Apache Dubbo
,本人只对原文部分内容进行排版和美化。
Dubbo 可扩展机制实战
Dubbo的扩展机制
在 Dubbo
的官网上,Dubbo
描述自己是一个高性能的 RPC
框架。今天我想聊聊 Dubbo
的另一个很棒的特性,就是它的可扩展性。 如同罗马不是一天建成的,任何系统都一定是从小系统不断发展成为大系统的,想要从一开始就把系统设计的足够完善是不可能的;相反的,我们应该关注当下的需求,然后再不断地对系统进行迭代。在代码层面,要求我们适当的对关注点进行抽象和隔离,在软件不断添加功能和特性时,依然能保持良好的结构和可维护性,同时允许第三方开发者对其功能进行扩展。在某些时候,软件设计者对扩展性的追求甚至超过了性能。
在谈到软件设计时,可扩展性一直被谈起,那到底什么才是可扩展性,什么样的框架才算有良好的可扩展性呢?它必须要做到以下两点:
作为框架的维护者,在添加一个新功能时,只需要添加一些新代码,而不用大量的修改现有的代码,即符合开闭原则;
作为框架的使用者,在添加一个新功能时,不需要去修改框架的源码,在自己的工程中添加代码即可。
Dubbo
很好的做到了上面两点。这要得益于 Dubbo
的 微内核 + 插件 的机制。接下来的章节中我们会慢慢揭开 Dubbo
扩展机制的神秘面纱。
可扩展的几种解决方案
通常可扩展的实现有下面几种:
Factory 模式
IOC 容器
OSGI 容器
Dubbo
作为一个框架,不希望强依赖其他的 IOC
容器,比如 Spring
、Guice
。OSGI
也是一个很重的实现,不适合 Dubbo
。最终 Dubbo
的实现参考了 Java
原生的 SPI
机制,但对其进行了一些扩展,以满足 Dubbo
的需求。
Dubbo 的 SPI 机制
Java
SPI
的使用很简单。也做到了基本的加载扩展点的功能。但 Java
SPI
有以下的不足:
需要遍历所有的实现,并实例化,然后我们在循环中才能找到我们需要的实现;
配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名,导致在程序中很难去准确的引用它们;
扩展如果依赖其他的扩展,做不到自动注入和装配;
不提供类似于
Spring
的IOC
和AOP
功能;扩展很难和其他的框架集成,比如扩展里面依赖了一个
Spring
bean
,原生的Java
SPI
不支持。
所以 Java
SPI
应付一些简单的场景是可以的,但对于 Dubbo
,它的功能还是比较弱的。Dubbo
对原生 SPI
机制进行了一些扩展,接下来,我们就更深入地了解下 Dubbo
的 SPI
机制。
Dubbo 扩展点机制基本概念
在深入学习 Dubbo
的扩展机制之前,我们先明确 Dubbo
SPI
中的一些基本概念,在接下来的内容中,我们会多次用到这些术语。
扩展点 (Extension Point)
是一个 Java
的接口。
扩展 (Extension)
扩展点的实现类。
扩展实例 (Extension Instance)
扩展点实现类的实例。
扩展自适应实例 (Extension Adaptive Instance)
第一次接触这个概念时,可能不太好理解(我第一次也是这样的…)。如果称它为扩展代理类,可能更好理解些。扩展的自适应实例其实就是一个 Extension
的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。比如一个 IRepository
的扩展点,有一个 save
方法。有两个实现 MysqlRepository
和 MongoRepository
。IRepository
的自适应实例在调用接口方法的时候,会根据 save
方法中的参数,来决定要调用哪个 IRepository
的实现。如果方法参数中有 repository=mysql
,那么就调用 MysqlRepository
的 save
方法。如果 repository=mongo
,就调用 MongoRepository
的 save
方法。和面向对象的延迟绑定很类似。为什么 Dubbo
会引入扩展自适应实例的概念呢?
Dubbo
中的配置有两种,一种是固定的系统级别的配置,在Dubbo
启动之后就不会再改了。还有一种是运行时的配置,可能对于每一次的RPC
,这些配置都不同。比如在XML
文件中配置了超时时间是10
秒钟,这个配置在Dubbo
启动之后,就不会改变了。但针对某一次的RPC
调用,可以设置它的超时时间是30
秒钟,以覆盖系统级别的配置。对于Dubbo
而言,每一次的RPC
调用的参数都是未知的。只有在运行时,根据这些参数才能做出正确的决定。很多时候,我们的类都是一个单例的,比如
Spring
的bean
,在Spring
bean
都实例化时,如果它依赖某个扩展点,但是在bean
实例化时,是不知道究竟该使用哪个具体的扩展实现的。这时候就需要一个代理模式了,它实现了扩展点接口,方法内部可以根据运行时参数,动态的选择合适的扩展实现。而这个代理就是自适应实例。自适应扩展实例在Dubbo
中的使用非常广泛,Dubbo
中,每一个扩展都会有一个自适应类,如果我们没有提供,Dubbo
会使用字节码工具为我们自动生成一个。所以我们基本感觉不到自适应类的存在。后面会有例子说明自适应类是怎么工作的。
@SPI
@SPI
注解作用于扩展点的接口上,表明该接口是一个扩展点。可以被 Dubbo
的 ExtentionLoader
加载。如果没有此注解 ExtensionLoader
调用会异常。
@Adaptive
@Adaptive
注解用在扩展接口的方法上。表示该方法是一个自适应方法。Dubbo
在为扩展点生成自适应实例时,如果方法有 @Adaptive
注解,会为该方法生成对应的代码。方法内部会根据方法的参数,来决定使用哪个扩展。 @Adaptive
注解用在类上代表实现一个装饰类,类似于设计模式中的装饰模式,它主要作用是返回指定类,目前在整个系统中 AdaptiveCompiler
、 AdaptiveExtensionFactory
这两个类拥有该注解。
ExtentionLoader
类似于 Java
SPI
的 ServiceLoader
,负责扩展的加载和生命周期维护。
扩展别名
和 Java
SPI
不同,Dubbo
中的扩展都有一个别名,用于在应用中引用它们。比如
1 | random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance |
其中的 random
,roundrobin
就是对应扩展的别名。这样我们在配置文件中使用 random
或 roundrobin
就可以了。
一些路径
和 Java
SPI
从 /META-INF/services
目录加载扩展配置类似,Dubbo
也会从以下路径去加载扩展配置文件:
META-INF/dubbo/internal
META-INF/dubbo
META-INF/services
Dubbo 的 LoadBalance 扩展点解读
在了解了 Dubbo
的一些基本概念后,让我们一起来看一个 Dubbo
中实际的扩展点,对这些概念有一个更直观的认识。
我们选择的是 Dubbo
中的 LoadBalance
扩展点。Dubbo
中的一个服务,通常有多个 Provider
,consumer
调用服务时,需要在多个 Provider
中选择一个,这就是一个 LoadBalance
。我们一起来看看在 Dubbo
中,LoadBalance
是如何成为一个扩展点的。
LoadBalance 接口
1 |
|
LoadBalance
接口只有一个 select
方法。select
方法从多个 invoker
中选择其中一个。上面代码中和 Dubbo
SPI
相关的元素有:
@SPI(RandomLoadBalance.NAME)
:@SPI
作用于LoadBalance
接口,表示接口LoadBalance
是一个扩展点。如果没有@SPI
注解,试图去加载扩展时,会抛出异常。@SPI
注解有一个参数,该参数表示该扩展点的默认实现的别名。如果没有显示的指定扩展,就使用默认实现。RandomLoadBalance.NAME
是一个常量,值是random
,是一个随机负载均衡的实现。random
的定义在配置文件META-INF/dubbo/internal/com.alibaba.dubbo.rpc.cluster.LoadBalance
中:
1 | random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance |
可以看到文件中定义了 4
个 LoadBalance
的扩展实现。由于负载均衡的实现不是本次的内容,这里就不过多说明。只用知道 Dubbo
提供了 4
种负载均衡的实现,我们可以通过 XML
文件,properties
文件,JVM
参数显式的指定一个实现。如果没有,默认使用随机。
@Adaptive(“loadbalance”)
:@Adaptive
注解修饰select
方法,表明方法select
方法是一个可自适应的方法。Dubbo
会自动生成该方法对应的代码,当调用select
方法时,会根据具体的方法参数来决定调用哪个扩展实现的select
方法。@Adaptive
注解的参数loadbalance
表示方法参数中的loadbalance
的值作为实际要调用的扩展实例。 但奇怪的是,我们发现select
的方法中并没有loadbalance
参数,那怎么获取loadbalance
的值呢?select
方法中还有一个URL
类型的参数,Dubbo
就是从URL
中获取loadbalance
的值的。这里涉及到Dubbo
的URL
总线模式,简单说,URL
中包含了RPC
调用中的所有参数。URL
类中有一个Map<String, String> parameters
字段,parameters
中就包含了loadbalance
。
获取 LoadBalance 扩展
Dubbo
中获取 LoadBalance
的代码如下:
1 | LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName); |
使用 ExtensionLoader.getExtensionLoader(LoadBalance.class)
方法获取一个 ExtensionLoader
的实例,然后调用 getExtension
,传入一个扩展的别名来获取对应的扩展实例。
自定义一个 LoadBalance 扩展
本节中,我们通过一个简单的例子,来自己实现一个 LoadBalance
,并把它集成到 Dubbo
中。我会列出一些关键的步骤和代码,也可以从这个地址 下载完整的 demo
。
实现 LoadBalance 接口
首先,编写一个自己实现的 LoadBalance
,因为是为了演示 Dubbo
的扩展机制,而不是 LoadBalance
的实现,所以这里 LoadBalance
的实现非常简单,选择第一个 invoker
,并在控制台输出一条日志。
1 | package com.dubbo.spi.demo.consumer; |
添加扩展配置文件
添加文件:META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance
。文件内容如下:
1 | demo=com.dubbo.spi.demo.consumer.DemoLoadBalance |
配置使用自定义 LoadBalance
通过上面的两步,已经添加了一个名字为 demo
的 LoadBalance
实现,并在配置文件中进行了相应的配置。接下来,需要显式的告诉 Dubbo
使用 demo
的负载均衡实现。如果是通过 Spring
的方式使用 Dubbo
,可以在xml文件中进行设置。
1 | <dubbo:reference id="helloService" interface="com.dubbo.spi.demo.api.IHelloService" loadbalance="demo" /> |
在 consumer
端的 dubbo:reference
中配置 <loadbalance=“demo”>
启动 Dubbo
启动 Dubbo
,调用一次 IHelloService
,可以看到控制台会输出一条
1 | DemoLoadBalance: Select the first invoker... |
日志,说明 Dubbo
的确是使用了我们自定义的 LoadBalance
。
总结
到此,我们从 Java
SPI
开始,了解了 Dubbo
SPI
的基本概念,并结合了 Dubbo
中的 LoadBalance
加深了理解。最后,我们还实践了一下,创建了一个自定义 LoadBalance
,并集成到 Dubbo
中。相信通过这里理论和实践的结合,大家对 Dubbo
的可扩展有更深入的理解。 总结一下,Dubbo
SPI
有以下的特点:
对
Dubbo
进行扩展,不需要改动Dubbo
的源码;自定义的
Dubbo
的扩展点实现,是一个普通的Java
类,Dubbo
没有引入任何Dubbo
特有的元素,对代码侵入性几乎为零;将扩展注册到
Dubbo
中,只需要在ClassPath
中添加配置文件,使用简单,而且不会对现有代码造成影响,符合开闭原则;Dubbo
的扩展机制设计默认值:@SPI(“dubbo”)
代表默认的SPI
对象;Dubbo
的扩展机制支持IOC
、AOP
等高级功能;Dubbo
的扩展机制能很好的支持第三方IOC
容器,默认支持Spring
Bean
,可自己扩展来支持其他容器,比如Google
的Guice
;切换扩展点的实现,只需要在配置文件中修改具体的实现,不需要改代码,使用方便。
Dubbo 可扩展机制源码解析
ExtensionLoader
ExtensionLoader
是最核心的类,负责扩展点的加载和生命周期管理,我们就以这个类开始吧。 ExtensionLoader
的方法比较多,比较常用的方法有:
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type)
public T getExtension(String name)
public T getAdaptiveExtension()
比较常见的用法有:
LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName)
RouterFactory routerFactory = ExtensionLoader.getExtensionLoader(RouterFactory.class).getAdaptiveExtension()
说明:在接下来展示的源码中,我会将无关的代码(比如日志,异常捕获等)去掉,方便大家阅读和理解。
getExtensionLoader
方法 这是一个静态工厂方法,入参是一个可扩展的接口,返回一个该接口的ExtensionLoader
实体类。通过这个实体类,可以根据name
获得具体的扩展,也可以获得一个自适应扩展。
1 | public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) { |
getExtension
方法
1 | public T getExtension(String name) { |
getExtension
方法中做了一些判断和缓存,主要的逻辑在 createExtension
方法中,我们继续看 createExtension
方法。
1 | private T createExtension(String name) { |
createExtension
方法做了以下事情:
先根据
name
来得到对应的扩展类,从ClassPath
下META-INF
文件夹下读取扩展点配置文件;使用反射创建一个扩展类的实例;
对扩展类实例的属性进行依赖注入,即
IOC
;如果有
wrapper
,添加wrapper
,即AOP
。
下面我们来重点看下这 4
个过程
- 根据
name
获取对应的扩展类 先看代码:
1 | private Map<String, Class<?>> getExtensionClasses() { |
过程很简单,先从缓存中获取,如果没有,就从配置文件中加载。配置文件的路径就是之前提到的:
META-INF/dubbo/internal
META-INF/dubbo
META-INF/services
使用反射创建扩展实例:这个过程很简单,使用
clazz.newInstance()
来完成。创建的扩展实例的属性都是空值。扩展实例自动装配:在实际的场景中,类之间都是有依赖的。扩展实例中也会引用一些依赖,比如简单的
Java
类,另一个Dubbo
的扩展或一个Spring
Bean
等。依赖的情况很复杂,Dubbo
的处理也相对复杂些。我们稍后会有专门的章节对其进行说明,现在,我们只需要知道,Dubbo
可以正确的注入扩展点中的普通依赖,Dubbo
扩展依赖或Spring
依赖等。扩展实例自动包装:自动包装就是要实现类似于
Spring
的AOP
功能。Dubbo
利用它在内部实现一些通用的功能,比如日志,监控等。关于扩展实例自动包装的内容,也会在后面单独讲解。
经过上面的 4
步,Dubbo
就创建并初始化了一个扩展实例。这个实例的依赖被注入了,也根据需要被包装了。到此为止,这个扩展实例就可以被使用了。
Dubbo SPI 高级用法之自动装配
自动装配的相关代码在 injectExtension
方法中:
1 | private T injectExtension(T instance) { |
要实现对扩展实例的依赖的自动装配,首先需要知道有哪些依赖,这些依赖的类型是什么。Dubbo
的方案是查找 Java
标准的 setter
方法。即方法名以 set
开始,只有一个参数。如果扩展类中有这样的 set
方法,Dubbo
会对其进行依赖注入,类似于 Spring
的 set
方法注入。 但是 Dubbo
中的依赖注入比 Spring
要复杂,因为 Spring
注入的都是 Spring
bean
,都是由 Spring
容器来管理的。而 Dubbo
的依赖注入中,需要注入的可能是另一个 Dubbo
的扩展,也可能是一个 Spring
Bean
,或是 Google
guice
的组件,或其他任何一个框架中的组件。Dubbo
需要能够从任何一个场景中加载扩展。在 injectExtension
方法中,是用 Object object = objectFactory.getExtension(pt, property)
来实现的。objectFactory
是 ExtensionFactory
类型的,在创建 ExtensionLoader
时被初始化:
1 | private ExtensionLoader(Class<?> type) { |
objectFacory
本身也是一个扩展,通过 ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension())
来获取。
ExtensionFactory
有三个实现:
SpiExtensionFactory
:Dubbo
自己的SPI
去加载Extension
;SpringExtensionFactory
:从Spring
容器中去加载Extension
;AdaptiveExtensionFactory
: 自适应的AdaptiveExtensionLoader
。
这里要注意 AdaptiveExtensionFactory
,源码如下:
1 |
|
AdaptiveExtensionLoader
类有 @Adaptive
注解。前面提到了,Dubbo
会为每一个扩展创建一个自适应实例。如果扩展类上有 @Adaptive
,会使用该类作为自适应类。如果没有,Dubbo
会为我们创建一个。所以 ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension())
会返回一个 AdaptiveExtensionLoader
实例,作为自适应扩展实例。 AdaptiveExtensionLoader
会遍历所有的 ExtensionFactory
实现,尝试着去加载扩展。如果找到了,返回。如果没有,在下一个 ExtensionFactory
中继续找。Dubbo
内置了两个 ExtensionFactory
,分别从 Dubbo
自身的扩展机制和 Spring
容器中去寻找。由于 ExtensionFactory
本身也是一个扩展点,我们可以实现自己的 ExtensionFactory
,让 Dubbo
的自动装配支持我们自定义的组件。比如,我们在项目中使用了 Google
的 guice
这个 IOC
容器。我们可以实现自己的 GuiceExtensionFactory
,让 Dubbo
支持从 guice
容器中加载扩展。
Dubbo SPI 高级用法之 AOP
在用 Spring
的时候,我们经常会用到 AOP
功能。在目标类的方法前后插入其他逻辑。比如通常使用 Spring
AOP
来实现日志,监控和鉴权等功能。 Dubbo
的扩展机制,是否也支持类似的功能呢?答案是 yes
。在 Dubbo
中,有一种特殊的类,被称为 Wrapper
类。通过装饰者模式,使用包装类包装原始的扩展点实例。在原始扩展点实现前后插入其他逻辑,实现 AOP
功能。
什么是 Wrapper 类
那什么样类的才是 Dubbo
扩展机制中的 Wrapper
类呢?Wrapper
类是一个有复制构造函数的类,也是典型的装饰者模式。下面就是一个 Wrapper
类:
1 | class A{ |
类 A
有一个构造函数 public A(A a)
,构造函数的参数是 A
本身。这样的类就可以成为 Dubbo
扩展机制中的一个 Wrapper
类。Dubbo
中这样的 Wrapper
类有 ProtocolFilterWrapper
, ProtocolListenerWrapper
等, 大家可以查看源码加深理解。
怎么配置 Wrapper 类
在 Dubbo
中 Wrapper
类也是一个扩展点,和其他的扩展点一样,也是在 META-INF
文件夹中配置的。比如前面举例的 ProtocolFilterWrapper
和 ProtocolListenerWrapper
就是在路径 dubbo-rpc/dubbo-rpc-api/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol
中配置的:
1 | filter=org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper |
在 Dubbo
加载扩展配置文件时,有一段如下的代码:
1 | try { |
这段代码的意思是,如果扩展类有复制构造函数,就把该类存起来,供以后使用。有复制构造函数的类就是 Wrapper
类。通过 clazz.getConstructor(type)
来获取参数是扩展点接口的构造函数。注意构造函数的参数类型是扩展点接口,而不是扩展类。 以 Protocol
为例。配置文件 dubbo-rpc/dubbo-rpc-api/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol
中定义了 filter=org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper
。 ProtocolFilterWrapper
代码如下:
1 | public class ProtocolFilterWrapper implements Protocol { |
ProtocolFilterWrapper
有一个构造函数 public ProtocolFilterWrapper(Protocol protocol)
,参数是扩展点 Protocol
,所以它是一个 Dubbo
扩展机制中的 Wrapper
类。ExtensionLoader
会把它缓存起来,供以后创建 Extension
实例的时候,使用这些包装类依次包装原始扩展点。
扩展点自适应
前面讲到过,Dubbo
需要在运行时根据方法参数来决定该使用哪个扩展,所以有了扩展点自适应实例。其实是一个扩展点的代理,将扩展的选择从 Dubbo
启动时,延迟到 RPC
调用时。Dubbo
中每一个扩展点都有一个自适应类,如果没有显式提供,Dubbo
会自动为我们创建一个,默认使用 Javaassist
。 先来看下创建自适应扩展类的代码:
1 | public T getAdaptiveExtension() { |
继续看 createAdaptiveExtension
方法
1 | private T createAdaptiveExtension() { |
继续看 getAdaptiveExtensionClass
方法
1 | private Class<?> getAdaptiveExtensionClass() { |
继续看 createAdaptiveExtensionClass
方法,绕了一大圈,终于来到了具体的实现了。看这个 createAdaptiveExtensionClass
方法,它首先会生成自适应类的 Java
源码,然后再将源码编译成 Java
的字节码,加载到 JVM
中。
1 | private Class<?> createAdaptiveExtensionClass() { |
Compiler
的代码,默认实现是 javassist
。
1 |
|
createAdaptiveExtensionClassCode()
方法中使用一个 StringBuilder
来构建自适应类的 Java
源码。方法实现比较长,这里就不贴代码了。这种生成字节码的方式也挺有意思的,先生成 Java
源代码,然后编译,加载到 JVM
中。通过这种方式,可以更好的控制生成的 Java
类。而且这样也不用 care
各个字节码生成框架的 api
等。因为 xxx.java
文件是 Java
通用的,也是我们最熟悉的。只是代码的可读性不强,需要一点一点构建 xx.java
的内容。 下面是使用 createAdaptiveExtensionClassCode
方法为 Protocol
创建的自适应类的Java代码范例:
1 | package org.apache.dubbo.rpc; |
大致的逻辑和开始说的一样,通过 url
解析出参数,解析的逻辑由 @Adaptive
的 value
参数控制,然后再根据得到的扩展点名获取扩展点实现,然后进行调用。如果大家想知道具体的构建 .java
代码的逻辑,可以看 createAdaptiveExtensionClassCode
的完整实现。 在生成的 Protocol$Adaptive
中,发现 getDefaultPort
和 destroy
方法都是直接抛出异常的,这是为什么呢?来看看 Protocol
的源码:
1 |
|
可以看到 Protocol
接口中有 4
个方法,但只有 export
和 refer
两个方法使用了 @Adaptive
注解。Dubbo
自动生成的自适应实例,只有 @Adaptive
修饰的方法才有具体的实现。所以,Protocol$Adaptive
类中,也只有 export
和 refer
这两个方法有具体的实现,其余方法都是抛出异常。