Dubbo笔记 ⑧ : 消费者启动流程 - ReferenceConfig#get

一、前言

本系列为个人Dubbo学习笔记,内容基于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章,仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。

系列文章地址:Dubbo源码分析:全集整理


在之前的文章中,我们分析了服务提供者服务导出以及处理请求的过程。
从本文开始,我们开始分析消费者调用提供者服务的过程。本文的分析会基于Main 方法的Dubbo调用,对于在Spring中的Dubbo流程,我们在 Dubbo笔记 ㉕ :Spring 执行流程概述 一文中进行了介绍。


这里先列出来我们的消费者代码,此代码并非在Spring中运行,后面会单独开设文章分析Spring 如何集成Dubbo。

    public static void main(String[] args) {
    	// 获取 ReferenceConfig
        ReferenceConfig<DemoService> referenceConfig = DubboUtil.referenceConfig("dubbo-demo");
        // 获取服务 接口
        DemoService demoService = referenceConfig.get();
        // 进行服务调用
        String result = demoService.sayHello("demo");
        System.out.println("result = " + result);
    }
	....
    public static ReferenceConfig<DemoService> referenceConfig(String applicationName) {
        ReferenceConfig<DemoService> referenceConfig = new ReferenceConfig<>();
        // 设置服务名称
        referenceConfig.setApplication(new ApplicationConfig(applicationName));
        // 设置注册中心地址
        RegistryConfig registryConfig = new RegistryConfig("zookeeper://localhost:2181");
        referenceConfig.setRegistry(registryConfig);
        // 设置暴露接口
        referenceConfig.setInterface(DemoService.class);
        referenceConfig.setTimeout(5000);
        // 设置版本号和分组 服务接口 + 服务分组 + 服务版本号确定唯一服务
        referenceConfig.setVersion("1.0.0");
        referenceConfig.setGroup("dubbo");
        return referenceConfig;
    }

上面的代码一目了然,我们可以很自然的发现关键逻辑在下面一句中:

 DemoService demoService = referenceConfig.get();

referenceConfig.get() 方法完成了对服务接口的代理,创建了服务提供者的网络服务连接。因此,我们首先看一下 referenceConfig.get() 的时序图(图源《深度剖析Apache Dubbo 核心技术内幕》):
在这里插入图片描述


下面我们开始对 ReferenceConfig#get 进行分析。

二、ReferenceConfig#get

通过上面代码我们可以看到 ReferenceConfig#get 是Dubbo消费者的入口。其实现如下:

    public synchronized T get() {
    	// 1. 检查并更新缺省配置
        checkAndUpdateSubConfigs();

        if (destroyed) {
            throw new IllegalStateException("Already destroyed!");
        }
        // 如果ref 为null 则说明没有初始化,进行初始化。
        if (ref == null) {
        	//2. 服务初始化
            init();
        }
        return ref;
    }

这里可以看到两个比较明显的方法:

  1. checkAndUpdateSubConfigs(); : 检查并更新缺省配置
  2. init(); : 消费者服务的初始化,核心逻辑。

下面我们来进行分析。

1. ReferenceConfig#checkAndUpdateSubConfigs

服务提供者在服务暴露时也有类似的配置检查过程。ReferenceConfig#checkAndUpdateSubConfigs的实现如下:

    public void checkAndUpdateSubConfigs() {
    	// 如果引用接口不合法直接抛出异常
        if (interfaceName == null || interfaceName.length() == 0) {
            throw new IllegalStateException("<dubbo:reference interface=\"\" /> interface not allow null!");
        }
        // 按照一定优先级整合配置 application > module > consumer
        completeCompoundConfigs();
        // 启动配置中心
        startConfigCenter();
        // get consumer's global configuration
        // 消费者缺省校验
        checkDefault();
        // 刷新配置
        this.refresh();
        // 设置泛化调用相关信息
        if (getGeneric() == null && getConsumer() != null) {
            setGeneric(getConsumer().getGeneric());
        }
        // 获取引用的接口Class
        if (ProtocolUtils.isGeneric(getGeneric())) {
            interfaceClass = GenericService.class;
        } else {
            try {
                interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
                        .getContextClassLoader());
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            checkInterfaceAndMethods(interfaceClass, methods);
        }
        // 解析映射文件赋值 ReferenceConfig#url 属性,
        // 依次尝试获取系统属性中的 {接口名}、dubbo.resolve.file、dubbo-resolve.properties 配置文件,如果前一个配置存在则不会往后加载。将加载后的配置解析后赋值给 url
        resolveFile();
        // 缺省校验
        checkApplication();
        // 元数据中心校验
        checkMetadataReport();
    }

可以看到 ReferenceConfig#checkAndUpdateSubConfigs 的逻辑并不复杂,整个过程就是对参数的校验,与提供者的参数校验类似,这里不再进行深入分析。毕竟下面的 ReferenceConfig#init 才是关键所在。

