查看原文
其他

快来!我从源码中学习到了一招Dubbo的骚操作!

why技术 why技术 2022-08-30
这是why的第 55 篇原创文章

荒腔走板


大家好,我是 why,欢迎来到我连续周更优质原创文章的第 55 篇。

老规矩,先来一个简短的荒腔走板,给冰冷的技术文注入一丝色彩。

魔幻的 2020 年的上半年过去了,很多人都在朋友圈和上半年说再见,我也不例外。

上面这张照片,就是我在朋友圈发的一张图片。

这张照片是我在公司去年年会的时候拍的,出处来自电影《飞驰人生》。

电影里面有人问张弛:你五年连续获得冠军的必胜绝招是什么?

张驰满怀深情的回答:必胜绝招只有两个字—奉献。就是把你的全部,奉献给你所热爱的一切。

什么是热爱?

可以用电影里面的一句台词来回答:

“巴音布鲁克,1462道弯,109公里,耍小聪明,赢得了100米,赢不了100公里。我每天在脑海里开20遍,5年,3万6千遍,我能记住每一个弯道。”

张弛在电影里面是一个卑微的角色,他卖炒饭、卖唱、偷车架、端着饭碗喝红酒......做了很多很多卑微的事情。

但是,他的心里一直记得巴音布鲁克,一直记得那 1462 个弯道。即使卑微到尘土,他最终还是拼了命的回到了赛道。

这就是热爱。

热爱,从来不是一件简单的事情。

这句话也让我想起了路遥先生在《早晨从中午开始》中的一句话:

只有初恋般的热情和宗教般的意志,人才有可能成就某种事业。

这也是热爱,对毕生所最追求之热爱。

我是一个普普通通的程序猿,但是我喜欢这个行业;我是一个平凡无奇的打工仔,但是我热爱我的生活。

你呢?你热爱着什么?又付出了多少?

2020 年的上半年,我每一天都在努力。

2020 年的下半年,愿你我共同成长。

好了,说回文章。

先说背景


前段时间有个读者问我,他说他们的 RPC 框架用的是 Dubbo,当对接一个新服务的接口时就需要开通对应的网络关系。

比如我是 A 服务,第一次对接 B 服务的 Dubbo 接口,那么我需要开通 A 服务到 B 服务的对应的 Dubbo 端口的网络访问权限。

但是有的时候总是有人忘记开通网络权限,导致业务展开的时候服务调用报错。已经吃过几次这样的亏了。

目前他们想到的解决方案是 A 服务启动后就调用 B 服务提供的一个专门用于测试能否调通的接口。如果不通,配合监控手段,这样就能主动发现问题了。

这是一个兜底方案,防止开发人员忘记或者不知道需要开通网络权限的情况。

这个解决方案的问题是每个服务都需要专门写一个接口,以供其他服务来调用。

每一个服务都要写,对系统的侵入性太大了。

有没有什么好的解决方案呢?

大家想想呢,这种问题其实还是挺普遍的。有点类似于心跳功能,虽然只需要跳一次。

Dubbo 服务启动成功后,你怎么主动判断需要用到的接口,都是可以访问到的?

了解到这问题后,我就回复了两段内容。

第一段是:Dubbo 启动时检查了解一下?回声测试了解一下?

第二段是:这样做除了每个服务都需要专门写一个接口外,还需要考虑一个情况。B 服务集群部署,比如有三个节点,负载均衡之后只会选择一个其中一个。如果恰好这个服务是开通了网络关系,但是另外两个都忘记了呢?怎么做?

文本就主要围绕这两个问题展开,重点是对回声测试的实现原理的剖析,看完之后你会由衷的感叹一句:这代码,使用了障眼法呀,是真的“骚”啊。
需要说明一下的是,本文中涉及到的源码均为目前最新的 Dubbo 2.7.7 版本。

启动时检查


在说回声测试之前,我得先简单的提一下 Dubbo 的启动时检查。

上面提到的这个问题,Dubbo 肯定也是考虑到了的,启动的时候就应该去检查依赖的服务是否可用。

我们看一下官网上怎么说的:
http://dubbo.apache.org/zh-cn/docs/user/demos/preflight-check.html

意思就是这个 check 你可以用但是有的场景下它支持的不是太好。

我一般是不用,会设置为 false。

那么这个参数怎么配置,可以在哪配置呢?

还是去看官网啊,写的很清楚的:

这是一种解决方案,但不是本文重点,所以这一节只是做介绍,实现原理不进行展开,有兴趣的朋友可以自己去翻翻源码。

啥是回声测试?


就算你们的 PRC 框架用的是 Dubbo,可能你根本就不知道回声测试这回事。

