DubboBootstrap启动流程

1. 前言

Dubbo一般很少单独使用,更多的是和Spring框架做集成,但是不管怎样,Dubbo最终都是创建并启动DubboBootstrap。Dubbo系列文章只研究Dubbo,因此不会和Spring扯上任何关系。

DubboBootstrap被设计成单例的,通过双重检查加锁的方式,这意味着在一个JVM进程内只能启动一个实例(不准确)。

从名字也可以看的出来,DubboBootstrap只是一个启动引导类,它本身并没有实现什么业务逻辑。它依赖于其他组件类,将它们进行装配,彼此协同工作。例如:ConfigManager管理着所有的配置类,Dubbo通过这些配置类去创建注册中心、元数据中心、配置中心等等。

2. 启动流程

Dubbo服务按照角色可以分为Provider和Consumer,同一个服务很可能既是提供者,也是消费者,因此DubboBootstrap的启动流程大致可以分为以下三个步骤:

  1. 初始化initialize()
  2. 暴露服务exportServices()
  3. 引用服务referServices()

暴露和引用服务是非常重要的核心流程,细节较多,会单独记录,这里暂时跳过。
DubboBootstrap启动流程

首先看一下DubboBootstrap标准启动代码:

DubboBootstrap.getInstance()
				.application("ApplicationName")
				.configCenter(ccc)
				.metadataReport(mrc)
				.registry(rc)
				.service(sc)
				.start();

首先是获取DubboBootstrap实例,前面说过了,它是单例的,只会创建一次,它的构造函数如下,做了三件事:

  1. SPI加载ConfigManager
  2. SPI加载Environment
  3. 注册钩子函数,优雅停机
private DubboBootstrap() {
    configManager = ApplicationModel.getConfigManager();
    environment = ApplicationModel.getEnvironment();
    DubboShutdownHook.getDubboShutdownHook().register();
    ShutdownHookCallbacks.INSTANCE.addCallback(new ShutdownHookCallback() {
        @Override
        public void callback() throws Throwable {
            DubboBootstrap.this.destroy();
        }
    });
}

然后就是装配各种Config,最后调用start()方法启动。这里面很多Config对象都是可选项,Dubbo会提供默认值。

ApplicationConfig是对应用的描述对象,包含应用名称、版本等信息。
ServiceConfig是对需要暴露的服务的描述对象,可以设置例如暴露的接口、版本、分组等信息。
RegistryConfig是对注册中心的描述对象,Dubbo会将暴露的服务注册到注册中心,这样Consumer就能动态感知到了。
ConfigCenterConfig是对配置中心的描述对象,Dubbo现在是支持动态配置中心的,服务配置通过配置中心下发,避免在每个项目里硬编码,难以维护。
MetadataReportConfig是对元数据中心的描述对象,Dubbo2.7版本将注册中心和元数据中心进行了拆分,大大减轻了注册中心的压力。

DubboBootstrap启动前,需要装配好各种所需的Config对象,这些对象最终被交给ConfigManager管理。ConfigManager内部使用一个双层嵌套的HashMap容器来存储Config对象,外层的Key是Config类的TagName,内层的Key是Config类的Id。这样管理的好处是,当要暴露服务时,可以方便的取出所有的ServiceConfig。

/**
 * Config管理器
 * 管理着Dubbo运行时各种各样的Config类
 * ServiceConfig、ReferenceConfig、RegistryConfig等
 */
public class ConfigManager extends LifecycleAdapter implements FrameworkExt {
    /**
     * Config容器
     * 外层:Config类TagName
     * 内层:id > Config
     */
    final Map<String, Map<String, AbstractConfig>> configsCache = newMap();
}

Config对象交给ConfigManager管理后,Map容器的结构示例如下:

{
	"registry":{
		"RegistryConfigId":Instance
	},
	"application":{
		"ApplicationConfigId":Instance
	},
	"service":{
		"ServiceA":InstanceA,
		"ServiceB":InstanceB
	}
}

Config类装配好后,就可以开始启动了。start()方法精简后如下:

