Dubbo笔记 ④ : 服务发布流程 - doExportUrlsFor1Protocol

一、前言

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

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

本文衍生篇:

  1. Dubbo笔记衍生篇③:ProtocolWrapper

Dubbo笔记③ : 服务发布流程 - ServiceConfig#export 中我们了解到服务发布时,首先会解析出来所有的注册中心 registryURLs,再针对不同的协议类型进行服务发布。如下:

	// org.apache.dubbo.config.ServiceConfig#doExportUrls
    private void doExportUrls() {
    	// 加载所有注册中心 URL
        List<URL> registryURLs = loadRegistries(true);
        for (ProtocolConfig protocolConfig : protocols) {
        	// 遍历所有协议,进行服务发布
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }

我们在之前的文章中已经分析了 ServiceConfig#loadRegistries 方法的实现,下面我们来看一下 ServiceConfig#doExportUrlsFor1Protocol 的实现:

二、ServiceConfig#doExportUrlsFor1Protocol

在加载出所有的注册中心URL后,此时的URL并非终点,还需要继续根据不同的协议进行解析 而 ServiceConfig#doExportUrlsFor1Protocol 对不同注册中心 和 不同协议的进行了服务发布,下面我们来详细看一下其实现过程。

	// org.apache.dubbo.config.ServiceConfig#doExportUrlsFor1Protocol
 	private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
 		=========================1. 下面开始解析配置参数 ===============
		// 解析各种配置参数,组成URL
 		// 获取当前协议的协议名,默认 dubbo,用于服务暴露
        String name = protocolConfig.getName();
        if (name == null || name.length() == 0) {
            name = Constants.DUBBO;
        }
		// 追加参数到map中
        Map<String, String> map = new HashMap<String, String>();
        map.put(Constants.SIDE_KEY, Constants.PROVIDER_SIDE);
        appendRuntimeParameters(map);
        appendParameters(map, application);
        appendParameters(map, module);
        appendParameters(map, provider, Constants.DEFAULT_KEY);
        appendParameters(map, protocolConfig);
        // 这里将 ServiceConfig 也进行的参数追加
        appendParameters(map, this);
        // 1.1 对 MethodConfig  的解析。MethodConfig  保存了针对服务暴露方法的配置,可以映射到  <dubbo:method />
        // 对 methods 配置的解析,即是对参数的解析并保存到 map 中
        if (methods != null && !methods.isEmpty()) {
			 for (MethodConfig method : methods) {
			 // 添加 MethodConfig 对象的字段信息到 map 中,键 = 方法名.属性名。
            // 比如存储 <dubbo:method name="sayHello" retries="2"> 对应的 MethodConfig,
            // 键 = sayHello.retries,map = {"sayHello.retries": 2, "xxx": "yyy"}
                appendParameters(map, method, method.getName());
                String retryKey = method.getName() + ".retry";
                if (map.containsKey(retryKey)) {
                    String retryValue = map.remove(retryKey);
                    // 检测 MethodConfig retry 是否为 false,若是,则设置重试次数为0
                    if ("false".equals(retryValue)) {
                        map.put(method.getName() + ".retries", "0");
                    }
                }
                 // 获取 ArgumentConfig 列表
                List<ArgumentConfig> arguments = method.getArguments();
                if (arguments != null && !arguments.isEmpty()) {
                    for (ArgumentConfig argument : arguments) {
                        // convert argument type
                        // 检测 type 属性是否为空,或者空串,如果指定了type 则按照 type进行解析
                        if (argument.getType() != null && argument.getType().length() > 0) {
                        	// 获取接口方法
                            Method[] methods = interfaceClass.getMethods();
                            // visit all methods
                            if (methods != null && methods.length > 0) {
                                for (int i = 0; i < methods.length; i++) {
                                    String methodName = methods[i].getName();
                                    // target the method, and get its signature
                                    // 比对方法名,查找 MethodConfig 配置的方法
                                    if (methodName.equals(method.getName())) {
                                        Class<?>[] argtypes = methods[i].getParameterTypes();
                                        // one callback in the method
                                        // argument.getIndex() 默认值是 -1 ,如果这里不为-1,说明用户不仅设置了type还设置了index
                                        // 则需要校验index索引的参数类型是否是 type类型
                                        if (argument.getIndex() != -1) {
	                                        // 检测 ArgumentConfig 中的 type 属性与方法参数列表
	                                        // 中的参数名称是否一致,不一致则抛出异常
                                            if (argtypes[argument.getIndex()].getName().equals(argument.getType())) {
	                                             // 添加 ArgumentConfig 字段信息到 map 中,
	                                            // 键前缀 = 方法名.index,比如:
	                                            // map = {"sayHello.3": true}
                                                appendParameters(map, argument, method.getName() + "." + argument.getIndex());
                                            } else {
                                                throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
                                            }
                                        } else {
                                            // multiple callbacks in the method
                                            // index =-1 则需要遍历所有参数列表,获取到指定type 类型的参数
                                            for (int j = 0; j < argtypes.length; j++) {
                                                Class<?> argclazz = argtypes[j];
                                                // 从参数类型列表中查找类型名称为 argument.type 的参数
                                                if (argclazz.getName().equals(argument.getType())) {
                                                    appendParameters(map, argument, method.getName() + "." + j);
                                                    if (argument.getIndex() != -1 && argument.getIndex() != j) {
                                                        throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        } else if (argument.getIndex() != -1) {
                         // 用户未配置 type 属性,但配置了 index 属性,且 index != -1
                         // 添加 ArgumentConfig 字段信息到 map 中
                            appendParameters(map, argument, method.getName() + "." + argument.getIndex());
                        } else {
                            throw new IllegalArgumentException("argument config must set index or type attribute.eg: <dubbo:argument index='0' .../> or <dubbo:argument type=xxx .../>");
                        }

                    }
                }
            } // end of methods for
        }
		// 1.2 针对 泛化调用的 参数,设置泛型类型( true、bean 和 nativejava)
        if (ProtocolUtils.isGeneric(generic)) {
            map.put(Constants.GENERIC_KEY, generic);
            map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
        } else {
        	// 获取服务版本信息
            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(Constants.METHODS_KEY, Constants.ANY_VALUE);
            } else {
                map.put(Constants.METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
            }
        }
        // 1.3 如果接口使用了token验证,则对token处理
        if (!ConfigUtils.isEmpty(token)) {
            if (ConfigUtils.isDefault(token)) {
                map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
            } else {
                map.put(Constants.TOKEN_KEY, token);
            }
        }
        // 1.4 本地导出属性设置,本地导出,不开端口,不发起远程调用,仅与JVM 内直接关联
        // LOCAL_PROTOCOL 值为 injvm
        if (Constants.LOCAL_PROTOCOL.equals(protocolConfig.getName())) {
            protocolConfig.setRegister(false);
            map.put("notify", "false");
        }
        // export service
        // 获取全局配置路径
        String contextPath = protocolConfig.getContextpath();
        if ((contextPath == null || contextPath.length() == 0) && provider != null) {
            contextPath = provider.getContextpath();
        }
		
		// 这里获取的host,port是dubbo暴露的地址端口
        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);
		// 1.5 确定是否存在当前协议的扩展的ConfiguratorFactory可以用来设计自己的URL组成规则
        if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                .hasExtension(url.getProtocol())) {
            url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                    .getExtension(url.getProtocol()).getConfigurator(url).configure(url);
        }
		=========================2. 下面开始服务导出 ===============
		// 2. 服务导出, 针对 本地导出和远程导出
        String scope = url.getParameter(Constants.SCOPE_KEY);
        // don't export when none is configured
        // scope 为SCOPE_NONE 则不导出服务
        if (!Constants.SCOPE_NONE.equalsIgnoreCase(scope)) {

            // export to local if the config is not remote (export to remote only when config is remote)
            // scope 不为SCOPE_NONE 则导出本地服务
            if (!Constants.SCOPE_REMOTE.equalsIgnoreCase(scope)) {
            	// 2.1 本地服务导出
                exportLocal(url);
            }
            // export to remote if the config is not local (export to local only when config is local)
            //scope 不为SCOPE_LOCAL则导出远程服务
            if (!Constants.SCOPE_LOCAL.equalsIgnoreCase(scope)) {
	    		// 	... 日志打印
	    		// 如果存在服务注册中心
                if (registryURLs != null && !registryURLs.isEmpty()) {
                    for (URL registryURL : registryURLs) {
                    	// 是否动态,该字段标识是有自动管理服务提供者的上线和下线,若为false 则人工管理
                        url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
                        // 加载 Dubbo 监控中心配置
                        URL monitorUrl = loadMonitor(registryURL);
                        // 如果监控中心配置存在,则添加到url中
                        if (monitorUrl != null) {
                            url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
                        }
                 
                     	// 代理配置解析
                        String proxy = url.getParameter(Constants.PROXY_KEY);
                        if (StringUtils.isNotEmpty(proxy)) {
                            registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
                        }
						// 2.2 远程服务导出
						//  将registryURL拼接export=providerUrl参数。 为服务提供类(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 {
                	// 如果没有注册中心,则采用直连方式,即没有服务注册和监听的过程
                    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
                    DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

                    Exporter<?> exporter = protocol.export(wrapperInvoker);
                    exporters.add(exporter);
                }
                /**
                 * @since 2.7.0
                 * ServiceData Store
                 */
                 // 元数据存储
                MetadataReportService metadataReportService = null;
                if ((metadataReportService = getMetadataReportService()) != null) {
                    metadataReportService.publishProvider(url);
                }
            }
        }
        this.urls.add(url);
    }

ServiceConfig#doExportUrlsFor1Protocol 的代码比较长,其中较大篇幅的内容都用于配置参数解析。我们将上面的代码分为两部分

  1. 解析配置参数:这一部分解析的URL 是待暴露服务的URL信息。包括 方法配置解析、泛化、token 等。
  2. 服务导出 : 将 服务 URL 解析完成后,则开始准备暴露服务。

关于这两步的具体内容,我们下面详细分析:

三. 解析配置参数

ServiceConfig#doExportUrlsFor1Protocol 方法将大部分篇幅都花在了解析配置参数的过程中,我们关注其中部分参数的解析。

关于Dubbo的配置参数意义,详情请参考 Dubbo – 系统学习 笔记 – 配置参考手册

1. MethodConfig的解析

这部分内容比较冗长,这里并没有放出来,职责就是对 MethodConfig 配置的解析,我们以 <dubbo:method> 标签为例 。<dubbo:method> 标签为<dubbo:service><dubbo:reference>的子标签,用于方法级别的控制:

如下:

<dubbo:reference interface="com.xxx.XxxService">
	<!-- 指定方法, 超时时间为3s,重试次数为2 -->
	<dubbo:method name="findXxx" timeout="3000" retries="2">
		<!-- 指定第一个参数(index=0,index 默认-1) 类型为callback -->
	    <dubbo:argument index="0" callback="true" />
	    <!-- 指定参数XXX 类型的(index=0) 类型为callback, index 和 type 二选一 -->
	    <dubbo:argument type="com.kingfish.XXX" callback="true"/>
	<dubbo:method>
</dubbo:reference>

Dubbo method 的配置有两种方式,指定类型 type 或者指定 索引 index。在 上面解析的中针对这两种情况进行了解析, 上面的注释写的比较详细,这里就不再赘述。

2. 泛化实现的解析

这里判断一下是否是泛化实现,如果是则添加 generic 参数。泛化调用的参数我们在 Dubbo笔记③ : 服务发布流程 - ServiceConfig#export ServiceConfig#checkAndUpdateSubConfigs 中提到过是如何解析。

	// 判断是否是泛化调用,判断逻辑就是 generic的值是否与 true、bean、nativejava相等
	if (ProtocolUtils.isGeneric(generic)) {
		 // 标记泛化的规则
          map.put(Constants.GENERIC_KEY, generic);
          // 设置匹配所有的方法 。Constants.ANY_VALUE 为 * 
          map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
    }

3. token属性的解析

token 作为 令牌验证,为空表示不开启,如果为true,表示随机生成动态令牌,否则使用静态令牌,令牌的作用是防止消费者绕过注册中心直接访问,保证注册中心的授权功能有效,如果使用点对点调用,需关闭令牌功能

	<dubbo:service interface="" token="123"/>

代码也很简单,如下:

       if (!ConfigUtils.isEmpty(token)) {
       		// 如果未 true 或者 default,则使用UUID
            if (ConfigUtils.isDefault(token)) {
                map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
            } else {
                map.put(Constants.TOKEN_KEY, token);
            }
        }

token 功能的实现依赖于 Dubbo的 TokenFilter 过滤器,如对其实现感兴趣,详参:
Dubbo笔记 ⑰ :Dubbo Filter 详解 中对 TokenFilter 解析的部分。

4. 本地服务的解析

Dubbo服务导出分为本地导出和远程导出,本地导出使用了injvm协议,这是一个伪协议,它不开启端口,不发起远程调用,只在JVM 内直接关联,但执行Dubbo的 Filter 链。

在默认情况下,Dubbo同时支持本地导出和远程协议导出,我们可以通过ServiceConfig 的setScope 方法进行配置,为none表示不导出服务,为 remote 表示只导出远程服务,为local表示只导出本地服务。

这里判断如果协议类型是 injvm 则直接设置为 本地导出服务。

 		// LOCAL_PROTOCOL 值为 injvm
      if (Constants.LOCAL_PROTOCOL.equals(protocolConfig.getName())) {
      	  // 设置当前服务不注册到注册中心
          protocolConfig.setRegister(false);
          // 设置不通知
          map.put("notify", "false");
      }

5. ConfiguratorFactory 的解析

这里是根据当前协议类型来获取到 SPI 接口 ConfiguratorFactory 的实现类,这里是Dubbo预留的一个扩展点,通过 ConfiguratorFactory 可以用来设计自己的URL生成策略。

      if (ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
              .hasExtension(url.getProtocol())) {
          url = ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class)
                  .getExtension(url.getProtocol()).getConfigurator(url).configure(url);
      }

而默认的实现为:

override=org.apache.dubbo.rpc.cluster.configurator.override.OverrideConfiguratorFactory
absent=org.apache.dubbo.rpc.cluster.configurator.absent.AbsentConfiguratorFactory

我们可以通过定义 org.apache.dubbo.rpc.cluster.ConfiguratorFactory 文件来自定义策略,如下(由于默认策略为dubbo,所以我们指定dubbo协议我们定制的ConfiguratorFactory ):
在这里插入图片描述
CustomConfiguratorFactory 的实现参考OverrideConfiguratorFactory 的实现,我们这里通过自定义的CustomConfiguratorFactory 策略给URL 添加了 custom 参数 value 为 demo。

public class CustomConfiguratorFactory extends AbstractConfigurator implements ConfiguratorFactory {
    public CustomConfiguratorFactory(URL url) {
        super(url);
    }
	// 返回当前定制下的 AbstractConfigurator  实现类
    @Override
    public Configurator getConfigurator(URL url) {
        return new CustomConfiguratorFactory(url);
    }

	// url 添加 custom 参数
    @Override
    protected URL doConfigure(URL currentUrl, URL configUrl) {
        return currentUrl.addParameter("custom", "demo");
    }
}

四、 服务导出

参数解析并非是重头戏,在上面一系列的解析结束后,Dubbo 开始进行服务导出的操作。我们这里假设使用zk作为注册中心,协议为dubbo。

在默认情况下,Dubbo同时支持本地导出和远程协议导出,我们可以通过ServiceConfig 的setScope 方法进行配置,scope 的取值有四种情况:

  • none :表示不导出服务,
  • remote :表示只导出远程服务,
  • local :表示只导出本地服务。
  • null :在默认情况下为null,会同时导出本地服务和远程服务

对于远程服务来说,提供者和调用者一般不在同一个环境中,通过网络进行调用,如下为远程服务调用图:
在这里插入图片描述
除此之外,Dubbo还提供了一种本地服务暴露和引用的方式,在同一个JVM 进行中发布和调用同一个服务时,这种方式可以避免一次远程调用,而直接在JVM 内进行通信。
在这里插入图片描述

由于我们这里没有设置 Scope, 所以本地服务和远程服务都会导出,下面我们一一来看:

1. 本地服务导出

本地服务导出并不需要与注册中心交互,也不需要开启远程服务,所以其实现比较简单,将某些参数限制,指定协议类型为injvm。

    private void exportLocal(URL url) {
    	// 如果 URL 的协议头等于 injvm,说明已经导出到本地了,无需再次导出
        if (!Constants.LOCAL_PROTOCOL.equalsIgnoreCase(url.getProtocol())) {
            URL local = URL.valueOf(url.toFullString())
            		// 设置协议为  injvm
                    .setProtocol(Constants.LOCAL_PROTOCOL)
                    // 设置host为127.0.0.1
                    .setHost(LOCALHOST)
                    // 设置端口为0
                    .setPort(0);
            // 创建 Invoker,并导出服务,这里的 protocol 会在运行时调用 InjvmProtocol 的 export 方法
            Exporter<?> exporter = protocol.export(
                    proxyFactory.getInvoker(ref, (Class) interfaceClass, local));
            // 添加到暴露的服务集合中
            exporters.add(exporter);
            logger.info("Export dubbo service " + interfaceClass.getName() + " to local registry");
        }
    }

因为Dubbo SPI 的原因,这里协议设置为 injvm,会使用 InjvmProtocol 来进行服务暴露。

关于 InJvmProtocol 的内容,我们在 Dubbo笔记 ⑤ : 服务发布流程 - Protocol#export 中进行了分析。


2 远程服务导出

相较于本地服务导出,远程服务导出需要考虑到协议类型,notify 通知等方面,所以逻辑就显得复杂许多。这里整个暴露流程简化流程如下:

在这里插入图片描述


下面我们来看具体实现:

		// 如果存在服务注册中心
      if (registryURLs != null && !registryURLs.isEmpty()) {
          for (URL registryURL : registryURLs) {
          	// 是否动态,该字段标识是有自动管理服务提供者的上线和下线,若为false 则人工管理
              url = url.addParameterIfAbsent(Constants.DYNAMIC_KEY, registryURL.getParameter(Constants.DYNAMIC_KEY));
              // 1. 加载 Dubbo 监控中心配置
              URL monitorUrl = loadMonitor(registryURL);
              // 如果监控中心配置存在,则添加注册中心的 URL。key 为 monitor
              if (monitorUrl != null) {
                  url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
              }
       
           		// 代理配置解析
              String proxy = url.getParameter(Constants.PROXY_KEY);
              if (StringUtils.isNotEmpty(proxy)) {
                  registryURL = registryURL.addParameter(Constants.PROXY_KEY, proxy);
              }
				// 2. 远程服务导出
              Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
              DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);

              Exporter<?> exporter = protocol.export(wrapperInvoker);
              // 将暴露的服务保存到 exporters中,exporters 保存了本机暴露出的服务
              exporters.add(exporter);
          }
      } else {
      		// 3. 直连方式,不存在服务注册中心,直连方式是和上面的区别就是不经过注册中心注册,并不订阅注册中心节点。
          Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
          DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
		 // 由于没有注册中心,这里会直接根据服务指定的协议,如Dubbo,则会直接使用 DubboProtocol 来暴露服务。
          Exporter<?> exporter = protocol.export(wrapperInvoker);
          exporters.add(exporter);
      }

这里需要注意Dubbo在这里区分了存在注册中心和不存在注册中心的情况,因为 Dubbo允许不使用注册中心而通过服务之间直接连接的方式来调用服务。其区别就是直连方式不会经过 Dubbo的 各种容错机制。


我们这里仍以上一篇的 URL 为例,这里为了方便描述,URL 做了简化,registryURL 和 URL 如下:

// 注册中心 URl
registryURL  = registry://localhost:2181/org.apache.dubbo.registry.RegistryService&registry=zookeeper
// 要暴露的服务 URL
url = dubbo://localhost:9999/com.kingfish.service.DemoService

2.1 注册监控中心

对监控中心的处理并不复杂,通过loadMonitor 生成一个关于监控中心的 URL ,并追加到 url 上。
loadMonitor 方法是完成监控URL的生成过程,并不复杂,这里篇幅问题不再展开。

  // 1. 加载 Dubbo 监控中心配置
   URL monitorUrl = loadMonitor(registryURL);
   // 如果监控中心配置存在,则添加
   if (monitorUrl != null) {
       url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
   }

我们假设为存在 localhost:8080 的监控中心,则此时 URL 变为(URL 有简化) :

dubbo://localhost:9999/com.kingfish.service.DemoService
	&monitor=http://localhost:8080?interface=org.apache.dubbo.monitor.MonitorService

2.2 服务的暴露

服务提供者导出服务具体使用的是下列代码:

	// 1. 生成代理类
	 Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
     DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
	// 2. 暴露服务
     Exporter<?> exporter = protocol.export(wrapperInvoker);
     // 将暴露的服务保存到 exporters中,当消费者调用时,提供者可以从中获取已经暴露的服务
     exporters.add(exporter);

这里需要需要注意的是,服务最终暴露出来的是 Exporter,其内部结构如下:
在这里插入图片描述
这里的 Invoker 是 通过JavassistProxyFactory#getInvoker 方法生成的匿名 AbstractProxyInvoker 类。从这里开始,慢慢进入了服务导出的核心内容,下面我们来继续具体分析这个过程。

2.2.1 生成代理对象

该部分代码如下,其中Invoker 是实体域,它是 Dubbo 的核心模型,其它模型都向它靠拢,或转换成它,它代表一个可执行体,可向它发起 invoke 调用,它有可能是一个本地的实现,也可能是一个远程的实现,也可能一个集群实现。

 Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));

这里需要注意:

  1. proxyFactory.getInvoker 的入参URL 为 registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString())。这里将 url 添加到了 registryURL 中,所以proxyFactory.getInvoker 的入参 URL 如下:

    registry://localhost:2181/org.apache.dubbo.registry.RegistryService&registry=zookeeper
    	&export=dubbo://localhost:9999/com.kingfish.service.DemoService
    	&monitor=http://localhost:8080?interface=org.apache.dubbo.monitor.MonitorService
    

    此时我们看一下 URL的结构:

    registry://... : 保存了注册中心的信息, registry=zookeeper 表明使用zk作为注册中心
    export=dubbo://... : 保存了要导出服务的信息
    monitor=http://... : 保存了监控中心的信息
    

    在后面我们会介绍,由于 registry:// 所以 Dubbo会选择 RegistryProtocol 来处理本地服务暴露,而 RegistryProtocol 中会将 export 的参数取出识别出 dubbo:// 选择 DubboProtocol 来进行服务暴露。

  2. proxyFactory 和 protocol 定义如下:

    private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
    
    private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
    

    可以看出 proxyFactory 和 protocol 都是 SPI 扩展接口的适配器类型。所以这里的 proxyFactory实际上是 ProxyFactory$Adaptive 类型,所以这里首先执行的是 ProxyFactory$Adaptive#getInvoker() 方法,而对于 ProxyFactory SPI 接口来说,默认的协议类型为 javassist,所以调用的是 JavassistProxyFactory#getInvoker 方法获取了代理类。而JavassistProxyFactory#getInvoker 代码如下 :

        @Override
        public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
            // TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
            // 将服务实现类转换成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 {
                    return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
                }
            };
        }
    

    可以看到,在JavassistProxyFactory#getInvoker中将SPI 实现类动态转换成了 Wrapper,并封装成Invoker类型返回。到这里,便完成了 Dubbo笔记 ② : 架构概述 中描述的 服务提供实现类Ref 到 Invoker 的转换


