实战 Cloud Nacos 配置加载流程和优先级

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

这一篇来看一下 Nacos Client 对配置的请求流程以及相关的配置

二 . 配置类及用法

2.1 基本使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
yml复制代码# 这也是最常见的案例模板
spring:
application:
name: nacos-multi-config
cloud:
nacos:
config:
extension-configs:
# 对应 Nacos DataId
- data-id: nacos-multi-config-A.yaml
group: DEFAULT_RSM
- data-id: nacos-multi-config-B.yaml
group: DEFAULT_RSM
- data-id: nacos-multi-config-C.yaml
group: DEFAULT_RSM
# 文件后缀
file-extension: yaml
server-addr: 127.0.0.1:8848
discovery:
server-addr: 127.0.0.1:8848

2.2 配置类解析

Nacos 对应的配置类为 NacosConfigProperties , 这里来看一下所有的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
java复制代码// Nacos 地址
private String serverAddr;

// 用户名 及密码
private String username;
private String password;

// 内容编码
private String encode;

// 所属组
private String group = "DEFAULT_GROUP";

// Config DataId 前缀
private String prefix;

// Config 后缀文件名
private String fileExtension = "properties";

// 获取配置超时时间
private int timeout = 3000;

// 最大重连次数
private String maxRetry;

// 获取配置长轮询超时时间
private String configLongPollTimeout;

// 配置获取错误的重试次数
private String configRetryTime;

// 自动获取及注册 Listener 监听 , 会有网络开销
private boolean enableRemoteSyncConfig = false;

// 服务域名,可动态获取服务器地址
private String endpoint;

// 命名空间,不同环境的分离配置
private String namespace;

// 访问键和访问密钥
private String accessKey;
private String secretKey;

// 容器地址
private String contextPath;

// 分片名
private String clusterName;

// naco配置dataId名称
private String name;

/**
* 共享配置集
* spring.cloud.nacos.config.shared-configs[0]=xxx .
*/
private List<Config> sharedConfigs;

/**
* 扩展配置集
* spring.cloud.nacos.config.extension-configs[0]=xxx .
*/
private List<Config> extensionConfigs;

// 是否刷新配置
private boolean refreshEnabled = true;

三 . 配置流程

下面看一下配置的加载和获取流程

3.1 开启配置的入口

配置得开启是基于 PropertySourceBootstrapConfiguration 开启的 ,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码public void initialize(ConfigurableApplicationContext applicationContext) {
List<PropertySource<?>> composite = new ArrayList<>();
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();

// 从不同的 locators 获取资源
for (PropertySourceLocator locator : this.propertySourceLocators) {

// 对于 Nacos , 此处会调用 NacosPropertySourceLocator
Collection<PropertySource<?>> source = locator.locateCollection(environment);

if (source == null || source.size() == 0) {
continue;
}
List<PropertySource<?>> sourceList = new ArrayList<>();
for (PropertySource<?> p : source) {

// 分别生成了 BootstrapPropertySource 和 SimpleBootstrapPropertySource
if (p instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) p;
sourceList.add(new BootstrapPropertySource<>(enumerable));
}
else {
sourceList.add(new SimpleBootstrapPropertySource(p));
}
}
composite.addAll(sourceList);
empty = false;
}

// 省略资源的处理 , 后文再说
}

3.2 Nacos 加载入口

期间会经过接口类的 PropertySourceLocator 来发起对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码// C- NacosPropertySourceLocator
public PropertySource<?> locate(Environment env) {

nacosConfigProperties.setEnvironment(env);

// ConfigService 是主要的查询
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);

// 准备前缀和 name
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);

// private List<Config> sharedConfigs 的处理
loadSharedConfiguration(composite);

// private List<Config> extensionConfigs 的处理
loadExtConfiguration(composite);

// 主配置文件获取
loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
return composite;
}

补充一 : loadSharedConfiguration 进行共享数据的处理

持多个共享 Data Id 的配置,优先级小于extension-configs , 适合于共享配置文件与项目默认配置文件处于相同Group时 (PS:只能在一个 Group 中)

主要流程为获取配置 , 校验准确性 , 调用 loadNacosConfiguration 发起配置的调用

PS: 主要流程看补充二 , 这也是为什么优先级没有 loadExtConfiguration 高的原因

补充二 : loadExtConfiguration 处理配置

主要流程为 nacosConfigProperties.getExtensionConfigs() , 判断是否存在 , 存在会先 checkConfiguration , 再调用 loadNacosConfiguration 进行主流程处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码private void loadNacosConfiguration(final CompositePropertySource composite,List<NacosConfigProperties.Config> configs) {

// 此处会循环调用 , 所以后面的实际上会覆盖前面的
for (NacosConfigProperties.Config config : configs) {
// getFileExtension 为文件后缀
loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(),
NacosDataParserHandler.getInstance().getFileExtension(config.getDataId()),
config.isRefresh());
}
}

// 后续逻辑就是调用 NacosPropertySourceBuilder 进行正在的发起查询和解析

NacosPropertySource build(String dataId, String group, String fileExtension,boolean isRefreshable) {
// Step 1 : 此处查询到对象并且完成解析 -> 3.3
List<PropertySource<?>> propertySources = loadNacosData(dataId, group,fileExtension);

// Step 2 : 将解析的对象封装为 NacosPropertySource
NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources,
group, dataId, new Date(), isRefreshable);

