本文基于Spring Cloud Version:
Spring Cloud Hoxton.SR8
,Spring Boot Version:2.3.2.RELEASE
,Nacos Version:1.3.3
在sprig boot 中,引入nacos-config-spring-boot-starter
后,通常是使用@NacosValue
注解来获取nacos server 中的配置以及自动刷新,官网中的实例如下:
@Controller
@RequestMapping("config")
public class ConfigController {
@NacosValue(value = "${useLocalCache:false}", autoRefreshed = true)
private boolean useLocalCache;
@RequestMapping(value = "/get", method = GET)
@ResponseBody
public boolean get() {
return useLocalCache;
}
}
在spring cloud alibaba中,引入spring-cloud-starter-alibaba-nacos-config
则使用了更优雅的方式支持spring 原生注解@Value来获取参数,通过 Spring Cloud 原生注解 @RefreshScope 实现配置自动更新,官网中的实例如下:
@RestController
@RequestMapping("/config")
@RefreshScope
public class ConfigController {
@Value("${useLocalCache:false}")
private boolean useLocalCache;
@RequestMapping("/get")
public boolean get() {
return useLocalCache;
}
}
spring-cloud-starter-alibaba-nacos-config
是把nacos server所有的配置都放在Environment中,下面简要分析一下源码看看是怎么整合
spring cloud alibaba是借助PropertySourceBootstrapConfiguration这个ApplicationContextInitializer,通过自定义类实现PropertySourceLocator接口来扩展,其原理详见《spring cloud 中Environment自定义扩展配置源码简析和实例》。
在NacosConfigBootstrapConfiguration
中创建了NacosConfigManager
、NacosPropertySourceLocator
两个关键类:
@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);
}
}
jar中springfactory.properties的配置:
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
NacosPropertySourceLocator的主要源码如下:
public class NacosPropertySourceLocator implements PropertySourceLocator {
@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;
}
}
三类配置的加载源码可以自行看看,其主要逻辑就是使用ConfigService从nacos server获取参数配置,并将参数追加到PropertySource中。
spring cloud alibaba,通过NacosContextRefresher
这个ApplicationListener
来实现,分析详见代码和注解:
public class NacosContextRefresher
implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
public void onApplicationEvent(ApplicationReadyEvent event) {
// many Spring context
// 1.spring 上下文启动后触发
if (this.ready.compareAndSet(false, true)) {
this.registerNacosListenersForApplications();
}
}
private void registerNacosListenersForApplications() {
if (isRefreshEnabled()) {
//2.遍历所有的NacosPropertySource,注册监听器
for (NacosPropertySource propertySource : NacosPropertySourceRepository
.getAll()) {
if (!propertySource.isRefreshable()) {
continue;
}
String dataId = propertySource.getDataId();
registerNacosListener(propertySource.getGroup(), dataId);
}
}
}
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
//3.当参数有变化时 分发RefreshEvent事件
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);
}
}
}
如上注解,其核心逻辑如下:
1.在spring上下文启动完毕后,通过NacosContextRefresher
这个ApplicationListener
出发监听器注册逻辑
2.来给nacos config中的各个data-id配置增加监听器。
3.当有配置发生变化时,nacos的监听器分发一个会在applicationContext
中分发一个RefreshEvent
事件
4.RefreshEventListener
中监听到RefreshEvent
事件后,调用其refreshEnvironment
方法,其核心逻辑就是:构建一个的SpringApplicationBuilder
对象,用该对象build一个精简版的applicationContext,并传入当前上下文的Environment,以及BootstrapApplicationListener
,ConfigFileApplicationListener
这两个关键创建Environment的类
5.最后调用这个精简版的applicationContext.run(),将evnrioment进行更新,其实就是又走了一遍章节 2. spring cloud alibaba 初始化将参数配置追加到Environment 中的逻辑。
RefreshEventListener
源码分析:
public class RefreshEventListener implements SmartApplicationListener {
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return ApplicationReadyEvent.class.isAssignableFrom(eventType)
|| RefreshEvent.class.isAssignableFrom(eventType);
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent) {
handle((ApplicationReadyEvent) event);
}
else if (event instanceof RefreshEvent) {
handle((RefreshEvent) event);
}
}
public void handle(RefreshEvent event) {
if (this.ready.get()) {
// don't handle events before app is ready
log.debug("Event received " + event.getEventDesc());
//刷新入口
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
}
断点进入ContextRefresher:refresh方法
public class ContextRefresher {
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
public synchronized Set<String> refreshEnvironment() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
ConfigurableApplicationContext addConfigFilesToEnvironment() {
ConfigurableApplicationContext capture = null;
try {
StandardEnvironment environment = copyEnvironment(
this.context.getEnvironment());
//4.构建一个的`SpringApplicationBuilder`对象
SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
.bannerMode(Mode.OFF).web(WebApplicationType.NONE)
.environment(environment);
// Just the listeners that affect the environment (e.g. excluding logging
// listener because it has side effects)
//4.传入当前上下文的Environment,以及`BootstrapApplicationListener`,`ConfigFileApplicationListener`这两个关键创建Environment的类
builder.application()
.setListeners(Arrays.asList(new BootstrapApplicationListener(),
new ConfigFileApplicationListener()));
//5.最后调用这个精简版的applicationContext.run(),将evnrioment进行更新,其实就是又走了一遍章节 2. spring cloud alibaba 初始化将参数配置追加到Environment 中的逻辑。
capture = builder.run();
if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
}
MutablePropertySources target = this.context.getEnvironment()
.getPropertySources();
String targetName = null;
for (PropertySource<?> source : environment.getPropertySources()) {
String name = source.getName();
if (target.contains(name)) {
targetName = name;
}
if (!this.standardSources.contains(name)) {
if (target.contains(name)) {
target.replace(name, source);
}
else {
if (targetName != null) {
target.addAfter(targetName, source);
// update targetName to preserve ordering
targetName = name;
}
else {
// targetName was null so we are at the start of the list
target.addFirst(source);
targetName = name;
}
}
}
}
}
return capture;
}
}