手把手带你阅读dubbo源码(一) 服务暴露

本篇文章主要是阅读了dubbo官方文档:http://dubbo.apache.org/zh-cn/docs/user/quick-start.html关于服务的暴露和引用,感觉很多细节还不是十分清楚,所以决定从自己手上的项目看起,然后一步步探究其中的实现,顺便记录下这个过程中学到的其他知识,由于dubbo是一个很成熟的框架了,用到的技术也很多,里面定义了很多类和接口十分复杂,所以我一步步去分析篇幅可能有些长,周边知识也非常多,我在文中都列了相关拓展知识的链接,建议大家clone一份源码跟着读,抛砖引玉,能对大家也有所帮助。dubbo的版本号是2.6.2。

1.从dubbo的配置讲起:

这里主要用到了spring自定标签功能,关于spring自定义配置,大家可以查看:https://www.cnblogs.com/mahuan2/p/7213866.html这篇文章.

这里主要定义了一个dubbo的命名空间,然后编写了对应的xsd文档,dubbo的xsd文档在jar包中的META-INF/dubbo.xsd。

然后dubbo.xsd中我们主要关注service和reference标签。

然后对标签的处理类dubbo中的定义写在META-INF/spring.handlers

我们看到dubbo标签的解析主要用到了DubboNamespaceHandler 这个类。然后我们打开DubboNamespaceHandler这个类的源码。

扫描二维码关注公众号,回复: 12741817 查看本文章

我们可以看到dubbo自定义了一个DubboBeanDefinitionParser类去解析上面的标签,并且自定义了ServiceBean和ReferenceBean。然后我们再打开DubboBeanDefinitionParser这个类。

这里我们主要关注parse这个方法,这个方法逻辑比较多,我也没有读得十分清楚,不过大体意思就是拿到xml中所有配置的基本信息,然后定义成spring中的BeanDefinition。关于BeanDefinition,读者可以看这篇文章:https://blog.csdn.net/windrui/article/details/52973915。大体就是对spring的Bean的抽象,主要保存类名、scope、属性、构造函数参数列表、依赖的bean、是否是单例类、是否是懒加载等,其实就是将Bean的定义信息存储到这个BeanDefinition相应的属性中,后面对Bean的操作就直接对BeanDefinition进行,例如拿到这个BeanDefinition后,可以根据里面的类名、构造函数、构造函数参数,使用反射进行对象创建。

这里我对其中一点比较感兴趣,就是service里面的ref参数:

因为这里把一个interface映射到了一个可实例化的class,而且还是运行时bean的名字,所以我看了这个ref的解析实现,这个解析主要有两个:

(1).写class属性,不写ref:

这段的意思是如果用了class标签,spring会生成相应的class的BeanDefinition,并创建了一个BeanDefinitionHolder来代表运行时的bean,并且这个bean的名字是id+Impl。

(2)写ref,不写class。

如果有ref,就用RuntimeBeanReference作为创建时的bean。

这样就可以将本来是interface,实例化的时候是另外一个bean,并且这个bean也会存在于ioc的Bean创建链条中。

如果ref和class都写了,则以ref为准,因为ref的代码在后面,哈哈^_^

以上就是dubbo如何从xml读取到定义成spring的BeanDefinition,接下来,我们再看一下bean实例化的时候是如何暴露服务的。

2.serviceBean是何时暴露服务的

我们看ServiceBean里面实现了ApplicationListener<ContextRefreshedEvent>,ApplicationContextAware,InitializingBean接口,这3个接口分别会在spring启动的不同时机依次调用,他们调用的为顺序 2 -> 1 -> 3:

然后我们看下ServiceBean的三个接口:

没什么好说的,初始化一些变量

基本做的也是一些初始化的变量,这里关注一个方法:

BeanFactoryUtils.beansOfTypeIncludingAncestors , 大概的作用就是获取这个上下文所有指定class的Bean,如上面的providerConfigMap,传入的参数就是ProviderConfig.class。

然后主要关注一下最后的方法export:

然后这个serviceBean实现的第3个接口最后执行,也没太多东西,其实也是调用export,估计是为了防止前面的方法没有执行吧!

然后我们关注这个export,这个export方法就是将本地服务暴露给外面调用的过程,这样就保证了spring容器在初始化完成的时候,所有的serivceBean都暴露服务了。接下来,我们再看看export做了哪些事情。

3.serviceBean暴露服务做了哪些事情

