首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…
一 . 前言
这一篇来看一下 Nacos Client 对配置的请求流程以及相关的配置
二 . 配置类及用法
2.1 基本使用案例
# 这也是最常见的案例模板
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 , 这里来看一下所有的参数:
// 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 开启的 ,
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 来发起对应
// 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 进行主流程处理
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;
}
补充三 : 主配置文件获取
该流程的优先级最高 , 是通过常规方式获取配置的流程
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 获取配置主流程
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 格式的数据
3.3.1 远程配置的获取
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 远程配置的解析
// 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();
}
补充 : YamlPropertySourceLoader 流程
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;
}
四 . 总结
篇幅有限 , 所以 本地配置的覆盖与集成 以及 配置的顺序加载及优先级 准备放在下一篇文档里面梳理 , 下面分享一张流程图
补充 : 配置的优先级
- application 主配置 > extensionConfigs > sharedConfigs
- extensionConfigs/sharedConfigs 排在后面的数组比前面的优先级高
- application 主逻辑 profile > 带后缀