Nacos配置中心1-配置文件的加载
欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
0. 环境
- nacos版本:1.4.1
- Spring Cloud : 2020.0.2
- Spring Boot :2.4.4
- Spring Cloud alibaba: 2.2.5.RELEASE
- Spring Cloud openFeign 2.2.2.RELEASE
1. 客户端初始化
@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);
}
}
在 NacosConfigManager
的构造方法中会调用 createConfigService
方法来创建 ConfigService
实例,内部调用工厂方法 ConfigFactory#createConfigService
通过反射实例化一个com.alibaba.nacos.client.config.NacosConfigService
的实例对象。
public static ConfigService createConfigService(Properties properties) throws NacosException {
try {
Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
Constructor constructor = driverImplClass.getConstructor(Properties.class);
ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
return vendorImpl;
} catch (Throwable e) {
throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
}
}
NacosPropertySourceLocator
继承 PropertySourceLocator
(PropertySourceLocator接口支持扩展自定义配置加载到 Spring Environment中)通过 locate
加载配置信息。
// SpringBoot在启动时会准备环境,此时会调用该方法。
// 该方法会从配置中心加载配置文件
@Override
public PropertySource<?> locate(Environment env) {
// 将bootstrap.yml文件内容加载到内存
nacosConfigProperties.setEnvironment(env);
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();
nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
timeout);
// 获取spring.cloud.nacos.config.name属性值,即要加载的配置文件的名称
String name = nacosConfigProperties.getName();
// 获取spring.cloud.nacos.config.prefix属性值,即要加载的配置文件的名称
String dataIdPrefix = nacosConfigProperties.getPrefix();
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = name;
}
// 若没有设置name与prefix属性,则要加载的配置文件名称取spring.application.name属性值
if (StringUtils.isEmpty(dataIdPrefix)) {
dataIdPrefix = env.getProperty("spring.application.name");
}
CompositePropertySource composite = new CompositePropertySource(
NACOS_PROPERTY_SOURCE_NAME);
// todo 加载共享配置
// 1)首先加载本地配置
// 2)若没有,则加载远程配置
// 3)若没有,则加载本地快照配置
loadSharedConfiguration(composite);
// todo 加载扩展配置
loadExtConfiguration(composite);
// todo 加载自身配置(注意,其会加载三类配置文件)
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}
下面,分别分析加载 共享配置、扩展配置、自身配置。
2. 加载共享配置
// com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadSharedConfiguration
private void loadSharedConfiguration(
CompositePropertySource compositePropertySource) {
// 获取到所有共享配置文件
List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
.getSharedConfigs();
// 只要存在共享配置文件,则加载它们
if (!CollectionUtils.isEmpty(sharedConfigs)) {
// todo 检测所有共享配置文件是否具有dataId
checkConfiguration(sharedConfigs, "shared-configs");
// todo 加载所有共享配置文件
loadNacosConfiguration(compositePropertySource, sharedConfigs);
}
}
private void checkConfiguration(List<NacosConfigProperties.Config> configs,
String tips) {
for (int i = 0; i < configs.size(); i++) {
// 若配置文件没有dataId属性,则抛出异常
String dataId = configs.get(i).getDataId();
if (dataId == null || dataId.trim().length() == 0) {
throw new IllegalStateException(String.format(
"the [ spring.cloud.nacos.config.%s[%s] ] must give a dataId",
tips, i));
}
}
}
private void loadNacosConfiguration(final CompositePropertySource composite,
List<NacosConfigProperties.Config> configs) {
for (NacosConfigProperties.Config config : configs) {
// todo 加载当前遍历的配置文件
loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(),
NacosDataParserHandler.getInstance()
.getFileExtension(config.getDataId()),
config.isRefresh());
}
}
主要通过 loadNacosDataIfPresent
读取配置信息, 其实我们可以通过参数看出,主要配置文件包含一下及部分: dataId, group, fileExtension
。
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;
}
// todo 加载指定名称的配置文件
NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
fileExtension, isRefreshable);
// 将加载的配置文件添加到composite中
this.addFirstPropertySource(composite, propertySource, false);
}
核心方法loadNacosPropertySource
:
private NacosPropertySource loadNacosPropertySource(final String dataId,
final String group, String fileExtension, boolean isRefreshable) {
// 处理不能自动刷新的情况
if (NacosContextRefresher.getRefreshCount() != 0) {
if (!isRefreshable) {
return NacosPropertySourceRepository.getNacosPropertySource(dataId,
group);
}
}
// todo 处理配置文件会自动刷新的情况
return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
isRefreshable);
}
NacosPropertySource build(String dataId, String group, String fileExtension,
boolean isRefreshable) {
// todo 加载配置文件
List<PropertySource<?>> propertySources = loadNacosData(dataId, group,
fileExtension);
NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources,
group, dataId, new Date(), isRefreshable);
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}
private List<PropertySource<?>> loadNacosData(String dataId, String group,
String fileExtension) {
String data = null;
try {
// todo 加载配置文件
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));
}
// 将加载到的配置文件String解析为List
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();
}
@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return getConfigInner(namespace, dataId, group, timeoutMs);
}
核心方法getConfigInner
:
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
group = null2defaultGroup(group);
ParamUtils.checkKeyParam(dataId, group);
ConfigResponse cr = new ConfigResponse();
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);
// todo 优先使用本地配置
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);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
try {
// todo 从远程读取
String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
cr.setContent(ct[0]);
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());
}
LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}", agent.getName(),
dataId, group, tenant, ContentUtils.truncateContent(content));
// todo 快照配置
content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}
核心步骤:
- 首先,加载本地配置,如果有就直接返回
- 其次,去远程获取配置,如果有就直接返回
- 最后,加载快照配置,如果有就直接返回
2.1 加载本地配置
// com.alibaba.nacos.client.config.impl.LocalConfigInfoProcessor#getFailover
public static String getFailover(String serverName, String dataId, String group, String tenant) {
File localPath = getFailoverFile(serverName, dataId, group, tenant);
if (!localPath.exists() || !localPath.isFile()) {
return null;
}
try {
return readFile(localPath);
} catch (IOException ioe) {
LOGGER.error("[" + serverName + "] get failover error, " + localPath, ioe);
return null;
}
}
static File getFailoverFile(String serverName, String dataId, String group, String tenant) {
File tmp = new File(LOCAL_SNAPSHOT_PATH, serverName + "_nacos");
tmp = new File(tmp, "data");
if (StringUtils.isBlank(tenant)) {
tmp = new File(tmp, "config-data");
} else {
tmp = new File(tmp, "config-data-tenant");
tmp = new File(tmp, tenant);
}
return new File(new File(tmp, group), dataId);
}
public static final String LOCAL_FILEROOT_PATH;
public static final String LOCAL_SNAPSHOT_PATH;
static {
LOCAL_FILEROOT_PATH =
System.getProperty("JM.LOG.PATH", System.getProperty("user.home")) + File.separator + "nacos"
+ File.separator + "config";
LOCAL_SNAPSHOT_PATH =
System.getProperty("JM.SNAPSHOT.PATH", System.getProperty("user.home")) + File.separator + "nacos"
+ File.separator + "config";
LOGGER.info("LOCAL_SNAPSHOT_PATH:{}", LOCAL_SNAPSHOT_PATH);
}
2.2 加载远程配置文件
// com.alibaba.nacos.client.config.impl.ClientWorker#getServerConfig
public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
throws NacosException {
String[] ct = new String[2];
if (StringUtils.isBlank(group)) {
group = Constants.DEFAULT_GROUP;
}
HttpRestResult<String> result = null;
try {
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);
}
// 发送请求
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());
ct[0] = result.getData();
if (result.getHeader().getValue(CONFIG_TYPE) != null) {
ct[1] = result.getHeader().getValue(CONFIG_TYPE);
} else {
ct[1] = ConfigType.TEXT.getType();
}
return ct;
case HttpURLConnection.HTTP_NOT_FOUND:
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, null);
return ct;
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);
}
}
}
agent 默认使用 MetricsHttpAgent
实现类。
2.3 加载快照文件
/**
* 获取本地缓存文件内容。NULL表示没有本地文件或抛出异常.
*/
// com.alibaba.nacos.client.config.impl.LocalConfigInfoProcessor#getSnapshot
public static String getSnapshot(String name, String dataId, String group, String tenant) {
if (!SnapShotSwitch.getIsSnapShot()) {
return null;
}
File file = getSnapshotFile(name, dataId, group, tenant);
if (!file.exists() || !file.isFile()) {
return null;
}
try {
return readFile(file);
} catch (IOException ioe) {
LOGGER.error("[" + name + "]+get snapshot error, " + file, ioe);
return null;
}
}
static File getSnapshotFile(String envName, String dataId, String group, String tenant) {
File tmp = new File(LOCAL_SNAPSHOT_PATH, envName + "_nacos");
if (StringUtils.isBlank(tenant)) {
tmp = new File(tmp, "snapshot");
} else {
tmp = new File(tmp, "snapshot-tenant");
tmp = new File(tmp, tenant);
}
return new File(new File(tmp, group), dataId);
}
public static final String LOCAL_FILEROOT_PATH;
public static final String LOCAL_SNAPSHOT_PATH;
static {
LOCAL_FILEROOT_PATH =
System.getProperty("JM.LOG.PATH", System.getProperty("user.home")) + File.separator + "nacos"
+ File.separator + "config";
LOCAL_SNAPSHOT_PATH =
System.getProperty("JM.SNAPSHOT.PATH", System.getProperty("user.home")) + File.separator + "nacos"
+ File.separator + "config";
LOGGER.info("LOCAL_SNAPSHOT_PATH:{}", LOCAL_SNAPSHOT_PATH);
}
2.4 小节
获取配置的主要方法是 NacosConfigService
类的 getConfigInner
方法,通常情况下该方法直接从本地文件中取得配置的值,如果本地文件不存在或者内容为空,则再通过 HTTP GET
方法从远端拉取配置,并保存到本地快照中。
当通过 HTTP
获取远端配置时,Nacos
提供了两种熔断策略,一是超时时间,二是最大重试次数,默认重试三次。
3. 加载扩展配置
// com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadExtConfiguration
private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
.getExtensionConfigs();
if (!CollectionUtils.isEmpty(extConfigs)) {
checkConfiguration(extConfigs, "extension-configs");
// todo
loadNacosConfiguration(compositePropertySource, extConfigs);
}
}
loadNacosConfiguration
和第2节分析的一样
4. 加载应用自身配置
// com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadApplicationConfiguration
private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {
// 获取配置文件扩展名
String fileExtension = properties.getFileExtension();
// 获取配置文件所在的groupId
String nacosGroup = properties.getGroup();
// load directly once by default
// todo 加载仅有文件名称,没有扩展名的配置文件
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);
// load with suffix, which have a higher priority than the default
// todo 加载有文件名称,也有扩展名的配置文件
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
// Loaded with profile, which have a higher priority than the suffix
// todo 加载有文件名称,有扩展名,且还包含多环境选择profile的配置文件
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}
}
加载自身文件主要有三类:
- 加载仅有文件名称,没有扩展名的配置文件
- 加载有文件名称,也有扩展名的配置文件
- 加载有文件名称,有扩展名,且还包含多环境选择profile的配置文件
5. 总结
以上三类配置文件的加载顺序
为, 共享配置 -> 扩展配置 -> 当前应用自身配置
,如果存在相同属性配置了不同属性值,则后加载的会将先加载的给覆盖。即优先级为: 共享配置 < 扩展配置 < 应用自身配置
。
对于每种配置文件的加载过程,又存在三种可用选择:在应用本地存在同名配置,远程
配置中心存在同名配置,本地磁盘快照 snapshot
中存在同名配置。这三种配置的优先级为: 本地配置 > 远程配置 > 快照配置
。只要前面的加载到了,后面的就不再加载。
若要在应用本地存放同名配置,则需要存放到当前用户主目录下的
nacos\config\fixed-localhost_8848_nacos\data\config-data\{groupId}
目录中。
若开启了配置的快照功能,则默认会将快照记录在当前用户主目录下的
nacos\config\fixed-localhost_8848_nacos\snapshot\{groupId}
目录中。
方法调用流程图如下:
参考文章
nacos-1.4.1-source源码注释 spring-cloud-alibaba-2020.0.0-src springcloud-source-study学习github地址 Spring Cloud Alibaba源码解析 Nacos 配置中心源码分析 图文解析 Nacos 配置中心的实现 nacos 动态配置源码解析
转载自:https://juejin.cn/post/7170244688329474079