public DubboBootstrap start() {
        if (started.compareAndSet(false, true)) {
            ready.set(false);
            // 初始化
            initialize();
            // 暴露服务
            exportServices();
            if (!isOnlyRegisterProvider() || hasExportedServices()) {
                // 暴露MetadataService
                exportMetadataService();
                // 将dubbo实例注册到专用于服务发现的注册中心
                registerServiceInstance();
            }
            // 引用服务
            referServices();
        }
        return this;
    }

2.1 初始化

这里我们重点关注initialize()方法,它主要做了以下事情:

  1. 初始化FrameworkExt
  2. 启动配置中心
  3. 加载Registry和Protocol
  4. 检查相关配置
  5. 启动元数据中心
  6. 初始化元数据服务
  7. 初始化事件监听

在我看来核心就是给服务的暴露和引用准备好环境,例如:加载配置中心的配置,相关Config对象的属性根据配置的优先级完成refresh。

public void initialize() {
    if (!initialized.compareAndSet(false, true)) {
        return;
    }
    // 初始化FrameworkExt
    ApplicationModel.initFrameworkExts();
    // 启动配置中心
    startConfigCenter();
    // 加载Registry和Protocol
    loadRemoteConfigs();
    // 检查相关配置
    checkGlobalConfigs();
    // 启动元数据中心
    startMetadataCenter();
    // 初始化元数据服务
    initMetadataService();
    // 初始化元数据导出服务
    initMetadataServiceExports();
    // 初始化事件监听
    initEventListener();
}

2.1.1 initFrameworkExts()

Dubbo提供了框架扩展接口FrameworkExt,初始化时,利用SPI加载所有的实现,然后调用它们的initialize()方法。目前只有Environment会初始化,它会保存默认配置中心的数据。

public static void initFrameworkExts() {
    Set<FrameworkExt> exts = ExtensionLoader.getExtensionLoader(FrameworkExt.class).getSupportedExtensionInstances();
    for (FrameworkExt ext : exts) {
        ext.initialize();
    }
}

2.1.2 startConfigCenter()

接下来是启动配置中心,ConfigCenterConfig是可选配置,如果没有配,Dubbo会尝试使用注册中心来作为配置中心。

private void useRegistryAsConfigCenterIfNecessary() {
    if (environment.getDynamicConfiguration().isPresent()) {
        // 动态配置已存在
        return;
    }
    if (CollectionUtils.isNotEmpty(configManager.getConfigCenters())) {
        // 存在配置中心
        return;
    }
    // 尝试将注册中心转换为配置中心
    configManager
        .getDefaultRegistries()
        .stream()
        .filter(this::isUsedRegistryAsConfigCenter)
        .map(this::registryAsConfigCenter)
        .forEach(configManager::addConfigCenter);
}

Dubbo是支持配置多个配置中心的,为了便于管理,Dubbo提供了CompositeDynamicConfiguration类来聚合多个动态的配置中心,底层采用HashSet存储。

public class CompositeDynamicConfiguration implements DynamicConfiguration {
    //聚合 多个动态配置
    private Set<DynamicConfiguration> configurations = new HashSet<>();
}

聚合之前,需要将ConfigCenterConfig转换成DynamicConfiguration,方法是prepareEnvironment()。这里会根据动态中心的URL协议通过SPI机制加载对应的DynamicConfiguration,例如使用Nacos作为配置中心,对应的就是NacosDynamicConfiguration了。另外,这里还会直接读取配置中心的内容,并保存到环境对象Environment中。

private DynamicConfiguration prepareEnvironment(ConfigCenterConfig configCenter) {
    // 根据url协议加载动态配置中心 NacosDynamicConfiguration
    DynamicConfiguration dynamicConfiguration = getDynamicConfiguration(configCenter.toUrl());
    // 读取配置内容(共享配置)
    String configContent = dynamicConfiguration.getProperties(configCenter.getConfigFile(), configCenter.getGroup());
    // 当前应用独占配置
    String appGroup = getApplication().getName();
    String appConfigContent = null;
    if (isNotEmpty(appGroup)) {
        appConfigContent = dynamicConfiguration.getProperties
            (isNotEmpty(configCenter.getAppConfigFile()) ? configCenter.getAppConfigFile() : configCenter.getConfigFile(),appGroup);
    }
    // 是否配置中心优先
    environment.setConfigCenterFirst(configCenter.isHighestPriority());
    // 内容保存到Environment
    environment.updateExternalConfigurationMap(parseProperties(configContent));
    environment.updateAppExternalConfigurationMap(parseProperties(appConfigContent));
    return dynamicConfiguration;
}

经过以上步骤,应用就已经读取到了配置中心的内容,此时需要将相关Config对象的属性进行refresh。Config对象的属性刷新很有意思,我们知道,Dubbo提供了多种配置方式,例如:dubbo.properties、环境变量、JVM参数等,这些配置的优先级都是不一样的,同样的配置在多个地方出现,是会按照优先级进行覆盖的,refresh()方法就实现了这一块的逻辑。

refresh()方法会通过反射获取ConfigClass的所有方法,然后判断这些方法是否是属性的Setter方法,如果是就会通过反射进行赋值,值从哪里来呢?这就要回到Dubbo的环境对象Environment了。Environment对象保存了Dubbo所有形式的配置,如下:

public class Environment extends LifecycleAdapter implements FrameworkExt {
    public static final String NAME = "environment";
    // dubbo.properties文件配置
    private final PropertiesConfiguration propertiesConfiguration;
    // JVM环境变量
    private final SystemConfiguration systemConfiguration;
    // 系统环境变量
    private final EnvironmentConfiguration environmentConfiguration;
    // 外部共享配置
    private final InmemoryConfiguration externalConfiguration;
    // App外部独占配置
    private final InmemoryConfiguration appExternalConfiguration;

    private CompositeConfiguration globalConfiguration;
    // 外部配置的内容
    private Map<String, String> externalConfigurationMap = new HashMap<>();
    private Map<String, String> appExternalConfigurationMap = new HashMap<>();

    private boolean configCenterFirst = true;
    // 动态配置,聚合了配置中心
    private DynamicConfiguration dynamicConfiguration;
}

refresh()方法首先会通过Environment获取ConfigClass前缀的CompositeConfiguration,它是一个组合配置,底层用有序的List存储,Environment会根据优先级编排该List,越靠前的配置优先级越高,获取属性值时只需要遍历List即可。如此一来,Config对象就可以根据配置的优先级完成属性的refresh了。

public synchronized CompositeConfiguration getPrefixedConfiguration(AbstractConfig config) {
    CompositeConfiguration prefixedConfiguration = new CompositeConfiguration(config.getPrefix(), config.getId());
    Configuration configuration = new ConfigConfigurationAdapter(config);
    if (this.isConfigCenterFirst()) {
        // 配置中心优先
        prefixedConfiguration.addConfiguration(systemConfiguration);
        prefixedConfiguration.addConfiguration(environmentConfiguration);
        prefixedConfiguration.addConfiguration(appExternalConfiguration);
        prefixedConfiguration.addConfiguration(externalConfiguration);
        prefixedConfiguration.addConfiguration(configuration);
        prefixedConfiguration.addConfiguration(propertiesConfiguration);
    } else {
        prefixedConfiguration.addConfiguration(systemConfiguration);
        prefixedConfiguration.addConfiguration(environmentConfiguration);
        prefixedConfiguration.addConfiguration(configuration);
        prefixedConfiguration.addConfiguration(appExternalConfiguration);
        prefixedConfiguration.addConfiguration(externalConfiguration);
        prefixedConfiguration.addConfiguration(propertiesConfiguration);
    }
    return prefixedConfiguration;
}

2.1.3 loadRemoteConfigs()

在上一步操作中,应用已经读取到了配置中心的所有内容了,接下来会加载RegistryConfig和ProtocolConfig。具体步骤是解析以dubbo.registries.dubbo.protocols.为前缀的配置,得到registryIdsprotocolIds,然后创建对应的RegistryConfig和ProtocolConfig对象并设置ID,然后调用refresh()方法,对象自身的属性会自动赋值。
解析到的RegistryConfig和ProtocolConfig也属于Config对象,自然也要交给ConfigManager管理。

