nacos 作为配置中心使用的 jar 包版本:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
里面使用的nacos-client 版本是:
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.4.2</version>
</dependency>
SpringBoot 项目启动加载 Nacos 配置过程:
SpringApplication 类的 prepareContext 方法 ---> applyInitializers(context)
---> PropertySourceBootstrapConfiguration.java 的 initialize(ConfigurableApplicationContext applicationContext) 方法。
在遍历 this.propertySourceLocators 中,会执行 NacosPropertySourceLocator实现的
locate(Environment env) 方法开始正式地获取 nacos 配置了。
NacosPropertySourceLocator 类
主要看 locate(Environment env) 中的 loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env) 这行代码。
private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {
String fileExtension = properties.getFileExtension();
String nacosGroup = properties.getGroup();
// load directly once by default
// 直接加载dataId,比如 config
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);
// load with suffix, which have a higher priority than the default
// 加载带文件后缀的dataId,优先级更高, 比如 config.yml
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
// Loaded with profile, which have a higher priority than the suffix
// 加载带环境profile的dataId配置,优先级最高, 比如 config-test.yml
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}
}
后面会进到 NacosPropertySourceBuilder 类的 build 方法:
NacosPropertySource build(String dataId, String group, String fileExtension,
boolean isRefreshable) {
// 加载 nacos 配置
List<PropertySource<?>> propertySources = loadNacosData(dataId, group,
fileExtension);
NacosPropertySource nacosPropertySource = new NacosPropertySource(propertySources,
group, dataId, new Date(), isRefreshable);
NacosPropertySourceRepository.collectNacosPropertySource(nacosPropertySource);
return nacosPropertySource;
}
loadNacosData 方法:
private List<PropertySource<?>> loadNacosData(String dataId, String group,
String fileExtension) {
String data = null;
try {
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));
}
//拿到nacos配置的数据,按照其格式解析成Spring中的 PropertySource,
//这样就可以使用 @Value 注解获取使用配置项了
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();
}
configService 是nacos-client jar 包中实现 ConfigService接口的 NacosConfigService 类实现的。
NacosConfigService 类
@Override
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
return getConfigInner(namespace, dataId, group, timeoutMs);
}
核心的 getConfigInner 方法,真正请求获取 nacos 配置的地方:
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);
// 优先使用本地配置
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);
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);
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={}",
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));
content = LocalConfigInfoProcessor.getSnapshot(agent.getName(), 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;
}
可以看到,程序中优先使用的Nacos本地文件中的配置,如果本地文件配置为空才会通过http请求获取nacos服务端配置的数据。
本地文件路径:\nacos\config\.......
文件路径参照 LocalConfigInfoProcessor 类的 getFailoverFile 方法。
ClientWorker 类
public ConfigResponse getServerConfig(String dataId, String group, String tenant, long readTimeout)
throws NacosException {
ConfigResponse configResponse = new ConfigResponse();
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:
// 如果http请求返回的状态码是 200,保存快照配置数据到本地文件中去
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.getData());
configResponse.setContent(result.getData());
String configType;
if (result.getHeader().getValue(CONFIG_TYPE) != null) {
configType = result.getHeader().getValue(CONFIG_TYPE);
} else {
configType = ConfigType.TEXT.getType();
}
configResponse.setConfigType(configType);
String encryptedDataKey = result.getHeader().getValue(ENCRYPTED_DATA_KEY);
LocalEncryptedDataKeyProcessor
.saveEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant, encryptedDataKey);
configResponse.setEncryptedDataKey(encryptedDataKey);
return configResponse;
case HttpURLConnection.HTTP_NOT_FOUND:
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, null);
LocalEncryptedDataKeyProcessor.saveEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant, null);
return configResponse;
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);
}
}
}
注意:
需要注意,nacos 优先从本地文件中获取的配置对应的文件路径和从nacos服务端获取到配置保存到本地的快照文件路径是不一样的。
对应 LocalConfigInfoProcessor 类中的两个不同方法:
//默认优先从本地文件中获取的文件路径
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);
}
//从服务端拉到配置数据后写到本地的快照文件路径
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);
}