本文转载自微信公众号《三太子敖丙》,作者三太子敖丙。转载本文请联系三太子敖丙公众号。在这篇文章中,我将带大家看一下Dubbo服务引入的全过程。写完这篇服务介绍,下一篇会开全链接。看你看完有没有秒脉。被连接的感觉。C在写文章的过程中也在官网上发现了一个小问题,下面会提到。话不多说,直入正题。服务引用的大致流程我们了解到,Provider暴露自己的服务并注册到注册中心,而Consumer无非是通过一波操作从注册中心学习Provider信息,然后封装一个调用类和Provider进行深入交流。在之前的文章中,我已经提到Dubbo中的一个可执行文件是Invoker,所有的调用都必须靠近Invoker,所以可以推断应该先生成一个Invoker,然后因为框架需要不侵入方向业务代码的开发,那么我们的Consumer需要无感知的调用远程接口,所以我们需要构建一个代理类来包装和屏蔽底层的细节。整体大致流程如下:服务引入的时机服务引入和服务暴露是一样的,通过spring自定义标签机制生成对应的Bean。ProviderService对应ServiceBean,ConsumerReference对应ReferenceBean。我们在上一篇文章中分析了服务暴露的时机。Spring容器刷新后开始曝光。引入服务有两个时机。第一种是饥饿式,第二种是懒惰式。饿了么中国式是在Spring的InitializingBean接口中实现afterPropertiesSet方法,容器通过调用ReferenceBean的afterPropertiesSet方法引入服务。惰性风格是只有当服务被注入到其他类中时才开始引入过程,也就是说,服务引入要等到被使用时才开始。默认情况下,Dubbo以惰性方式引入服务。如果需要使用饿了么风格,可以通过配置dubbo:reference的init属性来开启。我们可以看到ReferenceBean也实现了FactoryBean接口。这里有一个关于Spring的面试要点,带大家分析一下。BeanFactory、FactoryBean、ObjectFactory就是这三个东西。我分别说一下。从字面上看,其实可以知道BeanFactory和ObjectFactory是工厂,FactoryBean是Bean。BeanFactory实际上是一个IOC容器。还有很多实现类我就不分析了。简单的说,Spring中所有的Bean都是由它来管理的,而FactoryBean也是一个Bean,所以也是由BeanFactory来管理的。FactoryBean是一个什么样的Bean?事实上,它封装了你真正想要的Bean。当你真正想要获取Bean时,容器会调用FactoryBean#getObject()方法,在这个方法中你可以进行一些复杂的组装操作。此方法封装了所需对象的复杂创建过程。这里其实很清楚了,就是在创建想要的Bean比较复杂的情况下,或者是一些第三方bean难以修改的情况下,用FactoryBean封装一层,屏蔽底层的细节创建,并促进Bean的创建。使用。ObjectFactory用于延迟查找场景。这是一个普通的工厂。当获取到ObjectFactory对象时,相当于还没有创建Bean。只有使用getObject()方法时,才会触发Bean实例化等生命周期。主要用于临时获取一个BeanHolder对象。如果过早加载,可能会导致一些意想不到的情况。比如当BeanA依赖于BeanB时,如果A初始化的太早,B中的状态可能处于中间状态,此时使用A很容易导致一些错误。总结一下,BeanFactory是一个IOC容器,FactoryBean是一种特殊的Bean,用来封装和创建更复杂的对象,而ObjectFactory主要用于对象的延迟查找场景和延迟实例化。服务引入的三种方式服务的引入分为三种,第一种是本地引入,第二种是直连导入远程服务,第三种是通过注册中心导入远程服务。当地介绍。不知道大家还有没有印象。在前面服务暴露的过程中,每个服务都会暴露在本地,遵循injvm协议(当然如果你设置了scope=remote,本地就没有引用了),因为有一个server既是Provider又是Consumer的情况,那么就可以调用自己的服务,所以做了本地引入,避免了远程网络调用的开销。所以服务引入会先去本地缓存看本地有没有服务。直接连接远程导入服务,这个其实是在日常测试的情况下使用的,不需要启动注册中心,Consumer直接配置硬编码Provider的地址,然后直接连接。注册中心引入远程服务。这是关键点。消费者通过注册中心了解提供商的相关信息,进而介绍服务。这也包括多个注册中心。在同一个服务的多个提供者的情况下,如何选择如何打包,如何进行负载均衡、容错并让用户无感知,是一个技术活。本文使用单一注册中心来介绍远程服务。我们来看看Dubbo是怎么做的。服务导入流程分析默认是惰性的,所以服务导入入口是ReferenceBean的getObject方法。可以看出很简单,就是调用get方法,如果没有这个引用,就执行init方法。官网上的一个小问题这个问题是在if(ref==null)这一行。其实有位老哥在调试的时候发现ref不等于null,所以无法进入init方法进行调试。后来他发现,IDEA为了显示对象的信息,会通过toString方法获取对象对应的信息。toString调用了AbstractConfig#toString,这个方法通过反射调用了ReferenceBean的getObject方法,触发了导入服务动作,所以断点的时候ref!=null。可以看出反射调用是通过方法名进行的,而getObject是以get开头的,所以会被调用。于是这哥们就提出了PR,一开始没有被接受。一位会员认为这不是错误,想法应该设置为不调用toString。不过另一位成员觉得这个PR很好,而且Dubbo项目的二代负责人30N也发言了,所以这个PR被采纳了。至此这个小问题我们已经知道了,官网其实也说的很清楚了。但是这里有一个小问题。文中提到我的源码版本是2.6.5,在github的releases中下载。其实我很久以前就知道这个tostring问题了。我觉得我是2.6.5的稳定组,谁知道翻车了。我调试的时候没有进入init方法,因为ref不等于null。我很惊讶。我检查了里面的toString方法。2.6.5版本没有修改?getObject没有被过滤,所以它仍然被调用。再次打开2.7.5版本的代码,发现是修改判断。我去下载了2.6.6版本的代码,发现也修改了,所以这个修改不是随2.6.5版本发布的,而是2.6.6发布的,除非我下载的是假包,就是我说的一个小问题,但不是一个大问题。其实这一段我想说的是PR。作为开源软件的出口商,很多细节也很重要。这个问题其实很影响源码的调试,因为如果不熟悉代码,肯定会一头雾水。谁知道异步引入了哪个后台线程。提这个PR的小哥花了两个小时才弄清楚真正的原因,所以这虽然不是bug,但是对那些想深入了解Dubbo内部结构的同学还是有影响的。这种配置修改适配是不可取的。幸运的是,最终的解决方案是更改代码。好了,回到今天的话题,接下来分析的是不让我进去的init方法。源码分析init方法很长,但大部分都是检查配置,然后把配置build进去一张地图。长篇大论我就不分析了。让我们看看构建的地图是什么样子的。然后进入关键方法createProxy。从名称中,您可以获取要创建的代理。因为代码很长,我会一段一段地分析。如果是走本地,那么直接使用本地协议构建一个URL,然后引入服务,即refprotocol.refer。这个方法后面会分析,本地导入就不深入了,直接去之前服务暴露出来的exporterMap中获取服务即可。如果它不是本地的,那么它一定是远程的。接下来判断是点对点直连provider还是通过注册中心获取provider信息再连接provider。我们来分析一下url的配置。如果配置了url,则不是直连。地址为注册中心地址。然后就是没有url配置,注册中心必须在这里引入远程服务。最终拼接的URL长这样。可以看出,这部分其实就是根据各种参数组装URL,因为我们的自适应扩展需要根据URL的参数进行。至此,我先画一张图,给大家快速概览一下。这其实就是整个过程。简单介绍就是先查看配置,通过配置构建一个map,然后使用map构建一个URL,然后在URL上使用协议使用自适应扩展机制调用相应的协议。参考获取相应的调用者。当有多个URL时,先遍历构造invoker,再通过StaticDirectory封装,再通过cluster合并,只暴露一个invoker。然后构建代理,封装调用者返回服务引用,然后消费者调用这个代理类。相信通过上面的图和总结已经知道了大概的服务引入流程,但是还有很多细节,比如如何从注册中心获取Provider地址,invoker里面有什么?别着急,我们继续往下看。从前面的截图我们可以看出此时的协议是registry,所以使用了RegistryProtocol#refer。让我们来看看这个方法。主要是获取registry实例,然后调用doRefer进行真正的refer。这个方法很重要。可以看出,RegistryDirectory目录是为插件registry实例而生成的,而且它还实现了NotifyListener接口,所以registry的监听其实就是由这家伙负责的。然后向注册中心注册自己的信息,将providers节点、configurators节点和routers节点订阅到注册中心。订阅后RegistryDirectory会收到这些节点下的信息,会触发DubboInvoker的生成,即用于远程调用的Invoker。然后通过集群封装Invoker,所以一个服务可能有多个provider,最后将这些信息记录在ProviderConsumerRegTable中,然后返回Invoker。所以我们知道Conusmer在RegistryProtocol#refer中向registry注册了他的信息,订阅了Provider和configuration的一些相关信息。让我们看看订阅返回的信息是什么。拿到Provider信息后,就可以通过监听触发DubboProtocol#refer(具体调用哪个协议取决于url的协议,这里是dubbo协议),整个触发过程我就不一一讲了,看调用堆栈很清楚。最后我们从注册中心获取远程Provider信息,然后引入服务。这里重点是getClients,因为毕竟要和远程服务进行网络调用,getClients就是用来获取客户端实例的。实例类型为ExchangeClient。底层依赖Netty进行网络通信,可以看到默认是共享连接。getSharedClient我就不分析了。就是通过远程地址找到客户端。这个客户端还有引用计数的功能。如果远程地址没有客户端,调用initClient。让我们看一下initClient方法。而这个connect最终返回的是封装在HeaderExchangeClient中的NettyClient。那么最终的Invoker就是这样的。可以看到很多记录的信息,基本上你需要的都有。我要说的是对应服务只有一个url的情况。多个url无非就是用directory和cluster。封装一层。最后,返回(T)proxyFactory.getProxy(invoker);会被调用返回一个代理对象,这里不分析。至此,整个过程分析完毕。不知道大家清楚了吗?我会加上之前的图,给你一个完整的流程再过一遍。小结分析整个过程相信不难。总结起来无非就是通过配置形成一个URL,然后通过自适应得到对应的实现类来引入服务。如果是注册中心,会向注册中心注册自己的信息,然后订阅注册中心的相关信息,获取远程提供者的ip等信息,然后通过netty客户端进行连接。并通过目录和集群,对底层多个服务提供者进行屏蔽、容错和负载均衡。在此之后,文章会详细分析,最终得到封装的调用者,再通过动态代理封装得到代理类,让接口调用者无感知调用方式。最后,今天看了这篇文章,相信大家应该对服务的介绍有了一个清晰的认识。其实这里面还有很多细节我没有分析,比如一些filterchain的组装。这个其实在服务暴露那篇文章中已经提到了。同样还有服务引用的过滤链,限于篇幅就不展开了。把握主线很重要。至此,带大家了解了Dubbo的整体概念和大致流程,介绍了DubboSPI机制,分析了服务暴露流程和服务引入流程。具体细节还得自己去摸索。我有大概的流程。就是这样。dubbo系列即将结束。虽然知道每次写硬核技术都会少很多朋友,但是还是想把这个系列写完。感谢您的支持。
