dubbo源码解析-服务暴露与发现

一、概述

dubbo是一个简单易用的RPC框架,通过简单的提供者,消费者配置就能完成无感的网络调用。那么在dubbo中是如何将提供者的服务暴露出去,消费者又是如何获取到提供者相关信息的呢?


二、dubbo与spring的整合

在了解dubbo的服务注册和服务发现之前,我们首先需要掌握一个知识点:Spring中自定义Schema


三、Spring自定义Schema

Dubbo 现在的设计是完全无侵入,也就是使用者只依赖于配置契约。在 Dubbo 中,可以使用 XML 配置相关信息,也可以用来引入服务或者导出服务。

配置完成,启动工程,Spring 会读取配置文件,生成注入相关Bean。那 Dubbo 如何实现自定义 XML 被 Spring 加载读取呢 ?


从 Spring 2.0 开始,Spring 开始提供了一种基于 XML Schema 格式扩展机制,用于定义和配置 bean


入门案例

学习和使用Spring XML Schema 扩展机制并不难,需要下面几个步骤:

1、创建配置属性的JavaBean对象

2、创建Spring.xml文件

3、编写Spring.handlers文件配置所有部件

4、自定义处理器类,并实现NamespaceHandler接口

5、自定义解析器,实现BeanDefinitionParser接口(最关键的部分)

6、编写Spring.schemas文件配置所有部件

7、创建一个 XML Schema 文件,描述自定义的合法构建模块,也就是xsd文件


1、定义JavaBean对象,在spring中此对象会根据配置自动创建

    public class User {
        private String id;  
        private String name;  
        private Integer age;
        //省略getter setter方法
    }


2、定义spring.xml配置文件,并导入对应约束

      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      xmlns:context="http://www.springframework.org/schema/context" 
      xmlns:util="http://www.springframework.org/schema/util" 
      xmlns:task="http://www.springframework.org/schema/task" 
      xmlns:aop="http://www.springframework.org/schema/aop" 
      xmlns:tx="http://www.springframework.org/schema/tx"
      xmlns:itheima="http://www.itheima.com/schema/user"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
              http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
              http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
              http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd
              http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
              http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
      http://www.itheima.com/schema/user http://www.itheima.com/schema/user.xsd">

          <itheima:user id="user" name="zhangsan" age="12"></itheima:user>

      </beans>


3、编写Spring.handlers文件配置所有部件

定义spring.handlers文件,根据Spring.xml的xsi:schemaLocation="http://www.itheima.com/schema/user" 与NamespaceHandler类的对应关系;必须放在classpath下的META-INF文件夹中

    http\://www.itheima.com/schema/user=com.itheima.schema.UserNamespaceHandler


4、自定义处理器类,并实现NamespaceHandler接口

自定义UserNamespaceHandler类

    package com.itheima.schema;
    import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

    public class UserNamespaceHandler extends NamespaceHandlerSupport {
        public void init() {
            registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
        }
    }


5、自定义解析器,实现BeanDefinitionParser接口(最关键的部分)

BeanDefinitionParser是标签对应的解析器,Spring读取到对应标签时会使用该类进行解析;

      public class UserBeanDefinitionParser extends
              AbstractSingleBeanDefinitionParser {

          protected Class getBeanClass(Element element) {
              return User.class;
          }

          protected void doParse(Element element, BeanDefinitionBuilder bean) {
              String name = element.getAttribute("name");
              String age = element.getAttribute("age");
              String id = element.getAttribute("id");
              if (StringUtils.hasText(id)) {
                  bean.addPropertyValue("id", id);
              }
              if (StringUtils.hasText(name)) {
                  bean.addPropertyValue("name", name);
              }
              if (StringUtils.hasText(age)) {
                  bean.addPropertyValue("age", Integer.valueOf(age));
              }
          }
      }


6、编写Spring.schemas文件配置所有部件

定义spring.schemas文件,根据Spring.xml的xsi:schemaLocation="http://www.itheima.com/schema/user.xsd" 与 user.xsd文件对应关系;必须放在classpath下的META-INF文件夹中。

      http\://www.itheima.com/schema/user.xsd=META-INF/user.xsd


7、创建一个 XML Schema 文件,描述自定义的合法构建模块,也就是xsd文件

