熟悉Spring的小伙伴都知道,Spring 提供了强大的扩展机制。其中包括 **ApplicationContextInitializer**,该扩展是在上下文准备阶段(prepareContext),容器刷新之前做一些初始化工作,比如我们常用的配置中心 client 基本都是继承该初始化器,在容器刷新前将配置从远程拉到本地,然后封装成 PropertySource 放到 Environment 中供使用。
在 SpringCloud 场景下,SpringCloud 规范中提供了 PropertySourceBootstrapConfiguration实现了 ApplicationContextInitializer接口,另外还提供了个 PropertySourceLocator,二者配合完成配置中心的接入。
从上述截图可以看出,在 PropertySourceBootstrapConfiguration 这个单例对象初始化的时候会将 Spring 容器中所有的 PropertySourceLocator 实现注入进来。然后在 initialize()方法中循环所有的 PropertySourceLocator 进行配置的获取,从这儿可以看出 SpringCloud 应用是支持我们引入多个配置中心实现的,获取到配置后调用 insertPropertySources 方法将所有的 PropertySource(封装的一个个配置文件)添加到 Spring 的环境变量 environment 中。
如果我们要使用Nacos的配置中心功能,需要在pom.xml中引入对应的依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
跟nacos自动注册和服务发现一样,nacos配置中文也是利用了springboot的自动装配原理来实现的。
我们查看spring-cloud-starter-alibaba-nacos-config-2.2.9.RELEASE.jar的META-INF/spring.factories文件:
上图展示了在 spring-cloud-starter-alibaba-nacos-config 包提供的自动装配类中引入了NacosConfigBootstrapConfiguration类。我们查看其定义:
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigBootstrapConfiguration {
@Bean
@ConditionalOnMissingBean
public NacosConfigProperties nacosConfigProperties() {
return new NacosConfigProperties();
}
@Bean
@ConditionalOnMissingBean
public NacosConfigManager nacosConfigManager(
NacosConfigProperties nacosConfigProperties) {
return new NacosConfigManager(nacosConfigProperties);
}
@Bean
public NacosPropertySourceLocator nacosPropertySourceLocator(
NacosConfigManager nacosConfigManager) {
return new NacosPropertySourceLocator(nacosConfigManager);
}
/**
* Compatible with bootstrap way to start.
* @param beans configurationPropertiesBeans
* @return configurationPropertiesRebinder
*/
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
@ConditionalOnNonDefaultBehavior
public ConfigurationPropertiesRebinder smartConfigurationPropertiesRebinder(
ConfigurationPropertiesBeans beans) {
// If using default behavior, not use SmartConfigurationPropertiesRebinder.
// Minimize te possibility of making mistakes.
return new SmartConfigurationPropertiesRebinder(beans);
}
}
从源码可以看到,NacosConfigBootstrapConfiguration自动配置类注入了NacosPropertySourceLocator这个bean,该类继承自上述说的 PropertySourceLocator,重写了 locate 方法进行配置的读取。
我们来分析下 NacosPropertySourceLocator#locate()的代码:
public PropertySource<?> locate(Environment env) {
nacosConfigProperties.setEnvironment(env);
// 通过反射创建出一个NacosConfigService实例
ConfigService configService = nacosConfigManager.getConfigService();
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
}
long timeout = nacosConfigProperties.getTimeout();
// 配置获取(使用 configService)、配置封装、配置缓存等操作
nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
timeout);
String name = nacosConfigProperties.getName();
String dataIdPrefix = nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}
CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
// 加载共享的配置信息
loadSharedConfiguration(composite);
// 加载扩展的配置信息
loadExtConfiguration(composite);
// 加载应用自身的配置信息
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}
可以看到 Nacos 启动会加载以下三种配置文件,加载到配置文件后会封装成 NacosPropertySource 返回。
- loadSharedConfiguration():加载共享配置
private void loadSharedConfiguration(
CompositePropertySource compositePropertySource) {
// 获取共享配置
List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
.getSharedConfigs();
if (!CollectionUtils.isEmpty(sharedConfigs)) {
checkConfiguration(sharedConfigs, "shared-configs");
// 加载配置
loadNacosConfiguration(compositePropertySource, sharedConfigs);
}
}
- loadExtConfiguration():加载扩展配置
private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
// 获取扩展配置
List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
.getExtensionConfigs();
if (!CollectionUtils.isEmpty(extConfigs)) {
checkConfiguration(extConfigs, "extension-configs");
// 加载配置
loadNacosConfiguration(compositePropertySource, extConfigs);
}
}
- loadApplicationConfiguration():加载应用自身配置
private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {
// 配置文件扩展名
String fileExtension = properties.getFileExtension();
// 配置组名
String nacosGroup = properties.getGroup();
// 不带扩展名后缀查询,application
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);
// 带扩展名后缀查询,application.yml
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
// 带环境,带扩展名后缀查询,application-prod.yml
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}
}
加载应用自身的配置信息loadApplicationConfiguration()时,同时会加载以下三种配置,分别是:
- 1、不带扩展名后缀查询,application
- 2、带扩展名后缀查询,application.yml
- 3、带环境,带扩展名后缀查询,application-prod.yml
依据后面加载的属性会覆盖掉之前加载的,可知,从上到下,优先级依次增高。
加载配置的核心方法是loadNacosDataIfPresent():
private void loadNacosDataIfPresent(final CompositePropertySource composite,
final String dataId, final String group, String fileExtension,
boolean isRefreshable) {
if (null == dataId || dataId.trim().length() < 1) {
return;
}
if (null == group || group.trim().length() < 1) {
return;
}
NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
fileExtension, isRefreshable);
this.addFirstPropertySource(composite, propertySource, false);
}
private NacosPropertySource loadNacosPropertySource(final String dataId,
final String group, String fileExtension, boolean isRefreshable) {
if (NacosContextRefresher.getRefreshCount() != 0) {
if (!isRefreshable) {
return NacosPropertySourceRepository.getNacosPropertySource(dataId,
group);
}
}
return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
isRefreshable);
}
nacosPropertySourceBuilder.build()方法调用 loadNacosData 获取配置,然后封装成 NacosPropertySource,并且将该对象缓存到 NacosPropertySourceRepository中,后续会用到。
NacosPropertySource build(String dataId, String group, String fileExtension,
boolean isRefreshable) {
// 获取配置
List<PropertySource<?>> propertySources = loadNacosData(dataId, group,
fileExtension);
// 将配置封装成NacosPropertySource
NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources,
group, dataId, new Date(), isRefreshable);
// 缓存NacosPropertySource:ConcurrentHashMap<String, NacosPropertySource> NACOS_PROPERTY_SOURCE_REPOSITORY
// key:{dataId,group}
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}
我们来看看如何获取配置的loadNacosData():
private List<PropertySource<?>> loadNacosData(String dataId, String group,
String fileExtension) {
String data = null;
try {
// 通过NacosConfigService加载配置内容
data = configService.getConfig(dataId, group, timeout);
if (StringUtils.isEmpty(data)) {
log.warn(
"Ignore the empty nacos configuration and get it based on dataId[{}] & group[{}]",
dataId, group);
return Collections.emptyList();
}
if (log.isDebugEnabled()) {
log.debug(String.format(
"Loading nacos data, dataId: '%s', group: '%s', data: %s", dataId,
group, data));
}
// 解析返回的字符串
return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,
fileExtension);
}
catch (NacosException e) {
log.error("get data from Nacos error,dataId:{} ", dataId, e);
}
catch (Exception e) {
log.error("parse data from Nacos error,dataId:{},data:{}", dataId, data, e);
}
return Collections.emptyList();
}
loadNacosData 方法中会将实际配置加载请求委托给NacosConfigService去做,然后解析返回的字符串,解析器实现了 PropertySourceLoader 接口,支持 yml、properties、xml、json 这几种。
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return getConfigInner(namespace, dataId, group, timeoutMs);
}
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
// 默认组名:DEFAULT_GROUP
group = blank2defaultGroup(group);
// 校验组名、dataId
ParamUtils.checkKeyParam(dataId, group);
ConfigResponse cr = new ConfigResponse();
// 设置配置信息,通过 namespace, dataId, group能唯一定位到一个配置文件
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
// We first try to use local failover content if exists.
// A config content for failover is not created by client program automatically,
// but is maintained by user.
// This is designed for certain scenario like client emergency reboot,
// changing config needed in the same time, while nacos server is down.
// 首先尝试使用本地如果存在的故障转移的配置内容,如果能读到失败转移的配置信息,则直接返回了。
// 优先使用失败转移,设计的目的是当server挂后,又需要修改配置,就可以读本地目录
String content = LocalConfigInfoProcessor.getFailover(worker.getAgentName(), dataId, group, tenant);
if (content != null) {
LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));
cr.setContent(content);
String encryptedDataKey = LocalEncryptedDataKeyProcessor.getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
cr.setEncryptedDataKey(encryptedDataKey);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
try {
// 去服务端拉取,这个就是正常逻辑,获取服务端存储的配置信息
ConfigResponse response = worker.getServerConfig(dataId, group, tenant, timeoutMs, false);
cr.setContent(response.getContent());
cr.setEncryptedDataKey(response.getEncryptedDataKey());
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
} catch (NacosException ioe) {
if (NacosException.NO_RIGHT == ioe.getErrCode()) {
throw ioe;
}
LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}", worker.getAgentName(), dataId, group, tenant, ioe.toString());
}
// 非鉴权失败的异常(NacosException.NO_RIGHT)的,尝试从本地快照中获取配置
content = LocalConfigInfoProcessor.getSnapshot(worker.getAgentName(), dataId, group, tenant);
if (content != null) {
LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));
}
cr.setContent(content);
String encryptedDataKey = LocalEncryptedDataKeyProcessor.getEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant);
cr.setEncryptedDataKey(encryptedDataKey);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
从源码可以看到,首先获取本地缓存文件的配置内容(故障转移的配置内容),如果有直接返回;如果从本地没找到相应配置文件,则通过grpc请求服务端拉取;如果grpc请求报除了鉴权失败之外的异常(NacosException.NO_RIGHT),例如超时,还会尝试从本地快照中获取配置,不至于偶尔超时了就配置没了。文章来源:https://www.toymoban.com/news/detail-826981.html
至此,在项目启动的时候(上下文准备阶段)通过NacosPropertySourceLocator就拉取到了远程 Nacos 中的配置信息,并且封装成 NacosPropertySource对象,然后PropertySourceBootstrapConfiguration依靠ApplicationContextInitializer机制(容器刷新之前支持一些自定义初始化工作),将前面封装好的NacosPropertySource对象放到了 Spring 的环境变量Environment 中。文章来源地址https://www.toymoban.com/news/detail-826981.html
到了这里,关于九、Nacos源码系列:Nacos配置中心原理(一)- SpringCloud应用启动时拉取配置的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!