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  阅读(129)  评论(0)    收藏  举报