Dubbo服务引入

转自:https://www.yuque.com/docs/share/10960cb9-449b-4177-94b6-493b8f4ff9b9?

dubbo

一、引言

服务引入是rpc调用不可或缺的一部分,本文会围绕以下几个问题对服务引入进行讲解。

  • 什么是服务引入?服务引入需要做什么事情?
  • spring作为当前java项目几乎必备的框架,如何将服务引入切入spring?
  • 服务引入如何做到降低对业务代码的侵入性?
  • 不同的提高方可能支持不同的通信协议,作为一个rpc框架怎么做到让调用端可以针对不同提高端使用不同的通信协议?甚至支持配置自定义协议?
  • 服务提供者可能出现部分宕机的情况,如何保证引入服务的可用性?

本文以Dubbo的实现进行分析,因为日常项目中常常是配合Spring使用的,所以本文会以Dubbo结合Spring的使用进行分析。

二、解析

2.1 概况

2.1.1 什么是服务引入?

RPC(Remote Procedure Call) 即远程过程调用,包含了两个最基本的角色,服务提供者和服务消费者。

既然是远程过程调用,那么消费者在调用提供者之前就得先拿到服务提供者的服务信息(比如最基本的提供者的服务地址),并对调用信息进行封装准备好,我们才能对提供者发起调用请求。

而服务引入就是引入提供者服务信息,并封装调用类的一个过程。

2.1.2 服务引入类型

服务引用****有三种方式

(1) 本地引入

一个服务即可以提供者,同时也可以是消费者。

所以会存在消费者消费的服务,同时也是当前服务提供的服务,如图所示:

image.png

图 2.1

针对这种情况,RPC框架应该避免发起网络请求,直接本地调用。封装的调用类应当通过本地导出的服务发起调用。

(2) 直接服务

直接服务指的是直接在消费端指定提供端的服务地址。

如图直接将提供者url由消费端直接指定,发起调用时直接根据配置好的url发起调用。
image.png

图 2.2

优点:方便测试,直接连接服务,不依赖第三方。

缺点:存在服务可用性问题,也不能动态伸缩服务,不建议在线上使用。

(3) 基于注册中心引入

为了避免提高可用性,引入了注册中心。服务提供者将服务信息注册到注册中心,消费者订阅注册中心获取提供者url、提供者配置等服务信息,根据获取到的服务信息封装调用类发起调用。如图所示:

image.png

图 2.3

优点:提供者服务不可用时自动删除提供者信息,重启时自动恢复,可以动态伸缩服务。

缺点:相比直接引用,需要依赖服务中心,且需要保证注册中心的可用性。

2.2 切入spring

spring框架作为当前java项目几乎必不可少的框架,如何接入spring也是一个RPC框架需要考虑的点。

那么Dubbo是怎么接入的spring的呢?

在spring项目中引入Dubbo服务,只需要配置<****dubbo****:reference/>标签,然后依赖注入提供者就可以实现RPC调用。

那么spring项目启动的时候,dubbo是如何做到让<****dubbo****:reference/>标签被spring识别并解析呢?

2.2.1 切入入口

Spring启动时ClassPathXmlApplicationContext会对引入的配置文件进行解析,并将bean注入到spring容器中。

但是spring并不认识第三方自定义的标签,为了支持解析外部自定义的标签,Spring提供了扩展点,会通过查找classPath下所有 spring.handlers 文件,从该文件中获取所有扩展的命名空间处理器。也就是获取外部的标签处理器。

文件中的内容需要以键值对的方式表示,NamespaceUrl为key,value为解析器。spring在解析到某个外部标签时,会以外部标签的NamespaceUrl为key,获取对应的解析器,解析该xml标签。

image.png

图 2.4

而Dubbo就是通过这种方式去扩展,如图2.4所示,我们可以看到在Dubbo包下在 META-INF/spring.handlers 文件中,以dubbo标签的NamespaceUrl为key,解析处理器全限定名为value存储。Spring解析到dubbo标签时会通过dubbo提供的命名空间处理器DubboNamespaceHandler进行解析。