private void loadRemoteConfigs() {
    List<RegistryConfig> tmpRegistries = new ArrayList<>();
    // 解析 dubbo.registries. 前缀的配置
    Set<String> registryIds = configManager.getRegistryIds();
    registryIds.forEach(id -> {
        if (tmpRegistries.stream().noneMatch(reg -> reg.getId().equals(id))) {
            tmpRegistries.add(configManager.getRegistry(id).orElseGet(() -> {
                // 转换成RegistryConfig
                RegistryConfig registryConfig = new RegistryConfig();
                registryConfig.setId(id);
                registryConfig.refresh();
                return registryConfig;
            }));
        }
    });
    configManager.addRegistries(tmpRegistries);

    List<ProtocolConfig> tmpProtocols = new ArrayList<>();
    // 解析 dubbo.protocols. 前缀的配置
    Set<String> protocolIds = configManager.getProtocolIds();
    protocolIds.forEach(id -> {
        if (tmpProtocols.stream().noneMatch(prot -> prot.getId().equals(id))) {
            tmpProtocols.add(configManager.getProtocol(id).orElseGet(() -> {
                // 转换成 ProtocolConfig
                ProtocolConfig protocolConfig = new ProtocolConfig();
                protocolConfig.setId(id);
                protocolConfig.refresh();
                return protocolConfig;
            }));
        }
    });
    configManager.addProtocols(tmpProtocols);
}

2.1.4 checkGlobalConfigs()

经过上述步骤,相关的Config对象该创建的创建了,属性也都refresh了,接下来就是对这些Config对象进行Check了,校验配置是否合法。
除了校验配置本身,这里也会对没有提供的配置对象进行自动创建,并给属性赋值。以ProviderConfig为例:

// check Provider
Collection<ProviderConfig> providers = configManager.getProviders();
if (CollectionUtils.isEmpty(providers)) {
    configManager.getDefaultProvider().orElseGet(() -> {
        ProviderConfig providerConfig = new ProviderConfig();
        configManager.addProvider(providerConfig);
        providerConfig.refresh();
        return providerConfig;
    });
}

2.1.5 startMetadataCenter()

接下来是启动元数据中心,MetadataReportConfig是可选项,没有提供Dubbo会尝试使用注册中心,转换过程和配置中心类似。
Dubbo允许配置多个元数据中心,但是2.7.8版本目前只会使用一个。解析到的MetadataReportConfig对象会通过MetadataReportInstance类进行初始化,根据元数据中心的地址URL协议通过SPI加载对应的MetadataReport实现类。

public static void init(URL metadataReportURL) {
    if (init.get()) {
        return;
    }
    // SPI加载自适应工厂
    MetadataReportFactory metadataReportFactory = ExtensionLoader.getExtensionLoader(MetadataReportFactory.class).getAdaptiveExtension();
    if (METADATA_REPORT_KEY.equals(metadataReportURL.getProtocol())) {
        // 改写URL,恢复真实的protocol
        String protocol = metadataReportURL.getParameter(METADATA_REPORT_KEY, DEFAULT_DIRECTORY);
        metadataReportURL = URLBuilder.from(metadataReportURL)
            .setProtocol(protocol)
            .removeParameter(METADATA_REPORT_KEY)
            .build();
    }
    /**
      * 根据URL协议创建MetadataReport
      * Nacos被废弃 {@link ConfigCenterBasedMetadataReport}
      */
    metadataReport = metadataReportFactory.getMetadataReport(metadataReportURL);
    init.set(true);
}

新版本中,NacosMetadataReport已经被废弃了,改为ConfigCenterBasedMetadataReport。元数据也属于配置,因此它底层依赖DynamicConfiguration,如果使用Nacos对应的实现就是NacosDynamicConfiguration,所谓的“导出元数据”其实就是将元数据配置发布到Nacos而已。

2.1.6 initMetadataService()