所以这里我们可以总结出来 ProxyFactory#getInvoker 生成代理类的流程如下:
在这里插入图片描述

其中Wrapper 是在 JavassistProxyFactory#getInvoker 中动态生成的,如对于DemoService 接口来说,

public interface DemoService {
    void sayMsg(String msg);

    String sayHello(String name);
}

其生成的Wrapper 类如下,可以看到这里的 invokeMethod 方法通过方法名称来匹配调用的方法,从而调用ref 对应的方法。:


/*
 * Decompiled with CFR.
 * 
 * Could not load the following classes:
 *  com.kingfish.service.impl.DemoServiceImpl
 */
package org.apache.dubbo.common.bytecode;

import com.kingfish.service.impl.DemoServiceImpl;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import org.apache.dubbo.common.bytecode.ClassGenerator;
import org.apache.dubbo.common.bytecode.NoSuchMethodException;
import org.apache.dubbo.common.bytecode.NoSuchPropertyException;
import org.apache.dubbo.common.bytecode.Wrapper;

public class Wrapper1
extends Wrapper
implements ClassGenerator.DC {
    public static String[] pns;
    public static Map pts;
    public static String[] mns;
    public static String[] dmns;
    public static Class[] mts0;
    public static Class[] mts1;
    public static Class[] mts2;
    public static Class[] mts3;

    @Override
    public String[] getPropertyNames() {
        return pns;
    }

    @Override
    public boolean hasProperty(String string) {
        return pts.containsKey(string);
    }

    public Class getPropertyType(String string) {
        return (Class)pts.get(string);
    }

    @Override
    public String[] getMethodNames() {
        return mns;
    }

    @Override
    public String[] getDeclaredMethodNames() {
        return dmns;
    }

    @Override
    public void setPropertyValue(Object object, String string, Object object2) {
        try {
            DemoServiceImpl demoServiceImpl = (DemoServiceImpl)object;
        }
        catch (Throwable throwable) {
            throw new IllegalArgumentException(throwable);
        }
        throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or setter method in class com.kingfish.service.impl.DemoServiceImpl.").toString());
    }

    @Override
    public Object getPropertyValue(Object object, String string) {
        try {
            DemoServiceImpl demoServiceImpl = (DemoServiceImpl)object;
        }
        catch (Throwable throwable) {
            throw new IllegalArgumentException(throwable);
        }
        throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or setter method in class com.kingfish.service.impl.DemoServiceImpl.").toString());
    }
	// 这里会根据方法名匹配到对应实例到的具体方法
    public Object invokeMethod(Object object, String string, Class[] classArray, Object[] objectArray) throws InvocationTargetException {
        DemoServiceImpl demoServiceImpl;
        try {
            demoServiceImpl = (DemoServiceImpl)object;
        }
        catch (Throwable throwable) {
            throw new IllegalArgumentException(throwable);
        }
        try {
            if ("sayMsg".equals(string) && classArray.length == 1) {
                demoServiceImpl.sayMsg((String)objectArray[0]);
                return null;
            }
          
            if ("sayHello".equals(string) && classArray.length == 1) {
                return demoServiceImpl.sayHello((String)objectArray[0]);
            }
           
        }
        catch (Throwable throwable) {
            throw new InvocationTargetException(throwable);
        }
        throw new NoSuchMethodException(new StringBuffer().append("Not found method \"").append(string).append("\" in class com.kingfish.service.impl.DemoServiceImpl.").toString());
    }
}