2. ReferenceConfig#init

ReferenceConfig#init 创建了服务提供者的代理类,是消费者端核心代码的入口。其实现如下:

 	private void init() {
 		// 如果消费者服务已经初始化过,则直接返回
        if (initialized) {
            return;
        }
        initialized = true;
        // 检查存根合法性
        checkStubAndLocal(interfaceClass);
        // 检查mock合法性
        checkMock(interfaceClass);
        // 拼接参数
        Map<String, String> map = new HashMap<String, String>();
		// 添加 side、协议版本信息、时间戳和进程号等信息到 map 中
        map.put(Constants.SIDE_KEY, Constants.CONSUMER_SIDE);
        appendRuntimeParameters(map);
        // 不是泛化调用
        if (!isGeneric()) {
        	// 获取版本
            String revision = Version.getVersion(interfaceClass, version);
            if (revision != null && revision.length() > 0) {
                map.put("revision", revision);
            }
			 // 获取接口方法列表,并添加到 map 中
            String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
            if (methods.length == 0) {
                logger.warn("NO method found in service interface " + interfaceClass.getName());
                map.put("methods", Constants.ANY_VALUE);
            } else {
                map.put("methods", StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
            }
        }
        map.put(Constants.INTERFACE_KEY, interfaceName);
        // 添加参数到 map中
        appendParameters(map, application);
        appendParameters(map, module);
        appendParameters(map, consumer, Constants.DEFAULT_KEY);
        appendParameters(map, this);
        Map<String, Object> attributes = null;
        // 对 <method> 标签的解析
        if (methods != null && !methods.isEmpty()) {
            attributes = new HashMap<String, Object>();
            for (MethodConfig methodConfig : methods) {
                appendParameters(map, methodConfig, methodConfig.getName());
                // 重试参数解析
                String retryKey = methodConfig.getName() + ".retry";
                if (map.containsKey(retryKey)) {
                    String retryValue = map.remove(retryKey);
                    if ("false".equals(retryValue)) {
                        map.put(methodConfig.getName() + ".retries", "0");
                    }
                }
                attributes.put(methodConfig.getName(), convertMethodConfig2AyncInfo(methodConfig));
            }
        }
		 // 获取消费者要使用注册的 ip 地址,多网卡情况下需要可以指定ip
        String hostToRegistry = ConfigUtils.getSystemProperty(Constants.DUBBO_IP_TO_REGISTRY);
        if (hostToRegistry == null || hostToRegistry.length() == 0) {
            hostToRegistry = NetUtils.getLocalHost();
        } else if (isInvalidLocalHost(hostToRegistry)) {
            throw new IllegalArgumentException("Specified invalid registry ip from property:" + Constants.DUBBO_IP_TO_REGISTRY + ", value:" + hostToRegistry);
        }
        map.put(Constants.REGISTER_IP_KEY, hostToRegistry);
		// 创建提供者代理
        ref = createProxy(map);
		// 根据服务名,ReferenceConfig,代理类构建 ConsumerModel,
    	// 并将 ConsumerModel 存入到 ApplicationModel 中
        ConsumerModel consumerModel = new ConsumerModel(getUniqueServiceName(), interfaceClass, ref, interfaceClass.getMethods(), attributes);
        ApplicationModel.initConsumerModel(getUniqueServiceName(), consumerModel);
    }

上面的代码很多都和服务提供者的类似,前期工作都是对配置参数的处理,关键内容在于 代理的创建 createProxy(map);,这一步创建了引用服务的代理。

下面我们来看看 ReferenceConfig#createProxy 的实现过程。

三、ReferenceConfig#createProxy

ReferenceConfig#createProxy 实现如下 (这一部分注释基本都是参考官网):

    private T createProxy(Map<String, String> map) {
        URL tmpUrl = new URL("temp", "localhost", 0, map);
        final boolean isJvmRefer;
        /*******  1. 服务引用判断  ******/
        // 根据 isInjvm 参数判断是否指定了本地引用
        if (isInjvm() == null) {
        	// 如果没有指定是否本地引用则 需要根据下面逻辑判断
        	 // 在指定本地引用的时候,但是 url 配置被指定,则不当做本地引用。
        	 // 该 url 在 ReferenceConfig#init 中 调用ReferenceConfig#resolveFile 方法解析获取。
            if (url != null && url.length() > 0) { // if a url is specified, don't do local reference
                isJvmRefer = false;
            } else {
                // by default, reference local service if there is
                // 根据 url 的协议、scope 以及 injvm 等参数检测是否需要本地引用
        		// 比如如果用户显式配置了 scope=local,此时 isInjvmRefer 返回 true
                isJvmRefer = InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl);
            }
        } else {
        	//如果进行了本地引用配置,则按照配置的决定
            isJvmRefer = isInjvm();
        }
		// 确定了本地引用
        if (isJvmRefer) {
        	/*******  2. 本地服务调用  ******/
        	// 生成一个本地引用的URL,协议类型为 injvm
            URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);
            // 调用 refer 方法构建 InjvmInvoker 实例
            invoker = refprotocol.refer(interfaceClass, url);
        } else {
        	/*******  3. 远程服务调用  ******/
        	// 如果 url不为空,则说明可能会进行点对点调用,即服务直连
        	// 这里的 url 是在 ReferenceConfig#init 中 调用 ReferenceConfig#resolveFile 方法解析获取。
            if (url != null && url.length() > 0) { // user specified URL, could be peer-to-peer address, or register center's address.
            // 当需要配置多个 url 时,可用分号进行分割,这里会进行切分
                String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url);
                if (us != null && us.length > 0) {
                    for (String u : us) {
                    	
                        URL url = URL.valueOf(u);
                        if (url.getPath() == null || url.getPath().length() == 0) {
                        	// 设置接口全限定名为 url 路径
                            url = url.setPath(interfaceName);
                        }
                        // 如果 url 协议类型是 Registry,则说明需要使用指定的注册中心
                        if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                        	// 将 map 转换为查询字符串,并作为 refer 参数的值添加到 url 中,与服务暴露时的url 异曲同工
                            urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                        } else {
                         	// 合并 url,移除服务提供者的一些配置(这些配置来源于用户配置的 url 属性),
	                        // 比如线程池相关配置。并保留服务提供者的部分配置,比如版本,group,时间戳等
	                        // 最后将合并后的配置设置为 url 查询字符串中
                            urls.add(ClusterUtils.mergeUrl(url, map));
                        }
                    }
                }
            } else { // assemble URL from register center's configuration
            	// 如果没有服务直连,则需要从注册中心中获取提供者信息
            	// 检查注册中心配置
                checkRegistry();
                // 加载注册中心 URL
                List<URL> us = loadRegistries(false);
                if (us != null && !us.isEmpty()) {
                    for (URL u : us) {
                    	// 加载监控中心
                        URL monitorUrl = loadMonitor(u);
                        if (monitorUrl != null) {
                        	// 添加监控中心信息
                            map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
                        }
                        // 到这里说明存在注册中心, 则添加 refer 参数到 url 中,并将 url 添加到 urls 
                        urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                    }
                }
                // 未配置注册中心,抛出异常
                if (urls.isEmpty()) {
                    throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
                }
            }
			// 单个注册中心或服务提供者(服务直连)
            if (urls.size() == 1) {
            	/*******  3.1 单URL场景的远程调用  ******/
            	 // 调用 RegistryProtocol 的 refer 构建 Invoker 实例
                invoker = refprotocol.refer(interfaceClass, urls.get(0));
            } else {
	            /*******  3.2 多URL场景的远程调用  ******/
            	// 多个注册中心或多个服务提供者,或者两者混合
                List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
                URL registryURL = null;
                // 获取所有的 Invoker
                for (URL url : urls) {
                	// 通过 refprotocol 调用 refer 构建 Invoker,refprotocol 会在运行时
                	// 根据 url 协议头加载指定的 Protocol 实例,并调用实例的 refer 方法
                	// 这里 refprotocol 是 Protocol$Adaptive 适配器类型
                    invokers.add(refprotocol.refer(interfaceClass, url));
                    if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                        registryURL = url; // use last registry url
                    }
                }
                if (registryURL != null) { // registry url is available
                    // use RegistryAwareCluster only when register's cluster is available
                     // 如果注册中心链接不为空,则将使用 RegistryAwareCluster
                    URL u = registryURL.addParameter(Constants.CLUSTER_KEY, RegistryAwareCluster.NAME);
                    // The invoker wrap relation would be: RegistryAwareClusterInvoker(StaticDirectory) -> FailoverClusterInvoker(RegistryDirectory, will execute route) -> Invoker
                    // 创建 StaticDirectory 实例,并由 Cluster 对多个 Invoker 进行合并
                    invoker = cluster.join(new StaticDirectory(u, invokers));
                } else { // not a registry url, must be direct invoke.
                    invoker = cluster.join(new StaticDirectory(invokers));
                }
            }
        }
		// 是否检查提供者的可用性
        Boolean c = check;
        if (c == null && consumer != null) {
            c = consumer.isCheck();
        }
        if (c == null) {
            c = true; // default true
        }
        // 如果 为true,则消费者启动时对 invoker 进行可用性检查
        if (c && !invoker.isAvailable()) {
            // make it possible for consumer to retry later if provider is temporarily unavailable
            initialized = false;
            throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
        }
        /**
         * @since 2.7.0
         * ServiceData Store
         */
         // 2.7.0 版本后,元数据中心发布消费者服务信息
         /*******  4. 元数据中心服务发布  ******/
        MetadataReportService metadataReportService = null;
        if ((metadataReportService = getMetadataReportService()) != null) {
            URL consumerURL = new URL(Constants.CONSUMER_PROTOCOL, map.remove(Constants.REGISTER_IP_KEY), 0, map.get(Constants.INTERFACE_KEY), map);
            metadataReportService.publishConsumer(consumerURL);
        }
        // create service proxy
        /*******  5. 代理类的创建  ******/
        // 生成代理类
        return (T) proxyFactory.getProxy(invoker);
    }