很正常,关于这部分的介绍官网上都写的极简,所有加一块,就只有这些内容:
http://dubbo.apache.org/zh-cn/docs/user/demos/echo-service.html
虽然你没有关心过回声测试,但是你的每一个 Dubbo 接口都支持回声测试。

这点我们从官网上的描述也可以看出来的:

所有服务自动实现 EchoService 接口,只需将任意服务引用强制转型为 EchoService,即可使用。

润物无声,牛不牛皮,惊不惊讶?
先整一个简单、直观的示例。

下面是一个 Dubbo 的接口(provider 端)和其实现类:

在 consumer 端进行调用,并输出调用结果如下:

第 26 行调用 sayHello 方法没啥说的,常规操作。

妙就妙在 28 和 29 行。

把 demoService 强转成了 EchoService,然后这个方法还有一个 $echo 方法。

这个方法的入参和出参都是 Object 类型:

在上面的案例中,输入“echo,why技术”,返回也是“echo,why技术”。

所以,EchoService 接口的 $echo 官方叫法是:回声测试。



很形象,是不是?

用法是非常简单了。总体来看就是如果你只需要看看 Dubbo 服务能否调通,但你又不想用启动时检查的方式,你也不需要为每个服务都专门提供一个诸如 sayHello 这样的接口。

调用方只需要把其中的一个服务引用强转为 EchoService 就可以了。

EchoService 就是一个接口:

框架已经给我们提供了这样的功能,接下来,带大家看看它的实现原理。

EchoService实现原理-大胆假设


用法是很简单的,就是把 demoService 这个服务引用强转为 EchoService:
EchoService demoService = (EchoService) this.demoService;
String echo = (String) demoService.$echo("echo,why技术");

只看上面这两行代码,其实大家应该就可以猜出一个大概。

首先第一行是一个类型强转,那么说明 demoService 这个代理类,不仅实现了 DemoService 接口,还在某个不为人知的地方实现了 EchoService 这个接口。

就类似于这样式儿的:
public class 代理类 implements DemoService, EchoService

因为只有这样强转的时候才不会报错。

然后第二行调用了 $echo 方法,一定是某个地方实现了这个接口,实现方式里面保持出参和入参一致。

所以我们提出两点猜测:

  1. DemoService 这个服务引用是由框架帮我们实现了 EchoService 接口。

  2. 同时框架帮我们实现了 $echo 方法,方法的逻辑是保证其出参和入参一致。


接着我们就去验证一下。

EchoService实现原理-小心求证


先看截图:

demoService 这个服务引用是一个动态代理的类。

可以清楚的看到,它其实是有三个方法的:

  1. EchoService 的 $echo 方法。这个方法就是我们要找的方法。

  2. DemoService 的 sayHello 方法。这个方法是我们提供的方法。

  3. Destroyable 的 $destory 方法。这个方法可以先不关心,最后我会简单的说一下。


所以,接下来,我们只需要找到生成动态代理类的地方,把 Dubbo 给我们生成的动态代理类打印出来,看一下就知道了是怎么回事了。

那么,我们在哪里创建的代理对象呢?

代码的入口为:
org.apache.dubbo.rpc.ProxyFactory#getProxy(org.apache.dubbo.rpc.Invoker<T>)

可以看到,这是一个 SPI 接口:
其默认实现是 javassist 的方式。

这个 SPI 接口的实现类有下面这三个:

stub 是做本地存根用的,不是本文重点,大家了解一下就行,其对应的官网介绍如下:
http://dubbo.apache.org/zh-cn/docs/user/demos/local-stub.html

jdk 和 javassist 是代理工厂的具体实现。

那为什么没有用 CGLIB 呢?

别问,问就是:别慌,等下再说。

到这里,面试题也就随之而来了:
请问 Dubbo 提供了哪些动态代理的实现方式?其默认实现是什么呢?

记住啦,只有 jdk 和 javassist 的实现方法,没有 CGLIB。其默认实现是 javassist。

所以,接下来我们主要看看 javassist 的实现过程:

在下面方法的第 79 行打上断点:
org.apache.dubbo.rpc.proxy.AbstractProxyFactory#getProxy(org.apache.dubbo.rpc.Invoker<T>, boolean)

标号为 ① 的地方是获取 interfaces 配置,本文中示例为 null,所以不会走进该 if 分支中。

标号为 ② 的地方是判断是否需要泛化调用,默认是 false。

标号为 ③ 的地方才是我们需要关注的地方。

哟,这不是巧了吗,这不是?

这里有我们自己的接口 DemoService,还有我们要找的接口 EchoService。

接下来 79 行会去调用 82 行的抽象方法 getProxy:

而这个方法,前面我们说了,有两个实现类。我们主要看默认实现 javassist。

最终会走到这个方法来:
org.apache.dubbo.common.bytecode.Proxy#getProxy(java.lang.ClassLoader, java.lang.Class<?>...)

