Dubbo笔记 ③ : 服务发布流程 - ServiceConfig#export

一、前言

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

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


这里贴出来个人总结的整个流程的描述:
在这里插入图片描述
详细描述如下:

在这里插入图片描述

1. Dubbo 中的 URL

在 Dubbo 中 URL 可以看做是一个被封装的信息承载结构,用于信息的传递。可以简单的认为 Dubbo 中的 URL 就是一个承载信息的实体类,在整个流程中负责数据传递。


下面内容来源: https://zhuanlan.zhihu.com/p/98561575

一个标准的 URL 格式至多可以包含如下的几个部分

protocol://username:password@host:port/path?key=value&key=value

在 dubbo 中,也使用了类似的 URL,主要用于在各个扩展点之间传递数据,组成此 URL 对象的具体参数如下:

  • protocol:一般是 dubbo 中的各种协议 如:dubbo thrift http zk
  • username/password:用户名/密码
  • host/port:主机/端口
  • path:接口名称
  • parameters:参数键值对

一些典型的 Dubbo URL如下:

dubbo://192.168.1.6:20880/moe.cnkirito.sample.HelloService?timeout=3000
描述一个 dubbo 协议的服务

zookeeper://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=demo-consumer&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=1214&qos.port=33333&timestamp=1545721981946
描述一个 zookeeper 注册中心

consumer://30.5.120.217/org.apache.dubbo.demo.DemoService?application=demo-consumer&category=consumers&check=false&dubbo=2.0.2&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=1209&qos.port=33333&side=consumer&timestamp=1545721827784
描述一个消费者

二、demo

我们这里以 Main 方法的形式进行代码分析,Spring 中的集成逻辑我们在 Dubbo笔记 ㉕ : Spring 执行流程概述 中会进行分析。

我们以如下代码进行分析,注册中心使用zk :

public class SimpleProvider {
    public static void main(String[] args) throws IOException {
    	// 进行一些Dubbo 配置
        ServiceConfig<SimpleDemoService> serviceConfig = DubboUtil.serviceConfig("dubbo-provider", SimpleDemoService.class, new MainSimpleDemoServiceImpl());
        serviceConfig.getRegistry().setSimplified(true);
        
//        MetadataReportConfig metadataReportConfig = new MetadataReportConfig();
//        metadataReportConfig.setAddress("zookeeper://127.0.0.1:2181");
//        serviceConfig.setMetadataReportConfig(metadataReportConfig);

//        ConfigCenterConfig configCenterConfig = new ConfigCenterConfig();
//        configCenterConfig.setAddress("zookeeper://127.0.0.1:2181");
//        serviceConfig.setConfigCenter(configCenterConfig);
		// 服务暴露
        serviceConfig.export();

        System.out.println("service is start");
        System.in.read();
    }
}

其中 DubboUtil#serviceConfig 是自己封装的一个简单方法,如下:

    public static <T> ServiceConfig<T> serviceConfig(String applicationName, Class<T> tClass, T obj) {
        ServiceConfig<T> serviceConfig = new ServiceConfig<>();
        // 设置服务名称
        final ApplicationConfig applicationConfig = new ApplicationConfig(applicationName);
        serviceConfig.setApplication(applicationConfig);
        // 设置注册中心地址
        RegistryConfig registryConfig = new RegistryConfig("zookeeper://localhost:2181");
        registryConfig.setSimplified(true);
        serviceConfig.setRegistry(registryConfig);

        // 设置暴露接口
        serviceConfig.setInterface(tClass);
        serviceConfig.setRef(obj);
		// 设置 版本 和 分组
        serviceConfig.setVersion("1.0.0");
        serviceConfig.setGroup("dubbo");

        return serviceConfig;
    }

三、暴露流程

我们以上面的 Demo 为例来分析整个服务暴露流程。上面可以看到,Dubbo 的服务暴露逻辑始于 ServiceConfig#export,下面我们来看看其实现:

    private static final ScheduledExecutorService delayExportExecutor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("DubboServiceDelayExporter", true));

    public synchronized void export() {
    	// 1. 对默认配置进行检查,某些配置没有提供时,提供缺省值。
        checkAndUpdateSubConfigs();
		// 获取到服务提供者的属性: 是否导出服务,是否延迟发布
        if (provider != null) {
            if (export == null) {
                export = provider.getExport();
            }
            if (delay == null) {
                delay = provider.getDelay();
            }
        }
        // 如果不导出服务,则直接结束
        if (export != null && !export) {
            return;
        }
		// 如果配置了延迟发布,则过了延迟时间再发布,这里 delayExportExecutor 是一个线程池
        if (delay != null && delay > 0) {
            delayExportExecutor.schedule(this::doExport, delay, TimeUnit.MILLISECONDS);
        } else {
        	// 2.否则直接发布
            doExport();
        }
    }