我们这里可以总结为五个过程:

  1. 服务引用判断 :首先需要根据用户参数确定当前服务引用是本地引用还是远程引用。二者的逻辑并不相同。
  2. 本地服务调用 :首先根据配置检查是否为本地调用,若是,则调用 InjvmProtocol#refer 方法生成 InjvmInvoker 实例。若不是,则读取直连配置项或注册中心 url,并将读取到的 url 存储到 urls 中。
  3. 远程服务调用 :远程服务的调用,分为两种情况,单URL 或 多URL场景。(这里的URL 指的是用户指定的注册中心或者直连URL)。通过这一步,将 URL 转化为了 Invoker。
    1. 单URL场景的远程调用 :根据 urls 元素数量进行后续操作。若 urls 元素数量为1,则说明注册中心或直连URL只有一个,则直接通过 Protocol 自适应拓展类构建 Invoker 实例接口。
    2. 多URL的远程调用:若 urls 元素数量大于1,即存在多个注册中心或服务直连 url,此时先根据 url 构建 Invoker。然后再通过 Cluster 合并多个 Invoker。
  4. 元数据中心服务发布 :Dubbo 2.7 版本之后新增了元数据中心和配置中心,其中元数据中心会存储当前暴露的服务信息。
  5. 代理类的创建 :在获取到Ref 的Invoker 后调用 ProxyFactory 生成代理类。

