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

4,601 阅读5分钟

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 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 格式的数据 image.png

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();
}

image.png

image.png

补充 : 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;
}

四 . 总结

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

image.png

补充 : 配置的优先级

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