Nacos配置自动刷新不生效

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Nacos配置自动刷新不生效相关的知识,希望对你有一定的参考价值。

参考技术A 碰到两个问题
1,修改nacos配置,服务加载配置时报错找不到$project.version,当前nacos-client版本是1.2.0,升级到1.4.0以上可解决。
2,修改配置没有动态刷新,@Value注解无效,改用配置类的方式@ConfigurationProperties,别忘了加上@RefreshScope。

Nacos基于Spring cloud客户端配置动态刷新,配置文件命名逻辑

我们知道Nacos可以作为配置中心来使用,并且Nacos官方给我们提供了基于spring cloud的自动导入模块,可以在项目中比较方便的引入,另外Nacos的spring cloud客户端配置可以动态刷新,接下来我们看下spring cloud nacos客户端怎么实现的,

一般我们在spring cloud项目中使用Nacos的配置中心功能时会引入如下pom:

<dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
                <version>2.2.3.RELEASE</version>
</dependency>

在githup的项目源码上,我们看到在META-INF/spring.factories下的内容如下:

org.springframework.cloud.bootstrap.BootstrapConfiguration=\\
com.alibaba.cloud.nacos.NacosConfigBootstrapConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\
com.alibaba.cloud.nacos.NacosConfigAutoConfiguration,\\
com.alibaba.cloud.nacos.endpoint.NacosConfigEndpointAutoConfiguration
org.springframework.boot.diagnostics.FailureAnalyzer=\\
com.alibaba.cloud.nacos.diagnostics.analyzer.NacosConnectionFailureAnalyzer
org.springframework.boot.env.PropertySourceLoader=\\
com.alibaba.cloud.nacos.parser.NacosJsonPropertySourceLoader,\\
com.alibaba.cloud.nacos.parser.NacosXmlPropertySourceLoader
org.springframework.context.ApplicationListener=\\
com.alibaba.cloud.nacos.logging.NacosLoggingListener

基于我们之前的分析springboot自动装配原理,底层源码分析,条件注解实现机制

这里会加载上述几个Bean,而Nacos 客户端配置实现和刷新主要在NacosConfigBootstrapConfigurationNacosConfigAutoConfiguration,在NacosConfigAutoConfiguration中注入了NacosContextRefresher,这个主要是用来刷新的,NacosConfigBootstrapConfiguration中注入了NacosConfigManagerNacosPropertySourceLocatorNacosConfigManager是管理Nacos配置的工具类,而NacosPropertySourceLocator借助NacosConfigManager去Nacos服务器加载对应的配置信息。

NacosPropertySourceLocator类继承结构如下:

可以看到,其实现了PropertySourceLocator接口,通过之前的分析我们知道,spring boot在准备环境的时候会调用当前容器中所有PropertySourceLocatorlocate方法,这里NacosPropertySourceLocator的实现如下:

public PropertySource<?> locate(Environment env) 
		nacosConfigProperties.setEnvironment(env);
		ConfigService configService = nacosConfigManager.getConfigService();
		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;
	

这里有个细节需要注意下:如果我们没有配置 spring.cloud.nacos.config.prefix 的话,那么默认是获取 spring.application.name 作为该值,也就是所谓的dataIdPrefix

另外这里会加载如下三个配置:1. loadSharedConfiguration共享配置 2. loadExtConfiguration扩展配置 3. loadApplicationConfiguration加载应用配置

在Nacos中,每个应用除了本应用的配置外,还额外支持共享和扩展配置。对于共享配置和扩展配置,多个应用都可以获取,可以通过如下方式配置:

spring.cloud.nacos.config.extension-configs[0].data-id=ext001
spring.cloud.nacos.config.extension-configs[0].group=extGroup001
spring.cloud.nacos.config.extension-configs[0].refresh=true

spring.cloud.nacos.config.shared-configs[0].data-id==share001
spring.cloud.nacos.config.shared-configs[0].group=shareGroup
spring.cloud.nacos.config.shared-configs[0].refresh=true

需要注意的是,这里的data-id必须配置,否则报错。

对于loadSharedConfiguration:

private void loadSharedConfiguration(
			CompositePropertySource compositePropertySource) 
		List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
				.getSharedConfigs();
		if (!CollectionUtils.isEmpty(sharedConfigs)) 
			checkConfiguration(sharedConfigs, "shared-configs");
			loadNacosConfiguration(compositePropertySource, sharedConfigs);
		
	
private void loadNacosConfiguration(final CompositePropertySource composite,
			List<NacosConfigProperties.Config> configs) 
		for (NacosConfigProperties.Config config : configs) 
			loadNacosDataIfPresent(composite, config.getDataId(), config.getGroup(),
					NacosDataParserHandler.getInstance()
							.getFileExtension(config.getDataId()),
					config.isRefresh());
		
	
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);
		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);
			
		
		return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
				isRefreshable);
	