其中 :Invoker 是 Dubbo 的核心模型,代表一个可执行体。在服务提供方,Invoker 用于调用服务提供类。在服务消费方,Invoker 用于执行远程调用。Invoker 是由 Protocol 实现类构建而来。


下面我们按照这四个过程来进行具体的分析:

1. 服务引用判断

这一部分的代码如下:其中注释已经解释的很清楚,这里不再赘述。

 		// 如果没有指定是否本地引用则 需要自己根据逻辑判断
        if (isInjvm() == null) {
        	 // 在指定本地引用的时候,但是 url 配置被指定,则不做本地引用
            if (url != null && url.length() > 0) { // if a url is specified, don't do local reference
                isJvmRefer = false;
            } else {
                // by default, reference local service if there is
                // 根据 url 的协议、scope 以及 injvm 等参数检测是否需要本地引用
        		// 比如如果用户显式配置了 scope=local,此时 isInjvmRefer 返回 true
                isJvmRefer = InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl);
            }
        } else {
        	//如果进行了本地引用配置,则按照配置的决定
            isJvmRefer = isInjvm();
        }

2. 本地服务调用

实际上,无论是本地还是远程调用,其核心方法都是 Protocol#refer 方法,区别在于由于协议的不同,调用的 Protocol 实例不同,本地服务调用调用的是 InjvmProtocol#refer
(需要注意的是无论是远程还是本地调用,这里的调用顺序都是经过 XxxProtocolWrapper 后才真正调用到InjvmProtocol#refer

     URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);
     invoker = refprotocol.refer(interfaceClass, url);

InjvmProtocol#refer 的实现如下:

    @Override
    public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
        return new InjvmInvoker<T>(serviceType, url, url.getServiceKey(), exporterMap);
    }

可以看到 InjvmProtocol#refer 的实现很简单,直接包装了一个InjvmInvoker 返回。

不过需要注意的是,这里将 exporterMap 传递给了 InjvmInvoker,而exporterMap中保存了本地暴露的服务信息。当消费者进行服务调用时,会调用 InjvmInvoker#doInvoke 方法,其实现如下,我们可以看到,InjvmInvoker#doInvoke 通过 exporterMap 获取到 调用的本地服务 Exporter 再调用

    @Override
    public Result doInvoke(Invocation invocation) throws Throwable {
        Exporter<?> exporter = InjvmProtocol.getExporter(exporterMap, getUrl());
        if (exporter == null) {
            throw new RpcException("Service [" + key + "] not found.");
        }
        RpcContext.getContext().setRemoteAddress(NetUtils.LOCALHOST, 0);
        return exporter.getInvoker().invoke(invocation);
    }

其中 Exporter 的结构如下,其内部保存了暴露服务的实例,所以可以通过 Exporter 直接调用本地服务。

在这里插入图片描述

3. 远程服务调用

远程服务调用首先要确定的就是从哪获取服务提供者。提供者的获取有两种可能

  1. 消费者通过配置直接指定了提供者的地址。
  2. 消费者通过注册中心获取提供者的地址。

