Dubbo内核源码调试与解读(上)
网上讲Dubbo源码的文章不少,但是涉及调试的不多;线下用Dubbo做项目的同学不少,但是懂内核的不多;面试扯谈SPI扩展原理的不少,但是理解特性用法的不多;那么这篇文章出来的目的,很简单:1、帮助搭建Dubbo的本地调试环境,2、Debug式一步步走读Dubbo源码,3、讲清楚Dubbo内核机制,不留疑惑。所以这篇文章的定位就是源码级、内核级,劝退效果良好,你读起来很干,我写起来也很肝,尽量用最简单的话把复杂的事情说清楚,朝闻道夕可眠矣!请拿出把妹的勇气和毅力,学完文章里对于Dubbo的干货总结,理解后将对编程世界会有更高的理解。
一、环境准备
•解压目录的路径不要有中文名•提前配置好系统的 JAVA_HOME 环境变量•如果还有未知异常,请在bat批处理脚本的末尾加一行 “pause” ,观察启动控制台输出的异常。
上面环境装备好以后, 第一步:先启动Zookeeper本地服务,到 \zookeeper-3.4.14\bin
中点击 zkServer.bat ,如果控制台输出如下内容,即是启动正常:
[myid:] - INFO [main:ServerCnxnFactory@117] - Using org.apache.zookeeper.server.NIOServerCnxnFactory
as server connection factory
[myid:] - INFO [main:NIOServerCnxnFactory@89] - binding to port 0.0.0.0/0.0.0.0:2181
<!-- use zookeeper registry center to export service -->
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
到此就完成了本地源码环境的准备工作,非常简单,启动也很简单,那么在源码调试前,先简单的扒拉一下 Dubbo 的前世今生,以便对整体框架和流程有一个清晰的认识。
二、RPC前世今生
在微服务大行其道的时候,RPC(Remote Procedure Call)即——远程过程调用是其中非常基础、非常关键的一个组件,因为它是解决分布式系统下网络通信的一大利器,它的核心特点就是可以像调用本地接口一样发起远程调用,而无需关心其底层的细节,这样的话,大家只需要关心各自的业务实现即可(当个安静的CURD boy)。但是一个完整的RPC流程需要做哪些工作呢?或者如果想自己去手写一个类似 Dubbo、gRPC 的轮子需要实现哪些功能?
1、序列化
既然是远程调用,那么必然涉及到网络传输,网络传输的数据必须是二进制数据,但调用的出入参数都是对象。对象是肯定没法直接在网络中传输的,需要提前把它转成可传输的二进制,并且要求转换算法是可逆的,这个过程我们一般叫做“序列化”。
2、通信协议
我们知道TCP是流式协议,如果我们不提前定义好消息的边界,那么在流式传输下,不可避免的会出现粘包半包问题,因此我们需要给消息建立一些“指示牌”,并在上面标明数据包的类型和长度,这样就可以正确的解析数据了。这样我们把数据格式的约定内容叫做“协议”。大多数的协议会分成两部分,分别是数据头和消息体。数据头一般用于身份识别,包括协议标识、数据大小、请求类型、序列化类型等信息;消息体主要是请求的业务参数信息和扩展属性等。
3、动态代理
有了序列化和协议,两个系统至少可以实现业务意义上的沟通了,但是每个系统在发起一个RPC请求的时候,都需要开发者去写一段同样的逻辑,比如调用方需要在每个调用接口都写一段:1、序列化,2、编码,封装协议,3、发起调用,4、监听端口,5、收到数据后反序列化,6、解码,拿到响应数据。每个接口下面都是一段公共的切面逻辑,是不是可以采用动态代理技术来处理这些操作呢?
RPC 框架根据调用的服务接口提前生成动态代理实现类,并通过依赖注入等技术注入到声明了该接口的相关业务逻辑里面。该代理实现类会拦截所有的方法调用,在提供的方法处理逻辑里面完成一整套的远程调用,并把远程调用结果返回给调用方,这样调用方在调用远程方法的时候就获得了像调用本地接口一样的体验。
4、引入集群
前面三个步骤已经可以完成一个点对点的RPC框架了,但是这个框架只能在单机版中自嗨一下,它没有集群能力。它还存在很多的问题,比如只能上到集群环境,一个接口有多个提供者,调用方只能静态的将提供者的ip写死到配置文件,一旦服务变更或者伸缩容,就会导致ip变化,还得修改配置文件来解决。还有提供者的健康状况也无法感知,服务者有一台宕机了也没人知道,可是大家还是会往那台机器发请求,他自己挂了也没法告诉别的调用方:“我挂了,请别搞我了”,这时候,集群能力规划上就需要安排上分布式系统里面所学说要的种种功能,譬如:服务发现,负载均衡、容错以及路由等等功能,这里不再废话。
5、如何设计一个灵活性强,扩展度高的RPC框架呢?
在P2P的通信能力基础之上加上集群模块,再按照分层设计的原则,就可以将RPC框架分为四层架构模型,大概就是下面这个图的:
也许这个架构现在看起来已经足够完善了,也能很好的运行,但是随着业务的发展,假设需要扩展其中序列化方式的功能点,比如之前提供了JDK、Json以及Hessian方式,现在要新增一个Protobuf序列化,比较搓的选择序列化方式是通过 if else
的硬编码,比如这样子写:
public Serializer getType(String type){
if("JDK".equals(type)){
return JDKSerializer;
}else if("JSON".equals(type)){
return JsonSerializer;
}else if("Hessian".equals(type)){
return HessianSerializer;
}
}
那么假设要新增一种 Protobuf 序列化,那没办法只能改源代码,在弄一个 else if
的判断出来,这样几乎没有一点扩展性可言,业内主流的可扩展的几种解决方案主要如下:
•Factory模式•IoC容器•OSGI容器
Dubbo作为一个框架,本身不希望强依赖其他的IoC容器,比如Spring,Guice。OSGI也是一个很重的实现,不适合Dubbo。最终的实现参考了Java原生的SPI机制,但对其进行了一些扩展,以满足Dubbo的需求。Dubbo将每一个层级接口都抽象成一个功能点,这个功能点是多种实现方法同时存在、且可以扩展的,因此Dubbo自然而然的把这种机制,抽象成为了可扩展点机制,把所有的实现都放到这个插件体系下,于是整个架构就变成了一个微内核架构,这个内核十分精简、无需改动,用户只需要通过插件体系去调用自己需要的实现方法,要是觉得不爽还可以自己去撸一个,真的是程序员防秃防脱、居家必备的良药。
三、Dubbo源码调试与解读
1.API 和 SPI
框架或组件通常有两类客户,一个是使用者,一个是扩展者。API (Application Programming Interface) 是给使用者用的,而 SPI (Service Provide Interface) 是给扩展者用的。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。
为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。JAVA SPI 就提供了这样的一个机制——为某个接口寻找服务实现的机制。有点类似 IOC 的思想,将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。Java SPI(Service Provider Interface) 是JDK内置的一种动态加载扩展点的实现。在ClassPath的 META-INF/services
目录下放置一个与接口同名的文本文件,文件的内容为接口的实现类,多个实现类用换行符分隔。JDK中使用 java.util.ServiceLoader
来加载具体的实现。
Dubbo 实现 “微内核+插件“ 机制的核心是 ExtensionLoader,它取代了 JDK 自带的 ServiceLoader。在 Dubbo 官方文档中提到,ExtensionLoader 改进了 JAVA ServiceLoader 的以下问题:
•JDK 标准的 SPI 会一次性实例化扩展点所有实现,没用上也加载,如果有扩展实现初始化很耗时,会很浪费资源。•如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。•增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
2.Dubbo SPI扩展点特性
Dubbo的扩展点特性也就是下面四个点,这四个特性的概念含义如果仅凭字面上的阅读,不亲自调试和理解一番永远都是蒙圈状态,那就结合源码+注释一点点铺展开讲解。
•扩展点自动包装•扩展点自动装配•扩展点自适应•扩展点自动激活
附上一张优秀的课代表总结大纲,后续所有的讲解和调试都在围绕这张图来的。
3.ExtensionLoader
ExtensionLoader 是最核心的类(中文翻译可以叫扩展点加载器,后面也会用加载器直接称呼这个类),负责扩展点的加载和生命周期管理。源码调试就从这个类开始吧。ExtensionLoader 的方法比较多,比较常用的方法有:
•public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type)
•public T getExtension(String name)
•public T getAdaptiveExtension()
getExtensionLoader
方法 这是一个静态工厂方法,入参是一个可扩展的接口,返回一个该接口的 ExtensionLoader 实体类。通过这个实体类,可以根据name获得具体的扩展,也可以获得一个自适应扩展。首先要将断点打到这个方法里,进入到 com.alibaba.dubbo.demo.provider.Provider
类里后,运行 main 方法就可以进入到 getExtensionLoader
方法,首先来拿扩展点是 Protocol:
Protocol 类简单来看下,就是一个协议接口的扩展点,加上了@SPI("dubbo")
注解,声明了是一个可扩展点,并且默认值为“dubbo”,下面还有两个方法加上了@Adaptive
的注解,且先忽略,只需要知道 Protocol 就是一个可扩展点即可。
紧接着方法会去 EXTENSION_LOADERS (这是一个Map容器,key为clazz,value就是这个 clazz 所对应的扩展点加载器 )缓存里去获取这个 Protocol 的 加载器,显然没有缓存中未取到,会进入到一个新增方法,调用 ExtensionLoader 的构造方法去实例化一个缓存起来,紧接着进入到这个构造方法里继续追踪。
这个时候进入到 ExtensionLoader 构造方法,只是将 Protocol 类赋值给了一个本地成员变量 type。然后会去判断这个 type 是不是 ExtensionFactory 类,一看就知道是一个生产对象的工厂类,此处跟着源码会进入到第二次的 getExtensionLoader 方法调用,而此时是去获取一个 ExtensionFactory 扩展点。
在往下看的时候先看看 ExtensionFactory 类是啥,明显这也是一个 SPI 扩展点,而接口方法只有一个就是去获取扩展点实例。
由于是第一次去拿 ExtensionFactory 的加载器,所以还是会去调用加载器的构造方法
这会导致第二次来到这个 ExtensionLoader 构造方法中,此时是来初始化了一便 ExtensionFactory 的加载器,这里看到 objectFactory 属性因为满足了等于条件,直接赋值为 null 即可返回。
由于拿到了 ExtensionFactory 的加载器,此时又再次回到前面第一步的 Protocol 得加载器获取阶段了,这也是大家调试的时候很容易劝退的第一道门槛,在两个简单的方法里已经转了两圈了,真正的高潮还没来临,没关系,紧接着继续调试进入到 getAdaptiveExtension() 方法。
这里的 getAdaptiveExtension() 就是要去获得接口的适配器对象,也就是 Protocol 这个类的适配器,先从缓存属性 cachedAdaptiveInstance 这里取,这里我们可以看到了一个双重检查锁的实践案例,因为 dubbo 启动的时候会去加载各种不同接口的适配器,因此要考虑到线程安全问题,采用了double check机制,显然第一次加载缓存中是没有的,得创建一个适配器出来,于是又会走到 createAdaptiveExtension() 方法中。
于是到 createAdaptiveExtension() 方法中就是创建适配器对象,又到了一个分叉口,毫无例外得从里层的方法先走起,选择到里面的那个 getAdaptiveExtensionClass() 往下走。
getAdaptiveExtensionClass() 就是去获得接口适配器类,适配器类方法一进来就要先进入到一个更底层的 getExtensionClasses() 方法,方法的洞穴很深,英雄请不要放弃,继续深入!走 getExtensionClasses() 方法进去里面。
getExtensionClasses() 方法是去获得扩展实现类数组,老规矩还是从缓存里先取一波,取不到又会进入到一个double check的 loadExtensionClasses() 方法起加载扩展类,没什么可讲的继续深入其中!
loadExtensionClasses() 方法是从配置文件中,加载拓展实现类数组,配置文件的路径都是按照契约定好的,分别是从下面这三个地方去取
//这是jdk的SPI扩展机制中配置文件路径,dubbo为了兼容jdk的SPI
private static final String SERVICES_DIRECTORY = "META-INF/services/";
//用于用户自定义的扩展实现配置文件存放路径
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
//用于dubbo内部提供的扩展实现配置文件存放路径
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
加载的过程也很精彩,不要错过细节,继续深入到 loadDirectory() 方法中。
loadDirectory() 方法完成的是从一个配置文件中,加载拓展实现类数组,它会拼接接口全限定名,得到完整的文件名,这个文件名是按照约定来的,因此如果是规范的扩展点实现都是能够找到其对应的文件路径和文件的,这里放出来我本地的调试得到的 url信息:META-INF/dubbo/internal/com.alibaba.dubbo.common.extension.ExtensionFactory
,这个文件包含的内容信息如下:
adaptive=com.alibaba.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=com.alibaba.dubbo.common.extension.factory.SpiExtensionFactory
组成格式为KV键值对,Key为类的名称,Value为扩展点实现的类名,loadDirectory() 已经完成了文件定位的工作并且找到 ExtensionFactory 接口的实现类列表,下一步就是要进入到 loadResource() 方法去完成类的真正加载了。
进入到 loadResource() 方法中,用一个 BufferedReader 完成了文件流的写入,用“=”切割符取KV值,如下图,Key就是adaptive,value为 AdaptiveExtensionFactory 类的全限定名称,拿到这个信息就要进行真正的类加载了,终于要见到真正的庐山真面目了,继续揭开 loadClass() 方法的神秘面纱。
由于我们现在所在的加载器还是处于 ExtensionFactory 域中,此处类如果是加载到了AdaptiveExtensionFactory 实现,代码就会走到适配器类Adaptive注解的判断分支中:
自适应的 AdaptiveExtensionLoader是一个特殊的扩展类,这里还是有必要单独拎出来讲讲,首先它是唯一一个 @Adaptive 注解在类上的扩展点,AdaptiveExtensionLoader类有@Adaptive注解。前面提到了,Dubbo会为每一个扩展创建一个自适应实例。如果扩展类上有@Adaptive,会使用该类作为自适应类。如果没有,Dubbo会为我们创建一个。
@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {
/**
* 扩展对象的集合,默认的可以分为dubbo 的SPI中接口实现类对象或者Spring bean对象
*/
private final List<ExtensionFactory> factories;
/**
* 在调用newInstance实例化方法的时候,会触发执行这个构造方法。
*/
public AdaptiveExtensionFactory() {
ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
//遍历所有支持的扩展名
for (String name : loader.getSupportedExtensions()) {
//扩展对象加入到集合中
list.add(loader.getExtension(name));
}
//返回一个不可修改的集合
factories = Collections.unmodifiableList(list);
}
@Override
public <T> T getExtension(Class<T> type, String name) {
for (ExtensionFactory factory : factories) {
//通过扩展接口和扩展名获得扩展对象
T extension = factory.getExtension(type, name);
if (extension != null) {
return extension;
}
}
return null;
}
}
AdaptiveExtensionLoader的方法循环里,cachedAdaptiveClass 会缓存 AdaptiveExtensionFactory 的实例,没看错是实例而不是类,因为进入 loadClass() 方法的时候已经调用了反射 Class.forName(line, true, classLoader)
实例化了该类的操作。下面的分支一样很重要的,只是现在还没走进来而已,我们跟着源码加载的思路继续往下走。
由于 ExtensionFactory 的实现类扩展点加载还没有完,总共有两个:SPI 的以及 Spring 的,所以循环中loadClass 方法还会走下面的代码,分别把 SpiExtensionFactory 和 SpringExtensionFactory 实例装载到 ExtensionFactory 的加载器缓存里。
于是穿越三层回到 loadExtensionClasses() 方法里,ExtensionFactory 这个扩展点已经加载完了两个工厂实现类,并且缓存了 AdaptiveExtensionFactory 这个特殊的实现类在 cachedAdaptiveClass 缓存属性里。
于是一层层的向上返回结果,再次回到了熟悉的 getAdaptiveExtensionClass() 方法里,cachedAdaptiveClass 因为已经缓存了实例,这里可以直接返回了加载到的实现类。
最后就回到了ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension())
,兜兜转转一大圈,我们看到 Protocol 加载器会拿到一个 AdaptiveExtensionLoader 实例,作为自适应扩展实例。AdaptiveExtensionLoader 会遍历所有的ExtensionFactory实现,尝试着去加载扩展。如果找到了,返回。如果没有,在下一个 ExtensionFactory 中继续找。Dubbo内置了两个 ExtensionFactory,分别从Dubbo自身的扩展机制和Spring容器中去寻找。由于 ExtensionFactory 本身也是一个扩展点,我们可以实现自己的ExtensionFactory,让Dubbo的自动装配支持我们自定义的组件。比如,我们在项目中使用了Google的guice这个 IOC 容器。我们可以实现自己的GuiceExtensionFactory,让Dubbo支持从guice容器中加载扩展。
总结一下,今天的内容,主要就是讲了加载器的初始化过程尤其是 ExtensionFactory 这一块的加载,上面的源码调试内容都是从 objectFactory 获取一步步调试而来的,而高级特性部分一个都还未涉及,没办法,这个过程不讲清楚,后面的高级特性也无法很好的理解,看懂源码最好的办法就是跟着源码一步步来Debug,别无捷径。肝完这篇,下篇开始特性讲解!
参考列表:
1.Dubbo官网.Dubbo可扩展机制源码解析
2.cyfonly.Dubbo原理和源码解析之“微内核+插件”机制
3.何小锋.架构设计:设计一个灵活的RPC框架