在Spring中spring.handlers文件最终由spring的DefaultNamespaceHandlerResolver加载并保存到标签处理器****集合handlerMappings中。获取handlerMappings源码如下:

image.png

图 2.5

如图2.5所示getHandllerMapping将DubboNamespaceHandler加载进了handlerMappings 中。

spring加载spring.handlers调用时序图如下:

image.png

图 2.6

由图2.6可知,Spring最终获取到解析xml标签的处理器之后,调用处理器的init方法、paser方法,最终获取到一个BeanDefinition注册到spring容器中。

(spring要求自定义命名空间处理器要实现NamespaceHandler接口,因此都会init方法和parse方法

2.2.2 切入细节

我们已经的得知切入Spring的入口,那么作为一个RPC框架,应该如何去实现这个命名空间解析器,如何将标签转换成spring的BeanDefinition

针对Dubbo的分析,我们已经得知dubbo标签最终会由DubboNamespaceHandler进行解析,并且最终Spring会调用命名空间解析器init方法和parse方法,最终转换成spring的BeanDefinition。那么DubboNamespaceHandler这两步做了什么事情?

DubboNamespaceHandler类图如下:

image.png

(1)init 方法

一个完整的RPC服务不止是包含服务引入,同时也是还有协议定义、服务导出、注册中心等等模块,而RPC框架就得针对不同的模块定义不同的声明标签。那么在正式解析之前,就得先将不同模块的解析区分开,Spring也考虑了这一点,提供了NamespaceHandler接口的实现抽象类NamespaceHandlerSupport,该类提供了针对不同模块的标签进行注册的方法,实际上就是以模块名为key,对应的解析器为value存储在一个Map集合。

init顾名思义就是做一些初始化操作的,Dubbo就选择在init方法中注册不同模块的标签解析器。

DubboNamespaceHandler#init 源码如下

public class DubboNamespaceHandler extends NamespaceHandlerSupport {

    static {
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }
    public void init() {
        registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
        registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
        registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
        registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
        registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
        registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
        registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
        // 服务引入标签解析 指定BeanDefinition BeanClass 为 ReferenceBean
        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
        registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
    }
}

代码块 2.1

如代码块2.2.1所示,DubboNamespaceHandler 实现了NamespaceHandler 的init方法,将各类型标签的对应的解析器注册到解析器集合Map中。并且可以看出dubbo每个标签都是通过DubboBeanDefinitionParser进行解析,只是指定解析后BeanDefinitionbeanClass不同。

(2) parse方法

parse也就是解析标签的方法,已经分析过Dubbo会注册不同标签的解析器,那么可以猜想parse方法会根据不同标签取出对应的解析器,再进行解析

NamespaceHandlerSuppor****t#parse 源码如下:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 根据标签名从解析器map拿出解析bean,根据解析bean的parse方法进行解析
    return findParserForElement(element, parserContext)
           .parse(element, parserContext);
}
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    // 根据标签名从解析器map拿出解析bean
    String localName = parserContext.getDelegate().getLocalName(element);
    BeanDefinitionParser parser = this.parsers.get(localName);
    if (parser == null) {
        parserContext.getReaderContext().fatal(
            "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
    }
    return parser;
}

代码块 2.2

如代码块2.2所示,根据标签名,取出对应的解析器,再通过解析器DubboBeanDefinitionParser进行解析。

已经拿到对应的解析器后,下一步就是将标签解析处理成BeanDefinition并注册到spring容器。

解析标签创建BeanDefinition,需要指定实际引用类beanClass, 保存标签类的各种配置信息,再指定beanName将BeanDefinition注册到spring容器

DubboBeanDefinitionParser 通过parse方法进行解析xml标签,部分源码如下:

public BeanDefinition parse(Element element, ParserContext parserContext) {
        // beanClass 即为Dubbo在init创建DubboNamespaceHandler时指定beanClass.
        return parse(element, parserContext, beanClass, required);
}
private static BeanDefinition parse(Element element, ParserContext parserContext, Class<?> beanClass, boolean required) {
        RootBeanDefinition beanDefinition = new RootBeanDefinition();
        // 配置RootBeanDefinition bean类型
        // 比如服务引用类型 则为ReferenceBean,id属性即为引用的bean名
        beanDefinition.setBeanClass(beanClass);
        String id = element.getAttribute("id");
        ...
        if (id != null && id.length() > 0) {
            if (parserContext.getRegistry().containsBeanDefinition(id))  {
                throw new IllegalStateException("Duplicate spring bean id " + id);
            }
            // 以id作为bean名注册beanDefinition 到spring容器
            parserContext.getRegistry().registerBeanDefinition(id, beanDefinition);
            beanDefinition.getPropertyValues().addPropertyValue("id", id);
        }
    
        ... 一系列配置bean过程
        ... 将标签上配置的参数存到beandefinition的PropertyValues属性中
 
        return beanDefinition;
}

代码块 2.3

以服务引用标签为例子,beanClass为ReferenceBean。

根据代码块2.3,DubboBeanDefinitionParser 通过 parse方法进行解析获取到beanClass为ReferenceBeanBeanDefinition,注册到spring容器并返回解析结果。

ps:

从该源码还可以看到,在解析的时候,发现spring容器中已经包含这个bean名,那么会报错重复的bean id,因此****dubbo****:reference标签指定的id不能是已经存在spring容器中的bean名

2.2.3 结论

基于Dubbo在服务引入切入Spring的方式,我们可以得到一下结论:

  • RPC框架服务引入切入spring可以通过spring提供的xml解析扩展点spring.handlers,对xml解析进行了扩展。
  • 框架定义的解析器,可以基于NamespaceHandlerSupport在init针对不同标签注册不同的自定义解析器,NamespaceHandlerSupport#****parse方法会去根据不同标签获取对应的自定义解析器。
  • 自定义解析器parse方法需要创建BeanDefinition,保存标签配置信息,注册BeanDefinition到Spring容器

2.3 服务引入实现分析

服务引入需要根据配置信息封装服务调用类,作为一个RPC框架还需要考虑如何支持区分多种服务引入类型,如何避免服务引入对业务代码的侵入,如何提高服务引入的可扩展性,支持用户自己指定引入服务调用使用的协议。

本节通过分析Dubbo的实现,解析Dubbo是怎么处理的。

根据2.2节的分析,xml解析只是将引入的bean转化成BeanDefinition,保存了配置一些信息,并没有去封装一个服务调用类。Dubbo的服务引用标签<reference>标签最终会被spring解析成ReferenceBean类型的BeanDefinition,并将标签上配置的参数注入到BeanDefinition(比如引用服务的权限定名),并加载到bean容器中。

我们根据以下几点做下分析

  • ReferenceBean是什么时候真正去封装服务调用类的
  • 我们依赖注入的是引用类, 为什么beanClass是ReferenceBean
  • 为什么我们直接注入引用类就可以关联上提供者类,并发起rpc调用

ReferenceBean类图如下:

image.png

图 2.7

2.3.1 封装调用类时机

(1)懒汉式

懒汉式即用到ReferenceBean这个引入服务被用到才去封装。

由图2.7可以看到ReferenceBean实现了FactoryBean,因此当我们依赖ReferenceBean的时候,spring会调用getObject()方法去获取真实的bean。

ReferenceBean#getObject部分源码如下:

public Object getObject() throws Exception {
    return get();
}
public synchronized T get() {
     if (destroyed){
         throw new IllegalStateException("Already destroyed!");
     }
     // ReferenceBean 全局变量ref 代表这个引用bean真实的业务bean
     if (ref == null) {
         init();
     }
     return ref;
}
 private void init() {
     // 做一些创建代理类前的校验和配置操作(比如校验接口合法性、封装URL参数到map(比如interface=com.xxService))
     ...
     // 根据参数map调用封装调用类
     // 根据调用类创建代理类,将代理类赋予ref变量   
     ref = createProxy(map);
     ...
}

代码块 2.4