下面的代码就针对这两种情况进行处理,最后获取的地址会保存到 urls 中,供后续处理。

   	// 如果 url不为空,则说明可能会进行点对点调用,即服务直连
   	// 这里的 url 是在 ReferenceConfig#init 中 调用 ReferenceConfig#resolveFile 方法解析获取。
       if (url != null && url.length() > 0) { // user specified URL, could be peer-to-peer address, or register center's address.
       // 当需要配置多个 url 时,可用分号进行分割,这里会进行切分
           String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url);
           if (us != null && us.length > 0) {
               for (String u : us) {
               	
                   URL url = URL.valueOf(u);
                   if (url.getPath() == null || url.getPath().length() == 0) {
                   	// 设置接口全限定名为 url 路径
                       url = url.setPath(interfaceName);
                   }
                   // 如果 url 协议类型是 Registry,则说明需要使用指定的注册中心
                   if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                   	// 将 map 转换为查询字符串,并作为 refer 参数的值添加到 url 中,与服务暴露时的url 异曲同工
                       urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                   } else {
                    	// 合并 url,移除服务提供者的一些配置(这些配置来源于用户配置的 url 属性),
                    // 比如线程池相关配置。并保留服务提供者的部分配置,比如版本,group,时间戳等
                    // 最后将合并后的配置设置为 url 查询字符串中
                       urls.add(ClusterUtils.mergeUrl(url, map));
                   }
               }
           }
       } else { // assemble URL from register center's configuration
       	// 如果没有服务直连,则需要从注册中心中获取提供者信息
       	// 检查注册中心配置
           checkRegistry();
           // 加载注册中心 URL
           List<URL> us = loadRegistries(false);
           if (us != null && !us.isEmpty()) {
               for (URL u : us) {
               	// 加载监控中心
                   URL monitorUrl = loadMonitor(u);
                   if (monitorUrl != null) {
                   	// 添加监控中心信息
                       map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
                   }
                   // 到这里说明存在注册中心, 则添加 refer 参数到 url 中,并将 url 添加到 urls 
                   urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
               }
           }
           // 未配置注册中心,抛出异常
           if (urls.isEmpty()) {
               throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
           }
       }

3.1 单URL场景的远程调用

这里的单URL 场景指的是 单一注册中心或 单一直连URL(服务提供者)时 远程调用服务Invoker 的创建,会直接调用 refprotocol.refer,如下:

			// 单个注册中心或服务提供者(服务直连)
            if (urls.size() == 1) {
            	/*******  3.1 单URL场景的远程调用  ******/
            	 // 调用 RegistryProtocol 的 refer 构建 Invoker 实例
                invoker = refprotocol.refer(interfaceClass, urls.get(0));
            }

refprotocol.refer 的调用需要分为两种情况,

  1. 指定单一注册中心 :我们这里假设注册中心是zk,接口协议为Dubbo,则调用顺序为 :

    refprotocol.refer =XxxProtocolWrapper#refer  => RegistryProtocol#refer =XxxProtocolWrapper#refer  => DubboProtocol#refer
    

    由于一个注册中心可能存在多个服务提供者,所以此时得到的Invoker 结构如下:

    在这里插入图片描述
    当消费者进行服务进行调用时,此时会通过下面的流程挑选出合适的Invoker用于服务调用。

     MockClusterInvoker#doInvoke -> FailoverClusterInvoker#doInvoke -> RegistryDirectory#list
    
  2. 指定单一直连URL :这里则不会出现上面注册中心的情况,一个直连 URL 即一个提供者。我们这里仍假设服务协议为Dubbo,则调用顺序为 :

    refprotocol.refer =XxxProtocolWrapper#refer  => DubboProtocol#refer
    

可以得知,关键的两个方法为 RegistryProtocol#referDubboProtocol#refer,而在RegistryProtocol#refer 过程中又调用了 DubboProtocol#refer 方法,RegistryProtocol#refer 主要处理和注册中心相关的内容,而 DubboProtocol#refer 而是创建与提供者的连接


关于 RegistryProtocol#referDubboProtocol#refer 方法的分析,由于篇幅所限,我们另开新篇。

  1. RegistryProtocol#referDubbo笔记⑨ : 消费者启动流程 - RegistryProtocol#refer
  2. DubboProtocol#referDubbo笔记⑩ : 消费者启动流程 - DubboProtocol#refer

3.2 多URL场景的远程调用

这里的多URL 场景可能是 多个注册中心或直连URL(服务提供者),或者两者混合。,相较于单URL 场景,无非是对每个URL构建Invoker,随后使用Cluster 合并多个Invoker。

