实操开发中nacos-config-spring-boot-autoconfigure遇到的一个BUG

王守钰 2022-03-07 22:03:43

背景

使用Nacos做为配置中心来进行读取配置文件,实现动态刷新一些配置文件功能。nacos-config-spring-boot-starter版本0.2.10

问题

当系统中有test1.yamltest2.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.yamltest2.yamltest1配置结果test2配置结果备注是否正确
test1test2test1test1初始化×
test1updatetest2test2test1变更test1.yaml×
test1updatetest2updatetest2test1update变更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中将beanFactoryrootBeanDefinition设置为NacosBootConfigurationPropertiesBinder

NacosBootConfigurationPropertiesBinder

NacosBootConfigurationPropertiesBinder继承了NacosConfigurationPropertiesBinderNacosConfigurationPropertiesBinder在配置文件发生变化时,会调用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配置文件内容
image
读取到配置文件后,将其放入到PropertySources
image
这时候,发现PropertySourcesname居然变成了test1,test1这里面有可能是我们配置的groupId,继续进行debug绑定bean
image
绑定完后,将其从environment中进行移除
image
在移除的时候发现,这里面的name为‘nacos-bootstrap + beanName’,而我们这里面设置的name是test1显然移除会失败。加下来的操作调试流程相同,同理会导致相同的问题。

environment.getPropertySources().addLast(propertySource);

environment调用addLast方法会先进行移除掉PropertySources,再进行add。也就是导致乱序的一个原因之一。

总结

在开发过程中经常会遇到一些奇葩的问题,在解决问题的时候,可以一点点的去定位问题,调试的过程中很容易出现断点打飞,数据量少,数据量相同导致无法复现的问题。所以一定要沉住气,另外要及时记笔记,去记录这些问题。

彩蛋

nacos-spring-boot-project的配置中心在进行修改mapcollection数据类型的字段时候会不生效,我们该如何去解决去修复呢?