Dubbo 就是靠它崭露头角!
Hola,我是 yes。
经过了 RPC 核心和 Dubbo 微内核两篇文章后,今天终于要稍稍深入一波 Dubbo 了。
作为一个通用的 RPC 框架,性能是很重要的一环,而易用性和扩展性也极为重要。
简单地、无侵入式地扩展和定制 RPC 各阶段功能是很多团队的述求,Dubbo 就满足了这些需求。
它通过微内核设计和 SPI 扩展,使得一些有特殊需求的业务团队可以在 Dubbo 中实现自己的扩展,而不需要修改源码。
Dubbo 的成功离不开这样的设计,今天咱们就来盘一盘 Dubbo 是如何实现无侵入扩展的,其间还会看到 Dubbo 的 IOC 和 AOP。
还有先打个预防针,今天的内容代码有点多的,毕竟想要深入剖析,源码必不可少,刚好也顺带提一下看源码的小技巧。
所以建议电脑上看,更加清晰和舒适。
还有如果有没看过源码的同学,来紧跟 Dubbo 这个系列吧,到时候再也不怕被面试官问看过源码没了。
SPI
Dubbo 就是利用 SPI (Service Provider Interface)来实现扩展机制的。
这个 SPI 想必你们都很熟悉,在大学写数据库大作业的时候就碰到了,访问数据库需要用到 java.sql.Driver
。
市面上的数据库五花八门,每个数据库厂商都有自己的实现,所以肯定需要定制一个接口,这样我们面向接口编程即可。
而具体的实现则可以通过配置来加载,JDK SPI 这时候就派上用场了。
其实一点都不神奇,就是约定一个地方,加载的时候就去那个地方找实现类。
约定一个地方直白点说就是代码里面写死了一个目录,这个目录就是 META-INF/services/
。
然后在这个目录下创建一个文件,用接口全限定名来命名,文件内容就是实现类的全限定名。
到时候要实现类就根据接口名来这里找,然后实例化就行了。
挺简单的吧,这就是 JDK SPI,但是它不满足 Dubbo 的需求。
因为 Dubbo 把自身的一些实现也剥离出来成为扩展,而这些实现还是有点多的,也不需要全部用上。
如果用 JDK SPI 会把配置文件里面的类全部加载,这就导致资源的浪费。用的时候还需要遍历过去才能找到对应的实现。
所以 Dubbo 就在 JDK SPI 的基础上实现了个 Dubbo 的 SPI,可以根据指定的名称按需加载实现类,比如拿 Cluster 来说就有这么多实现类。
约定的地方改了一下,一共有三个目录。
META-INF/dubbo/internal/ :这里是存放 Dubbo 内部使用的 SPI 配置文件。
META-INF/dubbo/ :这里是存放用户自定义 SPI 配置文件。
META-INF/services/:兼容 JDK SPI
然后文件里面的内容是key=value
形式,这样就可以根据 key 找到对应的实现类。
然后在注解上可以配置默认的 key 来选择默认的实现类,比如 Cluster 默认的实现是 failover。
也可以通过 URL 参数来选择实现类。
还有像 JDK SPI 扩展点加载失败的话,连扩展点名称都拿不到,到时候报错也不知道哪里出问题。
而 Dubbo SPI 则不会吃了错误,并且还提供了扩展点的自动注入和 AOP 功能。
大致了解了 Dubbo SPI 之后,我们再来深入看看实现细节。
Dubbo SPI 实现细节
Dubbo SPI 的核心实现在 ExtensionLoader 中,它负责扩展点的加载和生命周期的维护,类似 JDK SPI 的 ServiceLoader。
这里要先提一点看源码的小技巧了。
开源框架都会有单元测试,而单元测试里面就会有我们看源码时候想要的各种功能实现,我们就可以从单元测试入手得知一些功能的划分,然后断点调试逐渐深入。
比如今天文章的 ExtensionLoader ,它在 dubbo-common 模块中,咱们就进入 test 来看看它测试用例怎么写的。
当然除了通过文件夹来找,直接用文件名搜也行。
找到了就好办了,数据都是造好的,找到你想要调试的方法,断点一设,箭头一点,这不就美滋滋了吗?
好了,小技巧分享完毕,回到 ExtensionLoader,我们简单点就用 Dubbo 单元测试的数据来看看实现。
有个叫 SimpleExt 的类,有三个实现,默认的实现是 impl1。
再来看看 SPI 配置文件的内容,可以看到为了测试还故意写了一些空格在配置文件中。
然后现在如果要找 impl2 这个实现,通过以下代码调用即可。
一个扩展接口对应有个 ExtensionLoader,找到对应的 ExtensionLoader,然后再加载对应名字的实现类。
接下来会有源码,不过没关系,还是很简单的,想要深入源码这关必须过。
可以看到getExtensionLoader
是静态的,里面逻辑也很简单就是从缓存找接口对应的 ExtensionLoader,找不到就新建一个返回。
现在有了 ExtensionLoader,咱们再来看看 getExtension 的逻辑,来看看是如何通过扩展点 name 找到对应的实现类的。
可以看到又是有个缓存操作,逻辑非常简单,先去缓存找实例,如果没有则创建实例。
要说细节就是用到了双检锁,然后用 holder 来保证可见性和防止指令重排。应该看到注释上的 holder 构造了吧,volatile 和双检锁的搭配,这里就不深入了。
我们来看看 createExtension,这是要创建扩展点了,代码有点长,但是我都做了相应的注释,包括绿色的注释。
逻辑还是很简单的,详细的代码没有具体展示,我先口述一下。
通过接口类名去三个目录找到对应的文件。
解析文件内容生成 class 对象,然后缓存到 cachedClasses 中。
然后通过 name 去 cachedClasses 中找到对应的 class 对象。
去缓存 EXTENSION_INSTANCES 看看是否已经实例化过了。
没有的话就实例化,然后调用 injectExtension 实现自动注入。
再通过 cachedWrapperClasses 实现包装,将最后的包装类返回。
有几点不清晰没关系,咱们接着分析,脑海中先大概知道要做什么,然后再来看看具体是怎么做的。
源码中的 loadDirectory 就是去目录找文件,然后解析,最终会调用 loadClass,这个方法很关键,我们详细分析一下,为了便于观看,删除了一些代码。
自适应咱们先略过,只要知道是在这里记录的即可。
然后上面提到的 AOP 相关的 cachedWrapperClasses 就是在这里记录的,如果判断它是包装类呢?
简单粗暴但是有效,只要有当前类作为构造器参数的类就是包装类,有点拗口,多读几遍就理解了。
现在我们再回过头来看看这段代码,Dubbo 的 AOP。
把扩展类对应的包装类都记录下来放在 cachedWrapperClasses 中,然后在实例化扩展类的时候就一层一层的把扩展类包起来,最终返回的就是包装类。
为什么说这就是 AOP 呢?因为等于把一些逻辑切进了扩展实现类中。
其实就是把扩展对象的公共逻辑移到包装类中,我们看下单元测试的例子就很清晰了。
从图中可以看到有两个扩展实现类,两个包装类,具体逻辑就不看了,不是重点,配置文件如下:
然后再看一下单元测试的运行结果,可以看到最终返回的其实是 Ext5Wrapper1 对象,并且它还包着 wrapper2 对象。
所以 echo 方法的调用链就是:
Ext5Wrapper1 ->Ext5Wrapper2->Ext5impl1
也就起到了 AOP 的效果。
接下来我们再来看看 injectExtension,是如何实现 Dubbo 的自动注入。
看了代码之后是不是有点失望,就这?
是的就是这么朴素地判断有没有 set 方法,然后根据参数找到对象,执行 set 方法注入即可。
所以说源码之下无秘密,看起来好像很高级的东西,就这。
上面代码中还有个objectFactory.getExtension()
,这个和扩展自适应有关系,还有个@Activate
也没说。
这些内容还是有点多的,也很重要,感觉上可能还有点绕,所以单独写一篇说。
最后
Dubbo 就是靠自己实现的 SPI 机制把通信协议、序列化格式、负载均衡、路由策略等各部分抽出来作为插件,实现扩展和定制。
通过微内核和 SPI 机制来满足用户定制化的需求,也保证了框架本身的稳定性和可持续性。
并且 Dubbo 自身也提供了很多已有的实现,像各种路由策略等等。
所以说一个好的框架不仅自己功能要全,还得对扩展开放,这样生态才会壮大。
今天的代码还是有点多的,如果看不懂的建议下载源码,跟着源码调试几遍就清晰了。
源码这一步一定要迈过去,迈过去了之后就轻松了。
Dubbo 系列持续更新,敬请期待,有问题可以留言,我会尽量解答。
欢迎关注我的公众号【yes 的练级攻略】,更多硬核文章等你来读。
微信搜 「yes 的练级攻略」干货满满,不然来掐我,回复【123】一份 20W 字的算法刷题笔记等你来领。 个人文章汇总:https://github.com/yessimida/yes 欢迎 star !
我是 yes,从一点点到亿点点,欢迎在看、转发、留言,我们下篇见。
版权声明: 本文为 InfoQ 作者【yes的练级攻略】的原创文章。
原文链接:【http://xie.infoq.cn/article/bf7bb375f67426cbe20da69e7】。文章转载请联系作者。
评论