前言:
前面我们分析了 Ncaos 的注册中心性格源码,了解了 Nacos 注册中的服务注册、心跳检测、健康检查、集群间数据同步等源码,本篇开始我们分析一下 Nacos 配置中心的源码。
Nacos 系列文章传送门:
Nacos 配置管理模型 – 命名空间(Namespace)、配置分组(Group)和配置集ID(Data ID)
Nacos Server 是如何通知 Nacos Client 服务下线?
Nacos Client 是如何接受 Nacos Server 推送的数据?
Nacos 故障转移源码分析(FailoverReactor)
SpringApplication#prepareContext 方法源码分析
SpringApplication#prepareContext 方法主要是做一些容器刷新之前的动作,完成各类初始化动作,其中有一个比较核心的操作,把启动类注入容器,为后续开启自动化配置奠定了基础,本篇我们重点关注 this.applyInitializers(context) 这行代码。
//org.springframework.boot.SpringApplication#prepareContext
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
//设置上下文环境
context.setEnvironment(environment);
//执行上下文后处理
this.postProcessApplicationContext(context);
//执行 ApplicationContextInitializer 本篇重点关注
this.applyInitializers(context);
//发布上下文准备好的监听事件
listeners.contextPrepared(context);
//关闭引导上下文
bootstrapContext.close(context);
if (this.logStartupInfo) {
this.logStartupInfo(context.getParent() == null);
this.logStartupProfileInfo(context);
}
//获取应用程序上下文的beanFactory 并设置一些属性
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
//注册单例bean springApplicationArguments 容器指定的参数封装成 bean 注入容器
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
//将 Banner 封装成 bean 注入到容器中
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
//是否是 DefaultListableBeanFactory 类型的容器
if (beanFactory instanceof DefaultListableBeanFactory) {
//是 强行覆盖
((DefaultListableBeanFactory)beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
//是否懒加载
if (this.lazyInitialization) {
//加 懒加载 beanFactory 处理器
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
//获取启动类指定的参数
Set<Object> sources = this.getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
//加载启动类 将启动类注入容器
this.load(context, sources.toArray(new Object[0]));
//发布容器加载完成事件
listeners.contextLoaded(context);
}
SpringApplication#applyInitializers 方法源码分析
SpringApplication#applyInitializers 方法获取了所有的 ApplicationContextInitializer,调用 ApplicationContextInitializer 的 initialize 方法执行初始化。
//org.springframework.boot.SpringApplication#applyInitializers
protected void applyInitializers(ConfigurableApplicationContext context) {
//迭代遍历 Initializers
Iterator var2 = this.getInitializers().iterator();
while(var2.hasNext()) {
//Initializers 都实现了 ApplicationContextInitializer 接口 强转为 ApplicationContextInitializer
ApplicationContextInitializer initializer = (ApplicationContextInitializer)var2.next();
//得到类型
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
//initializer 执行初始化
initializer.initialize(context);
}
}
PropertySourceBootstrapConfiguration#initialize 方法源码分析
PropertySourceBootstrapConfiguration#initialize 方法真正实现了配置中心的加载,PropertySourceBootstrapConfiguration 类实现了 ApplicationContextInitializer 接口,PropertySourceBootstrapConfiguration 类在容器初始化的时候从配置中心拉取配置到本地,封装成 PropertySource 存入到 Environment 中提供给应用服务使用。
//org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration#initialize
public void initialize(ConfigurableApplicationContext applicationContext) {
//属性来源
List<PropertySource<?>> composite = new ArrayList();
//propertySourceLocators 排序 propertySourceLocators 其实就是配置中心的相关信息
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
//获取容器的环境配置环境信息
ConfigurableEnvironment environment = applicationContext.getEnvironment();
//迭代遍历配置
Iterator var5 = this.propertySourceLocators.iterator();
while(true) {
Collection source;
do {
do {
if (!var5.hasNext()) {
if (!empty) {
//可变属性
MutablePropertySources propertySources = environment.getPropertySources();
//日志配置
String logConfig = environment.resolvePlaceholders("${logging.config:}");
//读取环境
LogFile logFile = LogFile.get(environment);
//得带遍历属性配置
Iterator var15 = environment.getPropertySources().iterator();
while(var15.hasNext()) {
PropertySource<?> p = (PropertySource)var15.next();
//如果 bootstrapProperties 包含 就移出
if (p.getName().startsWith("bootstrapProperties")) {
propertySources.remove(p.getName());
}
}
//插入属性来源
this.insertPropertySources(propertySources, composite);
//重新初始化日志系统
this.reinitializeLoggingSystem(environment, logConfig, logFile);
//设置日志级别
this.setLogLevels(applicationContext, environment);
//出来配置文件
this.handleIncludedProfiles(environment);
}
return;
}
//属性来源定位器
PropertySourceLocator locator = (PropertySourceLocator)var5.next();
//调用 PropertySourceLocator#locateCollection 方法来读取配置 这里重点关注一下
source = locator.locateCollection(environment);
} while(source == null);
} while(source.size() == 0);
//属性list
List<PropertySource<?>> sourceList = new ArrayList();
//迭代遍历属性
Iterator var9 = source.iterator();
while(var9.hasNext()) {
PropertySource<?> p = (PropertySource)var9.next();
if (p instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource)p;
sourceList.add(new BootstrapPropertySource(enumerable));
} else {
sourceList.add(new SimpleBootstrapPropertySource(p));
}
}
logger.info("Located property source: " + sourceList);
//加入属性集合
composite.addAll(sourceList);
empty = false;
}
}
debugger 中 propertySourceLocators 信息:
debugger 中 PropertySource 信息:
PropertySourceLocator 类
我们知道了 PropertySourceBootstrapConfiguration 类在应用服务启动时候从配置中心拉取配置到本地供应用服务使用,这里只是加载拉取了配置中心的配置,而且我们知道配置来源自 propertySourceLocators 属性,propertySourceLocators 是 PropertySourceBootstrapConfiguration 类中自动注入的,源码如下:
@Autowired(
required = false
)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList();
propertySourceLocators 是个 List,从这里可以知道 Spring Cloud 应用是可以支持多配置中心的,从这里我们也可以知道 Spring Cloud 通过 PropertySourceBootstrapConfiguration 和 PropertySourceLocator 完成了配置配置中心配置的加载,那到底是如何加载 Nacos 配置中心的呢?
NacosConfigBootstrapConfiguration 类源码分析
跟踪 PropertySourceLocator 源码,发现 PropertySourceLocator 只是一个接口类,PropertySourceLocator 的唯一实现类是 NacosPropertySourceLocator 类,NacosPropertySourceLocator 肯定和 Nacos 配置中心的加载有着很大的关系,那 NacosPropertySourceLocator 何时被加载的?
package com.alibaba.cloud.nacos;
import com.alibaba.cloud.nacos.client.NacosPropertySourceLocator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
name = {"spring.cloud.nacos.config.enabled"},
matchIfMissing = true
)
public class NacosConfigBootstrapConfiguration {
public NacosConfigBootstrapConfiguration() {
}
//Nacos 配置信息
@Bean
@ConditionalOnMissingBean
public NacosConfigProperties nacosConfigProperties() {
return new NacosConfigProperties();
}
//创建 NacosConfigService
@Bean
@ConditionalOnMissingBean
public NacosConfigManager nacosConfigManager(NacosConfigProperties nacosConfigProperties) {
return new NacosConfigManager(nacosConfigProperties);
}
//Nacos 配置中心的重点类
@Bean
public NacosPropertySourceLocator nacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {
return new NacosPropertySourceLocator(nacosConfigManager);
}
}
可以看到 NacosPropertySourceLocator 是在 NacosConfigBootstrapConfiguration 类中被加载的,根据类名可以知道 NacosConfigBootstrapConfiguration 是个配置类,NacosConfigBootstrapConfiguration 是通过 Spring Boot 自动配置加载的,至此 NacosPropertySourceLocator 类的加载已经水落石出。
PropertySourceLocator 类源码分析
上面在分析 PropertySourceBootstrapConfiguration#initialize 方法时候,提到了 PropertySourceLocator#locateCollection 方法,该方法调用了 NacosPropertySourceLocator#locate 方法,Nacos 配置中心的加载逻辑就在该方法中。
public interface PropertySourceLocator {
PropertySource<?> locate(Environment environment);
default Collection<PropertySource<?>> locateCollection(Environment environment) {
return locateCollection(this, environment);
}
static Collection<PropertySource<?>> locateCollection(PropertySourceLocator locator, Environment environment) {
//调用 NacosPropertySourceLocator#locate 方法
PropertySource<?> propertySource = locator.locate(environment);
//propertySource 为空判断
if (propertySource == null) {
return Collections.emptyList();
} else if (CompositePropertySource.class.isInstance(propertySource)) {
//封装结果返回
Collection<PropertySource<?>> sources = ((CompositePropertySource)propertySource).getPropertySources();
List<PropertySource<?>> filteredSources = new ArrayList();
Iterator var5 = sources.iterator();
while(var5.hasNext()) {
PropertySource<?> p = (PropertySource)var5.next();
if (p != null) {
filteredSources.add(p);
}
}
return filteredSources;
} else {
return Arrays.asList(propertySource);
}
}
}
NacosPropertySourceLocator#locate 方法源码解析
NacosPropertySourceLocator 实现了 PropertySourceLocator 接口,重写了 locate 方法,locate 方法会加载配置文件,将配置文件内容封装成 NacosPropertySource 返回,从源码中可以看出分别加载了共享配置、扩展配置、应用自身配置,我们分析加载应用自身配置这条链路,其实其他两条链路最终都调用了 NacosPropertySourceLocator#loadNacosDataIfPresent 方法。
//com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#locate
public PropertySource<?> locate(Environment env) {
//设置环境
this.nacosConfigProperties.setEnvironment(env);
//获取 ConfigService
ConfigService configService = this.nacosConfigManager.getConfigService();
//ConfigService 为空判断
if (null == configService) {
log.warn("no instance of config service found, can't load config from nacos");
return null;
} else {
//超时时间
long timeout = (long)this.nacosConfigProperties.getTimeout();
//赋值 nacosPropertySourceBuilder
this.nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService, timeout);
//获取配置文件 name
String name = this.nacosConfigProperties.getName();
//获取前缀
String dataIdPrefix = this.nacosConfigProperties.getPrefix();
//前缀为空判断
if (StringUtils.isEmpty(dataIdPrefix)) {
//赋值
dataIdPrefix = name;
}
//再次前缀为空判断
if (StringUtils.isEmpty(dataIdPrefix)) {
//默认使用 spring.application.name 的值
dataIdPrefix = env.getProperty("spring.application.name");
}
//属性配置对象
CompositePropertySource composite = new CompositePropertySource("NACOS");
//加载共享配置
this.loadSharedConfiguration(composite);
//加载拓展配置
this.loadExtConfiguration(composite);
//加载应用配置
this.loadApplicationConfiguration(composite, dataIdPrefix, this.nacosConfigProperties, env);
return composite;
}
}
NacosPropertySourceLocator#loadApplicationConfiguration 方法源码解析
NacosPropertySourceLocator#loadApplicationConfiguration 方法会获取应用的所有配置文件,循环调用 NacosPropertySourceLocator#loadNacosDataIfPresent 方法处理。
//com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadApplicationConfiguration
private void loadApplicationConfiguration(CompositePropertySource compositePropertySource, String dataIdPrefix, NacosConfigProperties properties, Environment environment) {
//配置文件扩展名
String fileExtension = properties.getFileExtension();
//配置组名
String nacosGroup = properties.getGroup();
//不带扩展名后缀查询 application
this.loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup, fileExtension, true);
//带扩展名后缀查询 application.yml
this.loadNacosDataIfPresent(compositePropertySource, dataIdPrefix + "." + fileExtension, nacosGroup, fileExtension, true);
//获取活跃的配置文件后缀 dev test prod
String[] var7 = environment.getActiveProfiles();
int var8 = var7.length;
for(int var9 = 0; var9 < var8; ++var9) {
//环境
String profile = var7[var9];
//dataId 确定配置文件使用
String dataId = dataIdPrefix + "-" + profile + "." + fileExtension;
//带环境 带扩展名后缀查询 application-dev.yml
this.loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup, fileExtension, true);
}
}
NacosPropertySourceLocator#loadNacosDataIfPresent 方法源码解析
NacosPropertySourceLocator#loadNacosDataIfPresent 方法对 dataId 和 group 进行简单判断之后会 NacosPropertySourceLocator#loadNacosPropertySource 方法加载配置中心。
//com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadNacosDataIfPresent
private void loadNacosDataIfPresent(final CompositePropertySource composite, final String dataId, final String group, String fileExtension, boolean isRefreshable) {
if (null != dataId && dataId.trim().length() >= 1) {
if (null != group && group.trim().length() >= 1) {
//经过 dataId group 判断后 开始加载配置中心
NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group, fileExtension, isRefreshable);
//读取的配置加入 propertySources
//private final Set<PropertySource<?>> propertySources = new LinkedHashSet();
this.addFirstPropertySource(composite, propertySource, false);
}
}
}
NacosPropertySourceLocator#loadNacosPropertySource 方法源码解析
NacosPropertySourceLocator#loadNacosPropertySource 方法进行简单判断之后调用 NacosPropertySourceBuilder#build 方法,因为 isRefreshable 为 true。
//com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadNacosPropertySource
private NacosPropertySource loadNacosPropertySource(final String dataId, final String group, String fileExtension, boolean isRefreshable) {
//isRefreshable 为 true
//因此会执行 this.nacosPropertySourceBuilder.build(dataId, group, fileExtension, isRefreshable)
return NacosContextRefresher.getRefreshCount() != 0L && !isRefreshable ? NacosPropertySourceRepository.getNacosPropertySource(dataId, group) : this.nacosPropertySourceBuilder.build(dataId, group, fileExtension, isRefreshable);
}
NacosPropertySourceBuilder#loadNacosData 方法源码解析
NacosPropertySourceBuilder#loadNacosData 方法会调用 NacosConfigService#getConfig 加载配置文件,并解析 Nacos 配置数据,封装成 NacosPropertySource 对象返回。
//com.alibaba.cloud.nacos.client.NacosPropertySourceBuilder#build
NacosPropertySource build(String dataId, String group, String fileExtension, boolean isRefreshable) {
//加载 Nacos 配置 重点关注
List<PropertySource<?>> propertySources = this.loadNacosData(dataId, group, fileExtension);
//创建 NacosPropertySource 对象
NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources, group, dataId, new Date(), isRefreshable);
//比较 NacosPropertySource 对象
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}
NacosConfigService#getConfig 方法源码解析
NacosConfigService#getConfig 方法会调用 NacosConfigService#getConfigInner 方法加载配置文件,NacosConfigService#getConfigInner 方法会优先判断是否有故障转移情况,如果有会优先获取故障转移配置文件,直接返回,如果没有的话才会去服务端获取配置文件,如果在在获取服务端配置文件出现了异常,最后会获取快照配置文件返回。
//com.alibaba.nacos.client.config.NacosConfigService#getConfig
@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
//调用 NacosConfigService#getConfigInner 方法加载配置中
return getConfigInner(namespace, dataId, group, timeoutMs);
}
//com.alibaba.nacos.client.config.NacosConfigService#getConfigInner
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
//组名为空就设置默认组名
group = blank2defaultGroup(group);
//dataId group 校验
ParamUtils.checkKeyParam(dataId, group);
//配置响应对象
ConfigResponse cr = new ConfigResponse();
//设置属性
cr.setDataId(dataId);
//设置租户 namespaceid
cr.setTenant(tenant);
cr.setGroup(group);
// 优先使用本地配置
//如果存在的故障转移的配置内容 且能读取到配置信息 则直接返回了 这样设计的目的是当 server 挂了 又需要修改配置 就可以读本地目录
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
//判断配置内容
if (content != null) {
LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
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);
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={}",
agent.getName(), dataId, group, tenant, ioe.toString());
}
//获取快照数据
content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
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;
}
ClientWorker#getServerConfig 方法源码解析
ClientWorker#getServerConfig 方法是真正去 Nacos Server 拉取配置的方法,该方法的主要内容就是构造请求参数、请求头,发起 HTTP 调用(Nacos 1.X 使用的是 HTTP 方式,2.X 使用的是 grpc 的方法是),得到结果集后,对结果集进行处理返回。
//com.alibaba.nacos.client.config.impl.ClientWorker#getServerConfig
public ConfigResponse getServerConfig(String dataId, String group, String tenant, long readTimeout)
throws NacosException {
//配置响应对象
ConfigResponse configResponse = new ConfigResponse();
//group 如果为空 设置为 DEFAULT_GROUP
if (StringUtils.isBlank(group)) {
group = Constants.DEFAULT_GROUP;
}
//http 响应结果
HttpRestResult<String> result = null;
try {
//http 请求入参封装(Nacos 1.x 版本使用 http 请求 2.X 版本使用 grpc 的方式)
Map<String, String> params = new HashMap<String, String>(3);
//租户为空判断 一般不为空
if (StringUtils.isBlank(tenant)) {
params.put("dataId", dataId);
params.put("group", group);
} else {
params.put("dataId", dataId);
params.put("group", group);
params.put("tenant", tenant);
}
//发起 http 请求到 Nacos Server url:/v1/cs/configs
result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout);
} catch (Exception ex) {
String message = String
.format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s",
agent.getName(), dataId, group, tenant);
LOGGER.error(message, ex);
throw new NacosException(NacosException.SERVER_ERROR, ex);
}
//结果集处理
switch (result.getCode()) {
//成功
case HttpURLConnection.HTTP_OK:
//保存数据快照到本地
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.getData());
//设置返回数据信息
configResponse.setContent(result.getData());
//配置类型
String configType;
//设置配置类型
if (result.getHeader().getValue(CONFIG_TYPE) != null) {
configType = result.getHeader().getValue(CONFIG_TYPE);
} else {
configType = ConfigType.TEXT.getType();
}
configResponse.setConfigType(configType);
//数据加密key
String encryptedDataKey = result.getHeader().getValue(ENCRYPTED_DATA_KEY);
//数据加密
LocalEncryptedDataKeyProcessor
.saveEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant, encryptedDataKey);
configResponse.setEncryptedDataKey(encryptedDataKey);
return configResponse;
case HttpURLConnection.HTTP_NOT_FOUND:
//没有找到接口 会删除本地配置快照文件
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, null);
//也是删除
LocalEncryptedDataKeyProcessor.saveEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant, null);
//返回
return configResponse;
case HttpURLConnection.HTTP_CONFLICT: {
//冲突 配置正在修改
LOGGER.error(
"[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, "
+ "tenant={}", agent.getName(), dataId, group, tenant);
throw new NacosException(NacosException.CONFLICT,
"data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
}
case HttpURLConnection.HTTP_FORBIDDEN: {
//无权限
LOGGER.error("[{}] [sub-server-error] no right, dataId={}, group={}, tenant={}", agent.getName(),
dataId, group, tenant);
throw new NacosException(result.getCode(), result.getMessage());
}
default: {
//默认的处理 抛出异常
LOGGER.error("[{}] [sub-server-error] dataId={}, group={}, tenant={}, code={}", agent.getName(),
dataId, group, tenant, result.getCode());
throw new NacosException(result.getCode(),
"http error, code=" + result.getCode() + ",dataId=" + dataId + ",group=" + group + ",tenant="
+ tenant);
}
}
}
至此 Nacos Client 拉取配置中心配置的流程分析完毕,我们知道了配置中心的加载是在应用启动且容器刷新之前加载的,加载的整个流程还是比较简单的,最终会把加载到的配置加入到环境变量中,希望本篇的分析可以帮助到有需要的朋友。
欢迎提出建议及对错误的地方指出纠正。