这部分代码如下:

		// 多个注册中心或多个服务提供者,或者两者混合
        List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
        URL registryURL = null;
        // 获取所有的 Invoker
        for (URL url : urls) {
        	// 通过 refprotocol 调用 refer 构建 Invoker,refprotocol 会在运行时
        	// 根据 url 协议头加载指定的 Protocol 实例,并调用实例的 refer 方法
        	// 这里 refprotocol 是 Protocol$Adaptive 适配器类型
            invokers.add(refprotocol.refer(interfaceClass, url));
            if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                registryURL = url; // use last registry url
            }
        }
        if (registryURL != null) { // registry url is available
            // use RegistryAwareCluster only when register's cluster is available
             // 如果注册中心链接不为空,则将使用 RegistryAwareCluster 容错策略
            URL u = registryURL.addParameter(Constants.CLUSTER_KEY, RegistryAwareCluster.NAME);
            // The invoker wrap relation would be: RegistryAwareClusterInvoker(StaticDirectory) -> FailoverClusterInvoker(RegistryDirectory, will execute route) -> Invoker
            // 创建 StaticDirectory 实例,并由 Cluster 对多个 Invoker 进行合并
            invoker = cluster.join(new StaticDirectory(u, invokers));
        } else { // not a registry url, must be direct invoke.
            invoker = cluster.join(new StaticDirectory(invokers));
        }

我们这里假设存在两个注册中心cluster.join 返回的 Invoker 结构如下:

在这里插入图片描述

当消费者进行服务调用时,Dubbo 会按照如下逻辑筛选出一个合适 Invoker 用于执行调用逻辑:

 MockClusterInvoker#doInvoke -> FailoverClusterInvoker#doInvoke -> StaticDirectory#list -> Invoker#invoke

需要注意我们上图中的 StaticDirectory 内部的Invoker是 MockClusterInvoker 类型,其结构如下:

因此当消费者发起调用后,完成逻辑如下:

MockClusterInvoker#doInvoke -> 
FailoverClusterInvoker#doInvoke -> 
StaticDirectory#list -> 
MockClusterInvoker#doInvoke -> 
FailoverClusterInvoker#doInvoke -> 
RegistryDirectory#list ->  
DubboInvoker#doInvoke

4. 元数据中心服务发布

Dubbo 在 2.7 版本之后将原先的 注册中心分割为了 注册中心、元数据中心、配置中心。元数据中心中保存了服务的元数据信息,如服务接口名,重试次数,版本号等等。这里则是将消费者的元数据信息发布到了元数据中心。这部分的逻辑本文不再赘述,如有需要详参: Dubbo笔记 ㉔ :元数据中心

        MetadataReportService metadataReportService = null;
        	// 获取元数据服务
        if ((metadataReportService = getMetadataReportService()) != null) {
            URL consumerURL = new URL(Constants.CONSUMER_PROTOCOL, map.remove(Constants.REGISTER_IP_KEY), 0, map.get(Constants.INTERFACE_KEY), map);
            // 发布当前消费者
            metadataReportService.publishConsumer(consumerURL);
        }

MetadataReportService#publishConsumer 实现如下:

    public void publishConsumer(URL consumerURL) throws RpcException {
    	// 消费者移除不需要的属性
        consumerURL = consumerURL.removeParameters(Constants.PID_KEY, Constants.TIMESTAMP_KEY, Constants.BIND_IP_KEY, Constants.BIND_PORT_KEY, Constants.TIMESTAMP_KEY);
        // 交由 metadataReport 来发布
        metadataReport.storeConsumerMetadata(new MetadataIdentifier(consumerURL.getServiceInterface(),
                consumerURL.getParameter(Constants.VERSION_KEY), consumerURL.getParameter(Constants.GROUP_KEY),Constants.CONSUMER_SIDE,
                consumerURL.getParameter(Constants.APPLICATION_KEY)), consumerURL.getParameters());
    }

5. 代理类的创建

经过上面的步骤后,消费者已经获取到服务提供者Invoker。而这一步,则是将Invoker 转化为 Ref 的过程了,即开始创建服务代理,当消费者发起调用时会委托给当前的 invoker#doInvoke 来处理

 return (T) proxyFactory.getProxy(invoker);

这里的 proxyFactory 也是 ProxyFactory$Adaptive,其实现如下:

package com.kingfish.main.stub;

import org.apache.dubbo.common.extension.ExtensionLoader;

public class ProxyFactory$Adaptive implements org.apache.dubbo.rpc.ProxyFactory {
    @Override
    public org.apache.dubbo.rpc.Invoker getInvoker(java.lang.Object arg0, java.lang.Class arg1, org.apache.dubbo.common.URL arg2) throws org.apache.dubbo.rpc.RpcException {
        if (arg2 == null) {
            throw new IllegalArgumentException("url == null");
        }
        org.apache.dubbo.common.URL url = arg2;
        String extName = url.getParameter("proxy", "javassist");
        if (extName == null) {
            throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])");
        }
        org.apache.dubbo.rpc.ProxyFactory extension = (org.apache.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName);
        return extension.getInvoker(arg0, arg1, arg2);
    }

    @Override
    public java.lang.Object getProxy(org.apache.dubbo.rpc.Invoker arg0, boolean arg1) throws org.apache.dubbo.rpc.RpcException {
        if (arg0 == null) {
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        }
        if (arg0.getUrl() == null) {
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
        }
        org.apache.dubbo.common.URL url = arg0.getUrl();
        String extName = url.getParameter("proxy", "javassist");
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])");
        org.apache.dubbo.rpc.ProxyFactory extension = (org.apache.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName);
        return extension.getProxy(arg0, arg1);
    }

    @Override
    public java.lang.Object getProxy(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
        if (arg0 == null) {
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        }
        if (arg0.getUrl() == null) {
            throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
        }
        org.apache.dubbo.common.URL url = arg0.getUrl();
        String extName = url.getParameter("proxy", "javassist");
        if (extName == null) {
            throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])");
        }
        org.apache.dubbo.rpc.ProxyFactory extension = (org.apache.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName);
        return extension.getProxy(arg0);
    }
}