可以看到这个方法就是调用init()方法初始化,并将引用ref返回, 也就是当引入服务被依赖到的时候,会去封装调用类。

(2)****饿汉式

饿汉式即引用类没有被依赖也会

ReferenceBean实现了InitializingBean,因此初始化ReferenceBean时,Spring容器会调用 ReferenceBean的afterPropertiesSet方法。

ReferenceBean部分源码如下:

 public void afterPropertiesSet() throws Exception {
     ····
         装载 监控器、注册中心信息、应用配置信息、消费端配置信息等等
     ····    
     Boolean b = isInit();
     if (b == null && getConsumer() != null) {
         b = getConsumer().isInit();
     }
     if (b != null && b.booleanValue()) {
         getObject();
     }
 }

代码块 2.5

通过代码块2.5 我们可以看到,afterPropertiesSet 方法主要做了一些初始化操作,最后判断是否初始化bean, 如果需要则会调用init()方法初始化bean,赋予ref变量。

ps:

默认是关闭状态,即不会开启。需要初始化可通过配置<dubbo:reference> 的 init 属性开启。

2.3.2 调用类封装细节

服务引入需要封装调用类,需要做哪些事情?

  1. 获取提供端地址。封装调用类,首先要知道提供者的地址,并且因为有多种类型的服务引入,得区分多种服务引入方式的服务地址。
  2. 获取传输协议。不同的提供者可能支持的传输协议不一致,因此需要获取传输协议类型。
  3. 根据获取到的配置信息创建调用类。
  4. 封装代理类。为了避免造成代码侵入,不能让业务代码直接依赖框架封装的调用类,所以需要支持让业务代码可以直接依赖提供端。那么RPC框架就需要根据提供端和调用类封装一个代理类。

2.3.2.1 获取提供端地址

前面我们讲过,服务引入分为三种类型(本地引入、直接引入、基于注册中心引入),三种引入类型获取的提供端地址也不同。

  • 本地引入

判断是否为本地调用,如果是本地调用则根据提供端信息拼接URL,格式为injvm:127.0.0.1:0/com.service?param

判断是否为本地调用流程图如下:

image.png

图 2.8

通过流程图我们可以看到,如果ReferenceBean指定的inJvm=ture或者scope=local则认为是本地调用(通过标签配置)。

否则如果作用域没有指定remote、并且不是泛化调用、并且本地暴露的服务包含该服务才认为是本地引用。

  • 直接引用

如果判断不是本地调用,则判断是否存在直接引用地址(通过标签的url指定)。

如果是存在直接引用URL,假设配置的URL是dubbo协议的,则url的格式为 dubbo://service-host/com.service?param

因为直接引用也可能是配置注册中心地址

因此Dubbo判断是直接引用是registry前缀的地址,则会加上refer参数,标示实际调用哪个提供者,如下。

  • 基于注册中心引用

如果没有指定引用URL,则会通过加载注册中心地址,获取到注册中心的地址集合,URL的格式为

registry://registry-host/org.apache.dubbo.registry.RegistryService?refer=URL.encode("consumer://host/com.Service?version=1.0.0")

即地址为注册中心地址,refer参数为实际引用的提供者

需要注意的是,直接引用都是可能配置多个地址的,而通过注册中心获取也可能会获取到多个提供端地址,因此获取到的地址可能是多个的。

2.3.2.2 获取传输协议

Dubbo针对不同协议都封装了对应的Protocol类,因此本节分析Dubbo如何根据当前传输协议获取对应的Protocol类。

截取ReferenceBean Protocol的获取如下,

private static final Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

我们可以看到ReferenceBean中并没有固定创建某一个ReferenceBean的实现类,那么Dubbo是怎么做到根据不同传输协议获取对应Protocol的呢?

分析getAdaptiveExtension()的实现,该方法最终创建了一个Protocol的代理对象,由该代理对象来根据当前传输URL获取对应的Protocol。

该方法内部根据代理的对象类型(比如:Protocol)动态拼接java代码,动态拼接code生成自适应扩展对象,并动态编译,通过类加载器加载到jvm中,返回代理对象。

流程图如下:

image.png

图 2.9

Protocol为例子,拼接后的java代码如下:

package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
    // 不被代理的方法 如果被调用直接报错
    public void destroy() {throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
    public int getDefaultPort() {throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
    // 代理导出方法
    public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.Invoker {
        if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }
    public void destroyServer() {throw new UnsupportedOperationException("method public default void com.alibaba.dubbo.rpc.Protocol.destroyServer() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
    // 代理引入方法
    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws java.lang.Class {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        // 获取url参数
        com.alibaba.dubbo.common.URL url = arg1;
        // 获取url上配置的协议
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        // 根据协议名称 从dubbo容器中获取对应的协议类
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        // 根据协议类创建调用对应的服务引入方法
        return extension.refer(arg0, arg1);
    }
}

dubbo就是通过动态拼接java code,在运行时生成自适应扩展bean,由这个bean来获取ReferenceBean配置的protocol和cluster对应的实现类.

动态拼接code逻辑如下:

image.png

图 2.10

主要思想:

通过@Adaptive注解表明哪些方法需要被代理,被代理的方法都要能提供URL参数,代理对象会根据URL以被代理类为key,获取对应参数值,从而返回对应的实现类的bean名,再通过bean名从Dubbo容器中获取对应的实现累。如果URL上没有指明用哪个实现类,则用@SPI注解上的值为key获取对应的实现类。

以Protocol为例,源码如下:

@SPI("dubbo")
public interface Protocol {
    int getDefaultPort();
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
    void destroy();
    void destroyServer();
}

代码块 2.6

export和refer方法都被@Adaptive修饰,因此这两个方法。生成的代理类从URL上获取以protocol为名的参数值作为key,从dubbo容器中取出对应的实现类去执行。如果没有获取到则以@SPI上的值dubbo作为key取出对应的protocol。即默认得到了DubboProtocol。

ps

同样通过这个机制,也进而得以支持Spi扩展,可以在运行时才确认使用哪个实现类,方便外部扩展。比如我新增一个自定义协议MyProtocol,配置指定服务引入的协议为myProtocol, 并将这个协议对应的实现类基于dubbo的spi扩展注入到Dubbo容器中。那么代理类就可以根据URL上的协议名,基于spi获取对应的协议实现类,再根据调用类调用refer方法。

2.3.2.3 封装调用类

获取到提供者的url和传输的协议对象后,就开始封装调用对象了。

封装调用对象需要考虑做几个事情

  • 先前基于注册中心引入服务的地址,并非最终发起调用的协议和地址,而是以注册中心地址为路径,提供端地址为参数组合。因此需要区分开来,封装真正的调用地址。并且registry协议并非真正传输协议,只是标识是注册中心引入,封装调用类还得替换成真正的传输协议,比如dubbo协议。
  • 由于提供者有可能有多个提供者,因此需要考虑如何将多个提供者封装成一个调用者,发起调用时如何处理。
  • 为了便于知道消费端消费情况,消费端也需要将消费的服务注册到注册中心。并且为了在提供者发生变动时收到通知,还需要订阅提供者的节点数据。

截取ReferenceConfig#createProxy封装调用类invoke源码如下:

// 截取部分注册中心和直接引用获取到URL集合后的代码
if (urls.size() == 1) {
    invoker = refprotocol.refer(interfaceClass, urls.get(0));
} else {
    ....
    // 遍历多个url 生成invoker集合
    for (URL url : urls) {
        invokers.add(refprotocol.refer(interfaceClass, url));
    }
     if (registryURL != null) { 
         // 指定Cluster为AvailableCluster 选择任意可用的服务
         // 如果是注册中心则说明当前遍历的是注册中心地址,所以使用AvailableCluster封装invoke
         URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME);
         // 基于Cluster合并多个invoker 基于集群容错策略调用
         invoker = cluster.join(new StaticDirectory(u, invokers));
     } else {
         invoker = cluster.join(new StaticDirectory(invokers));
     }
}

代码块 2.7

如代码块2.7所示,只有一个URL时直接通过协议类封装一个invoker对象,如果有多个URL(即多个服务提供者)则通过Cluster封装多个,后续基于集群容错策略做调用(关于Cluster相关本文不做讲解,属于负载均衡处理模块的范畴)。

Protocol为例,调用该代理对象refer方法,代理对象会解析refer方法传入的URL不同的协议获取到不同的Protocol实现类,通过对应协议类****Protocol创建对应invoke实现类,不同协议类会创建不同的invoke类。

关于refer方法的实现,本地引入和直接引入都是直接根据URL、interfaceClass创建Invoke实现类, 重点讲一下RegistryProtocol。

RegistryProtocol的refer方法,截取关键源码如下:

public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    // 修改url的协议内容 根据参数配置的协议进行修改,如果没有配置默认为dubbo
    url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
    // 连接注册中心
    Registry registry = registryFactory.getRegistry(url);
    ...
    return doRefer(cluster, registry, type, url);
}
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
    directory.setRegistry(registry);
    directory.setProtocol(protocol);
    // 生成消费服务地址 如consumer:consumer-host/com.xxService?param  
    // com.xxService表示消费的提供者全限制定名
    URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, NetUtils.getLocalHost(), 0, type.getName(), directory.getUrl().getParameters());
    ...
    // 在注册中心消费者目录注册消费端地址  
    // 例如:在zk注册消费端地址的目录为 /分组名/服务权限定名/consumers/subscribeUrl
    registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
                                                     Constants.CHECK_KEY, String.valueOf(false)));
    // 订阅注册中心节点数据 providers、routers、configurators
    // 订阅的时候会
    directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY, Constants.PROVIDERS_CATEGORY + 
                                                  "," + Constants.CONFIGURATORS_CATEGORY + "," + Constants.ROUTERS_CATEGORY));
    // 这里使用cluster是因为注册中心可能会有多个提供者,因此返回的invoke是具备选择提供者能力的invoke
    return cluster.join(directory);
}