ServiceConfig#export 的逻辑还是比较清楚的 ,如下:

  1. 对暴露的服务进行参数校验,这里是通过 ServiceConfig#checkAndUpdateSubConfigs 完成。
  2. 判断服务是否需要发布。
  3. 判断服务是否需要延迟发布。
  4. ServiceConfig#doExport 来发布服务。

上面我们只关注 ServiceConfig#checkAndUpdateSubConfigs 参数校验的过程 和 ServiceConfig#doExport 服务暴露的过程。


1. ServiceConfig#checkAndUpdateSubConfigs

ServiceConfig#checkAndUpdateSubConfigs 不仅仅是基本配置进行检查,对于一些用户没有进行定义的配置,将提供配置的缺省值。其详细代码如下:

 	public void checkAndUpdateSubConfigs() {
        //  1.1 使用显示配置的provider、module、application 来进行一些全局配置,其优先级为 ServiceConfig > provider > module > application 
        completeCompoundConfigs();
        // Config Center should always being started first.
        // 1.2 如果配置了配置中心,这里会启动配置中心,并加载外部化配置。
        startConfigCenter();
        // 下面的步骤就是检查provider、application、registry、protocol是否存在,如果不存在,就默认一个,并调用refresh方法
        // 1.3 参数的处理
        checkDefault();
        checkApplication();
        checkRegistry();
        checkProtocol();
        this.refresh();
        // 1.4 元数据中心的处理
        checkMetadataReport();
		// 接口参数校验
        if (interfaceName == null || interfaceName.length() == 0) {
            throw new IllegalStateException("<dubbo:service interface=\"\" /> interface not allow null!");
        }
        // 1.5 对泛化实现的处理
        if (ref instanceof GenericService) {
            interfaceClass = GenericService.class;
            if (StringUtils.isEmpty(generic)) {
                generic = Boolean.TRUE.toString();
            }
        } else {
            try {
            	// 根据接口全路径名反射获取 Class
                interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
                        .getContextClassLoader());
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            // 校验 MethodConfig 配置的是否是接口中的方法
            checkInterfaceAndMethods(interfaceClass, methods);
            // 校验 ref 引用是否是null 和是否是接口类的实现
            checkRef();
            // 设置泛化调用为false
            generic = Boolean.FALSE.toString();
        }
        //1.6 对local 和 sub 属性的检查
        if (local != null) {
        	// 如果 local 为 true,则认为是默认情况local 接口是 接口名 + Local
            if ("true".equals(local)) {
                local = interfaceName + "Local";
            }
            // 否则通过反射去获取指定 Local 类
            Class<?> localClass;
            try {
                localClass = ClassHelper.forNameWithThreadContextClassLoader(local);
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            // 获取的Local 不是接口的子类抛出异常
            if (!interfaceClass.isAssignableFrom(localClass)) {
                throw new IllegalStateException("The local implementation class " + localClass.getName() + " not implement interface " + interfaceName);
            }
        }
        // local 已经过时,现在都使用 stub 作为本地存根,逻辑同 local
        if (stub != null) {
            if ("true".equals(stub)) {
                stub = interfaceName + "Stub";
            }
            Class<?> stubClass;
            try {
                stubClass = ClassHelper.forNameWithThreadContextClassLoader(stub);
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            if (!interfaceClass.isAssignableFrom(stubClass)) {
                throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + interfaceName);
            }
        }
        // 对 local  和 stub 的检查
        checkStubAndLocal(interfaceClass);
        // 对 mock 数据的检查
        checkMock(interfaceClass);
    }

这一步对 提供者启动时的大部分参数进行了合法性校验。包括配置中心、元数据中心、本地存根、本地mock等功能。

1.1 completeCompoundConfigs

这一步的目的是 使用显示配置的provider、module、application 来进行一些全局配置,其优先级为 ServiceConfig > provider > module > application 。即按照优先级合并一些重复的配置。其实现如下:

	// 当前是在 org.apache.dubbo.config.ServiceConfig#completeCompoundConfigs 方法中执行
	// 所以这里的 provider 、application  等都是 ServiceConfig 的属性
    private void completeCompoundConfigs() {
    	// 如果 provider 不为空
        if (provider != null) {
			// 如果 application 没设置,则使用 provider  提供的 application。下面以此类推
            if (application == null) {
                setApplication(provider.getApplication());
            }
            if (module == null) {
                setModule(provider.getModule());
            }
            if (registries == null) {
                setRegistries(provider.getRegistries());
            }
            if (monitor == null) {
                setMonitor(provider.getMonitor());
            }
            if (protocols == null) {
                setProtocols(provider.getProtocols());
            }
            if (configCenter == null) {
                setConfigCenter(provider.getConfigCenter());
            }
        }
        if (module != null) {
            if (registries == null) {
                setRegistries(module.getRegistries());
            }
            if (monitor == null) {
                setMonitor(module.getMonitor());
            }
        }
        if (application != null) {
            if (registries == null) {
                setRegistries(application.getRegistries());
            }
            if (monitor == null) {
                setMonitor(application.getMonitor());
            }
        }
    }

1.2 startConfigCenter

在 Dubbo 2.7 及以上版本, Dubbo除了注册中心外还提供了配置中心和元数据中心。配置中心中可以存储一些配置信息。我们这里的逻辑是启动加载配置中心的配置,如下:

 	void startConfigCenter() {
    	// 如果未配置中心,尝试从 ConfigManager 中加载
        if (configCenter == null) {
            ConfigManager.getInstance().getConfigCenter().ifPresent(cc -> this.configCenter = cc);
        }
		// 如果获取到了配置中心的配置
        if (this.configCenter != null) {
            // TODO there may have duplicate refresh
            // 1. 刷新配置中心,按照优先级合并配置信息,因为配置文件具有优先级,系统配置优先级最高,如下配置顺序
            // isConfigCenterFirst = true : SystemConfiguration -> ExternalConfiguration -> AppExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
            // isConfigCenterFirst = false : SystemConfiguration -> AbstractConfig -> ExternalConfiguration -> AppExternalConfiguration -> PropertiesConfiguration
            this.configCenter.refresh();
            // 2. 环境准备,读取配置中心的配置加载到 Environment 中
            prepareEnvironment();
        }
        // 3. 刷新全部配置,将外部配置中心的配置应用到本地
        ConfigManager.getInstance().refreshAll();
    }

这里可以分为三步:

  1. this.configCenter.refresh(); :这里会刷新配置中心的配置,合并当前关于配置中心的属性。如配置中心的地址、协议等可能会在环境变量、外部配置、代码执行等多种方式指定,此时需要根据优先级来获取优先级最高的配置属性参数作为最终参数用于初始化配置中心。简单来说就是确定需要加载的配置中心的一些信息。
  2. prepareEnvironment(); : 根据第一步中获取到的配置中心属性获取到配置中心实例,并读取配置中心的配置文件的内容,并保存到 Environment 中。这里会根据配置的优先级进行保存,优先级高的可以先获取到。
  3. ConfigManager.getInstance().refreshAll(); :这里会触发其他配置类的配置刷新操作,其他配置类会从 Environment 中读取到配置中心设置的内容,以完成自身内容的更新。

关于这三个方法具体分析的逻辑,详参配置中心的内容:Dubbo笔记 ㉒ :配置中心

这里的逻辑可以简单理解为: 如果存在配置中心配置,则通过 this.configCenter.refresh(); 首先确定配置中心的配置。在确定配置中心的信息后在 prepareEnvironment(); 中加载配置中心实例,并获取配置中心上的配置内容,保存到 Environment 中。ConfigManager.getInstance().refreshAll(); 触发其他Dubbo 配置类的刷新操作,这个刷新操作会从 Environment 中获取属于自己的配置信息并加载。

1.3 默认参数的处理

这一部分都是,对默认参数的处理。

		// 对默认参数的检查,如果不存在则补充一个缺省值
  		checkDefault();
        checkApplication();
        checkRegistry();
        checkProtocol();
        // 将当前配置添加到环境中, 并且循环方法,并且获取覆盖值并将新值设置回方法
        this.refresh();
        // 元数据中心的检查
        checkMetadataReport();

我们这里主要来看 ServiceConfig#refresh,这里的实现是 AbstractConfig#refresh:

    public void refresh() {
        try {	
        	//  1. 这里从根据指定的前缀和 id 从环境中获取配置
    	    // getPrefix() 是按照一定规则拼接 : 对  ServiceConfig 来说 为dubbo.service.{interfaceName}
        	// getId 获取的是  interfaceName 名,对  ServiceConfig 来说会在 serviceConfig.setInterface 时会赋值 id
            CompositeConfiguration compositeConfiguration = Environment.getInstance().getConfiguration(getPrefix(), getId());
            // 2. 构建当前类的配置类 InmemoryConfiguration
            InmemoryConfiguration config = new InmemoryConfiguration(getPrefix(), getId());
            config.addProperties(getMetaData());
            // 3. 按照规则配置将现在的配置添加到复合配置中。
            if (Environment.getInstance().isConfigCenterFirst()) {
                // The sequence would be: SystemConfiguration -> ExternalConfiguration -> AppExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
                compositeConfiguration.addConfiguration(3, config);
            } else {
                // The sequence would be: SystemConfiguration -> AbstractConfig -> ExternalConfiguration -> AppExternalConfiguration -> PropertiesConfiguration
                compositeConfiguration.addConfiguration(1, config);
            }

            // loop methods, get override value and set the new value back to method
            // 4. 和复合配置中的属性合并,这里通过反射将自身的一些属性设置为复合配置中的属性。
            Method[] methods = getClass().getMethods();
            for (Method method : methods) {
                if (ClassHelper.isSetter(method)) {
                    try {
                        String value = compositeConfiguration.getString(extractPropertyName(getClass(), method));
                        // isTypeMatch() is called to avoid duplicate and incorrect update, for example, we have two 'setGeneric' methods in ReferenceConfig.
                        if (StringUtils.isNotEmpty(value) && ClassHelper.isTypeMatch(method.getParameterTypes()[0], value)) {
                            method.invoke(this, ClassHelper.convertPrimitive(method.getParameterTypes()[0], value));
                        }
                    } catch (NoSuchMethodException e) {
                        logger.info("Failed to override the property " + method.getName() + " in " +
                                this.getClass().getSimpleName() +
                                ", please make sure every property has getter/setter method provided.");
                    }
                }
            }
        } catch (Exception e) {
            logger.error("Failed to override ", e);
        }
    }

  1. 从根据指定的前缀和 id 从环境中获取配置。
  2. 构建当前类的配置类 InmemoryConfiguration,用于后面合并当前配置类的属性
  3. 按照规则配置将现在的配置添加到复合配置中。
  4. 和复合配置中的属性合并,这里通过反射将自身的一些属性设置为复合配置中的属性。经过这一步后,当前配置类中的属性配置就是最新的配置。

关于上面的复合配置 :

由于 Dubbo 中存在很多作用域的配置,如注册中心的配置、配置中心的配置、服务接口的配置等, Dubbo将这些配置保存到 Environment 中,不同的配置存在不同前缀,如配置中心的前缀 dubbo.config-center、监控中心的前缀dubbo.monitor 等。当需要加载不同的配置时只需要指定前缀,如果配置精确到服务级别则使用 id来区分不同的服务。又由于 Dubbo 相同配置间存在优先级,所以在 Environment 中每个优先级存在一个 Map,而在上面的代码中,我们看到,如果设置了 configCenterFirst = true。则优先级为 SystemConfiguration -> ExternalConfiguration -> AppExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
否则为 SystemConfiguration -> AbstractConfig -> ExternalConfiguration -> AppExternalConfiguration -> PropertiesConfiguration

在 Environment 中每个优先级声明为 一个 Map,其key 是作用域,value 为对应配置。下图为Environment 中定义的配置Map,其中 systemConfigs 是系统级别配置、externalConfigs 和 appExternalConfigs 是配置中心外部化配置,propertiesConfigs 是 属性配置等。
在这里插入图片描述

那么这里 我们再来看看这里 Environment#getConfiguration的实现如下:

    public CompositeConfiguration getConfiguration(String prefix, String id) {
        CompositeConfiguration compositeConfiguration = new CompositeConfiguration();
        // Config center has the highest priority
        compositeConfiguration.addConfiguration(this.getSystemConfig(prefix, id));
        compositeConfiguration.addConfiguration(this.getAppExternalConfig(prefix, id));
        compositeConfiguration.addConfiguration(this.getExternalConfig(prefix, id));
        compositeConfiguration.addConfiguration(this.getPropertiesConfig(prefix, id));
        return compositeConfiguration;
    }

这里可以看到,Environment#getConfiguration 返回的符合保存了多个级别针对于 prefix + id 的配置。CompositeConfiguration 使用 一个 List 保存添加的配置。

1.4 checkMetadataReport

ServiceConfig#checkMetadataReport 是对元数据中心的检查,元数据中心也是 Dubbo 2.7 及以上版本提供的功能,用于保存服务的元数据信息。其实现如下:

    protected void checkMetadataReport() {
        // TODO get from ConfigManager first, only create if absent.
        // 如果未配置元数据中心,则默认创建一个
        if (metadataReportConfig == null) {
            setMetadataReportConfig(new MetadataReportConfig());
        }
        // 刷新元数据中心的配置
        metadataReportConfig.refresh();
        if (!metadataReportConfig.isValid()) {
            logger.warn("There's no valid metadata config found, if you are using the simplified mode of registry url, " +
                    "please make sure you have a metadata address configured properly.");
        }
    }

这里我们需要注意,metadataReportConfig.refresh(); 的实现是 AbstractConfig#refresh,这个我们在上面讲过:这里会获取Dubbo配置 (包括配置中心、系统配置,配置文件配置等)中关于元数据中心的配置,如果获取到则通过反射填充到 metadataReportConfig 中。

1.5 对泛化实现的处理

泛化实现:泛化接口实现主要用于服务提供端没有API接口类及模型类元(比如入参和出参的POJO 类)的情况下使用。消费者发起接口请求时需要将相关信息转换为 Map 传递给 提供者,由提供者根据信息找到对应的泛型实现来进行处理。

简单来说 : 泛化实现即服务提供者端启用了泛化实现,而服务消费者端则是正常调用。

而这里的代码就是在服务暴露前根据暴露接口判断是否泛化实现:如果提供者暴露的接口是 GenericService,则会被认为当前暴露的接口是泛化实现,则将泛化参数 generic 设置为 true。否则的话检查 MethodConfig、InterfaceRef 是否合法。

        if (ref instanceof GenericService) {
            interfaceClass = GenericService.class;
            if (StringUtils.isEmpty(generic)) {
                generic = Boolean.TRUE.toString();
            }
        } else {
            try {
            	// 根据接口全路径名反射获取 Class
                interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
                        .getContextClassLoader());
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            // 校验 MethodConfig 配置的是否是接口中的方法
            checkInterfaceAndMethods(interfaceClass, methods);
            // 校验 ref 引用是否是null 和是否是接口类的实现
            checkRef();
            // 设置泛化调用为false
            generic = Boolean.FALSE.toString();
        }

关于泛化实现的代码分析,如有需要详参: Dubbo笔记 ⑱ :泛化调用 & 泛化实现

1.6 对 local 和 sub 属性的处理

远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,比如:做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub 1,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。如下图:
在这里插入图片描述
local 和 stub 逻辑基本完全相同,并且 local 好像已经过时,这里以 stub (本地存根为例)
具体使用详参官方文档: 本地存根


如果需要使用本地存根,可以通过 其中 stub 参数可以 为true ,此时存根类默认为为 {intefaceName}+ Stub;stub 参数 也可以为 存根类路径名。此时存根类为stub 指向的类。如下:

 // stub 为 ture。 存根类为 {intefaceName}+ Stub,即 com.foo.BarServiceStub
<dubbo:service interface="com.foo.BarService" stub="true" />
 // 存根类为 stub 指定的类 com.foo.BarServiceStub
<dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />

需要注意,存根类有两个条件:

  1. 存根类也需要实现暴露的服务接口
  2. 存根类需要一个有参构造函数,入参为服务接口的实现实例。

那么这里的代码就很简单了:

        //1.5 对local 和 sub 属性的检查
        if (local != null) {
			.... 同 stub 
        }
        // local 已经过时,现在都使用 stub 作为本地存根,逻辑同 local
        if (stub != null) {
        	// 如果 stub 为true ,则使用 interfaceName  + Stub 作为存根类
            if ("true".equals(stub)) {
                stub = interfaceName + "Stub";
            }
            Class<?> stubClass;
            try {
            	// 否则反射获取存根类
                stubClass = ClassHelper.forNameWithThreadContextClassLoader(stub);
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            // 如果存根类没有实现服务接口则抛出异常
            if (!interfaceClass.isAssignableFrom(stubClass)) {
                throw new IllegalStateException("The stub implementation class " + stubClass.getName() + " not implement interface " + interfaceName);
            }
        }
        // 对 local  和 stub 的检查:是否存在入参为 Interface 的构造函数
        checkStubAndLocal(interfaceClass);

参数校验我们就先分析到这,下面我们来看看 Dubbo的服务暴露过程。

2. ServiceConfig#doExport

上面我们可以看到 Dubbo 的服务暴露是通过ServiceConfig#doExport 方法完成,其详细代码如下:

  	protected synchronized void doExport() {
        if (unexported) {
            throw new IllegalStateException("Already unexported!");
        }
        // 1. 如果已经暴露则返回
        if (exported) {
            return;
        }
        exported = true;

        if (path == null || path.length() == 0) {
            path = interfaceName;
        }
         //2.  ProviderModel 表示服务提供者模型,此对象中存储了与服务提供者相关的信息。
    	// 比如服务的配置信息,服务实例等。每个被导出的服务对应一个 ProviderModel。
    	// ApplicationModel 持有所有的 ProviderModel。
        ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), ref, interfaceClass);
        // 分组 + 版本号 + 接口 是一个服务的唯一id
        ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
        // 3. 继续进行服务发布
        doExportUrls();
    }