这里简单分析下,是怎么去获取配置的,首先根据spring.cloud.nacos.config.shared-configs的配置,逐个去加载的。对于文件的类型fileExtension,Nacos客户端通过判断dataId中是否有.,如果有.则获取最后一个.后面的内容为文件格式,如: cmmon-share.yml,返回的是yml,否则返回默认,为properties
如果不需要刷新,首先判断有没有加载过,如果加载过了,则直接从本地缓存加载,否则通过nacosPropertySourceBuilder.build(dataId, group, fileExtension,isRefreshable);加载

NacosPropertySource build(String dataId, String group, String fileExtension,
			boolean isRefreshable) 
		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 
			data = configService.getConfig(dataId, group, timeout);
			if (StringUtils.isEmpty(data)) 
				return Collections.emptyList();
			
			return NacosDataParserHandler.getInstance().parseNacosData(dataId, data,
					fileExtension);
		catch (Exception e) 
		
		return Collections.emptyList();
	


可以看到,最终通过Nacos的ConfigService去进行实际的配置获取了。
对于loadSharedConfigurationloadExtConfiguration两者基本一致,唯一不同的就是dataId不同,加载不同的配置。

private void loadApplicationConfiguration(
			CompositePropertySource compositePropertySource, String dataIdPrefix,
			NacosConfigProperties properties, Environment environment) 
		// 	fileExtension 默认为 properties
		String fileExtension = properties.getFileExtension(); 
		// nacosGroup 默认为 DEFAULT_GROUP
		String nacosGroup = properties.getGroup();
		loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
				fileExtension, true);
		loadNacosDataIfPresent(compositePropertySource,
				dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
		for (String profile : environment.getActiveProfiles()) 
			String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
			loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
					fileExtension, true);
		
	

这里首先回去加载以dataIdPrefix为名称的配置,如果我们没配置spring.cloud.nacos.config.prefix那么就会将spring.application.name的配置,例如配置了spring.cloud.nacos.config.prefix=test001,那么这里首先会获取dataId=test001这个名称的配置。
然后在获取dataId=dataIdPrefix + DOT + fileExtension的配置,这里可以理解为获取test001.properties这个配置。
然后会获取当前spring上下文中active的profile,然后获取dataId= dataIdPrefix + SEP1 + profile + DOT + fileExtension的配置,比如这个环境是测试,那么这里就会获取test001-test.propreties这个配置。接着获取Nacos的配置方式与上面的都一样。
最后addFirstPropertySource来加入到整个配置环境中去,这里loadSharedConfigurationloadExtConfigurationloadApplicationConfiguration最后都会调用这个方法,即:
loadApplicationConfiguration 会覆盖loadExtConfiguration的配置,loadExtConfiguration会覆盖loadSharedConfiguration的配置。
这样,我们就把Nacos的配置都加载到当前Spring的环境中来了,那么怎么刷新呢 ?
刷新的机制在NacosContextRefresher中实现,其实现了ApplicationListener<ApplicationReadyEvent>接口:

public void onApplicationEvent(ApplicationReadyEvent event) 
		// many Spring context
		if (this.ready.compareAndSet(false, true)) 
			this.registerNacosListenersForApplications();
		
	

当spring容器全部就绪之后,就会触发ApplicationReadyEvent事件,继而触发registerNacosListenersForApplications事件:

private void registerNacosListenersForApplications() 
		if (isRefreshEnabled()) 
			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
						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);
		
	

这里通过configService.addListener(ConfigService增加了监听器,Nacos客户端通过Long Polling的方式去Nacos服务器获取最新的配置,当有配置更新时,Nacos客户端则向Spring上下文发布一个RefreshEvent事件,而根据之前spring cloud配置实现、刷新,spring cloud config实现原理,刷新
的分析,触发该事件之后,Spring boot会启动一个新的空的spring容器,这个容器会去加载一次整个配置,会告知应用哪些配置有变动。这时候被RefreshScope和ConfigurationProperties注解修饰的类会重新创建并初始化。

另外可以看到,这里在spring cloud中使用Nacos客户端去获取配置的时候通过
dataIdPrefix+'-"activeProfile+"."+fileExtension ,同时需要指定namespace和group
dataIdPrefix通过spring.cloud.nacos.config.prefix配置项获取,没有配置的话则获取spring.application.name

activeProfile则是我们启动spring cloud指定的profile
fileExtension通过spring.cloud.nacos.config.file-extension配置项获取,没有配置的话默认为properties
group通过spring.cloud.nacos.config.group配置项获取,没有配置的话默认为DEFAULT_GROUP
namespace通过spring.cloud.nacos.config.namespace获取,没有配置的话,默认public

以上是关于Nacos配置自动刷新不生效的主要内容,如果未能解决你的问题,请参考以下文章

Nacos基于Spring cloud客户端配置动态刷新,配置文件命名逻辑

SpringBoot配置热启动HTML不刷新

springboot nacos配置详解

springboot nacos配置详解

关于gateway的一些知识细节

关于gateway的一些知识细节