前言
Nacos Spring Cloud的使用方法与Nacos Spring Boot的使用方法类似,都是通过添加依赖与在配置文件中增加配置来完成接入。需要注意的是在Spring Cloud应用中必须要配置spring.application.name,因为它是构成 Nacos 配置管理dataId字段的一部分,在Nacos Spring Cloud中配置文件的dataId完整格式为prefix-spring.profiles.active.file-extension,当然Nacos Spring Cloud也支持通过配置扩展属性字段来完成其他的配置项加载。在使用上Nacos Spring Cloud相对来说更为简单,主要利用的是Spring Cloud原生注解@RefreshScope来完成配置的自动刷新,如下代码片段所示。
@RestController
@RequestMapping("/config")
@RefreshScope
public class ConfigController {
@Value("${useLocalCache:false}")
private boolean useLocalCache;
@RequestMapping("/get")
public boolean get() {
return useLocalCache;
}
}
复制代码
依旧是从META-INF/spring.factories文件入手,Nacos Spring Cloud自动配置主要包含了以下几个关键的类:
- NacosConfigBootstrapConfiguration: 负责Bootstrap上下文创建时的自动配置
- NacosConfigAutoConfiguration:负责应用上下文创建时的自动配置
- NacosConfigEndpointAutoConfiguration:负责配置Actuator相关端点
- NacosConnectionFailureAnalyzer:配置错误分析器
- NacosJsonPropertySourceLoader:配置Json格式属性解析器
- NacosXmlPropertySourceLoader:配置Xml格式属性解析器
- NacosLoggingListener:负责日志模块的配置重新加载
本文将从以上的类中挑选几个关键的类进行源码的分析,简要的阐述Nacos Spring Cloud的工作原理。
NacosConfigBootstrapConfiguration
A Spring Cloud application operates by creating a “bootstrap” context, which is a parent context for the main application. It is responsible for loading configuration properties from the external sources and for decrypting properties in the local external configuration files. The two contexts share an
Environment
, which is the source of external properties for any Spring application. By default, bootstrap properties (notbootstrap.properties
but properties that are loaded during the bootstrap phase) are added with high precedence, so they cannot be overridden by local configuration.
在Spring Cloud应用中,应用的ApplicationContext创建于Bootstrap的ApplicationContext之上。如文档上所述,Bootstrap Context负责从外部配置源加载配置文件,默认情况下拥有最高优先级的配置。属性文件默认来源于boostrap.yml配置文件,还可以通过在Bootstrap Context中配置PropertySourceLocators来自定义配置文件的加载。NacosConfigBootstrapConfiguration则是负责在Bootstrap的ApplicationContext上定义需要的工具类Bean来完成远程配置属性的加载。在NacosConfigBootstrapConfiguration中配置了三种Bean,分别是用于属性定义的NacosConfigProperties,用于创建ConfigService的NacosConfigManager,以及用于注入配置文件的NacosPropertySourceLocator。
其他两个Bean相对比较简单,我们重点看一下NacosPropertySourceLocator是如何实现的。NacosPropertySourceLocator实现了PropertySourceLocator接口,如上面所述实现了PropertySourceLocator的Bean会被用于在Bootstrap创建时加载自定义的属性。在locate方法中,首先会利用NacosConfigManager创建一个ConfigService,随后是对默认应用dataId的解析,最后创建一个组合“Nacos”属性,分别对配置文件中的Shared配置、Ext配置以及应用本身的配置进行加载。
@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;
}
复制代码
loadSharedConfiguration与loadExtConfiguration实际上加载配置的底层逻辑是相同的,最终都会调用loadNacosDataIfPresent,而在函数内部使用的NacosPropertySourceBuilder来完成远程配置的加载并封装成PropertySource加入到CompositePropertySource中。shared配置与ext配置唯一有区别的是优先级,先加载的配置优先级低,因此application>ext>shared。而在loadApplicationConfiguration中,我们可以看到,应用会进行多次的配置文件加载,首先是直接文件(不带文件后缀)的加载,然后是加上后缀名的文件加载,最后根据Profiles再根据不同环境的配置文件进行加载。
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);
}
}
复制代码
NacosConfigAutoConfiguration
NacosConfigAutoConfiguration负责的则是在应用层的ApplicationContext完成Nacos相关功能Bean的配置,分别是配置定义相关的NacosConfigProperties与NacosRefreshProperties、用于保存刷新记录的NacosRefreshHistory、NacosConfigManager以及用于完成配置刷新的NacosContextRefresher。在NacosConfigProperties的配置代码上面我们可以看到,如果当前应用的ApplicationContext的父类中已经配置了NacosConfigProperties,那么就不会再继续创建而是直接沿用。这里我们重点关注一下用于动态配置刷新的NacosContextRefresher。
NacosContextRefresher主要利用的是Spring框架的事件系统,NacosContextRefresher监听了ApplicationReadyEvent事件,在ApplicationReadyEvent事件的回调处理中注册了配置文件的监听器,当收到远程配置更新时,首先会增加一条刷新记录,随后发送一个RefreshEvent事件,当收到RefreshEvent事件后ApplicationContext就会开始刷新容器,对于加上了@RefreshScope注解的Bean则会重新创建,对于其中的属性也会重新从Environment中获取。从代码注释上面我们也可以得知,当任意一个配置文件收到更新时,对于整个容器都会进行一次刷新。
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
refreshCountIncrement();
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
// todo feature: support single refresh for listening
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug(String.format(
"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
group, dataId, configInfo));
}
}
});
try {
configService.addListener(dataKey, groupKey, listener);
}
catch (NacosException e) {
log.warn(String.format(
"register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
groupKey), e);
}
}
复制代码
NacosConfigEndpointAutoConfiguration
在NacosConfigEndpointAutoConfiguration中主要配置了与actuator相关的两个功能Bean,一个是用于获取当前Nacos配置与信息的NacosConfigEndpoint,一个是用于获取Nacos组件健康状况的NacosConfigHealthIndicator。
NacosConfigEndpoint和NacosConfigHealthIndicator相对来说工作原理较为简单,就不展开细述,NacosConfigEndpoint获取的配置信息包含了刷新的历史记录以及当前NacosConfig的配置,NacosConfigHealthIndicator判断Service的状态则是直接利用ConfigService的serverStatus字段。
NacosConnectionFailureAnalyzer
在Spring Boot框架中提供了一种方法来拦截在应用程序启动期间发生的导致应用程序启动失败的异常,换言之,FailureAnalyzer与我们在项目中常用的GloableExceptionHandler区别之处在于FailureAnalyzer是针对应用程序启动失败的异常。利用FailureAnalyzer可以将异常的堆栈信息转换为更具可读性的消息。在org.springframework.boot.diagnostics的包中可以看到多个常见的启动异常分析器,例如NoUniqueBeanDefinitionException、UnsatisfiedDependencyException等等。NacosConnectionFailureAnalyzer是Nacos Spring Cloud中针对NacosConnectionFailureException异常的分析器,仅是输出了异常的信息与建议,代码相对简单,就不展开细述。
NacosJsonPropertySourceLoader/NacosXmlPropertySourceLoader
NacosJsonPropertySourceLoader与NacosXmlPropertySourceLoader都实现了PropertySourceLoader,PropertySourceLoader是用于Spring Boot加载属性源的,其中包含了两个方法,getFileExtensions用于获取Loader支持的扩展文件类型,load方法用于从属性源中加载属性并转换成PropertySource。这些自定义的PropertySourceLoader会在NacosDataParserHandler的构造函数中利用SpringFactoriesLoader进行加载,NacosDataParserHandler是用于协助前面提到的NacosPropertySourceBuilder用于解析并装载远程配置文件的。我们以NacosJsonPropertySourceLoader为例,简单看一下装载的过程,实际上就是利用ObjectMapper将Resource转换成一个Map,然后会调用flattenedMap把顶层的Map铺平,最后用OriginTrackedMapPropertySource封装成一个PropertySource。OriginTrackedMapPropertySource实际上与MapPropertySource区别并不大,实现了一个OriginLookup的接口,如果属性值被封装,可以直接获取到被封装之前的值,类似的使用方法可以在YamlPropertySourceLoader中找到。
@Override
protected List<PropertySource<?>> doLoad(String name, Resource resource)
throws IOException {
Map<String, Object> result = new LinkedHashMap<>(32);
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> nacosDataMap = mapper.readValue(resource.getInputStream(),
LinkedHashMap.class);
flattenedMap(result, nacosDataMap, null);
return Collections.singletonList(
new OriginTrackedMapPropertySource(name, this.reloadMap(result), true));
}
复制代码