零入侵構(gòu)造多數(shù)據(jù)源
??中大型項(xiàng)目常常會(huì)配有多個(gè)數(shù)據(jù)庫(kù)昧捷,而需要使用數(shù)據(jù)庫(kù)做curd的我們需要一種能很好的解決多數(shù)據(jù)庫(kù)連接的問(wèn)題往枷。零入侵并不是說(shuō)不需要寫(xiě)代碼蜂莉,而是在不改動(dòng)原代碼的基礎(chǔ)上實(shí)現(xiàn)需要的功能碎罚。
??本章所講的多數(shù)據(jù)源是采用ThreadLocal(線(xiàn)程變量)實(shí)現(xiàn)的会傲,在應(yīng)用需要做增刪改查的時(shí)候锅棕,會(huì)先獲取當(dāng)前線(xiàn)程的線(xiàn)程變量拙泽,根據(jù)獲取到的線(xiàn)程變量來(lái)選擇對(duì)應(yīng)的數(shù)據(jù)源。具體的執(zhí)行流程如下:
- 為線(xiàn)程A的ThreadLocal賦值
- 連接DynamicDataSource數(shù)據(jù)源
- DynamicDataSource數(shù)據(jù)源根據(jù)ThreadLocal所持有的值去選擇通過(guò)DataSource1還是DataSource2執(zhí)行該SQL語(yǔ)句
開(kāi)始構(gòu)造多數(shù)據(jù)源
由于多數(shù)據(jù)源需要配合ThreadLocal來(lái)實(shí)現(xiàn)裸燎,所以數(shù)據(jù)源的配置以java config的方式配置比較直觀(guān)顾瞻,如果之前是按照SpringBoot配置方式配置的可以更換成java config方式再來(lái)學(xué)習(xí)本章。
1.多數(shù)據(jù)源枚舉類(lèi)
由于demo比較簡(jiǎn)單德绿,所以直接使用1荷荤、2的方式命名,需要多少個(gè)數(shù)據(jù)源就添加多少個(gè)
public enum DatabaseType {
DATASOURCE1,DATASOURCE2
}
2. ThreadLocal
新建DatabaseContextHolder 類(lèi)移稳,該類(lèi)持有ThreadLocal對(duì)象
public class DatabaseContextHolder {
private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>();
public static void setDatabaseType(DatabaseType type) {
contextHolder.set(type);
}
public static DatabaseType getDatabaseType() {
return contextHolder.get();
}
}
3蕴纳、Spring動(dòng)態(tài)數(shù)據(jù)源
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getDatabaseType();
}
}
4.application.yml添加多數(shù)據(jù)源配置
datasource1.jdbc:
driverClassName: com.mysql.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/technology-integration
username: root
password: 123456
min-idle: 10
datasource2.jdbc:
driverClassName: com.mysql.jdbc.Driver
jdbcUrl: jdbc:mysql://localhost:3306/technology-integration2
username: root
password: 123456
min-idle: 10
5.MybatisConfig配置
修改之前創(chuàng)建的MyBatisConfig類(lèi)
@Configuration
@MapperScan("com.viu.technology.mapper")
public class MybatisConfig extends HikariConfig{
@Autowired
private Environment env;
//使用Hikaricp數(shù)據(jù)源
@Bean("datasource1")
@ConfigurationProperties(prefix = "datasource1.jdbc")
public DataSource dataSource1() {
log.info("開(kāi)始配置DataSource數(shù)據(jù)源1");
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName(env.getProperty("datasource1.jdbc.driverClassName"));
dataSource.setJdbcUrl(env.getProperty("datasource1.jdbc.url"));
dataSource.setUsername(env.getProperty("datasource1.jdbc.username"));
dataSource.setPassword(env.getProperty("datasource1.jdbc.password"));
dataSource.setMaximumPoolSize(100);
dataSource.setPoolName("hikari-");
return dataSource;
}
//配置第二個(gè)數(shù)據(jù)源
@Bean("datasource2")
@ConfigurationProperties(prefix = "datasource2.jdbc")
public DataSource dataSource2() {
log.info("開(kāi)始配置DataSource數(shù)據(jù)源2");
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName(env.getProperty("datasource2.jdbc.driverClassName"));
dataSource.setJdbcUrl(env.getProperty("datasource2.jdbc.url"));
dataSource.setUsername(env.getProperty("datasource2.jdbc.username"));
dataSource.setPassword(env.getProperty("datasource2.jdbc.password"));
return dataSource;
}
//配置多數(shù)據(jù)源,使用ThreadLocal對(duì)對(duì)應(yīng)的線(xiàn)程保存不用的類(lèi)型个粱,根據(jù)類(lèi)型獲取不同的數(shù)據(jù)源
@Bean("datasource")
@Primary
public DynamicDataSource dataSource(@Qualifier("datasource1") DataSource dataSource1,
@Qualifier("datasource2") DataSource dataSource2) {
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DatabaseType.DATASOURCE1, dataSource1);
targetDataSource.put(DatabaseType.DATASOURCE2, dataSource2);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSource);
dataSource.setDefaultTargetDataSource(dataSource1);
return dataSource;
}
@Bean
@Primary
public SqlSessionFactory sqlSessionFactory( DataSource dataSource) throws Exception {
SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
fb.setDataSource(dataSource);
fb.setTypeAliasesPackage("com.viu.technology.po");
fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapping/*.xml"));
return fb.getObject();
}
}
接下來(lái)主要解析一下AbstractRoutingDataSource 這個(gè)類(lèi)古毛,這個(gè)類(lèi)是Spring框架自帶的一個(gè)動(dòng)態(tài)切換數(shù)據(jù)源的類(lèi)。
首先需要看一下我們剛剛在MybatisConfig中注冊(cè)的DynamicDataSource這個(gè)Bean
溫馨提示:DynamicDataSource也是DataSource的實(shí)現(xiàn)類(lèi)
- 先是把兩個(gè)數(shù)據(jù)源添加到targetDataSource這個(gè)Map集合中
- 接著實(shí)例化出DynamicDataSource這個(gè)類(lèi)
- 設(shè)置dynamicDataSource的目標(biāo)數(shù)據(jù)源(也就是我們?cè)贛ybatisConfig中注冊(cè)的兩個(gè)數(shù)據(jù)源)
- 設(shè)置dynamicDataSource默認(rèn)使用的數(shù)據(jù)源都许,也就是沒(méi)有為T(mén)hreadLocal賦值的情況下默認(rèn)使用的的數(shù)據(jù)源
-
返回dynamicDataSource稻薇,完成DataSource Bean的注冊(cè)
注冊(cè)多數(shù)據(jù)源的整合主要就是在dynamicDataSource的實(shí)現(xiàn),接下來(lái)我們Ctrl+鼠標(biāo)左鍵進(jìn)入AbstractRoutingDataSource類(lèi)的源碼中
- 可以看到AbstractRoutingDataSource里面的變量胶征,targetDataSources的值就是我們剛才在DynamicDataSource Bean中注入的目標(biāo)數(shù)據(jù)源塞椎。
- defaultTargetDataSource則是我們剛才設(shè)置的默認(rèn)數(shù)據(jù)源
afterPropertiesSet()是AbstractRoutingDataSource的核心方法,我們則從這個(gè)方法開(kāi)始解析代碼
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
可以看到這句代碼弧烤,如果目標(biāo)數(shù)據(jù)源為空則會(huì)拋出IllegalArgumentException異常忱屑,而我們剛才已經(jīng)在MybatisConfig類(lèi)里面的dynamicDataSource()為其注入了兩個(gè)數(shù)據(jù)源
this.targetDataSources == null
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DatabaseType.DATASOURCE1, dataSource1);
targetDataSource.put(DatabaseType.DATASOURCE2, dataSource2);
DynamicDataSource dataSource = new DynamicDataSource();
如果this.targetDataSources不為null,afterPropertiesSet()方法,則會(huì)將targetDataSource轉(zhuǎn)換為resolvedDataSources 暇昂。主要就是將targetDataSource中所有的value由Object對(duì)象轉(zhuǎn)換為DataSource對(duì)象莺戒,兩個(gè)map中的key存放的是我們自定義的DatabaseType 枚舉類(lèi)。這里的forEach使用的是lambda表達(dá)式急波,等同于foreach循環(huán)
private Map<Object, Object> targetDataSources;
private Map<Object, DataSource> resolvedDataSources;
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
下面的代碼主要的操作使用resolveSpecifiedDataSource()方法从铲,將Object對(duì)象轉(zhuǎn)換為DataSource對(duì)象。如果Object對(duì)象為DataSource的實(shí)例澄暮,則直接強(qiáng)轉(zhuǎn)名段;如果Object對(duì)象為String的實(shí)例,則根據(jù)jndi的名字獲取DataSource泣懊;否則將拋出異常
protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
if (dataSource instanceof DataSource) {
return (DataSource)dataSource;
} else if (dataSource instanceof String) {
return this.dataSourceLookup.getDataSource((String)dataSource);
} else {
throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and
String supported: " + dataSource);
}
}
最后AbstractRoutingDataSource會(huì)根據(jù)defaultTargetDataSource是否為空為resolvedDefaultDataSource賦值伸辟,resolvedDefaultDataSource是轉(zhuǎn)變后的DataSource默認(rèn)使用的數(shù)據(jù)源
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
多數(shù)據(jù)源的存儲(chǔ)解析到此為止,接下來(lái)我們看看AbstractRoutingDataSource是如何切換數(shù)據(jù)源的馍刮。
這個(gè)方法則是AbstractRoutingDataSource切換數(shù)據(jù)源的關(guān)鍵信夫,讓我們來(lái)一步步解析該方法。
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource 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 + "]");
} else {
return dataSource;
}
}
判斷resolvedDataSources是否為空,如果為空則拋出IllegalArgumentException異常
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
這個(gè)方法為讀取resolvedDataSources的key静稻,用于獲取對(duì)應(yīng)的DataSource
Object lookupKey = this.determineCurrentLookupKey();
我們可以看到該類(lèi)下的方法為抽象方法警没,而我們繼承AbstractRoutingDataSource的DynamicDataSource類(lèi)則實(shí)現(xiàn)了這個(gè)抽象方法,方法返回了當(dāng)前線(xiàn)程所持有的線(xiàn)程變量,也就是DatabaseType其中的值振湾。
protected abstract Object determineCurrentLookupKey();
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getDatabaseType();
}
}
determineTargetDataSource()根據(jù)實(shí)現(xiàn)類(lèi)重寫(xiě)的determineCurrentLookupKey()方法獲取到了當(dāng)前線(xiàn)程所持有的線(xiàn)程變量杀迹,根據(jù)獲取到的線(xiàn)程變量取出resolvedDataSources存儲(chǔ)的DataSource。
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
如果從resolvedDataSources獲取到的DataSource為空押搪,且當(dāng)前的線(xiàn)程變量為空树酪、lenientFallback 為true的情況下會(huì)使用resolvedDefaultDataSource中的DataSource進(jìn)行數(shù)據(jù)操作。lenientFallback 的意思是嵌言,當(dāng)datasource這個(gè)變量為空的時(shí)候會(huì)使用resolvedDefaultDataSource進(jìn)行數(shù)據(jù)源操作嗅回。
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
getConnection那些方法就不進(jìn)行解析了及穗,以上所有對(duì)源代碼的解讀均為個(gè)人理解珊膜,如有錯(cuò)誤歡迎指出窗骑。
Dao中切換數(shù)據(jù)源
只需要在執(zhí)行SQL語(yǔ)句前調(diào)用setDatabaseType方法為線(xiàn)程本地變量contextHolder賦值即可
/**
* 使用數(shù)據(jù)源2
* @param id
* @return
*/
public User selectUserById(String id) {
DatabaseContextHolder.setDatabaseType(DatabaseType.DATASOURCE2);
return userMapper.selectByPrimaryKey(id);
}