首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Spring 整合 MyBatis 多數據源場景下 失效坑點解析與修復

     編輯:Java 相關知識

在 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 下,為什麼佔位符不生效?

二、根因深度剖析

核心衝突:BeanDefinition 解析階段 vs PropertyPlaceholderConfigurer 處理階段

  1. MapperScannerConfigurer 實現了 BeanDefinitionRegistryPostProcessor,在 Spring 容器 BeanDefinition 註冊階段 就會被調用(早於普通 Bean 初始化)。
  2. 當配置 <property name="sqlSessionFactory" ref="record_sqlSessionFactory"/> 時,Spring 為了解析這個引用,會提前初始化 record_sqlSessionFactory。
  3. SqlSessionFactoryBean 初始化過程中會讀取 dataSource 屬性,進而觸發 record_dataSource 的創建。
  4. 此時 PropertyPlaceholderConfigurer 尚未執行佔位符替換(它在 BeanFactoryPostProcessor 階段,但順序在 MapperScannerConfigurer 之後),導致 ${record.jdbc.url} 以字符串字面值傳入 HikariDataSource,最終拋出異常。

關鍵洞見:sqlSessionFactory 屬性接收 Bean 引用會觸發提前初始化鏈路;而 sqlSessionFactoryBeanName 僅接收字符串名稱,不觸發 Bean 創建,從而避開了生命周期衝突。

三、修復方案

✅ 正確配置:使用 sqlSessionFactoryBeanName

<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 只記錄名稱,在真正掃描 Mapper 時再通過名稱從容器獲取 Bean,此時 PropertyPlaceholderConfigurer 已完成佔位符替換。

四、進階:單數據源場景為何不出問題?

單數據源時,MapperScannerConfigurer 默認查找名為 sqlSessionFactory 的 Bean。若你的 Bean 正好叫這個名字,無需顯式配置 sqlSessionFactory 屬性,也就不會觸發提前初始化,佔位符正常生效。

五、最佳實踐建議

  1. 多數據源必須顯式指定 sqlSessionFactoryBeanName,避免引用提前初始化。
  2. Bean 命名規範:[business]_sqlSessionFactory、[business]_dataSource、[business]_transactionManager,清晰區分。
  3. 屬性文件分離:jdbc-record.properties、jdbc-im.properties,配合 location 多文件加載。
  4. 使用 Spring Boot:@ConfigurationProperties + DataSourceBuilder 自動綁定,完全避開 XML 佔位符解析順序坑。
  5. 調試技巧:啟用 DEBUG 日誌觀察 Bean 創建順序,或實現 BeanFactoryPostProcessor 打印佔位符解析時機。

六、原理擴展:PropertyPlaceholderConfigurer vs MapperScannerConfigurer 執行順序

// 簡化的 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 的執行順序,是解決此類「明明配置對卻不生效」疑難雜症的關鑰鑰匙。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved