服务暴露与发现


服务暴露与发现

概述

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

Spring中自定义Schema

Dubbo 现在的设计是完全无侵入,也就是使用者只依赖于配置契约。在 Dubbo 中,可以使用 XML 配置相关信息,也可以用来引入服务或者导出服务。配置完成,启动工程,Spring 会读取配置文件,生成注入相关Bean。那 Dubbo 如何实现自定义 XML 被 Spring 加载读取呢?

从 Spring 2.0 开始,Spring 开始提供了一种基于 XML Schema 格式扩展机制,用于定义和配置bean。也就是约束那个xml的格式,内容等。

案例使用

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

  1. 创建配置属性的JavaBean对象
  2. 创建一个 XML Schema 文件,描述自定义的合法构建模块,也就是xsd文件。
  3. 自定义处理器类,并实现 NamespaceHandler 接口。
  4. 自定义解析器,实现 BeanDefinitionParser 接口(最关键的部分)。
  5. 编写Spring.handlers和Spring.schemas文件配置所有部件

以下为案例演示:
项目结构:
在这里插入图片描述

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

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

②在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"> 
                <!-- 此处就约定了user的属性-->
                    <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读取xml文件时,会根据标签的命名空间找到其对应NamespaceHandler,我们在NamespaceHandler内会注册标签对应的解析器BeanDefinitionParser


public class UserNamespaceHandler extends NamespaceHandlerSupport {
    public void init() {
        /***
         *  user.xsd文件中 name="user"
         *  解析user节点
         */
        registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
    }
}

④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));
        }
    }
}

⑤定义spring.handlers文件,内部保存命名空间与NamespaceHandler类的对应关系;必须放在classpath下的META-INF文件夹中。

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

⑥定义spring.schemas文件,内部保存命名空间对应的xsd文件位置;必须放在classpath下的META-INF文件夹中。

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

⑦代码准备好了之后,就可以在spring工程中进行使用和测试,定义spring配置文件,导入对应约束

<?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">

    <!--    创建了一个对象,并交给SpringIOC容器管理-->
    <itheima:user id="user" name="zhangsan" age="12"></itheima:user>

</beans>

⑧编写测试类,通过spring容器获取对象user
在这里插入图片描述

dubbo中的相关对象

Dubbo是运行在spring容器中,dubbo的配置文件也是通过spring的配置文件applicationContext.xml来加载,所以dubbo的自定义配置标签实现,其实同样依赖spring的xml schema机制。
查看DubboNamespaceHandler的源码:
在这里插入图片描述
可以看出Dubbo所有的组件都是由 DubboBeanDefinitionParser解析,并通过registerBeanDefinitionParser方法来注册到spring中,最后解析对应的对象。这些对象中我们重点关注的有以下几个:

  • ServiceBean:服务提供者暴露服务的核心对象
  • ReferenceBean:服务消费者发现服务的核心对象
  • RegistryConfig:定义注册中心的核心配置对象

服务暴露机制

术语解释

在 Dubbo 的核心领域模型中:

  • Invoker 是实体域,简单来说,在消费方向服务方发起请求的这个途中就叫invoker,对服务提供方来说在执行调用本地方法的这个过程就是invoker。
    在这里插入图片描述

  • Protocol 是服务域,它是 Invoker 暴露和引用的主功能入口,它负责 Invoker 的生命周期管理。

    • export:暴露远程服务
    • refer:引用远程服务
  • proxyFactory:获取一个接口的代理类

    • getInvoker:针对server端,将服务对象,如DemoServiceImpl包装成一个Invoker对象
    • getProxy:针对client端,创建接口的代理对象,例如DemoService的接口。
  • Invocation 是会话域,它持有调用过程中的变量,比如方法名,参数等

流程机制

在详细探讨服务暴露细节之前 , 我们先看一下整体duubo的服务暴露原理
请添加图片描述
在整体上看,Dubbo 框架做服务暴露分为两大部分 , 第一步将持有的服务实例通过代理转换成Invoker, 第二步会把 Invoker 通过具体的协议 ( 比如 Dubbo ) 转换成 Exporter, 框架做了这层抽象也大大方便了功能扩展 。

服务提供方暴露服务的蓝色初始化链,时序图如下:
请添加图片描述

源码分析

(1) 导出入口
服务导出的入口方法是 ServiceBeanonApplicationEvent。onApplicationEvent 是一个事件响应方法,该方法会在收到 Spring 上下文刷新事件后执行服务导出操作。方法代码如下:
在这里插入图片描述
onApplicationEvent 方法在经过一些判断后,会决定是否调用 export 方法导出服务。在export 根据配置执行相应的动作。最终进入到doExportUrls导出服务方法:
在这里插入图片描述
关于多协议多注册中心导出服务首先是根据配置,以及其他一些信息组装 URL。前面说过,URL 是Dubbo 配置的载体,通过 URL 可让 Dubbo 的各种配置在各个模块之间传递。我们来看核心方法doExportUrlsFor1Protocol()
在这里插入图片描述