这里会根据 url.getParameter("proxy", "javassist") 来获取 ProxyFactory 的具体实现类。默认情况下,我们会跳转 JavassistProxyFactory。经过父类调用后会调用 JavassistProxyFactory#getProxy,其实现如下:

    @Override
    @SuppressWarnings("unchecked")
    public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
    	// 创建了 interfaces 的代理对象,处理器为 InvokerInvocationHandler
        return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
    }

也即是说,消费者在启动时会为引用的接口服务创建一个代理对象,这个代理对象的增强处理器为 InvokerInvocationHandler。 其实现如下:


/**
 * InvokerHandler
 */
public class InvokerInvocationHandler implements InvocationHandler {
    private static final Logger logger = LoggerFactory.getLogger(InvokerInvocationHandler.class);
    private final Invoker<?> invoker;

    public InvokerInvocationHandler(Invoker<?> handler) {
        this.invoker = handler;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(invoker, args);
        }
        if ("toString".equals(methodName) && parameterTypes.length == 0) {
            return invoker.toString();
        }
        if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
            return invoker.hashCode();
        }
        if ("equals".equals(methodName) && parameterTypes.length == 1) {
            return invoker.equals(args[0]);
        }
		// 这里创建了一个 RpcInvocation 来作为调用参数载体
		// 执行 invoker#invoke 来调用方法。
        return invoker.invoke(createInvocation(method, args)).recreate();
    }

    private RpcInvocation createInvocation(Method method, Object[] args) {
        RpcInvocation invocation = new RpcInvocation(method, args);
        // 如果返回值为 CompletableFuture 则进行标记
        if (RpcUtils.hasFutureReturnType(method)) {
            invocation.setAttachment(Constants.FUTURE_RETURNTYPE_KEY, "true");
            invocation.setAttachment(Constants.ASYNC_KEY, "true");
        }
        return invocation;
    }

}

这里我们可以知道,消费者进行调用的时候,传参类型为 RpcInvocation。这和之前服务提供者处理消息时接口参数的类型相匹配。
而对于调用方法,除去一些特殊方法(如toString、hashCode、equals)其余的方法调用则是委托给了 内部 invoker 类完成。这里我们注意这个内部 invoker 就是我们在 2. 本地服务调用3. 远程服务调用 部分创建的 Invoker。


我们这里以在单注册中心为例,当前 Invoker 结构如下:

消费者服务调用的时序图如下 :

在这里插入图片描述

关于消费者调用流程,如有需要详参 Dubbo笔记 ⑪ : 消费者调用流程

四、总结

1. 代理类结构

经过上面的介绍,我们了解到消费者获取到服务提供者代理的整个过程。那么对于服务提供者代理来说,其结构需要简单说明一下。

1.1 单注册中心

单注册中心时时使用的服务目录(Directory)实例是 RegistryDirectory,RegistryDirectory 代表一个注册中心,此时整个服务代理类的结构如下:
在这里插入图片描述

其中RefProxy 为服务提供者代理,忽略部分细节后其调用顺序是:

RefProxy#sayHello -> MockClusterInvoker#invoke -> FailoverClusterInvoker#list -> RegistryDirectory#doList

而在 RegistryDirectory#doList 中则会从 Invoker 根据路由规则筛选出满足规则的Invoker,随后交由FailoverClusterInvoker 根据负载均衡算法筛选出合适的Invoker进行调用。

1.2 多注册中心

多URL 场景使用了 StaticDirectory 作为服务目录,StaticDirectory 作为静态服务目录,其内部存放的 Invoker 是不会变动的。此时服务代理类的整个结构如下:
在这里插入图片描述
此时需要注意的是,StaticDirectory 中 的 Invoker1、 Invoker2其结构与单注册中心时的Invoker相同,如下:
在这里插入图片描述

其中RefProxy 为服务提供者代理,忽略部分细节后其调用顺序是:

RefProxy#sayHello -> MockClusterInvoker#invoke -> FailoverClusterInvoker#list -> StaticDirectory#doList

StaticDirectory#doList 也会根据路由规则筛选出符合规则的Invoker列表,交由FailoverClusterInvoker 根据负载均衡算法筛选出合适的Invoker进行调用。