这个方法的代码特别长,而且很难读懂。所以我就不带着大家一行行的解读了。


先看个大概:

主要是要理解 136 行的 ccp 和 ccm 是干啥的。这是这个方法最重要的东西。


ccp 用于为服务接口生成代理类,我们示例中的 DemoService 接口的动态代理对象,就是由 ccp 生成的。

ccm 用于为 org.apache.dubbo.common.bytecode.Proxy 抽象类生成子类,主要是实现 Proxy 类的 newInstance 抽象方法。

我常常说源码之下无秘密,这两个类是由源码生成的源码,不能直观的看到。

接下来,配合 idea 的 Evaluate Expression 计算表达式窗口教大家一个骚操作。

在 Debug 模式下,按快捷键 Alt + F8 就可以打开Evaluate Expression计算表达式窗口。

先看 ccp,通过 debugWriterFile 命令就能把生成的代理类写到本地(注意是首字母小写的 proxy0)

同理,ccm 也可以这样取出来,这里我们换一个目录(注意是首字母大写的 Proxy0)::

然后我们把生成在本地的代理类打开看一下,D 盘这个 Proxy0.class 就是 ccm 生成的,很简单,大家看一下就行:

玄机就藏在 13 行这个 proxy0 里面,而这个 proxy0,就是 ccp 生成的动态代理对象,也就是我们放在 E 盘的 proxy0:

从 15 行可以看出,这个代理类不仅实现了我们的 DemoService 接口,还悄悄帮我们实现了 EchoService 接口。

所以我们之前的第一个猜测是正确的。DemoService 这个服务引用是由框架帮我们实现了 EchoService 接口。

这样,强制类型转换的时候就不会有问题了。

那么这个接口的方法 $echo 是怎么实现的呢?

你只有一个动态代理也没有用啊,没有地方去实现这个方法,真正调用的时候也会出错的呀。

这个时候就要祭出 Dubbo 的 Filter 链了:

在 EchoFilter 这个拦截器里面,判断了如果调用方法是 $echo,有且仅有一个参数,就直接把参数返回。

走到这个 EchoFilter 拦截器了,就说明服务是可用的了,探测任务已经完成,也就不需要继续往下走了。

在这个过程中,这个 EchoFilter 拦截器相当于是方法的具体实现了。

动态代理的类里面有这个方法,但实际上这个方法没有具体实现。

这是障眼法啊,这操作够骚啊。

所以,我们前面的这个猜测是不正确的:框架帮我们实现了 $echo 方法,方法的逻辑是保证其出参和入参一致。

框架并没有帮我们实现 $echo 方法,而是基于其拦截链机制,拦截到是这个方法后,就返回入参,相当于另外一种方法的实现。

有的同学就说了,我的系统里面倒是用到了动态代理,但是我也没有这种拦截链的机制啊。

朋友,思维发散点,别只盯着拦截链呀。

给大家简单的看一下 $destory 方法的操作方式,你就明白了。

这是 Dubbo 2.7.5 版本之后加入的停机相关的方法,也是所有代理对象都自动实现 Destroyable 接口。

给大家上一个对比图吧,左边是 Dubbo 2.7.4.1 版本生成的动态代理类,右边是Dubbo 2.7.7 版本生成的动态代理类:

$destory 这个方法的障眼法是怎么使的呢?还是基于 Filter 吗?

你想一想,现在是要销毁这个代理了,是不是应该在方法调用的时候就立即触发了,还花这么大劲走到 Filter 里面去干啥?

给大家演示一下:

这个方法是怎么被拦截的呢?

请看,直接在 invoke 里面,方法调用的入口处就“硬编码”的拦截住了,就是这么灵性:

$destory 和 $echo 的实现差不多,只是拦截时机不同而已。

所以,其实这就是一种思想,基于动态代理我们可以搞很多事情,接口里面的方法,也不是非得实现,只要我们能拦截到这个方法就行。

关键是,你得分析清楚,在什么时机去拦截。

所以,我们能从 Dubbo 源码中学到的这个骚操作是在创建动态代理对象的时候,可以神不知鬼不觉的给代理对象加一个接口,而且不需要真正的去实现接口里面的方法,只需要拦截下来就行。

这个时候,你再回想回想 Mybatis ,是不是也是只有接口,没有实现类,也是通过动态代理的方式把接口和 SQL 关联起来的。

你就想,多联想,品一品这个味道。自己多咂摸咂摸。

Filter里面搞点事情


$echo 既然它是基于 EchoFilter 的,而 Filter 又是一个 SPI 接口。那我们又可以搞事情了。

比如我们小小的改动一下,返回这个请求是负载到了哪个服务提供者中:

需要注意的是我们的自定义 Filter 需要在框架的 EchoFilter 之前执行。

所以,我们的 order 需要比 EchoFilter 小一点。