上面的代码首先是将一些信息,比如版本、时间戳、方法名以及各种配置对象的字段信息放入到 map中,最后将 map 和主机名等数据传给 URL 构造方法创建 URL 对象。前置工作做完,接下来就可以进行服务导出了。服务导出分为导出到本地 (JVM),和导出到远程。在深入分析服务导出的源码前,我们先来从宏观层面上看一下服务导出逻辑。如下:(依旧还是doExportUrlsFor1Protocol()的源码在这里插入图片描述

上面代码根据 url 中的 scope 参数决定服务导出方式,分别如下:

  • scope = none,不导出服务
  • scope != remote,导出到本地
  • scope != local,导出到远程

不管是导出到本地,还是远程。进行服务导出之前,均需要先创建 Invoker,这是一个很重要的步骤。因此下面先来分析 Invoker 的创建过程。Invoker 是由 ProxyFactory 创建而来,Dubbo 默认的ProxyFactory 实现类是JavassistProxyFactory。下面我们到 JavassistProxyFactory 代码中,探索Invoker 的创建过程。如下:
在这里插入图片描述
如上,JavassistProxyFactory 创建了一个继承自 AbstractProxyInvoker 类的匿名对象,并覆写了抽象方法 doInvoke。

(2) 导出服务到本地
Invoke创建成功之后,接下来我们来看本地导出。也就是ServiceConfig类中的exportLocal()
在这里插入图片描述
exportLocal 方法比较简单,首先根据 URL 协议头决定是否导出服务。若需导出,则创建一个新的 URL并将协议头、主机名以及端口设置成新的值。然后创建 Invoker,并调用 InjvmProtocol 的 export 方法导出服务。下面我们来看一下 InjvmProtocol的 export 方法都做了哪些事情。
在这里插入图片描述
如上,InjvmProtocol 的 export 方法仅创建了一个 InjvmExporter,无其他逻辑。到此导出服务到本地就分析完了。

总结

  1. 在有注册中心,需要注册提供者地址的情况下,ServiceConfig 解析出的 URL 格式为:registry:// registry-host/org.apache.dubbo.registry.RegistryService?export=URL.encode("dubbo://service-host/{服务名}/{版本号}")
  2. 基于 Dubbo SPI 的自适应机制,通过 URL registry:// 协议头识别,就调用
    RegistryProtocol#export() 方法
    1. 将具体的服务类名,比如 DubboServiceRegistryImpl ,通过 ProxyFactory 包装成Invoker 实例
    2. 调用 doLocalExport 方法,使用 DubboProtocol 将 Invoker 转化为 Exporter 实例,并打开Netty 服务端监听客户请求
    3. 创建 Registry 实例,连接 Zookeeper,并在服务节点下写入提供者的 URL 地址,注册服务
    4. 向注册中心订阅 override 数据,并返回一个 Exporter 实例

服务发现

服务发现流程

在详细探讨服务暴露细节之前 , 我们先看一下整体duubo的服务消费原理
请添加图片描述
在整体上看 , Dubbo 框架做服务消费也分为两大部分 , 第一步通过持有远程服务实例生成Invoker,这个 Invoker 在客户端是核心的远程代理对象 。 第二步会把 Invoker 通过动态代理转换成实现用户接口的动态代理引用 。
服务消费方引用服务的蓝色初始化链,时序图如下:
请添加图片描述

总结

  1. 从注册中心发现引用服务:在有注册中心,通过注册中心发现提供者地址的情况下,ReferenceConfig 解析出的 URL 格式为: registry://registryhost:/org.apache.registry.RegistryService?refer=URL.encode("conumerhost/com.foo.FooService?version=1.0.0")
  2. 通过 URL 的registry://协议头识别,就会调用RegistryProtocol#refer()方法
    1. 查询提供者 URL,如 dubbo://service-host/com.foo.FooService?version=1.0.0 ,来 获取注册中心
    2. 创建一个 RegistryDirectory 实例并设置注册中心和协议
    3. 生成 conusmer 连接,在 consumer 目录下创建节点,向注册中心注册
    4. 注册完毕后,订阅 providers,configurators,routers 等节点的数据
    5. 通过 URL 的 dubbo:// 协议头识别,调用 DubboProtocol#refer() 方法,创建一个ExchangeClient 客户端并返回 DubboInvoker 实例
  3. 由于一个服务可能会部署在多台服务器上,这样就会在 providers 产生多个节点,这样也就会得到多个 DubboInvoker 实例,就需要 RegistryProtocol 调用 Cluster 将多个服务提供者节点伪装成一个节点,并返回一个 Invoker
  4. Invoker 创建完毕后,调用 ProxyFactory 为服务接口生成代理对象,返回提供者引用

文章作者: fFee-ops
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 fFee-ops !
评论
  目录