下面是个人对 StaticDirectory 不可变Invoker的理解:

如下图,存在两个注册中心Registry A, Registry B:
在这里插入图片描述

其中,红色边框所代表的是 StaticDirectory 中包含的不可变的执行体(Invoker)集合,该Invoker集合和注册中心一一对应。蓝色边框的是RegistryDirectory 中可变的执行体集合(Invoker)。当消费者进行调用时。即StaticDirectory 中 Invoker 指的是注册中心的可执行体不可变,每个注册中心内部的Invoker列表还是会动态变化。

也就是说,消费者启动后,当前引用服务的 StaticDirectory 中 Invoker列表 (提供服务的注册中心列表)就不会再变化,即一定是 Registry A Invoker 、Registry B Invoker。但是对于 Registry A Invoker 、Registry B Invoker 内部的 RegistryDirectory 来说,RegistryDirectory 中的 Invoker 还是会根据服务动态变化(每个注册中心内部的服务提供者列表还是会变化)。

1.3 多分组情况

首先可以需要明确在Dubbo中是通过 服务接口 + 服务分组 + 服务版本号确定唯一服务。当一个接口有多种实现时,可以用group区分,所以一个服务可能存在多个不同分组,而一个消费者可以同时引用一个接口的不同分组实现。

	// 引用服务分组为 aaa, bbb 的 demeService 服务
    @Reference(version = "1.0.0", group = "aaa,bbb")
    private DemoService demoService;

需要注意的是,默认情况下,Dubbo会挑选其中一个分组的接口调用并返回接口。但是某些情况下,我们可能需要合并多个分组的返回接口。比如菜单服务,接口一样,但有多种实现,用group区分,现在消费方需从每种group中调用一次返回结果,合并结果返回,这样就可以实现聚合菜单项。此时可以通过 merger 参数指定合并策略。详参:https://blog.csdn.net/abcde474524573/article/details/53026110


对于上述一个消费者可能引用多个分组服务场景。 Dubbo会对每个分组创建一个对应的StaticDirectory对象。此时对于上述模型中的RegistryDirectory 其结构如下:
在这里插入图片描述

默认情况下,当消费者进行服务调用时,RegistryDirectory 的内部Invoker 会选择其中一个 gruop 的接口调用返回。这个选择的过程发生在 MergeableClusterInvoker#doInvoke 方法中。

2. 流程总结

这里我们以zk为注册中心,Dubbo为服务协议简单总结:

  1. 消费者启动时会通过 ReferenceConfig#get 获取服务引用Ref,首先通过ReferenceConfig#checkAndUpdateSubConfigs 检查配置合法性,随后从缓存中获取Ref,如果缓存没有命中,则准备通过 ReferenceConfig#createProxy 创建 Ref 代理 RefProxy。
  2. ReferenceConfig#createProxy 创建过程中首先会检查是否是本地服务调用,如果是则按照本地服务调用,返回 InjvmInvoker。
  3. 否则判断是否是单一注册中心或者单一直连URL,多URL场景相较于 单URL场景 多了一步通过 Cluster 合并多个 Invoker。不过前置过程大致类似: RegistryDirectory 订阅zk注册中心的providers、configurators、routers 节点,如果节点发生变化,则会通过回调方式通知 RegistryDirectory 进行相应的服务列表、配置、路由信息的更新。同时获取到当前的节点信息,对节点信息进行解析,对于 providers 子节点URL,开始准备 URL 转换为 Invoker。
  4. URL 转Invoker 时,会通过 DubboProtocol#refer 将 URL转化为 DubboProtocol#refer ,在 转换过程中会创建Netty客户端并与服务提供者的Netty服务端建立连接。此时Invoker 中包含对应提供者的Netty客户端连接。
  5. RegistryDirectory 获取到URL 转换的Invoker 后, 会更新本地 Invokers 列表以供后续服务调用。随机 RegistryDirectory 被包装成 MockClusterInvoker -》 FailoverClusterInvoker -》RegistryDirectory 形式。如果是多URL场景,则在这里通过 Cluster 合并多个 Invoker。
  6. 随后通过 proxyFactory.getProxy(invoker) 将 MockClusterInvoker 转化为 Ref 代理对象 供消费者调用。
  7. 当消费者发起调用时会调用 Ref 代理对象,而 Ref 代理对象会将请求委托给内部 Invoker, 即 MockClusterInvoker 。
  8. MockClusterInvoker 经过一定调用后最终交由 DubboInvoker#invoke 来发起远程调用。关于 Dubbo 消费者调用的详细流程,如有需要详参:Dubbo笔记 ⑪ : 消费者调用流程

以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/
https://xobo.org/dubbo-debug-direct-provider/
https://blog.csdn.net/weixin_38308374/article/details/106736721
https://blog.csdn.net/abcde474524573/article/details/53026110
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值