1. 前言
Dubbo一般很少单独使用,更多的是和Spring框架做集成,但是不管怎样,Dubbo最终都是创建并启动DubboBootstrap。Dubbo系列文章只研究Dubbo,因此不会和Spring扯上任何关系。
DubboBootstrap被设计成单例的,通过双重检查加锁的方式,这意味着在一个JVM进程内只能启动一个实例(不准确)。
从名字也可以看的出来,DubboBootstrap只是一个启动引导类,它本身并没有实现什么业务逻辑。它依赖于其他组件类,将它们进行装配,彼此协同工作。例如:ConfigManager管理着所有的配置类,Dubbo通过这些配置类去创建注册中心、元数据中心、配置中心等等。
2. 启动流程
Dubbo服务按照角色可以分为Provider和Consumer,同一个服务很可能既是提供者,也是消费者,因此DubboBootstrap的启动流程大致可以分为以下三个步骤:
- 初始化initialize()
- 暴露服务exportServices()
- 引用服务referServices()
暴露和引用服务是非常重要的核心流程,细节较多,会单独记录,这里暂时跳过。
首先看一下DubboBootstrap标准启动代码:
DubboBootstrap.getInstance()
.application("ApplicationName")
.configCenter(ccc)
.metadataReport(mrc)
.registry(rc)
.service(sc)
.start();
首先是获取DubboBootstrap实例,前面说过了,它是单例的,只会创建一次,它的构造函数如下,做了三件事:
- SPI加载ConfigManager
- SPI加载Environment
- 注册钩子函数,优雅停机
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()
方法,它主要做了以下事情:
- 初始化FrameworkExt
- 启动配置中心
- 加载Registry和Protocol
- 检查相关配置
- 启动元数据中心
- 初始化元数据服务
- 初始化事件监听
在我看来核心就是给服务的暴露和引用准备好环境,例如:加载配置中心的配置,相关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.
为前缀的配置,得到registryIds
和protocolIds
,然后创建对应的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分为local
和remote
,前者数据仅保留在内存,后者数据会发布到远程。
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,最后初始化元数据相关的服务。
初始化完成后,就是服务的暴露和引用了,这一块细节较多,篇幅原因,本文没有展开,会单独记录。