我们看看export的源码:

主要就判断该服务是否已经打开,以及是否需要延迟打开,实际逻辑在doExport里面,我们再看doExport的代码。

别的没什么好说的,monitor是监控中心的配置,registries是注册中心配置,protocols 是发布者配置,别的暂不知道干嘛用,暂时不管,这些都会在checkDefault里面初始化provider这个变量的实话初始化,打开这个类看到

主要就是host,port,环境路径等信息,然后这些信息都是从系统变量中拿的,看checkDefault里面就知道。

然后取的都是系统变量里面的dubbo.前缀的变量,dubbo每个服务运行的时候都自动会设置这些参数吧(我猜)。另外这里还有个初始化这个类的测试单元,很好理解了。

然后继续回到doExport。

这里复习了一下Class.forName,具体内容可以参考:https://www.cnblogs.com/xingzc/p/5760166.html,主要就是把接口类加载进来。

checkInterfaceAndMethods检查interfaceClass不能为空,必须是interface类型,验证方法必须存在。

checkRef检查ref不能为空,必须实现interface接口,具体代码自己看了。

然后后面checkApplication初始化application变量

checkRegistry初始化registries变量

checkProtocol初始化protocols变量

appendProperties方法前面有提过

checkStubAndMock方法不知道干嘛的,忽略

主要看看doExportUrls方法

然后loadRegistries

这个方法是将registries变量里面的每个地址,拼上application和registryconfig里面的参数,拼成一个registryUrl(不是最后生成的url)带参数的标准格式,如:www.xxx.com?key1=value1&key2=value2。然后返回这些url的列表,自己一个服务生成的url例子:

registryUrl:

registry://172.23.2.101:2181/com.alibaba.dubbo.registry.RegistryService?application=oic-dubbo-provider&dubbo=2.6.1&logger=slf4j&pid=15258&register=true&registry=zookeeper&timestamp=1528958780785

然后再遍历protocols变量,将protocols列表中的每个protocol根据url暴露出去,主要是doExportUrlsFor1Protocol方法。

然后这个方法前期就一堆塞参数到map,最后也是跟上面生成registryUrl差不多,只不过多加了一些module,provider和自己的一些参数,拼成一个更长的url。下面这个就是我上面那个服务生成的完整url:

dubbo://10.8.0.28:12000/com.tyyd.oic.service.PushMessageService?accepts=1000&anyhost=true&application=oic-dubbo-provider&bind.ip=10.8.0.28&bind.port=12000&buffer=8192&charset=UTF-8&default.service.filter=dubboCallDetailFilter&dubbo=2.6.1&generic=false&interface=com.tyyd.oic.service.PushMessageService&iothreads=9&logger=slf4j&methods=deletePushMessage,getPushMessage,batchPushMessage,addPushMessage,updatePushMessage,qryPushMessage&payload=8388608&pid=15374&queues=0&retries=0&revision=1.0.0&serialization=hessian2&side=provider&threadpool=fixed&threads=100&timeout=6000&timestamp=1528959454516&version=1.0.0

上面可以看到,url地址包含了版本号,接口名,方法列表,序列化方法,过期时间等这个接口bean所有需要用到的上下文信息,并且地址头也由registry改成了dubbo。因为包含了所有上下文的信息,所以这个url的用处很大,后面看代码就知道了。

这部分后面url还拼加了一些拓展类,监控url等信息,具体就不细看了,主要看看转成invoker和并创建exporter这个阶段

这里的话,我比较好奇proxyFactory这个变量是如何生成的,这个根据官方文档说可能是javassistProxyFactory或者JdkProxyFactory中的任意一个,当然,这里是一个重点,就是动态代理,关于动态代理的知识可以看这里:https://www.cnblogs.com/gonjan-blog/p/6685611.html

然后javassistProxyFactory和JdkProxyFactory都是动态代理的生成技术,只不过一个是用字节码生成,一个是用jdk生成。

然后这个proxyFactory变量的生成我们看到:

然后网上查了下,原来这里有个很牛批的技术,叫做SPI,相关说明参考:

https://blog.csdn.net/qiangcai/article/details/77750541

大致就是同一个接口,可以有不同的实现类,仅通过配置就可以动态修改具体用哪个实现类,而且这个拿到的实现还是单例模式

具体dubbo中的说明文章中也有描述,可以看到

proxyFactory接口写了SPI标签,所以这里默认使用的就是javassistProxyFactory。