// Step 3: NacosPropertySourceRepository 中有个 ConcurrentHashMap<String, NacosPropertySource>
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}

补充三 : 主配置文件获取

该流程的优先级最高 , 是通过常规方式获取配置的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {

// 获取文件后缀和组
String fileExtension = properties.getFileExtension();
String nacosGroup = properties.getGroup();

// 第一次加载 : 默认情况下直接加载一次
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);

// 第二次加载 : 加载带有后缀,具有比默认值更高的优先级
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);

// 第三次加载 :用配置文件 Profile 加载,它比后缀具有更高的优先级
// 此处循环所有的 Profiles , 分别进行处理
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}

}

可以看到 , 这里加载了多次 , 同时通过这种方式确定优先级

3.3 Nacos 获取配置主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码private List<PropertySource<?>> loadNacosData(String dataId, String group,String fileExtension) {
String data = null;

// Step 1 : 获取远程配置信息
data = configService.getConfig(dataId, group, timeout);
if (StringUtils.isEmpty(data)) {
return Collections.emptyList();
}

// Step 2 : 解析远程配置信息
return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,fileExtension);

// 此处省略了异常处理逻辑 , 出现异常不会抛出 ,而是返回空集合
// PS : 这里也导致部分错误不好从日志判断原因
// return Collections.emptyList();
}

PS : 这里不止是个字符串 , 复制到文本里面可以看出就是一个 yaml 格式的数据
image.png

3.3.1 远程配置的获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
group = null2defaultGroup(group);
ParamUtils.checkKeyParam(dataId, group);
ConfigResponse cr = new ConfigResponse();

// 准备查询对象
// {dataId=nacos-multi-config-B.yaml, tenant=, group=DEFAULT_RSM}
cr.setDataId(dataId);
cr.setTenant(tenant);
cr.setGroup(group);

// 优先使用本地配置
String content = LocalConfigInfoProcessor.getFailover(agent.getName(), dataId, group, tenant);
// 如果此处有本地配置 , 则直接返回 , 此处省略

try {
// 发起远程调用
// HttpAgent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout)
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;
}

}

// 如果不是 403 , 则会通过快照获取
content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), dataId, group, tenant);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr);
content = cr.getContent();
return content;
}

3.3.2 远程配置的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
java复制代码// C- NacosDataParserHandler
public List<PropertySource<?>> parseNacosData(String configName, String configValue,
String extension) throws IOException {


if (StringUtils.isEmpty(configValue)) {
return Collections.emptyList();
}

// 文件后缀
if (StringUtils.isEmpty(extension)) {
extension = this.getFileExtension(configName);
}

// 此处的 PropertySourceLoader 主要有四种 :
// - NacosXmlPropertySourceLoader : XML 文件格式
// - PropertiesPropertySourceLoader : Properties 文件格式
// - YamlPropertySourceLoader : YAML 文件格式
// - NacosJsonPropertySourceLoader : JSON 文件格式
for (PropertySourceLoader propertySourceLoader : propertySourceLoaders) {

if (!canLoadFileExtension(propertySourceLoader, extension)) {
// 如果不能加载 , 直接退出
continue;
}
NacosByteArrayResource nacosByteArrayResource;
// 省略转换为Byte逻辑 : new NacosByteArrayResource

nacosByteArrayResource.setFilename(getFileName(configName, extension));

// 核心逻辑 , 此处将 yaml 格式文本转换为了 OriginTrackedMapPropertySource
List<PropertySource<?>> propertySourceList = propertySourceLoader
.load(configName, nacosByteArrayResource);
if (CollectionUtils.isEmpty(propertySourceList)) {
return Collections.emptyList();
}

return propertySourceList.stream().filter(Objects::nonNull)
.map(propertySource -> {
if (propertySource instanceof EnumerablePropertySource) {
// 获得 Name 名称
String[] propertyNames = ((EnumerablePropertySource) propertySource)
.getPropertyNames();
if (propertyNames != null && propertyNames.length > 0) {
Map<String, Object> map = new LinkedHashMap<>();

// 此处是将 OriginTrackedValue 转换为对应类型的值
Arrays.stream(propertyNames).forEach(name -> {
map.put(name, propertySource.getProperty(name));
});

// 最终构建一个 PropertySource 的 List 集合
return new OriginTrackedMapPropertySource(
propertySource.getName(), map, true);
}
}
return propertySource;
}).collect(Collectors.toList());
}
return Collections.emptyList();
}

image.png

image.png

补充 : YamlPropertySourceLoader 流程

1
2
3
4
5
6
7
8
java复制代码public List<PropertySource<?>> load(String name, Resource resource) throws IOException {

// 核心 : 通过 OriginTrackedYamlLoader 进行 Loader 加载 , 此处就不深入了 , 属于工具类的功能
List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();

// 省略判空和添加逻辑 -> propertySources.add(new OriginTrackedMapPropertySource
return propertySources;
}

四 . 总结

篇幅有限 , 所以 本地配置的覆盖与集成 以及 配置的顺序加载及优先级 准备放在下一篇文档里面梳理 , 下面分享一张流程图

image.png

补充 : 配置的优先级

  • application 主配置 > extensionConfigs > sharedConfigs
  • extensionConfigs/sharedConfigs 排在后面的数组比前面的优先级高
  • application 主逻辑 profile > 带后缀

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%