likes
comments
collection
share

Nacos配置中心1-配置文件的加载

作者站长头像
站长
· 阅读数 51

欢迎大家关注 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

测试代码:github.com/hsfxuebao/s…

1. 客户端初始化

Nacos配置中心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;
}

核心步骤:

  1. 首先,加载本地配置,如果有就直接返回
  2. 其次,去远程获取配置,如果有就直接返回
  3. 最后,加载快照配置,如果有就直接返回

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 方法从远端拉取配置,并保存到本地快照中。

Nacos配置中心1-配置文件的加载

当通过 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-配置文件的加载

参考文章

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
评论
请登录