我们这里很显然可以看到,发布工作是在 ServiceConfig#doExportUrls 中完成,其实现如下:

 	private void doExportUrls() {
 		// Dubbo 支持多协议 多注册中心
 		// 1. 解析出所有注册中心
        List<URL> registryURLs = loadRegistries(true);
        // 2. 遍历协议类型,进行服务发布
        for (ProtocolConfig protocolConfig : protocols) {
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }

Dubbo 允许我们使用不同的协议导出服务,也允许我们向多个注册中心注册服务。因此 Dubbo 在 ServiceConfig#doExportUrls 方法中对多协议,多注册中心进行了支持,主要逻辑在下面两个方法:

  1. loadRegistries(true); :加载所有注册中心,因为Dubbo允许多协议多注册中心的实现,所以这里会解析出所有的注册中心。
  2. doExportUrlsFor1Protocol(protocolConfig, registryURLs); :上面解析出了所有的注册中心,现在开始遍历协议类型,对服务进行不同注册中心的不同协议的发布。

下面我们依此来看:

2.1. loadRegistries(true);

该方法加载所有注册中心,将注册中心解析成 URL 返回。由于一个服务可以被注册到多个服务注册中心,这里加载所有的服务注册中心对象。

下面我们来看详细代码:

	// org.apache.dubbo.config.AbstractInterfaceConfig#loadRegistries
   protected List<URL> loadRegistries(boolean provider) {
        // check && override if necessary
        List<URL> registryList = new ArrayList<URL>();
        // 如果注册中心的配置不为空
        if (registries != null && !registries.isEmpty()) {
        	// 遍历所有注册中心
            for (RegistryConfig config : registries) {
                String address = config.getAddress();
                // 如果注册中心地址为空,则设置为 0.0.0.0
                if (StringUtils.isEmpty(address)) {
                    address = Constants.ANYHOST_VALUE;
                }
                // 排除不可用的地址 (地址信息为 N/A)
                if (!RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
                	// 参数会拼接到map 中,最后会将map 转换成url
                    Map<String, String> map = new HashMap<String, String>();
                    /************* 1. 参数解析,将参数添加到 Map 中 **************/
                    // 添加 ApplicationConfig 中的字段信息到 map 中
                    appendParameters(map, application);
                    // 添加 RegistryConfig 字段信息到 map 中
                    appendParameters(map, config);
                     // 添加 path、pid,protocol 等信息到 map 中
                     // 设置注册中心的 path 为 RegistryService
                    map.put("path", RegistryService.class.getName());
                    // 拼接运行时参数
                    appendRuntimeParameters(map);
                    // 设置默认协议类型为 dubbo
                    if (!map.containsKey("protocol")) {
                        map.put("protocol", "dubbo");
                    }
                    /************* 2. 根据 address 和 map 将信息转化为 URL **************/
                    // 根据协议类型将map转化成URL
                    // dubbo 协议如下格式: zookeeper://localhost:2181/org.apache.dubbo.registry.RegistryService?application=Api-provider&dubbo=2.0.2&pid=24736&release=2.7.0&timestamp=1615539839228
                    // 这里返回URL 列表,因为address 可能包含多个注册中心。address 被正则切割,每个地址对应一个URL
                    List<URL> urls = UrlUtils.parseURLs(address, map);
                     /************* 3. 对 URL 进行进一步处理 **************/
                    for (URL url : urls) {
                    	// 保存服务暴露使用的注册中心的协议
                        url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());
                        // 设置 url 协议为 registry,表示当前URL 用于配置注册中心
                        url = url.setProtocol(Constants.REGISTRY_PROTOCOL);
                        // 通过判断条件,决定是否添加 url 到 registryList 中
         				// 满足两个条件会往里添加:1、是服务提供者且需要想注册中心注册;2、不是提供者但订阅了注册中心
                        if ((provider && url.getParameter(Constants.REGISTER_KEY, true))
                                || (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) {
                            registryList.add(url);
                        }
                    }
                }
            }
        }
        // 返回封装好的注册中心 URL
        return registryList;
    }

这一步的目的是筛选并生成注册中心的URL,其逻辑如下

  1. 参数解析:解析ApplicationConfig、RegistryConfig 中的配置,用于作为注册中心的配置。这里会通过 AbstractConfig#appendParameters 将参数解析并保存到 Map 中。
  2. Map 转换 :经过解析后, Map 中保存的是注册中心相关的信息,这里会根据 address 和 map 将信息转化为 URL。
  3. 对 URL 进行进一步处理 :这里将 URL的协议类型替换为 registry,并保存了原先协议类型。

下面我们详细来看每一步的过程:

2.1.1 参数解析
        Map<String, String> map = new HashMap<String, String>();
        // 解析 ApplicationConfig 中的参数保存到 map 中
        appendParameters(map, application);
        // 解析 RegistryConfig  中配的配置保存到 Map 中
        appendParameters(map, config);
        // 设置 url 的 path 为 org.apache.dubbo.registry.RegistryService
        map.put("path", RegistryService.class.getName());
        // 添加运行时参数
        appendRuntimeParameters(map);
        // 填充 protocol
        if (!map.containsKey("protocol")) {
            map.put("protocol", "dubbo");
        }

其中 .AbstractConfig#appendParameters 方法会获取 config 中的所有 get 方法,如下:

  protected static void appendParameters(Map<String, String> parameters, Object config, String prefix) {
        if (config == null) {
            return;
        }
        // 反射获取方法
        Method[] methods = config.getClass().getMethods();
        for (Method method : methods) {
            try {
                String name = method.getName();
                // 判断是 get 方法
                if (ClassHelper.isGetter(method)) {
                	// 如果方法被 Parameter  注解修饰,则解析  Parameter  注解
                    Parameter parameter = method.getAnnotation(Parameter.class);
                    // 返回类型为 Object || (Parameter  注解不为空且被排除在为) 直接跳过
                    if (method.getReturnType() == Object.class || parameter != null && parameter.excluded()) {
                        continue;
                    }
                    // 如果 Parameter  不为空,则使用 Parameter 的key为作为属性名,否则从get 方法上解析属性名
                    String key;
                    if (parameter != null && parameter.key().length() > 0) {
                        key = parameter.key();
                    } else {
                        key = calculatePropertyFromGetter(name);
                    }
                    // 执行get 方法获取值
                    Object value = method.invoke(config);
                    String str = String.valueOf(value).trim();
                    // 如果返回值不为空则将其添加到Map中
                    if (value != null && str.length() > 0) {
                        if (parameter != null && parameter.escaped()) {
                            str = URL.encode(str);
                        }
                        if (parameter != null && parameter.append()) {
                            String pre = parameters.get(Constants.DEFAULT_KEY + "." + key);
                            if (pre != null && pre.length() > 0) {
                                str = pre + "," + str;
                            }
                            pre = parameters.get(key);
                            if (pre != null && pre.length() > 0) {
                                str = pre + "," + str;
                            }
                        }
                        if (prefix != null && prefix.length() > 0) {
                            key = prefix + "." + key;
                        }
                        parameters.put(key, str);
                    } else if (parameter != null && parameter.required()) {
                    	// Parameter  标记为必须但是值为空则抛出异常
                        throw new IllegalStateException(config.getClass().getSimpleName() + "." + key + " == null");
                    }
                } else if ("getParameters".equals(name)
                        && Modifier.isPublic(method.getModifiers())
                        && method.getParameterTypes().length == 0
                        && method.getReturnType() == Map.class) {
                     // 对 getParameters 方法的特殊处理,由于 getParameters 方法返回的是个 Map,所以这里需要将整个Map 添加
                    Map<String, String> map = (Map<String, String>) method.invoke(config, new Object[0]);
                    if (map != null && map.size() > 0) {
                        String pre = (prefix != null && prefix.length() > 0 ? prefix + "." : "");
                        for (Map.Entry<String, String> entry : map.entrySet()) {
                            parameters.put(pre + entry.getKey().replace('-', '.'), entry.getValue());
                        }
                    }
                }
            } catch (Exception e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
        }
    }
2.1.2 UrlUtils#parseURLs

我们在指定多注册中心时可能通过指定格式如下:

    <dubbo:registry protocol="zookeeper" address="zookeeper://localhost:2181, zookeeper://localhost:2182"/>

在这里我们需要将 zookeeper://localhost:2181, zookeeper://localhost:2182 的地址进行解析,分别生成 URL,其实现如下:

    public static List<URL> parseURLs(String address, Map<String, String> defaults) {
        if (address == null || address.length() == 0) {
            return null;
        }
        // 将注册中心地址分割,因为注册中心可能会存在多个
        String[] addresses = Constants.REGISTRY_SPLIT_PATTERN.split(address);
        if (addresses == null || addresses.length == 0) {
            return null; //here won't be empty
        }
        List<URL> registries = new ArrayList<URL>();
        for (String addr : addresses) {
        	// 解析出来的每个地址和 参数 转化为 URL 
            registries.add(parseURL(addr, defaults));
        }
        return registries;
    }
2.1.3 对 URL 进行进一步处理

经历上面几步之后,假设当前阶段解析的URL如下:

zookeeper://localhost:2181/org.apache.dubbo.registry.RegistryService?application=spring-dubbo-provider&dubbo=2.0.2&pid=10404&release=2.7.0&timestamp=1628842310884

下面我们看一下是如何 对 URL 进行处理的:

    for (URL url : urls) {
     	// 1. 保存服务暴露使用的注册中心的协议
         url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());
         // 2. 设置 url 协议为 registry,表示当前URL 用于配置注册中心
         url = url.setProtocol(Constants.REGISTRY_PROTOCOL);
         //  3. 通过判断条件,决定是否添加 url 到 registryList 中
         // 满足两个条件会往里添加:1、是服务提供者且需要想注册中心注册;2、不是提供者但订阅了注册中心
         if ((provider && url.getParameter(Constants.REGISTER_KEY, true))
                 || (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) {
             registryList.add(url);
         }
     }

第一步 和 第二步执行结束后URL 为:

registry://localhost:2181/org.apache.dubbo.registry.RegistryService?application=spring-dubbo-provider&dubbo=2.0.2&pid=10404&registry=zookeeper&release=2.7.0&timestamp=1628842310884

这里可以看到第一步和第二部的目的就是 将原始 URL 的协议类型从 zookeeper 替换成 registry,并且在URL 中添加 registry=zookeeper 用于保存注册中心的协议。


这么做的目的是为了让 RegistryProtocol 来处理服务。因为在后面的代码中Dubbo会通过 Protocol#export 方法来暴露服务。而由于 Dubbo SPI机制的存在,所以 Dubbo在加载一些 SPI 接口的时候,是根据参数或者属性判断的,对于 Protocol 接口,则是通过url.getProtocol 返回的协议类型判断加载哪个实现类,而这里的协议类型是 registry,则会加载 RegistryProtocol 来处理, 而 RegistryProtocol 可以通过 registry 属性得知注册中心的真正协议 。而对于多种多样的注册中心(如 zk,nacos,redis), RegistryProtocol 会根据注册中心的实际协议类型来选择合适的 Registry 实现类来完成操作。


其中 关于 Dubbo SPI ,以 Protocol 为例, Dubbo中存在 多个实现,如 RegistryProtocol 、DubboProtocol、InjvmProtocol 等,每个实现都有一个唯一的name,如 RegistryProtocol 为 registry, DubboProtocol 为 dubbo, InjvmProtocol 为 injvm。

Dubbo 会为每个 SPI 接口生成一个适配器,用根据URL的参数来选择使用哪个实现类。如 :对于 Protocol,Protocol$Adaptive 是 Dubbo 自动生成的针对于 Protocol 接口的适配器。在调用 Protocol#export 会先调用 Protocol$Adaptive#export ,在这个方法中会根据 Url 的协议类型选择合适的 Protocol 来处理,这里协议类型为 registry, 则会选择 RegistryProtocol 来处理服务。

关于 Dubbo SPI 的详细分析请阅 Dubbo笔记衍生篇②:Dubbo SPI 原理


2.2. doExportUrlsFor1Protocol

loadRegistries(true); 解析出来所有的注册中心后,doExportUrlsFor1Protocol(protocolConfig, registryURLs) 开始针对不同的协议和注册中心进行服务发布。


需要注意,Dubbo 允许不通过注册中心,而是直连的方式进行调用。在这种情况下,由于没有注册中心的存在,最终暴露的URL也不相同,如下(URL 有简化):

  • 存在注册中心:在上面我们提到了如果存在注册中心,则会使用 RegistryProtocol 来处理服务。这就导致 URL 变更为了 registry://localhost:2181/org.apache.dubbo.registry.RegistryService,但是这个URL中并没有包含暴露的接口的信息,所以URL 在暴露服务时会添加一个参数 export来记录需要暴露的服务信息。此时 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方法进行暴露服务端口。

上面的总结出来 存在注册中心 和不存在注册中心的区别,其详细实现由于篇幅所限,故另开新篇:Dubbo笔记④ : 服务发布流程 - doExportUrlsFor1Protocol


以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://zhuanlan.zhihu.com/p/98561575
https://www.cnblogs.com/java-zhao/p/7625596.html
https://www.cnblogs.com/Cubemen/p/12312981.html
https://blog.csdn.net/u012881904/article/details/95891448
https://www.jianshu.com/p/e03bbd88e18f
https://blog.csdn.net/zcw4237256/article/details/78369213
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

猫吻鱼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值