META-INF下定义user.xsd文件,使用xsd用于描述标签的规则

        <?xml version="1.0" encoding="UTF-8"?>  
        <xsd:schema   
            xmlns="http://www.itheima.com/schema/user"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"   
            xmlns:beans="http://www.springframework.org/schema/beans"  
            targetNamespace="http://www.itheima.com/schema/user"
            elementFormDefault="qualified"   
            attributeFormDefault="unqualified">  
            <xsd:import namespace="http://www.springframework.org/schema/beans" />  
            <xsd:element name="user">
                <xsd:complexType>  
                    <xsd:complexContent>  
                        <xsd:extension base="beans:identifiedType">  
                            <xsd:attribute name="name" type="xsd:string" />  
                            <xsd:attribute name="age" type="xsd:int" />  
                        </xsd:extension>  
                    </xsd:complexContent>  
                </xsd:complexType>  
            </xsd:element>  
        </xsd:schema>

编写测试类,通过spring容器获取对象user

    public class SchemaDemo {
        public static void main(String[] args) {
            ApplicationContext ctx = new ClassPathXmlApplicationContext("/spring/applicationContext.xml");
            User user = (User)ctx.getBean("user");
            System.out.println(user);
        }
    }


四、dubbo中的相关对象

Dubbo是运行在spring容器中,dubbo的配置文件也是通过spring的配置文件applicationContext.xml来加载,所以dubbo的自定义配置标签实现,其实同样依赖spring的xml schema机制

可以看出Dubbo所有的组件都是由DubboBeanDefinitionParser解析,并通过registerBeanDefinitionParser方法来注册到spring中最后解析对应的对象。这些对象中我们重点关注的有以下两个:

  • ServiceBean:服务提供者要暴露服务的核心对象

  • ReferenceBean:服务消费者发现服务的核心对象

  • RegistryConfig:定义注册中心的核心配置对象


五、服务暴露

前面主要探讨了 Dubbo 中 schema 、 XML 的相关原理 , 这些内容对理解框架整体至关重要 , 在此基础上我们继续探讨服务是如何依靠前面的配置进行服务暴露


六、名词解释

  • Invoker 是执行器,它是 Dubbo 的核心模型,其它模型都向它靠扰,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,

它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。在服务提供方,Invoker用于调用服务提供类。在服务消费方,Invoker用于执行远程调用

  • Protocol 是通信协议,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理

  • export:暴露远程服务

  • refer:引用远程服务

  • proxyFactory:获取一个接口的代理类

  • getInvoker:针对server端,将服务对象,如DemoServiceImpl包装成一个Invoker对象

  • getProxy:针对client端,创建接口的代理对象,例如DemoService的接口

  • Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等


七、整体流程

在详细探讨服务暴露细节之前 , 我们先看一下整体duubo的服务暴露原理

在整体上看,Dubbo 框架做服务暴露分为两大部分 , 第一步将持有的服务实例通过代理转换成 Invoker(执行器), 第二步会把 Invoker 通过具体的协议 ( 比如 Dubbo ) 转换成 Exporter(暴露器)


服务提供方暴露服务的蓝色初始化链,时序图如下:


八、源码分析


1、服务暴露的入口

ServiceBean 继承了InitializingBean 和 ApplicationEvent。 在Spring实例化完bean之后,进行容器ContextRefreshedEvent刷新,会回调ServiceBean的 onApplicationEvent方法中的export开启服务暴露

      public void onApplicationEvent(ContextRefreshedEvent event) {
          // 是否有延迟暴露 && 是否已暴露 && 是不是已被取消暴露
          if (isDelay() && !isExported() && !isUnexported()) {
              // 暴露服务
              export();
          }
      }

onApplicationEvent 方法在经过一些判断后,会决定是否调用 export 方法暴露服务。在export 根据配置执行相应的动作。最终进入到doExportUrls暴露服务方法

      private void doExportUrls() {
          // 加载注册中心链接
          List<URL> registryURLs = loadRegistries(true);
          // 遍历 protocols,并在每个协议下暴露服务
          for (ProtocolConfig protocolConfig : protocols) {
              doExportUrlsFor1Protocol(protocolConfig, registryURLs);
          }
      }

