背景
使用Nacos做为配置中心来进行读取配置文件,实现动态刷新一些配置文件功能。nacos-config-spring-boot-starter
版本0.2.10
。
问题
当系统中有test1.yaml
和test2.yaml
配置文件,并且两个配置文件中都有test
属性值。不同的类进行读取配置文件中的属性值时,发现属性值读取错误,并且在配置文件发生变更刷新时,读取到的配置文件也是错误的。
PR
https://github.com/nacos-group/nacos-spring-boot-project/pull/232
测试验证
test1.yaml
test: test1
test2.yaml
test: test2
Test1Config.java
@Configuration
@NacosConfigurationProperties( dataId = "test1.yaml", groupId = "test1", autoRefreshed = true)
public class Test1Config {
private String test;
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
Test2Config.java
@Configuration
@NacosConfigurationProperties( dataId = "test2.yaml", groupId = "test2", autoRefreshed = true)
public class Test2Config {
private String test;
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
TestApplication.java
@Slf4j
@EnableScheduling
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
@Autowired
private Test1Config test1Config;
@Autowired
private Test2Config test2Config;
@Scheduled(cron = "0/5 * * * * ?")
public void useNewSymbolFeeConfig() {
log.info("test1: {}, test2:{}", test1Config.getTest(), test2Config.getTest());
}
}
启动后执行结果
INFO 86480 --- [ scheduling-1] c.e.test.TestApplication : test1:test1, test2:test1
更新test1.yaml
test: test1update
执行结果
INFO 86480 --- [ scheduling-1] c.e.test.TestApplication : test1:test2, test2:test1
更新test2.yaml
test: test2update
执行结果
INFO 86480 --- [ scheduling-1] c.e.test.TestApplication : test1:test2, test2:test1update
记录对比
test1.yaml | test2.yaml | test1配置结果 | test2配置结果 | 备注 | 是否正确 |
---|---|---|---|---|---|
test1 | test2 | test1 | test1 | 初始化 | × |
test1update | test2 | test2 | test1 | 变更test1.yaml | × |
test1update | test2update | test2 | test1update | 变更test2.yaml | × |
问题分析
nacos-config-spring-boot-autoconfigute中spring.factories
配置了自动装配NacosConfigAutoConfiguration
。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.boot.nacos.config.autoconfigure.NacosConfigAutoConfiguration
#org.springframework.context.ApplicationContextInitializer=\
# com.alibaba.boot.nacos.config.autoconfigure.NacosConfigApplicationContextInitializer
org.springframework.boot.env.EnvironmentPostProcessor=\
com.alibaba.boot.nacos.config.autoconfigure.NacosConfigEnvironmentProcessor
org.springframework.context.ApplicationListener=\
com.alibaba.boot.nacos.config.logging.NacosLoggingListener
NacosConfigAutoConfiguration
@ConditionalOnProperty(name = NacosConfigConstants.ENABLED, matchIfMissing = true)
@ConditionalOnMissingBean(name = CONFIG_GLOBAL_NACOS_PROPERTIES_BEAN_NAME)
@EnableConfigurationProperties(value = NacosConfigProperties.class)
@ConditionalOnClass(name = "org.springframework.boot.context.properties.bind.Binder")
@Import(value = { NacosConfigBootBeanDefinitionRegistrar.class })
@EnableNacosConfig
public class NacosConfigAutoConfiguration {
}
NacosConfigAutoConfiguration
又进行importNacosConfigBootBeanDefinitionRegistrar
。
NacosConfigBootBeanDefinitionRegistrar
@Configuration
public class NacosConfigBootBeanDefinitionRegistrar
implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
.rootBeanDefinition(NacosBootConfigurationPropertiesBinder.class);
defaultListableBeanFactory.registerBeanDefinition(
NacosBootConfigurationPropertiesBinder.BEAN_NAME,
beanDefinitionBuilder.getBeanDefinition());
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
}
}
在NacosConfigBootBeanDefinitionRegistrar
中将beanFactory
的rootBeanDefinition
设置为NacosBootConfigurationPropertiesBinder
。
NacosBootConfigurationPropertiesBinder
NacosBootConfigurationPropertiesBinder
继承了NacosConfigurationPropertiesBinder
,NacosConfigurationPropertiesBinder
在配置文件发生变化时,会调用bind
方法,bind
最终由doBind
来进行执行。NacosBootConfigurationPropertiesBinder
又进行重写了doBind
方法。
@Override
protected void doBind(Object bean, String beanName, String dataId, String groupId,
String configType, NacosConfigurationProperties properties, String content,
ConfigService configService) {
synchronized (this) {
String name = "nacos-bootstrap-" + beanName;
NacosPropertySource propertySource = new NacosPropertySource(name, dataId, groupId, content, configType);
environment.getPropertySources().addLast(propertySource);
ObjectUtils.cleanMapOrCollectionField(bean);
Binder binder = Binder.get(environment);
ResolvableType type = getBeanType(bean, beanName);
Bindable<?> target = Bindable.of(type).withExistingValue(bean);
binder.bind(properties.prefix(), target);
publishBoundEvent(bean, beanName, dataId, groupId, properties, content, configService);
publishMetadataEvent(bean, beanName, dataId, groupId, properties);
environment.getPropertySources().remove(name);
}
}
在构建NacosPropertySource
的时候,问题就已经出现了。
public NacosPropertySource(String dataId, String groupId, String name,
String nacosConfig, String type) {
super(name, toProperties(dataId, groupId, nacosConfig, type));
this.type = type;
}
NacosPropertySource
的构造方法中的顺序为dataId,groupId,name,nacosConfig,type
,而传入的参数为name,dataId,groupId,content,configType
。在environment.getPropertySources().remove(name)
方法中进行移除的时候,是根据name
来进行移除,这时候的name
对应的也就是上文中的groupId
,所以移除失败,当出现同一属性,在不同的配置文件中再去加载就会读取到上一次设置的内容。
验证执行流程
当配置文件test1.yaml进行加载,读取test1.yaml配置文件内容
读取到配置文件后,将其放入到PropertySources
中
这时候,发现PropertySources
的name
居然变成了test1
,test1这里面有可能是我们配置的groupId,继续进行debug绑定bean
绑定完后,将其从environment中进行移除
在移除的时候发现,这里面的name为‘nacos-bootstrap + beanName’,而我们这里面设置的name是test1
显然移除会失败。加下来的操作调试流程相同,同理会导致相同的问题。
environment.getPropertySources().addLast(propertySource);
environment调用addLast
方法会先进行移除掉PropertySources
,再进行add。也就是导致乱序的一个原因之一。
总结
在开发过程中经常会遇到一些奇葩的问题,在解决问题的时候,可以一点点的去定位问题,调试的过程中很容易出现断点打飞,数据量少,数据量相同导致无法复现的问题。所以一定要沉住气,另外要及时记笔记,去记录这些问题。
彩蛋
nacos-spring-boot-project
的配置中心在进行修改map
和collection
数据类型的字段时候会不生效,我们该如何去解决去修复呢?