概述
日常的業(yè)務開發(fā)項目中只會配置一套數(shù)據(jù)源店量,如果需要獲取其他系統(tǒng)的數(shù)據(jù)往往是通過調(diào)用接口公壤, 或者是通過第三方工具比如kettle將數(shù)據(jù)同步到自己的數(shù)據(jù)庫中進行訪問堤舒。
但是也會有需要在項目中引用多數(shù)據(jù)源的場景踢京。比如如下場景:
- 自研數(shù)據(jù)遷移系統(tǒng)誉碴,至少需要新、老兩套數(shù)據(jù)源瓣距,從老庫讀取數(shù)據(jù)寫入新庫
- 自研讀寫分離中間件黔帕,系統(tǒng)流量增加,單庫響應效率降低蹈丸,引入讀寫分離方案成黄,寫入數(shù)據(jù)是一個數(shù)據(jù)源,讀取數(shù)據(jù)是另一個數(shù)據(jù)源
環(huán)境說明
- spring boot
- mysql
- mybatis-plus
- spring-aop
項目目錄結(jié)構(gòu)
- controller: 存放接口類
- service: 存放服務類
- mapper: 存放操作數(shù)據(jù)庫的mapper接口
- entity: 存放數(shù)據(jù)庫表實體類
- vo: 存放返回給前端的視圖類
- context: 存放持有當前線程數(shù)據(jù)源key類
- constants: 存放定義數(shù)據(jù)源key常量類
- config: 存放數(shù)據(jù)源配置類
- annotation: 存放動態(tài)數(shù)據(jù)源注解
- aop: 存放動態(tài)數(shù)據(jù)源注解切面類
- resources.config: 項目配置文件
- resources.mapper: 數(shù)據(jù)庫xml文件
關鍵類說明
忽略掉controller/service/entity/mapper/xml介紹逻杖。
- jdbc.properties: 數(shù)據(jù)源配置文件奋岁。雖然可以配置到Spring boot的默認配置文件application.properties/application.yml文件當中,但是如果數(shù)據(jù)源比較多的話荸百,根據(jù)實際使用闻伶,最佳的配置方式還是獨立配置比較好。
- DynamicDataSourceConfig: 數(shù)據(jù)源配置類
- DynamicDataSource: 動態(tài)數(shù)據(jù)源配置類
- DataSourceRouting: 動態(tài)數(shù)據(jù)源注解
- DynamicDataSourceAspect: 動態(tài)數(shù)據(jù)源設置切面
- DynamicDataSourceContextHolder: 當前線程持有的數(shù)據(jù)源key
- DataSourceConstants: 數(shù)據(jù)源key常量類
開發(fā)流程
動態(tài)數(shù)據(jù)源流程
Spring Boot 的動態(tài)數(shù)據(jù)源管搪,本質(zhì)上是把多個數(shù)據(jù)源存儲在一個 Map 中虾攻,當需要使用某個數(shù)據(jù)源時,從 Map 中獲取此數(shù)據(jù)源進行處理更鲁。
在 Spring 中已提供了抽象類 AbstractRoutingDataSource 來實現(xiàn)此功能霎箍,繼承AbstractRoutingDataSource類并覆寫其determineCurrentLookupKey()
方法即可,該方法只需要返回數(shù)據(jù)源key即可澡为,也就是存放數(shù)據(jù)源的Map的key漂坏。
因此,我們在實現(xiàn)動態(tài)數(shù)據(jù)源的,只需要繼承它顶别,實現(xiàn)自己的獲取數(shù)據(jù)源邏輯即可谷徙。AbstractRoutingDataSource頂級繼承了DataSource,所以它也是可以做為數(shù)據(jù)源對象驯绎,因此項目中使用它作為主數(shù)據(jù)源完慧。
AbstractRoutingDataSource原理
AbstractRoutingDataSource中有一個重要的屬性:
- targetDataSources: 目標數(shù)據(jù)源,即項目啟動的時候設置的需要通過AbstractRoutingDataSource管理的數(shù)據(jù)源剩失。
- defaultTargetDataSource: 默認數(shù)據(jù)源屈尼,項目啟動的時候設置的默認數(shù)據(jù)源,如果沒有指定數(shù)據(jù)源拴孤,默認返回改數(shù)據(jù)源脾歧。
- resolvedDataSources: 也是存放的數(shù)據(jù)源,是對targetDataSources進行處理后進行存儲的演熟”拗矗可以看一下源碼。
- resolvedDefaultDataSource: 對默認數(shù)據(jù)源進行了二次處理芒粹,源碼如上圖最后的兩行代碼兄纺。
AbstractRoutingDataSource中所有的方法和屬性:
比較重要的是determineTargetDataSource
方法。
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Nullable
protected abstract Object determineCurrentLookupKey();
這個方法主要就是返回一個DataSource對象是辕,主要邏輯就是先通過方法determineCurrentLookupKey
獲取一個Object對象的lookupKey囤热,然后通過這個lookupKey到resolvedDataSources中獲取數(shù)據(jù)源(resolvedDataSources就是一個Map猎提,上面已經(jīng)提到過了)获三;如果沒有找到數(shù)據(jù)源,就返回默認的數(shù)據(jù)源锨苏。determineCurrentLookupKey就是程序員配置動態(tài)數(shù)據(jù)源需要自己實現(xiàn)的方法疙教。
問題
配置多數(shù)據(jù)源后啟動項目報錯:Property 'sqlSessionFactory' or 'sqlSession Template' are required。
翻譯過來就是:需要屬性“sqlSessionFactory”或“sqlSessionTemplate”伞租。也就是說 注入數(shù)據(jù)源的時候需要這兩個數(shù)據(jù)贞谓,但是這兩個屬性在啟動容器中沒有找到。
當引入mybatis-plus依賴mybatis-plus-boot-starter
后葵诈,會添加一個自動配置類 MybatisPlusAutoConfiguration 裸弦。 其中有兩個方法sqlSessionFactory()
、sqlSessionTemplate()
作喘。這兩個方法就是 給容器中注入“sqlSessionFactory”或“sqlSessionTemplate”兩個屬性理疙。
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
this.applyConfiguration(factory);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (this.properties.getTypeAliasesSuperType() != null) {
factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
factory.setTypeHandlers(this.typeHandlers);
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
Objects.requireNonNull(factory);
this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory);
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (!ObjectUtils.isEmpty(this.languageDrivers)) {
factory.setScriptingLanguageDrivers(this.languageDrivers);
}
Optional var10000 = Optional.ofNullable(defaultLanguageDriver);
Objects.requireNonNull(factory);
var10000.ifPresent(factory::setDefaultScriptingLanguageDriver);
this.applySqlSessionFactoryBeanCustomizers(factory);
GlobalConfig globalConfig = this.properties.getGlobalConfig();
Objects.requireNonNull(globalConfig);
this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
this.getBeansThen(IKeyGenerator.class, (i) -> {
globalConfig.getDbConfig().setKeyGenerators(i);
});
Objects.requireNonNull(globalConfig);
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
Objects.requireNonNull(globalConfig);
this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
factory.setGlobalConfig(globalConfig);
return factory.getObject();
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
}
}
這里主要關注配置類上面的注解,詳細如下:
@Configuration(
proxyBeanMethods = false
)
// 當類路徑下有SqlSessionFactory.class泞坦、SqlSessionFactoryBean.class時才生效
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
// 容器中只能有一個符合條件的DataSource
// 因為容器中有3個數(shù)據(jù)源窖贤,且沒有指定主數(shù)據(jù)源,這個條件不通過,就不會初始化這個配置類了
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {
}
因為MybatisPlusAutoConfiguration不滿足@ConditionalOnSingleCandidate(DataSource.class) 條件赃梧,所以自動配置不會生效滤蝠,就不會執(zhí)行sqlSessionFactory()
、sqlSessionTemplate()
兩個方法授嘀。 容器中就不會有sqlSessionFactory
物咳、sqlSessionTemplate
兩個bean對象,因此會報錯蹄皱。
所以在數(shù)據(jù)源配置類中的動態(tài)數(shù)據(jù)源配置上添加@Primary注解即可所森。
@Configuration
@PropertySource("classpath:config/jdbc.properties")
@MapperScan(basePackages = {"com.xinxing.learning.datasource.mapper"})
public class DynamicDataSourceConfig {
@Bean
@Primary
public DataSource dynamicDataSource() {
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());
targetDataSource.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSource);
dataSource.setDefaultTargetDataSource(masterDataSource());
return dataSource;
}
}