关于多协议多注册中心暴露服务首先是根据配置,以及其他一些信息组装 URL。前面说过,URL 是 Dubbo 配置的载体,通过 URL 可让 Dubbo 的各种配置在各个模块之间传递

      private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
          String name = protocolConfig.getName();
          // 如果协议名为空,或空串,则将协议名变量设置为 dubbo
          if (name == null || name.length() == 0) {
              name = "dubbo";
          }

          Map<String, String> map = new HashMap<String, String>();

          //略

          // 获取上下文路径
          String contextPath = protocolConfig.getContextpath();
          if ((contextPath == null || contextPath.length() == 0) && provider != null) {
              contextPath = provider.getContextpath();
          }

          // 获取 host 和 port
          String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
          Integer port = this.findConfigedPorts(protocolConfig, name, map);

          // 组装 URL
          URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);

          // 省略无关代码
      }

上面的代码首先是将一些信息,比如版本、时间戳、方法名以及各种配置对象的字段信息放入到 map 中,最后将 map 和主机名等数据传给 URL 构造方法创建 URL 对象。

前置工作做完,接下来就可以进行服务暴露了;服务暴露分为暴露到本地 (JVM),和暴露到远程


在深入分析服务暴露的源码前,我们先来从宏观层面上看一下服务导出逻辑,如下

    private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {

        // 省略无关代码
        String scope = url.getParameter(Constants.SCOPE_KEY);
        // 如果 scope = none,则什么都不做
        if (!Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {
            // scope != remote,暴露到本地
            if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
                exportLocal(url);
            }
            // scope != local,暴露到远程
            if (!Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope)) {
                if (registryURLs != null && !registryURLs.isEmpty()) {
                    for (URL registryURL : registryURLs) {
                        //省略无关代码

                        // 为服务提供类(ref)生成 Invoker
                        Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
                        // DelegateProviderMetaDataInvoker 用于持有 Invoker 和 ServiceConfig
                        DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

                        // 暴露服务,并生成 Exporter
                        Exporter<?> exporter = protocol.export(wrapperInvoker);
                        exporters.add(exporter);
                    }

                // 不存在注册中心,仅暴露服务
                } else {
                    //略
                }
            }
        }
        this.urls.add(url);
    }

上面代码根据 url 中的 scope 参数(这个参数实际是在《dubboSservice》标签上设置的)决定服务导出方式,分别如下:

  • scope = none,不暴露服务

  • scope != remote,暴露到本地

  • scope != local,暴露到远程


不管是暴露到本地,还是远程。进行服务暴露之前,均需要先创建 Invoker(目标方法执行器),这是一个很重要的步骤。

因此下面先来分析 Invoker 的创建过程。Invoker 是由 ProxyFactory 创建而来,Dubbo 默认的 ProxyFactory 实现类是 JavassistProxyFactory


下面我们到 JavassistProxyFactory 代码中,探索 Invoker 的创建过程,如下:

      public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
          // 为目标类创建 Wrapper
          final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
          // 创建匿名 Invoker 类对象,并实现 doInvoke 方法。
          return new AbstractProxyInvoker<T>(proxy, type, url) {
              @Override
              protected Object doInvoke(T proxy, String methodName,
                                        Class<?>[] parameterTypes,
                                        Object[] arguments) throws Throwable {
                  // 调用 Wrapper 的 invokeMethod 方法,invokeMethod 最终会调用目标方法
                  return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
              }
          };
      }

如上,JavassistProxyFactory 创建了一个继承自 AbstractProxyInvoker 类的匿名对象,并覆写了抽象方法 doInvoke


2、暴露服务到本地

Invoke创建成功之后,接下来我们来看本地暴露

    private void exportLocal(URL url) {
        // 如果 URL 的协议头等于 injvm,说明已经暴露到本地了,无需再次暴露
        if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
            URL local = URL.valueOf(url.toFullString())
                .setProtocol(Constants.LOCAL_PROTOCOL)    // 设置协议头为 injvm
                .setHost(LOCALHOST)
                .setPort(0);
            ServiceClassHolder.getInstance().pushServiceClass(getServiceClass(ref));
            // 创建 Invoker,并暴露服务,这里的 protocol 会在运行时调用 InjvmProtocol 的 export 方法
            Exporter<?> exporter = protocol.export(
                proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
            exporters.add(exporter);
        }
    }

