springcloud中nacos加载配置文件流程源码分析

发布时间 2023-11-23 11:36:39作者: mingshan

在spring体系中,配置的概念非常重要,无论是spring xml配置,还是springboot中yml/properties配置,以及spring cloud体系中的配置中心,都脱离不了spring 的配置框架,区别是配置的存储格式不同,存储位置不一样。不熟悉spring配置体系的可以参考:https://mp.weixin.qq.com/s/gTSHekcN427jZ5H1LPfBFg

调用入口:PropertySourceBootstrapConfiguration

在spring cloud体系中,配置文件的加载入口是 org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration,该类实现了ApplicationContextInitializer接口,该接口的调用位置是:SpringApplication#run -》prepareContext -》applyInitializers,在springboot应用启动流程中调用。我们来看下PropertySourceBootstrapConfiguration的实现。initialize方法源码如下:

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
    // 1.拿到所有的propertySourceLocator,进行排序
	List<PropertySource<?>> composite = new ArrayList<>();
    AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
    boolean empty = true;
    ConfigurableEnvironment environment = applicationContext.getEnvironment();

    // 2. 循环调用propertySourceLocator,进行配置文件的读取
    for (PropertySourceLocator locator : this.propertySourceLocators) {
        Collection<PropertySource<?>> source = locator.locateCollection(environment);
        if (source == null || source.size() == 0) {
            continue;
        }
        List<PropertySource<?>> sourceList = new ArrayList<>();
        for (PropertySource<?> p : source) {
            if (p instanceof EnumerablePropertySource) {
                EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) p;
                sourceList.add(new BootstrapPropertySource<>(enumerable));
            }
            else {
                sourceList.add(new SimpleBootstrapPropertySource(p));
            }
        }
        logger.info("Located property source: " + sourceList);
        composite.addAll(sourceList);
        empty = false;
    }
    if (!empty) {
        // 3. 将配置信息设置到environment中
        MutablePropertySources propertySources将配置信息 = environment中.getPropertySources();
        String logConfig = environment.resolvePlaceholders("${logging.config:}");
        LogFile logFile = LogFile.get(environment);
        for (PropertySource<?> p : environment.getPropertySources()) {
            if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
                propertySources.remove(p.getName());
            }
        }
        insertPropertySources(propertySources, composite);
        reinitializeLoggingSystem(environment, logConfig, logFile);
        setLogLevels(applicationContext, environment);
        handleIncludedProfiles(environment);
    }
}

从上面源码中可以看出,该方法主要有三个步骤:

  1. 拿到所有的propertySourceLocator,进行排序
  2. 循环调用propertySourceLocator,进行配置文件的读取
  3. 将配置信息设置到environment中

由于我们使用nacos作为配置中心,所以目前这里的propertySourceLocator集合只有NacosPropertySourceLocator一个实现类,该类位于com.alibaba.cloud.nacos.client包下。

nacos配置加载入口:NacosPropertySourceLocator#locate方法

该方法的源码如下:

@Override
public PropertySource<?> locate(Environment env) {
    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);
    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);
    
    // 从共享配置中读取
    loadSharedConfiguration(composite);
    // 从扩展配置中读取
    loadExtConfiguration(composite);
    // 读取当前应用的配置信息
    loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
    return composite;
}

该方法内部逻辑比较清晰,我们主要看最后几行代码,这几行是加载具体配置的入口,由于nacos的配置文件区分共享配置和应用配置,且应用配置优先级比共享配置高,加载顺序如下:

  • 从共享配置中读取
  • 从扩展配置中读取
  • 读取当前应用的配置信息

我们以加载共享配置为例,下面是本类的调用链路,主要是做数据校验。

// 获取共享配置,并检查
private void loadSharedConfiguration(
        CompositePropertySource compositePropertySource) {
    List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
            .getSharedConfigs();
    if (!CollectionUtils.isEmpty(sharedConfigs)) {
        checkConfiguration(sharedConfigs, "shared-configs");
        loadNacosConfiguration(compositePropertySource, sharedConfigs);
    }
}

// 循环加载
private void loadNacosConfiguration(final CompositePropertySource composite,
        List<NacosConfigProperties.Config> configs) {
    for (NacosConfigProperties.Config config : configs) {
        loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(),
                NacosDataParserHandler.getInstance()
                        .getFileExtension(config.getDataId()),
                config.isRefresh());
    }
}

// 作数据校验,继续加载
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;
    }
    NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
            fileExtension, isRefreshable);
    this.addFirstPropertySource(composite, propertySource, false);
}

private NacosPropertySource loadNacosPropertySource(final String dataId,
        final String group, String fileExtension, boolean isRefreshable) {
    if (NacosContextRefresher.getRefreshCount() != 0) {
        if (!isRefreshable) {
            return NacosPropertySourceRepository.getNacosPropertySource(dataId,
                    group);
        }
    }
    return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
            isRefreshable);
}

下面直接来看nacosPropertySourceBuilder#build 这个方法。

加载&解析:NacosPropertySourceBuilder#build

当前方法的参数是nacos的概念,关于dataId和group概念可以参考官网介绍,这里直接给出示例:
dataId : xxxxx.yaml
group: DEFAULT_GROUP
fileExtension: yaml

源码如下:

/**
 * @param dataId Nacos dataId
 * @param group Nacos group
 */
NacosPropertySource build(String dataId, String group, String fileExtension,
        boolean isRefreshable) {
    // 加载配置
    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 {
        // 从nacos服务上读取数据
        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));
        }

        // 解析数据
        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();
}

build方法:

  1. 读取配置
  2. 缓存配置

loadNacosData方法:

  1. 从nacos服务读取数据
  2. 解析字符串数据,形成PropertySourc列表

从naocs服务上读取:NacosConfigService#getConfigInner

下面我们来看最后的读取逻辑:

  1. 优先有本地缓存文件中读取
  2. 再从nacos服务读取,使用grpc交互
  3. 最后兜底机制,使用快照
private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
    group = blank2defaultGroup(group);
    ParamUtils.checkKeyParam(dataId, group);
    ConfigResponse cr = new ConfigResponse();
    
    cr.setDataId(dataId);
    cr.setTenant(tenant);
    cr.setGroup(group);
    
    // use local config first
    String content = LocalConfigInfoProcessor.getFailover(worker.getAgentName(), dataId, group, tenant);
    if (content != null) {
        LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}",
                worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));
        cr.setContent(content);
        String encryptedDataKey = LocalEncryptedDataKeyProcessor
                .getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
        cr.setEncryptedDataKey(encryptedDataKey);
        configFilterChainManager.doFilter(null, cr);
        content = cr.getContent();
        return content;
    }
    
    try {
        ConfigResponse response = worker.getServerConfig(dataId, group, tenant, timeoutMs, false);
        cr.setContent(response.getContent());
        cr.setEncryptedDataKey(response.getEncryptedDataKey());
        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={}",
                worker.getAgentName(), dataId, group, tenant, ioe.toString());
    }

    // 兜底机制,使用本地快照缓存
    LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}",
            worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));
    content = LocalConfigInfoProcessor.getSnapshot(worker.getAgentName(), dataId, group, tenant);
    cr.setContent(content);
    String encryptedDataKey = LocalEncryptedDataKeyProcessor
            .getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
    cr.setEncryptedDataKey(encryptedDataKey);
    configFilterChainManager.doFilter(null, cr);
    content = cr.getContent();
    return content;
}

参考:

  1. Nacos 配置中心原理分析