在使用 Spring Boot 和 MyBatis 動態切換資料來源時遇到的問題以及解決方法
1. org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)
在使用了動態資料來源後遇到了該問題,從錯誤資訊來看是因為沒有找到
*.xml
檔案而導致的,但是在配置檔案中
確實添加了相關的配置,這種錯誤的原因是因為設定資料來源後沒有設定SqlSessionFactoryBean
的typeAliasesPackage
和mapperLocations
屬性或屬性無效導致的;
- 解決方法:
如果在應用的入口類中添加了
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
,
在DataSourceConfigure
類的中設定相關屬性:
@Bean @ConfigurationProperties(prefix = "mybatis")
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
return sqlSessionFactoryBean;
}
或者直接配置(不推薦該方式):
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setTypeAliasesPackage("typeAliasesPackage");
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("mapperLocations" ));
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
return sqlSessionFactoryBean;
}
2. Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
該異常在錯誤資訊中已經說的很清楚了,是因為有多個
DataSource
的例項,所以無法確定該引用那個例項
- 解決方法:
為資料來源的某個
Bean
新增@Primary
註解,該Bean
應當是通過DataSourceBuilder.create().build()
得到的Bean
,而不是通過new AbstractRoutingDataSource
的子類實現的Bean
,在本專案中可以是master()
或slave()
得到的DataSource
,不能是dynamicDataSource()
得到的DataSource
3. 通過註解方式動態切換資料來源無效
- 請確認註解沒有放到 DAO 層方法上, 因為會在 Service 層開啟事務,所以當註解在 DAO 層時不會生效
- 請確認以下
Bean
正確配置:
@Bean("dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put("master", master());
dataSourceMap.put("slave", slave());
// Set master datasource as default
dynamicRoutingDataSource.setDefaultTargetDataSource(master());
// Set master and slave datasource as target datasource
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
// To put datasource keys into DataSourceContextHolder to judge if the datasource is exist
DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());
}
return dynamicRoutingDataSource;
}
@Bean
@ConfigurationProperties(prefix = "mybatis")
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
// Here is very important, if don't config this, will can't switch datasource
// put all datasource into SqlSessionFactoryBean, then will autoconfig SqlSessionFactory
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
return sqlSessionFactoryBean;
}
4. @Transactional
註解無效,發生異常不回滾
- 請確認該
Bean
得到正確配置,並且@Transactional
的rollbackFor
配置正確
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
5. 通過 AOP 判斷 DAO 層方法名時切換資料來源無效
當切面指向了 DAO 層後無論如何設定切面的順序,都無法在執行查詢之前切換資料來源,但是切面改為 Service 層後可以正常工作
解決方法: 請確認
@Transactional
註解是加在方法上而不是 Service 類上,添加了@Transactional
的方法因為在 Service 層開啟了事務,
會在事務結束之後才會切換資料來源檢出
DataSourceTransactionManager
Bean 注入正確
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
6. The dependencies of some of the beans in the application context form a cycle
- 錯誤資訊:
The dependencies of some of the beans in the application context form a cycle:
produceController (field private cn.com.hellowood.dynamicdatasource.service.ProductService cn.com.hellowood.dynamicdatasource.controller.ProduceController.productService)
↓
productService (field private cn.com.hellowood.dynamicdatasource.mapper.ProductDao cn.com.hellowood.dynamicdatasource.service.ProductService.productDao)
↓
productDao defined in file [/Users/hehuimin/Downloads/Dev/SpringBoot/DynamicDataSource/out/production/classes/cn/com/hellowood/dynamicdatasource/mapper/ProductDao.class]
↓
sqlSessionFactoryBean defined in class path resource [cn/com/hellowood/dynamicdatasource/configuration/DataSourceConfigurer.class]
┌─────┐
| dynamicDataSource defined in class path resource [cn/com/hellowood/dynamicdatasource/configuration/DataSourceConfigurer.class]
↑
↓
| master defined in class path resource [cn/com/hellowood/dynamicdatasource/configuration/DataSourceConfigurer.class]
↑
↓
| dataSourceInitializer
└─────┘
這是因為在注入
DataSource
的例項的時候產生了迴圈呼叫,第一個注入的 Bean 依賴於其他的 Bean, 而被依賴的 Bean 產生依賴傳遞,依賴第一個
注入的 Bean, 陷入了迴圈,無法啟動專案
- 解決方法:將
@Primary
註解指向沒有依賴的 Bean,如:
/**
* master DataSource
* @Primary 註解用於標識預設使用的 DataSource Bean,因為有三個 DataSource Bean,該註解可用於 master
* 或 slave DataSource Bean, 但不能用於 dynamicDataSource Bean, 否則會產生迴圈呼叫
* * @ConfigurationProperties 註解用於從 application.properties 檔案中讀取配置,為 Bean 設定屬性
* @return data source
*/
@Bean("master")
@Primary
@ConfigurationProperties(prefix = "application.server.db.master")
public DataSource master() {
return DataSourceBuilder.create().build();
} @Bean("slave")
@ConfigurationProperties(prefix = "application.server.db.slave")
public DataSource slave() {
return DataSourceBuilder.create().build();
} @Bean("dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put("master", master());
dataSourceMap.put("slave", slave());
// Set master datasource as default
dynamicRoutingDataSource.setDefaultTargetDataSource(master());
// Set master and slave datasource as target datasource
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
// To put datasource keys into DataSourceContextHolder to judge if the datasource is exist
DynamicDataSourceContextHolder.dataSourceKeys.addAll(dataSourceMap.keySet());
return dynamicRoutingDataSource;
}