exportLocal 方法比较简单,首先根据 URL 协议头决定是否暴露服务。若需暴露,则创建一个新的 URL 并将协议头、

主机名以及端口设置成新的值。然后创建 Invoker(目标执行器),并调用 InjvmProtocol export 方法暴露服务。


下面我们来看一下 InjvmProtocol export 方法都做了哪些事情

    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        // 创建 InjvmExporter
        return new InjvmExporter<T>(invoker, invoker.getUrl().getServiceKey(), exporterMap);
    }

如上,InjvmProtocol 的 export 方法仅创建了一个 InjvmExporter,无其他逻辑。到此暴露服务到本地就分析完了


3、暴露服务到远程

接下来,我们继续分析暴露服务到远程的过程。暴露服务到远程包含:服务暴露与服务注册两个过程。

先来分析服务暴露逻辑,我们把目光移动到 RegistryProtocol export 方法上

      public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
          // 暴露服务
          final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);

          // 获取注册中心 URL
          URL registryUrl = getRegistryUrl(originInvoker);

          // 根据 URL 加载 Registry 实现类,比如 ZookeeperRegistry
          final Registry registry = getRegistry(originInvoker);

          // 获取已注册的服务提供者 URL,比如:
          final URL registeredProviderUrl = getRegisteredProviderUrl(originInvoker);

          // 获取 register 参数
          boolean register = registeredProviderUrl.getParameter("register", true);

          // 向服务提供者与消费者注册表中注册服务提供者
          ProviderConsumerRegTable.registerProvider(originInvoker, registryUrl, registeredProviderUrl);

          // 根据 register 的值决定是否注册服务
          if (register) {
              // 向注册中心注册服务
              register(registryUrl, registeredProviderUrl);
              ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
          }

          // 获取订阅 URL,比如:
          final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl);

          // 创建监听器
          final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
          overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);

          // 向注册中心进行订阅 override 数据
          registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

          // 创建并返回 DestroyableExporter
          return new DestroyableExporter<T>(exporter, originInvoker, overrideSubscribeUrl, registeredProviderUrl);
      }

上面代码看起来比较复杂,主要做如下一些操作:

1、调用 doLocalExport 暴露服务

2、向注册中心注册服务

3、向注册中心进行订阅 override 数据

4、创建并返回 DestroyableExporter


下面先来分析 doLocalExport 方法的逻辑,如下:

      private <T> ExporterChangeableWrapper<T> doLocalExport(final Invoker<T> originInvoker) {
          String key = getCacheKey(originInvoker);
          // 访问缓存
          ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
          if (exporter == null) {
              synchronized (bounds) {
                  exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
                  if (exporter == null) {

                      // 创建 Invoker 
                      final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));

                      // 调用 protocol 的 export 方法暴露服务
                      exporter = new ExporterChangeableWrapper<T>((Exporter<T>) protocol.export(invokerDelegete), originInvoker);

                      // 写缓存
                      bounds.put(key, exporter);
                  }
              }
          }
          return exporter;
      }

接下来,我们把重点放在 Protocol 的 export 方法上。假设运行时协议为 dubbo,

此处的 protocol 变量会在运行时加载 DubboProtocol,并调用 DubboProtocol 的 export 方法

    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        URL url = invoker.getUrl();

        // 获取服务标识,理解成服务坐标也行。由服务组名,服务名,服务版本号以及端口组成。比如:
        // demoGroup/com.alibaba.dubbo.demo.DemoService:1.0.1:20880
        String key = serviceKey(url);

        // 创建 DubboExporter
        DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);

        // 将 <key, exporter> 键值对放入缓存中
        exporterMap.put(key, exporter);

        //省略无关代码

        // 开启Netty服务
        openServer(url);

        // 优化序列化
        optimizeSerialization(url);
        return exporter;
    }


4、 开启Netty服务

如上,我们重点关注 DubboExporter 的创建以及 openServer 方法,其他逻辑看不懂也没关系,不影响理解服务导出过程。下面分析 openServer 方法。

    private void openServer(URL url) {
        // 获取 host:port,并将其作为服务器实例的 key,用于标识当前的服务器实例
        String key = url.getAddress();
        boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
        if (isServer) {
            // 访问缓存
            ExchangeServer server = serverMap.get(key);
            if (server == null) {
                // 创建Netty服务器实例
                serverMap.put(key, createServer(url));
            } else {
                // 服务器已创建,则根据 url 中的配置重置服务器
                server.reset(url);
            }
        }
    }