代码块 2.8

如代码块2.8所示,RegistryProtocol的refer方法主要是做了几个事情

  • 将原本url为registry协议,修改成真正发起调用使用的协议,默认为dubbo。
  • 连接注册中心,创建注册中心实例。
  • 注册消费端地址。
  • 订阅注册中心提供端和配置数据。
  • 基于RegistryDirectory和cluster创建invoke对象返回。

思考:

  1. 当多注册中心和多提供者时,怎么选择注册中心和怎么选提供者流程是怎么设计?
  2. 注册中心最大的作用是可以提高服务调用的可用性,某个提供者服务挂了之后,自动下线该提供者,避免调用到该提供者,或者流量增大服务无法应对,动态扩容提供者。那么如何在提供者变动时去更新invoke.
image.png

图 2.10

针对第一点,dubbo基于多个注册中心url生成invoke集合,再通过StaticDirectory包装,cluster固定使用****AvailableCluster进行选择任意可用的节点(代码块2.7)。获取到clusterInvoke之后,再根据cluster策略(服务引入配置)选择一个提供者。

针对第二点,dubbo处理某个注册中心url时,返回的是通过RegistryDirectory与cluster创建的invoke,RegistryDirectory会监听注册中心的通知,动态更新提供者集合。

dubbo基于RegistryDirectory订阅注册中心,订阅的时候会将当前RegistryDirectory作为监听器,当订阅的节点发生变动的时候就会通知RegistryDirectory更新invoke集合notity方法源码截取如下

public synchronized void notify(List<URL> urls) {
    // 1. 根据URL的协议类型封装各种订阅URL集合
    // 2. 更新configurators URL
    // 3. 更新routers URL
    ...
    // 4. 更新providers URL
    refreshInvoker(invokerUrls);
}
    
private void refreshInvoker(List<URL> invokerUrls){
    ...
    // 将URL列表转成Invoker列表    
    Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls) ;
    ...
    this.urlInvokerMap = newUrlInvokerMap;
    // 关闭未使用的Invoker 
    destroyUnusedInvokers(oldUrlInvokerMap,newUrlInvokerMap);
}