初始化元数据服务,比较简单,主要就是根据metadataType使用SPI加载对应的WritableMetadataService实现。
metadataType分为localremote,前者数据仅保留在内存,后者数据会发布到远程。

private void initMetadataService() {
    this.metadataService = WritableMetadataService.getExtension(getMetadataType());
}

Dubbo在暴露服务时,会将服务元数据进行发布。

Exporter<?> exporter = PROTOCOL.export(wrapperInvoker);
WritableMetadataService metadataService = WritableMetadataService.getExtension(url.getParameter(METADATA_KEY, DEFAULT_METADATA_STORAGE_TYPE));
if (metadataService != null) {
    // 发布服务元数据
    metadataService.publishServiceDefinition(url);
}

2.1.7 initMetadataServiceExports()

初始化元数据导出服务,通过SPI加载MetadataServiceExporter实现,存储到Set容器中。
MetadataServiceExporter接口的职责就是专门用来暴露MetadataService接口的,Provider通过暴露MetadataService接口,这样Consumer就可以通过RPC调用的方式来获取接口元数据了。

private void initMetadataServiceExports() {
    /**
      * @see org.apache.dubbo.config.metadata.RemoteMetadataServiceExporter
      */
    this.metadataServiceExporters = getExtensionLoader(MetadataServiceExporter.class)
        .getSupportedExtensionInstances();
}

2.1.8 initEventListener()

初始化事件监听器,EventDispatcher接口是Dubbo的事件分发器,负责维护EventListener和分发事件。DubboBootstrap本身就是一个事件监听器EventListener,初始化时会将自身加入到EventDispatcher。

public DubboBootstrap addEventListener(EventListener<?> listener) {
    eventDispatcher.addEventListener(listener);
    return this;
}

2.2 暴露服务

在初始化方法中,已经装配好了相关的Config对象,为服务的暴露和引用准备了条件。Dubbo会从ConfigManager中提取出所有的ServiceConfig,然后挨个进行服务暴露,最终调用的是ServiceConfig的export()方法。

服务暴露的细节,单独写文章记录。

private void exportServices() {
    configManager.getServices().forEach(sc -> {
        // TODO, compatible with ServiceConfig.export()
        ServiceConfig serviceConfig = (ServiceConfig) sc;
        serviceConfig.setBootstrap(this);
        if (exportAsync) {
            // 异步暴露
            ExecutorService executor = executorRepository.getServiceExporterExecutor();
            Future<?> future = executor.submit(() -> {
                sc.export();
                exportedServices.add(sc);
            });
            asyncExportingFutures.add(future);
        } else {
            sc.export();
            exportedServices.add(sc);
        }
    });
}

2.3 引用服务

大多数时候,服务很可能既是提供者,也是消费者。因此,除了暴露自身提供的服务,也要引用所依赖的服务。Dubbo会从ConfigManager中提取出所有的ReferenceConfig,然后分别调用它们的get()方法,引用远程服务,创建代理对象。

服务引用的细节,单独写文章记录。

private void referServices() {
    configManager.getReferences().forEach(rc -> {
        ReferenceConfig referenceConfig = (ReferenceConfig) rc;
        referenceConfig.setBootstrap(this);
        if (rc.shouldInit()) {
            if (referAsync) {
                CompletableFuture<Object> future = ScheduledCompletableFuture.submit(
                    executorRepository.getServiceExporterExecutor(),
                    () -> cache.get(rc)
                );
                asyncReferringFutures.add(future);
            } else {
                cache.get(rc);
            }
        }
    });
}

3. 总结

DubboBootstrap是Dubbo服务的启动引导类,服务启动时,首先会完成自身环境的一个初始化,为后续服务的暴露和引用做好准备。初始化做的主要工作就是读取配置中心的数据,然后完成相关Config对象的装配,将Config对象的属性根据优先级进行refresh,最后初始化元数据相关的服务。
初始化完成后,就是服务的暴露和引用了,这一块细节较多,篇幅原因,本文没有展开,会单独记录。

上一篇:springcloud和dubbo的区别


下一篇:Dubbo之同步异步调用