接下来分析Netty服务器实例的创建过程,如下:

      private ExchangeServer createServer(URL url) {
          url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY,
          // 添加心跳检测配置到 url 中
          url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));

          // 获取 server 参数,默认为 netty
          String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER);

          // 通过 SPI 检测是否存在 server 参数所代表的 Transporter 拓展,不存在则抛出异常
          if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str))
              throw new RpcException("Unsupported server type: " + str + ", url: " + url);

          // 添加编码解码器参数
          url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
          ExchangeServer server;
          try {
              // 创建 ExchangeServer
              server = Exchangers.bind(url, requestHandler);
          } catch (RemotingException e) {
              throw new RpcException("Fail to start server...");
          }

          // 获取 client 参数,可指定 netty,mina
          str = url.getParameter(Constants.CLIENT_KEY);
          if (str != null && str.length() > 0) {
              // 获取所有的 Transporter 实现类名称集合,比如 supportedTypes = [netty, mina]
              Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions();

              // 检测当前 Dubbo 所支持的 Transporter 实现类名称列表中,
              // 是否包含 client 所表示的 Transporter,若不包含,则抛出异常
              if (!supportedTypes.contains(str)) {
                  throw new RpcException("Unsupported client type...");
              }
          }
          return server;
      }

如上,createServer 包含三个核心的逻辑。

第一是检测是否存在 server 参数所代表的 Transporter 拓展,不存在则抛出异常。

第二是创建服务器实例。第三是检测是否支持 client 参数所表示的 Transporter 拓展,不存在也是抛出异常

两次检测操作所对应的代码比较直白了,无需多说。但创建服务器的操作目前还不是很清晰,我们继续往下看

    public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }
        if (handler == null) {
            throw new IllegalArgumentException("handler == null");
        }
        url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange");
        // 获取 Exchanger,默认为 HeaderExchanger。
        // 紧接着调用 HeaderExchanger 的 bind 方法创建 ExchangeServer 实例
        return getExchanger(url).bind(url, handler);
    }

上面代码比较简单,就不多说了,下面看一下 HeaderExchanger 的 bind 方法

    public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
        // 创建 HeaderExchangeServer 实例,该方法包含了多个逻辑,分别如下:
        //   1. new HeaderExchangeHandler(handler)
        //   2. new DecodeHandler(new HeaderExchangeHandler(handler))
        //   3. Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler)))
        return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
    }

如上,getTransporter() 方法获取的 Transporter 是在运行时动态创建的,类名为 TransporterAdaptive,也就是自适应拓展类。

TransporterAdaptive 会在运行时根据传入的 URL 参数决定加载什么类型的 Transporter,默认为 NettyTransporter

调用NettyTransporter.bind(URL, ChannelHandler)方法。创建一个NettyServer实例。调用NettyServer.doOPen()方法,服务器被开启,服务也被暴露出来了


5、服务注册

本节内容以 Zookeeper 注册中心作为分析目标,其他类型注册中心大家可自行分析。下面从服务注册的入口方法开始分析,

我们把目光再次移到 RegistryProtocol 的 export 方法上。如下:

    public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {

        // ${暴露服务}

        // 省略其他代码

        boolean register = registeredProviderUrl.getParameter("register", true);
        if (register) {
            // 注册服务
            register(registryUrl, registeredProviderUrl);
            ProviderConsumerRegTable.getProviderWrapper(originInvoker).setReg(true);
        }

        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registeredProviderUrl);
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl, originInvoker);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        // 订阅 override 数据
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);

        // 省略部分代码
    }

RegistryProtocol 的 export 方法包含了服务暴露、注册、以及数据订阅等逻辑。其中服务暴露逻辑上一节已经分析过了,本节将分析服务注册逻辑,相关代码如下:

    public void register(URL registryUrl, URL registedProviderUrl) {

        // 获取 Registry
        Registry registry = registryFactory.getRegistry(registryUrl);

        // 注册服务
        registry.register(registedProviderUrl);
    }