然后我们看看javassistProxyFactory的实现里面写了什么东西.

这里生成了一个Wrapper,这个Wrapper是通过字节码生成的,大概是对于传入的proxy实现类进一步抽象,可以根据方法名,参数等去调用相应实现类的方法。更多可以看看这篇文章:

https://blog.csdn.net/synpore/article/details/79148260

然后wrapper被封装到一个Invoker里面,url主要放在invoker里面。

然后回到前面,用生成的invoker再封装成一个DelegateProviderMetaDataInvoker,这个类跟invoker区别不大,只是多放了this这个serviceBean的信息,方便后面使用

然后调用用portocol的export方法,我们看看portocol是哪来的

这个是又是熟悉的SPI技术,我们再看看Portocol接口的SPI标签

标签是dubbo,使用我打开DubboPortocol类

然后上面有一些获取url的参数,还是上面那个例子,打断点看到的值是:

具体怎么拿就不看了。下面主要看看openServer方法。

这里也直接跟进来,看到具体的值了,然后到createServer里面

也没太多东西,主要关注Exchangers.bind(url,requestHandler).

然后我们一路跟进去,发现

默认生成的Exchanger是headerExchanger。我们再看看HeaderExchanger.

HeaderExchanger的参数是一个Transporters.bind参数,我们继续跟进去

然后getTransporter方法继续跟:

老套路了,我们看Transporter接口

看到是nettyTransporter,继续跟:

到这里,终于看到有点熟悉的NettyServer了,我们再看看NettyServer这个类

这个方法用了父类AbstractServer构造函数,还有ChannelHandlers.wrap方法,我们先看看这个方法

可以看到ChannelHandlers是一个单例模式,这里又用了SPI创建了Dispatcher类

AllDispatcher.NAME 是值是all ,我们打开AllDispatcher类

再看AllChannelHandler

super的父类

定义了一个线程池,SPI创建的,是fixThreadPool,就不进去看了(终于找到jdk基本的类了)

然后关于fixThreadPool的可以看这篇:https://blog.csdn.net/czd3355/article/details/52608567

还有个dataStore:

大体就是一个ConcurrentMap套ConcurrentMap的类,这边就看到这里吧

回到nettyServer的父类AbstractServer的构造方法

然后还是之前那个服务,我们能看到用到了url里面的参数,所以说dubbo的所有服务的上下文基本都在生成的url里面了。 这里我们主要看下doOpen方法

然后这里开始基本就是些跟jdk本身比较相关的地方了,还有就是终于看到netty相关的包了。

这里dubbo封装了一个NamedThreadFactory(发现dubbo也用了好多工厂模式),打开这个类

看到这个类自定义了类名前缀和,线程组,感觉是个不错的工厂类,以后可以拿给自己用,哈哈~

然后后面就是netty的使用了,另外说一下,dubbo默认用的还是比较老的netty3,用法介绍:

https://www.cnblogs.com/java-zhao/p/7625557.html 然后netty的详解看这篇:https://blog.csdn.net/haoyuyang/article/details/53231585

netty是同步非阻塞的,阻塞非阻塞是针对应用程序而言的,同步非同步的是针对操作系统而言的。

然后这里自己实现了一个netty的编解码器,dubbo这边封装了很多自己的类,剥丝抽茧其实还是很简单的,可以参考这篇:https://www.cnblogs.com/ll409546297/p/8036954.html

然后dubbo编码序列化效率的分析可以看这篇文章:https://blog.csdn.net/moonpure/article/details/53175519

具体的源码我就不细看了,默认的序列化协议是hessian2.

然后回到最前面,想想我们走了这么远到底做了什么?

是不是给一个service标签的serviceBean生成了一个代理好的Invoker,这个invoker放在一个叫exporter的对象下,然后这个exporter放在了serviceBean的一个exporters变量下,准备被调用,然后创建的nettyServer放在了serviceBean的变量protocol下的一个变量serverMap里面,这样一个serverBean的netty服务,方法代理类是不是都生成好了。这里注意protocol是单例生成的,所以如果有bean打开过nettyServer,别的bean就不会再打开。

然后回到很前面的ServiceConfig的doExport里面:

发现他把生成好了serviceBean放到了一个ApplicationModel里面,后面consumer也会放到这里面,具体不知道干嘛用的。

这就是一个服务暴露的过程。真不容易啊..

猜你喜欢

转载自blog.csdn.net/qq_33762302/article/details/114983188