Dubbo服务是怎么暴露的?看源码就知道了

562 阅读10分钟

​ 先放一张官网的服务暴露时序图,对我们梳理源码有很大的帮助。注:不论是暴露还是导出或者是其他翻译,都是描述export的,只是翻译不同。

0.配置解析

​ 在Spring的配置文件中,Dubbo指明了DubboNamespaceHandler类作为标签解析。

​ 与服务相关的显然就是service,找到对应的ServiceBean类,进入这个类,开始服务暴露的源码分析。这个类位于Dubbo源码config模块-spring模块下的根目录。

1.开始export

​ export也是上面时序图中最开始的一个方法,从这个方法名也知道,这就是服务暴露或者叫出口最关键的方法。进入ServiceBean类,在这个类中一共有两处调用了此方法。即onApplicationEventafterPropertiesSet,了解过Spring Bean生命周期的朋友看到这两个方法肯定眼熟,果然,这个类实现了相关的接口:

​ 看一下onApplicationEvent方法:

​ 从它的if判断条件调用的几个方法名可以看出,如果是延迟暴露、还未暴露过且支持暴露就可以执行export方法了。这里说一下,这个isDelay方法有点迷惑,字面意思应该为是否延迟,返回ture代表延迟。但是实际意思却为返回true代表不延迟,因为这个判断条件是delay==null || delay==-1,代表没有设置延迟。所以这个方法中的export才是第一个触发的。

​ 接着进入到export方法。这个方法会跳转到ServiceConfig类,是ServiceBean的父类,也正好符合时序图。

​ 这几个if的作用就是判断是否需要暴露和延迟暴露。如果不需要暴露就返回,否则都会执行doExport方法的。进入这个方法,这个方法代码很多,前面一堆if都是检测配置信息的,关注的重点在doExportUrls方法。

​ Dubbo是支持多注册中心和多协议的,在这里就表现出来了。获取到的注册中心URL放到一个list里面。其中loadRegistries方法就是根据配置组装成相关的URL并返回,如加载注册中心地址、检查地址是否合法、添加配置信息等。咱们先关注重点,这个方法就不跟下去了,不然没完没了。至于组装后的URL可以debug自己看看,大概样子如下:

2.组装URL

​ 进入到doExportUrlsFor1Protocol方法,这个比较重要。从它的名字可以看出,它的作用是组装暴露URL。

​ 这个方法很长,主要就是创建一个map然后添加各种值,包括配置信息、提供的服务等等。由于这个方法分支非常多,官网给了各个分支含义的解释,配合源码能很好理解其意思:

// 获取 ArgumentConfig 列表
for (遍历 ArgumentConfig 列表) {
    if (type 不为 null,也不为空串) {    // 分支1
        1. 通过反射获取 interfaceClass 的方法列表
        for (遍历方法列表) {
            1. 比对方法名,查找目标方法
        	2. 通过反射获取目标方法的参数类型数组 argtypes
            if (index != -1) {    // 分支2
                1. 从 argtypes 数组中获取下标 index 处的元素 argType
                2. 检测 argType 的名称与 ArgumentConfig 中的 type 属性是否一致
                3. 添加 ArgumentConfig 字段信息到 map 中,或抛出异常
            } else {    // 分支3
                1. 遍历参数类型数组 argtypes,查找 argument.type 类型的参数
                2. 添加 ArgumentConfig 字段信息到 map 中
            }
        }
    } else if (index != -1) {    // 分支4
		1. 添加 ArgumentConfig 字段信息到 map 中
    }
}

​ 当然,如果你没有配置相关的信息,如dubbo:method,在debug源码时,压根就不会进入到这些分支里面。现在我们看一下URL长啥样:

​ 可以看到协议已经变成了dubbo,具体的服务接口也显示了出来。而map的值就存在parameters当中。

3.服务暴露

​ 依旧在doExportUrlsFor1Protocol方法里,具体的服务URL已经组装好了,接下来就是服务暴露了。先看这么一段代码:

​ 这段代码有两个关键点,已经在图中标注。**第一处是先进行本地暴露。第二处判断如果有注册中心,就会进行远程暴露。**注册中心的URL在doExportUrls中已经获取了。

​ 先看本地暴露,进入到exportLocal方法:

​ exportLocal方法比较简单,根据协议头判断是否需要暴露服务,如果需要,就创建一个新的URL

​ 我们看一下这个URL长啥样:

​ 协议变成了injvm,从这个协议名称就可以猜测到,这个在一个jvm内的协议。IP地址也从远程注册中心的IP地址变成了本机地址。

​ 本地URL组装好后,会创建一个exporter对象。这个对象是由protocol的export方法生成,我们点进这个抽象方法,会发现它有一个@Adaptive注解。这个注解修饰方法时会生成一个代理类。主要配合SPI机制使用,SPI的作用简单的说就是提供一个标准化的接口,可能有不同的实现,而这个实现类的路径我们就放在一个固定的位置,让框架去读取。同样的用法也在proxyFactory.getInvoker()中。关于SPI的解析放在最后。这个export的具体实现方法如下图:

​ 所在类为InjvmProtocol。这个实现方法就不说了,主要就是根据传入的参数进行封装,我们直接看最终的exporter:

​ 可以看到,已经找到了服务接口的实现类了。最后就是将exporter添加到exporters中,这个exporters是本地的一个集合,专门缓存exporter。