至于怎么配置让我们自定义的 WhyEchoFilter 生效,这里就不介绍了,大家可以去查一下。

配置好之后,跑一下测试用例,就会走到我们自定义的 WhyEchoFilter 中:

可以看到,输出的时候带出了这个请求是负载到了 20882 端口的服务提供者。

这里只是一个小例子,invoker 参数里面的信息非常的丰富,大家可以自由发挥。

集群模式怎么搞


不知道大家有没有发现一个问题。

一次请求只会调用到一个服务提供者(负载均衡配置的是广播模式的不在这次的考虑范围内)。

一般来说我们都有两个以上的服务提供者。

基本本文的需求,我们一次探测,应该调用到所有的服务提供者,这样才放心。

所以,核心问题是要获取到所有的服务提供者,那我们怎么实现这个需求呢?

首先肯定不能在 Filter 里面搞事情了,因为走到 Filter 的时候,已经经过负载均衡后选定了某一个服务提供者了。

我这里没有去实现这个需求,但是提供两个思路,源码里面都有,我们可以照葫芦画瓢:

第一个思路是看看 Dubbo 源码里面怎么获取到所有 invokers 的:
org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker#invoke

第二个思路是看看 Dubbo-Admin 管理台对应的源码里面是怎么获取到这个列表的:

实现起来可能会有点麻烦,但是源码都摆在上面的两个思路里面了。借鉴一下就行了。

推荐一本书和一个博客


前面还留下了一个问题:Dubbo 动态代理的实现为什么不用 CGLIB 的方式?

答案可以在我最近看的《深入理解JVM字节码》这本书里面找到。

这书的第 9 章《字节码的应用》这一节里面专门说到了字节码在 Dubbo 上的应用:

这一节也提到了文章中说到的回声测试:

在介绍字节码在 Dubbo 上的应用时,他是这样的说的:

Dubbo 的作者提到,使用 Javassit 来作为动态代理方案的主要考虑因素是性能。在他们的性能测试中,性能 Javassit>cglib>JDK。

这个地方提到了 Dubbo 的作者的说法,但是没有给出对应的依据。

于是,我去找了一下。

 Dubbo 的作者是梁飞,而梁飞的几篇博客就放在官网上的:

随便打开一个,就能找到他的博客地址:

在他的博客里面很容易就找到了对应的文章:
https://www.iteye.com/blog/javatar-814426
这篇文章他分别从下面几个方面构成:

  1. 测试结果

  2. 测试结论

  3. 差异原因

  4. 最终选型

  5. 测试代码

  6. 字节码对比


可以说是有理有据,感兴趣的朋友建议去读一下,跑一跑测试用例。

我这里只截取一个结论给大家看,证明书上是没有胡说的:

这本书分别从原理和应用的角度去讲学了字节码,里面有非常多的实战内容。还是很不错的。

推荐这本书,本文我会送出五本,参与方式下一小节说。
然后推荐一下梁飞大佬的博客,很久没有更新了,但是还是有很多干货的,建议有时间的可以去翻一翻:
https://www.iteye.com/category/7506

最后说一句(求关注)


文中提到的深入理解JVM字节码我也联系到了机械工业出版社的工作人员,出版社决定给我这个小小号主赞助五本书。
我当然会回馈给读者朋友们啦。

其中三本我会通过抽奖的方式送出去,在公众号后台回复关键字【抽奖】即可参与活动。

另外两本,我会在前面三本开奖后,在我的技术群里通过抢红包的方式送出去,手气最佳者可得。

如果你等不及,也可以直接购买:

感谢大家的支持。

还有,在其他平台收到一个评论说我的文章里面的表情包是花里胡哨的东西。其实我已经很克制的在加表情包了。

沙雕表情包很有趣的,哈哈哈哈。


另外,最近微信公众号改版,对我这样的小号主可以说是非常打击了。阅读量直线下降,正反馈持续减弱。

所以安排个“一键三连”(转发、在看、点赞)吧,周更很累的,不要白嫖我,需要一点正反馈。
才疏学浅,难免会有纰漏,如果你发现了错误的地方,由于本号没有留言功能,还请你在后台留言指出来,我对其加以修改。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

我是 why,一个被代码耽误的文学创作者,不是大佬,但是喜欢分享,是一个又暖又有料的四川好男人。

还有,重要的事情说三遍:

欢迎关注我呀。
欢迎关注我呀。
欢迎关注我呀。

往期推荐


我从LongAdder中窥探到了高并发的秘籍,上面只写了两个字...


震惊!ConcurrentHashMap里面也有死循环,作者留下的“彩蛋”了解一下?


吐血输出:2万字长文带你细细盘点五种负载均衡策略。


转发、点赞、在看、一键三连。

别白嫖我,好吗?

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存