记一次关于读取配置文件的BUG

675 阅读3分钟

问题描述

在使用spring创建bean时,bean的值由properties文件中取出,但是取出的值却与文件中的值不一致,spring配置文件中的相关配置如下

<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
        <property name="driverClassName" value="${driver}"/>
</bean>

db.properties

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT%2B8&useSSL=false
username=root
password=密码
maxActive=100
maxIdle=50

但是创建出来的dataSource的属性却有点问题

初步分析

我写的明明是username=root但是这里确实username=ko,仔细想了一下,这个有点ko熟悉,猛然发现这是我电脑上的用户名

如果变量名改了的话这个问题就不存在了,但是怎么能就这样结束呢,接着分析。

spring bean的创建

spring中bean由beanFactory来创建,beanFactory中保存了bean创建所需要的信息(BeanDefinition),上面的问题大概率就出现在这里。之前看过一点spring的源码,所以直接定位了到相关的函数。 AbstractApplicationContext中有个refresh()方法,源码如下

// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				// Invoke factory processors registered as beans in the context.
				//调用注册在上下文中的工厂后置处理器
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();

关于bean工厂后置处理器

spring中很多特性都由后置处理器来实现,猜测填充配置文件中beanDefinition的属性应该是在invokeBeanFactoryPostProcessors(beanFactory)中实现,但是并不清楚是哪个后置处理器,只能一步一步跟进去看了。跟进去之后定位到一个关键函数DefaultListableBeanFactory中的doGetBeanNamesForType()方法

for (String beanName : this.beanDefinitionNames) {
			// Only consider bean as eligible if the bean name
			// is not defined as alias for some other bean.
			if (!isAlias(beanName)) {
				try {
					//在这个地方下个条件断点(beanName.equal("dataSource"))
					RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
					// Only check bean definition if it is complete.
					if (!mbd.isAbstract() && (allowEagerInit ||
							(mbd.hasBeanClass() || !mbd.isLazyInit() || isAllowEagerClassLoading()) &&

断点停下五次到六次时this.mergedBeanDefinitions中的关于dataSource的beanDefinition的定义就发生了变化,

由原来的占位符变为了实际的值
由堆栈信息得到两次调用这个函数的位置都在PostProcessorRegistrationDelegate中的invokeBeanFactoryPostProcessors()方法

String[] postProcessorNames =
				beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);//第五次调用
				/*省略部分代码*/
		// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
		sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
		//这里就是关键代码
		invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

		/*省略部分代码*/
		invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);//第六次调用

接着就是一步一步看到底是哪变化了,经过分析,就是invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);这个方法,这个后置处理器的名字为PropertySourcesPlaceholderConfigurer,看来确实就是这个了

PropertySourcesPlaceholderConfigurer

在PropertyPlaceholderHelper这个类的parseStringValue()方法中

	// Recursive invocation, parsing placeholders contained in the placeholder key.
	//解析占位符
	placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
	// Now obtain the value for the fully resolved key...
	//解析占位符的值
	String propVal = placeholderResolver.resolvePlaceholder(placeholder);

就是从propertySources中解析出相应的值

propertySourceList包含两个propertySourceList,一个是environmentProperties,另一个是localProperties,username这个属性在两个里都有,我希望的值存储在后者中,但是spring会按照顺序先从environmentProperties中取,如果取到了,直接结束,否则再从localProperties中取。

environmentProperties->systemProperties->systemEnviroment

这里面确实有个USERNAME的属性,貌似spring取这个值的时候不分大小写

结语

命名冲突引发的“血案”,总算理解为什么看到许多的配置文件中XX.username的写法了