​ 接着就是远程暴露了,其实和本地暴露的目的一样,都要封装成invoker——>exporter,最后添加到exporters中,还多了一步注册。首先依旧是通过getInvoker封装成invoker。(这里说句题外话,可以根据参数的协议类型找到这些抽象方法的实现类。Dubbo命名很严谨,比如参数中,URL的协议为registry,那么其实现类就是RegistryProtocol。至于为什么要封装成invoker我们最后再分析,现在只需理解这么做是为了屏蔽细节,统一暴露)。

​ 封装成invoker后又弄了一层wrapperInvoker,点进这个类,可以发现其实就给invoker额外封装一层,可以提供更多信息以及一些工具方法,比如ServiceConfig、检测是否有效。

​ 接着主要区别在export方法当中,其实现方法在RegistryProtocol类中(因为参数wrapperInvoker的url协议为registry)。实现方法部分截图如下:

​ 这个方法主要做了如下工作:

​ 1.调用doLocalExport导出服务

​ 2.向注册中心注册

​ 3.向注册中心订阅override数据

​ 4.创建并返回DestroyableExporter

​ 首先进入到doLocalExport方法,这个方法主要就是会调用DubboProtocol的export方法,为了避免过多的代码截图把自己弄昏了,就不贴这个方法了。这个方法开头同样的,根据invoker获取URL,关键在于它调用了一个openServer。看到这个方法名应该知道是啥意思了,即打开服务。好家伙,终于要结束了么。

​ 这个方法很清晰,获取注册中心的IP和端口号、检查缓存、创建server。接着跟进源码,bind过程,主要关注Transports的bind方法。这里Dubbo也是用Adaptive注解和SPI机制,实现了拓展功能。它会根据传入的参数选择不同类型的Transport,默认是NettyTransporter。接下来就是Netty服务启动的相关过程了,以前写过相关博客,就不跟进了。

​ 接着,我们看上上张截图,有一个if会判断是否需要注册,如果需要注册就会向注册中心注册。我们接着跟踪源码,一直到如下方法:

​ 看到了Zookeeper客户端,到这里就明白了,是向Zookeeper添加信息。我们最后看一下Zookeeper里面的内容。我们打开Zookeeper客户端,查看一下服务:

​ 可以发现,已经有我们注册的服务了。最好下个可视化的Zookeeper客户端,可以进入到这些目录,可以找到Provider的IP地址。

疑问解析

  • 为什么要本地暴露?

    • 调用本地服务时,避免网络通信。
  • 为什么要封装成invoker和export?

    • 前面的源码分析中,本地和远程都经过了封装invoker和export两个步骤。export是服务暴露的最终形态,其包含invoker以及其他更多信息,比如注册中心、服务接口、实现类等等信息。下面是官网的一张截图:

    • 官网是这么说的:由于 Invoker 是 Dubbo 领域模型中非常重要的一个概念,很多设计思路都是向它靠拢、或转换为它。这个所谓的靠拢就如图中显示的那样,不管在消费者方还是服务提供方,均会出现Invoker,它代表一个可执行体,并屏蔽了内部细节。既然它这么重要,我们就看一下它是如果创建的。
    • 其是由proxyFactory.getInvoker创建而来,通过debug找到它的实现类:

    • 上面的方法在JavassistProxyFactory类中,其重写了doInvoke方法,比较简单,只是转发了invokeMethod。其中AbstractProxyInvoker是一个抽象类,实现了Invoker接口。而这个Wrapper的作用是包裹目标类,仅可通过getWrapper(Classs)创建子类。子类可以对入参Class进行解析,拿到类方法、成员变量等信息。在这里,目标类就是暴露服务的实现类。
    • 关于Wrapper的分析内容非常多,这里记录一下官网的解析:dubbo.apache.org/zh/docs/v2.…
  • SPI是什么?

    • SPI(Service Provider Interface),其作用前面也说了,就是定义一个标准接口,这个接口的实现由用户决定。这样做的好处就是提高了框架的拓展性。但是这个接口的实现放在哪,得让框架知道。在Java SPI中,规定在META-INF/services/ 目录下,创建一个以接口全路径名命名的文件,文件中写出接口实现类的全路径名。然后Java就会去遍历加载这些实现类并创建实例。
    • 前面说了Java SPI,但是Dubbo并没有用Java规定的方法,而是自己实现了SPI机制。可以从ServiceLoader.load()方法跟踪源码看一下,Java SPI机制是遍历了所有的实现类,而不是按需加载,造成了不必要的浪费。说到Dubbo SPI,那么它的规定目录在哪?在META-INF/dubbo/internal目录下。我们从源码的该路径下找个文件看看。

​ 可以看到Dubbo SPI的配置文件内容是键值对的形式,这样就可以实现按需加载。根据key值,获取全路径名,然后加载。 如果需要自己自定义,就直接在MEATA-INF/dubbo/目录下创建配置文件即可。同样的,类似Java SPI中的ServiceLoader,Dubbo中叫ExtensionLoader。这个类的几个方法,作用很明确,也不复杂,这里就不跟踪了。其中getExtensionLoader方法,入参是需要加载的接口,这个方法会检查是否有对应类型的ExtensionLoader对象,如果没有就新建一个。createExtension方法就是根据名字获取对应的实现类,这样就实现了按需加载。