这里就可以知道 ProxyFactory#getInvoker 返回的是由 JavassistProxyFactory#getInvoker 生成的匿名代理类 Invoker。


我们这里再来梳理一下:

  1. 当服务提供者暴露服务接口时,会调用 ProxyFactory#getInvoker 来生成一个 Invoker,此处由于 Dubbo SPI 的存在实际调用的是 JavassistProxyFactory#getInvoker。我们这里为了方便描述,把 JavassistProxyFactory#getInvoker 方法返回的 Invoker 称为 Javassist Invoker。

  2. JavassistProxyFactory#getInvoker 方法中 第一步会生成一个 Wrapper 类来包装暴露的接口实例。如果需要调用实例的方法,则通过 Wrapper#invokeMethod 方法来调用,在 Wrapper#invokeMethod 中会根据调用的methodName 来直接调用实例的方法。如下代码:

      final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type)
    
  3. JavassistProxyFactory#getInvoker 方法中 第二步会创建一个AbstractProxyInvoker 的匿名实现类并返回,即我们上面提到的 Javassist Invoker。而 Javassist Invoker 的doInvoke 方法具体实现会委托给 Wrapper#invokeMethod 方法来处理。

           @Override
           protected Object doInvoke(T proxy, String methodName,
                                     Class<?>[] parameterTypes,
                                     Object[] arguments) throws Throwable {
               return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
           }
    
  4. 当消费者调用提供者服务时,提供者会调用 Javassist Invoker 的 doInvoke 方法,该方法会委托给 Wrapper#invokeMethod 来处理,而 Wrapper#invokeMethod 中会根据 methodName 来调用实例的具体方法。即整个流程简化如下:

    Invoker#invoke -> AbstractProxyInvoker#doInvoke -> Wrapper#invokeMethod -> 调用 Ref 具体方法
    

