spring-cloud-starter-alibaba-nacos-config包提供了spring cloud集成nacos功能,对于spring boot项目,可以在项目启动时,直接去nacos配置中心拉取配置。
NacosConfigBootstrapConfiguration类会在项目开始启动时进行加载,在spring.factories里配置如下:
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
NacosConfigBootstrapConfiguration会加载三个Bean,NacosConfigProperties是用来加载nacos配置文件相关信息的,主要包括nacos访问地址,group、namespace等信息。NacosConfigManager会初始化nacos api SDK里的ConfigService类,后续跟nacos交互都是通过SDK里提供的这个对象。核心代码如下:
static ConfigService createConfigService(
NacosConfigProperties nacosConfigProperties) {
if (Objects.isNull(service)) {
synchronized (NacosConfigManager.class) {
try {
if (Objects.isNull(service)) {
service = NacosFactory.createConfigService(
nacosConfigProperties.assembleConfigServiceProperties());
}
}
catch (NacosException e) {
log.error(e.getMessage());
throw new NacosConnectionFailureException(
nacosConfigProperties.getServerAddr(), e.getMessage(), e);
}
}
}
return service;
}
NacosPropertySourceLocator实现了PropertySourceLocator接口,是用来在项目启动时,从nacos拉取项目配置并解析后,写入到spring boot项目本地配置中。这个类加载启动时配置的核心类。核心代码如下:
@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;
}
// dataId前缀为空的话,会取配置的名字,也为空的话,直接取spring配置的项目名
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;
}
加载各类配置逻辑类似,只看loadApplicationConfiguration方法。加载配置顺序如下,以dataId前缀为application,文件扩展名为yml,active profile为test为例,会先加载data id 为application的配置项,再加载application.yml,最后再加载application-test.yml。代码如下:
private void loadApplicationConfiguration(
CompositePropertySource compositePropertySource, String dataIdPrefix,
NacosConfigProperties properties, Environment environment) {
String fileExtension = properties.getFileExtension();
String nacosGroup = properties.getGroup();
// load directly once by default
loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
fileExtension, true);
// load with suffix, which have a higher priority than the default
loadNacosDataIfPresent(compositePropertySource,
dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
// Loaded with profile, which have a higher priority than the suffix
for (String profile : environment.getActiveProfiles()) {
String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
fileExtension, true);
}
}
loadNacosDataIfPresent会调用nacosPropertySourceBuilder的build方法去nacos拉取配置,代码如下:
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);
//把nacos配置加载到spring boot配置中
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);
}
}
//去nacos拉取配置,然后添加到本地缓存中
return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
isRefreshable);
}
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;
}
private List<PropertySource<?>> loadNacosData(String dataId, String group,
String fileExtension) {
String data = null;
try {
//用nacos提供的api SDK拉取配置
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();
}
parseNacosData方法如下:
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);
}
for (PropertySourceLoader propertySourceLoader : propertySourceLoaders) {
//轮询查找可以支持的解析器
if (!canLoadFileExtension(propertySourceLoader, extension)) {
continue;
}
NacosByteArrayResource nacosByteArrayResource;
if (propertySourceLoader instanceof PropertiesPropertySourceLoader) {
// PropertiesPropertySourceLoader internal is to use the ISO_8859_1,
// the Chinese will be garbled, needs to transform into unicode.
nacosByteArrayResource = new NacosByteArrayResource(
NacosConfigUtils.selectiveConvertUnicode(configValue).getBytes(),
configName);
}
else {
nacosByteArrayResource = new NacosByteArrayResource(
configValue.getBytes(), configName);
}
nacosByteArrayResource.setFilename(getFileName(configName, extension));
//解析配置文件
List<PropertySource<?>> propertySourceList = propertySourceLoader
.load(configName, nacosByteArrayResource);
if (CollectionUtils.isEmpty(propertySourceList)) {
return Collections.emptyList();
}
//针对EnumerablePropertySource类型特殊处理一下
return propertySourceList.stream().filter(Objects::nonNull)
.map(propertySource -> {
if (propertySource instanceof EnumerablePropertySource) {
String[] propertyNames = ((EnumerablePropertySource) propertySource)
.getPropertyNames();
if (propertyNames != null && propertyNames.length > 0) {
Map<String, Object> map = new LinkedHashMap<>();
Arrays.stream(propertyNames).forEach(name -> {
map.put(name, propertySource.getProperty(name));
});
return new OriginTrackedMapPropertySource(
propertySource.getName(), map, true);
}
}
return propertySource;
}).collect(Collectors.toList());
}
return Collections.emptyList();
}