Nacos配置中心1-配置文件的加载
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. 客户端初始化
@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; } 复制代码
核心步骤:
首先,加载本地配置,如果有就直接返回
其次,去远程获取配置,如果有就直接返回
最后,加载快照配置,如果有就直接返回
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
方法从远端拉取配置,并保存到本地快照中。
当通过 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}
目录中。
方法调用流程图如下:
作者:hsfxuebao
链接:https://juejin.cn/post/7170244688329474079