注: Wrapper 的存在是为了减少调用 接口实例的反射。如果没有Wrapper,在 Javassist Invoker 的 doInvoke 方法中,则会根据 methodName 直接反射调用 proxy 对象。而 Wrapper 中动态生成了针对当前对象的方法,会根据方法名是否相同来直接调用实例方法,免去了反射调用 proxy 对象的过程。


2.2.2 服务暴露

经过了上面的过程, 暴露的接口对象的代理对象已经创建,这一步开始对该代理对象进行暴露。
该部分代码如下:

 // 使用 DelegateProviderMetaDataInvoker  包装了一下 代理invoker。DelegateProviderMetaDataInvoker  中除了 Invoker 还保存了服务配置的数据
 DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
 // 进行服务暴露
 Exporter<?> exporter = protocol.export(wrapperInvoker);

当执行到 protocol.export(wrapperInvoker); 时,同样由于Dubbo SPI 机制,实际调用的是 Protocol$Adaptive#export() 方法。

并且由于Dubbo SPI 的扩展点使用了Wrapper自动增强,对于 Protocol 来说,存在三个Wrapper 增强,如下:
在这里插入图片描述

我们在 Dubbo笔记衍生篇②:Dubbo SPI 原理 四、扩展点的自动包装 章节中介绍过 扩展点的自动包装,得知SPI 接口的包装类 会先于 SPI 接口执行,并且由于 Wrapper在加载时的Class是通过 ExtensionLoader#cachedWrapperClasses 保存,由于Set 无序,所以包装类的实际加载顺序是无序的,如果再人工添加一个ProtocolWrapper,顺序并非是直接追加。

