在 Spring 整合 MyBatis 多數據源項目中,配置 <context:property-placeholder> 後發現 ${jdbc.url} 等佔位符無法解析,導致數據源初始化失敗。這是一個經典的 Bean 生命周期順序衝突 問題。本文深度剖析根因、複現場景、修復方案與最佳實踐。
<!-- 屬性文件加載 -->
<context:property-placeholder location="classpath:config.properties" />
<!-- 數據源 1 -->
<bean id="record_dataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="${record.jdbc.url}" />
<property name="username" value="${record.jdbc.username}" />
<property name="password" value="${record.jdbc.password}" />
</bean>
<!-- 數據源 2 -->
<bean id="im_dataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="${im.jdbc.url}" />
<property name="username" value="${im.jdbc.username}" />
<property name="password" value="${im.jdbc.password}" />
</bean>
<!-- SqlSessionFactory 1 -->
<bean id="record_sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="record_dataSource" />
</bean>
<!-- MapperScannerConfigurer 1 -->
<bean id="config1" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.xxx.record.dao" />
<property name="sqlSessionFactory" ref="record_sqlSessionFactory" /> <!-- 關鍵點:直接注入 Bean 引用 -->
</bean>
<!-- SqlSessionFactory 2 -->
<bean id="im_sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="im_dataSource" />
</bean>
<!-- MapperScannerConfigurer 2 -->
<bean id="config2" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.xxx.im.dao" />
<property name="sqlSessionFactory" ref="im_sqlSessionFactory" /> <!-- 關鍵點 -->
</bean>
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'record_dataSource':
Could not resolve placeholder 'record.jdbc.url' in value "${record.jdbc.url}"
明明配置了 property-placeholder,屬性文件也在 classpath 下,為什麼佔位符不生效?
<property name="sqlSessionFactory" ref="record_sqlSessionFactory"/> 時,Spring 為了解析這個引用,會提前初始化 record_sqlSessionFactory。SqlSessionFactoryBean 初始化過程中會讀取 dataSource 屬性,進而觸發 record_dataSource 的創建。${record.jdbc.url} 以字符串字面值傳入 HikariDataSource,最終拋出異常。關鍵洞見:
sqlSessionFactory屬性接收 Bean 引用會觸發提前初始化鏈路;而sqlSessionFactoryBeanName僅接收字符串名稱,不觸發 Bean 創建,從而避開了生命周期衝突。
<bean id="config1" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.xxx.record.dao" />
<property name="sqlSessionFactoryBeanName" value="record_sqlSessionFactory" /> <!-- 字符串名稱,不觸發初始化 -->
</bean>
<bean id="config2" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.xxx.im.dao" />
<property name="sqlSessionFactoryBeanName" value="im_sqlSessionFactory" />
</bean>
sqlSessionFactoryBeanName 對應屬性類型為 String,需用 value 而非 ref。單數據源時,MapperScannerConfigurer 默認查找名為 sqlSessionFactory 的 Bean。若你的 Bean 正好叫這個名字,無需顯式配置 sqlSessionFactory 屬性,也就不會觸發提前初始化,佔位符正常生效。
[business]_sqlSessionFactory、[business]_dataSource、[business]_transactionManager,清晰區分。jdbc-record.properties、jdbc-im.properties,配合 location 多文件加載。@ConfigurationProperties + DataSourceBuilder 自動綁定,完全避開 XML 佔位符解析順序坑。DEBUG 日誌觀察 Bean 創建順序,或實現 BeanFactoryPostProcessor 打印佔位符解析時機。// 簡化的 Spring 容器啟動關鍵階段順序
1. BeanDefinitionReader 讀取 XML -> 生成 BeanDefinition
2. BeanFactoryPostProcessor 階段(按 order 排序):
- PropertyPlaceholderConfigurer (order=Ordered.LOWEST_PRECEDENCE)
- MapperScannerConfigurer (order=Ordered.LOWEST_PRECEDENCE, 但實現了 PriorityOrdered 實際更早)
3. BeanPostProcessor 階段
4. 初始化單例 Bean (getBean -> doCreateBean -> populateBean -> initializeBean)
因為 MapperScannerConfigurer 實現了 PriorityOrdered,其 postProcessBeanDefinitionRegistry 比普通 BeanFactoryPostProcessor 更早執行,導致在佔位符解析前就觸發了 Bean 初始化鏈路。
理解 Spring 容器啟動生命周期與各 Processor 的執行順序,是解決此類「明明配置對卻不生效」疑難雜症的關鑰鑰匙。