代码块 2.10

2.3.2.4 创建代理类

根据引用的服务类和invoke对象生成代理对象返回。

// ReferenceConfig源码 代理工厂代理类,根据url获取对应代理类
private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
proxyFactory.getProxy(invoker);

代码块 2.9

默认代理方式支持Javassist和Jdk代理,可以通过引用标签配置proxy指定。

与上面讲述的Protocol一样,是一个动态生成的类,会根据url上的参数获取对应的动态代理实现类。

为什么要创建代理类?

假设我们不创建代理类,那么生成的就是invoke对象,客户端就不能通过直接注入提供服务类方式,而是要依赖invoke对象,造成代码入侵。

有了代理类我们就可以直接注入提供者,实际上调用的时候就是通过invoke发起调用了。

2.4 拓展思考回顾

  1. 本地通过<dubbo:provider>暴露了dubbo服务,那么我们调用本地dubbo服务时是否会发起网络请求?
  2. 通过手动创建T****estService注入spring容器,又通过<dubbo:reference>引用服务T****estService,依赖注入获取到的是哪个bean?
  3. 使用懒汉式时,只通过Spring注入引用类就不会立即创建调用类,而是实际用到才创建吗?

问题解答:

  1. 不会,使用的是本地服务调用
  2. 以注入beanName为主,beanName无法对应则随机取一个。
  3. 会取封装调用类。

针对第三个问题进行解析,在依赖注入的时候,注入的bean就会被加载了,因此ReferenceBean实现的getObject方法就会被调用,调用类也会被封装创建。

ReferenceBean****懒加载常规情况下,只能保证当你服务中没有依赖引入的服务时,保证getObject不会被执行。

这时有人可能想问了,ReferenceBeanBeanDefinition不是都加入到Spring容器中了吗,Spring容器不是会对进行所有BeanDefinition进行初始化创建吗?

其实Spring加载所有BeanDefinition去创建时,BeanDefinition因为本身实际上是ReferenceBean,会先以 &beanName 创建ReferenceBean本身。然后再判断要不要是否需要早期初始化,如果需要才会去创建真实的bean。

所以如果在没有被依赖的情况下,也就不会以beanName去创建bean,所以也就不会去调用getObject。

public void preInstantiateSingletons() throws BeansException {
        ...
        for (String beanName : beanNames) {
            RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
            // 是否为懒加载
            if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
                // 判断是否为FactoryBean
                if (isFactoryBean(beanName)) {
                    // FACTORY_BEAN_PREFIX = & 
                    // &beanName 表示获取实现FactoryBean的类本身
                    final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
                    // 判断是否是SmartFactoryBean 
                    // 如果只是实现FactoryBean,则默认不会去创建真实的object
                    boolean isEagerInit = (factory instanceof SmartFactoryBean &&
                                ((SmartFactoryBean<?>) factory).isEagerInit());
                    // 不需要早期初始化 因此不会去调用getObject
                    if (isEagerInit) {
                        // 获取beanName对应的bean 真正创建调用getObject创建bean
                        getBean(beanName);
                    }
                }
                ....
            }
        }
    }

怎么实现懒汉式加载ReferenceBean****,但是又要依赖引用类?

可以搭配@Lazy使用,让依赖的bean被懒加载,这时获取到的是懒加载bean代理类,只有真正发起调用时才会去获取bean,这样就可以实现在真正发起调用才调用getObject创建服务引用调用类。

三、总结

服务引入最基本的实现就是根据提供者信息封装成一个调用类,但是作为一个优秀的RPC框架,得考虑方方面面的问题。

  • 避免代码侵入。为引入服务生成代理类。
  • 提高可用性。引入了注册中心处理机制。
  • 提高扩展性。引入了自定义适应类,根据url参数自动选择对应的实现类。同时也牵扯到Dubbo实现了自己的IOC容器。
  • 提高启动性能,避免加载无效引入。引入了懒加载机制。
  • Spring作为广泛使用的框架如何接入启动。基于Spring的扩展机制,实现了一套加载机制。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352