而这里由于只有已知的这三个Wrapper,并且加载顺序不影响功能,为了方便描述,这里忽略这三个 Wrapper的排序问题。所以整个调用过程是 Protocol$Adaptive#export() => QosProtocolWrapper#export() => ProtocolListenerWrapper #export() => ProtocolFilterWrapper#export() => XxxProtocol#export()


其流程如图:
在这里插入图片描述

关于三个包装类的作用并不影响服务发布主流程,如有需要详细解读请阅:Dubbo笔记衍生篇③:ProtocolWrapper


Dubbo笔记 ③ : 服务发布流程 - ServiceConfig#export 一文中我们知道,对于 远程导出 的URL 来说:

  • 存在注册中心:会使用 RegistryProtocol 来处理服务。这就导致 URL 变更为了 registry://localhost:2181/org.apache.dubbo.registry.RegistryService,但是这个URL中并没有包含暴露的接口的信息,所以URL 在暴露服务时会添加一个参数 export来记录需要暴露的服务信息(即本文中 2.2.1 生成代理类 中所提到的)。此时 URL 会变成 registry://localhost:2181/org.apache.dubbo.registry.RegistryService&export=URL.encode("dubbo://localhost:9999/com.kingfish.service.DemoService?version=1.0.0")。 而之后基于 Dubbo SPI的 自适应机制,根据 URL registry 协议会选择RegistryProtocol 来暴露服务,而 RegistryProtocol 只负责处理注册中心相关的内容,额外的暴露服务,会根据 export 参数指定的 URL 信息选择。这里URL 协议为 dubbo,则说明服务的暴露需要使用 Dubbo协议,则会使用 DubboProtocol 来进行服务暴露。

  • 不存在注册中心 :不存在注册中心时,最终暴露服务的URL 为 dubbo://localhost:9999/com.kingfish.service.DemoService?version=1.0.0,此时会根据 Dubbo SPI选择 DubboProtocol中的export方法进行暴露服务端口。这里 URL 也可能是别的协议,此时会寻找对应的Protocol 来处理,我们这里还是以 Dubbo 为例。


所以这里对于 XxxProtocol,存在三种情况:

  1. 本地服务导出:这种情况我们在上面说了,其内部根据URL 中Protocol类型为 injvm,会选择InjvmProtocol
  2. 远程服务导出 & 有注册中心:其内部根据URL 中 Protocol 类型为 registry,会选择RegistryProtocol
  3. 远程服务导出 & 没有注册中心:根据服务协议头类型判断,我们这里假设是 dubbo ,则会选择 DubboProtocol

关于服务暴露的具体内容,由于篇幅所限,所以另开新篇 : Dubbo笔记⑤ : 服务发布流程 - Protocol#export


以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/
https://www.cnblogs.com/ClassNotFoundException/p/6973958.html
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

猫吻鱼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值