register 方法包含两步操作:第一步是获取注册中心实例,第二步是向注册中心注册服务。接下来分两节内容对这两步操作进行分析。


这里以 Zookeeper 注册中心为例进行分析。下面先来看一下 getRegistry 方法的源码,这个方法由 AbstractRegistryFactory 实现,如下:

      public Registry getRegistry(URL url) {
          url = url.setPath(RegistryService.class.getName())
                  .addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName())
                  .removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY);
          String key = url.toServiceString();
          LOCK.lock();
          try {
              // 访问缓存
              Registry registry = REGISTRIES.get(key);
              if (registry != null) {
                  return registry;
              }

              // 缓存未命中,创建 Registry 实例
              registry = createRegistry(url);
              if (registry == null) {
                  throw new IllegalStateException("Can not create registry...");
              }

              // 写入缓存
              REGISTRIES.put(key, registry);
              return registry;
          } finally {
              LOCK.unlock();
          }
      }

      protected abstract Registry createRegistry(URL url);

如上,getRegistry 方法先访问缓存,缓存未命中则调用 createRegistry 创建 Registry


在此方法中就是通过new ZookeeperRegistry(url, zookeeperTransporter)实例化一个注册中心

    public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
        super(url);
        if (url.isAnyHost()) {
            throw new IllegalStateException("registry address == null");
        }

        // 获取组名,默认为 dubbo
        String group = url.getParameter(Constants.GROUP_KEY, DEFAULT_ROOT);
        if (!group.startsWith(Constants.PATH_SEPARATOR)) {
            // group = "/" + group
            group = Constants.PATH_SEPARATOR + group;
        }
        this.root = group;
        // 创建 Zookeeper 客户端,默认为 CuratorZookeeperTransporter
        zkClient = zookeeperTransporter.connect(url);

        // 添加状态监听器
        zkClient.addStateListener(new StateListener() {
            @Override
            public void stateChanged(int state) {
                if (state == RECONNECTED) {
                    try {
                        recover();
                    } catch (Exception e) {
                        logger.error(e.getMessage(), e);
                    }
                }
            }
        });
    }

在上面的代码代码中,我们重点关注 ZookeeperTransporter 的 connect 方法调用,这个方法用于创建 Zookeeper 客户端


创建好 Zookeeper 客户端,意味着注册中心的创建过程就结束了。接下来,再来分析一下 Zookeeper 客户端的创建过程。


九、总结,服务暴露的底层实现步骤


1、dubbo的配置文件也是通过spring的配置文件applicationContext.xml来加载

Dubbo是运行在spring容器中,dubbo的配置文件也是通过spring的配置文件applicationContext.xml来加载,所以dubbo的自定义配置标签实现,其实同样依赖spring的xml schema机制

可以看出Dubbo所有的组件都是由DubboBeanDefinitionParser解析,并通过registerBeanDefinitionParser方法来注册到spring中最后解析对应的对象。这些对象中我们重点关注的有以下两个:

  • ServiceBean:服务提供者要暴露服务的核心对象

  • ReferenceBean:服务消费者发现服务的核心对象

  • RegistryConfig:定义注册中心的核心配置对象


2、服务暴露初始化阶段

触发时机: 因为 ServiceBean 继承了InitializingBean 和 ApplicationEvent。 在Spring实例化完bean之后,进行容器ContextRefreshedEvent刷新,

会回调ServiceBean的 onApplicationEvent方法中的export开启服务暴露


3、多协议多注册中心处理

Dubbo支持同时向多个协议和多个注册中心暴露服务:


4、服务URL组装

构建服务的统一URL模型,包含所有配置参数:


5、本地暴露(Injvm协议)


6、远程暴露核心过程


(1)、创建Invoker(目标执行器)

将具体的服务类名,比如 DubboServiceRegistryImpl,通过 ProxyFactory 包装成 Invoker 实例


(2)、根据不同协议Protocol.export() 暴露服务

以Dubbo协议为例:


(3)、启动Netty服务器监听客户URL请求


(4)、服务端Handler处理


7、注册服务到注册中心


(1)、ZooKeeper节点创建

Dubbo在ZooKeeper上创建的节点结构:


8、服务暴露的完整时序图

posted @ 2025-04-13 12:22  jock_javaEE  阅读(196)  评论(0)    收藏  举报