零入侵构造多数据源
中大型项目常常会配有多个数据库,而需要使用数据库做curd的我们需要一种能很好的解决多数据库连接的问题。零入侵并不是说不需要写代码,而是在不改动原代码的基础上实现需要的功能。
本章所讲的多数据源是采用ThreadLocal(线程变量)实现的,在应用需要做增删改查的时候,会先获取当前线程的线程变量,根据获取到的线程变量来选择对应的数据源。具体的执行流程如下:
- 为线程A的ThreadLocal赋值
- 连接DynamicDataSource数据源
- DynamicDataSource数据源根据ThreadLocal所持有的值去选择通过DataSource1还是DataSource2执行该SQL语句
开始构造多数据源
由于多数据源需要配合ThreadLocal来实现,所以数据源的配置以java config的方式配置比较直观,如果之前是按照SpringBoot配置方式配置的可以更换成java config方式再来学习本章。
1.多数据源枚举类
由于demo比较简单,所以直接使用1、2的方式命名,需要多少个数据源就添加多少个
public enum DatabaseType {
DATASOURCE1,DATASOURCE2
}
2. ThreadLocal
新建DatabaseContextHolder 类,该类持有ThreadLocal对象
public class DatabaseContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
public static void setDatabaseType(DatabaseType type) {
contextHolder.set(type);
}
public static DatabaseType getDatabaseType() {
return contextHolder.get();
}
}
3、Spring动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getDatabaseType();
}
}
4.application.yml添加多数据源配置
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配置
修改之前创建的MyBatisConfig类
@Configuration
@MapperScan("com.viu.technology.mapper")
public class MybatisConfig extends HikariConfig{
@Autowired
private Environment env;
//使用Hikaricp数据源
@Bean("datasource1")
@ConfigurationProperties(prefix = "datasource1.jdbc")
public DataSource dataSource1() {
log.info("开始配置DataSource数据源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;
}
//配置第二个数据源
@Bean("datasource2")
@ConfigurationProperties(prefix = "datasource2.jdbc")
public DataSource dataSource2() {
log.info("开始配置DataSource数据源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;
}
//配置多数据源,使用ThreadLocal对对应的线程保存不用的类型,根据类型获取不同的数据源
@Bean("datasource")
@Primary
public DynamicDataSource dataSource(@Qualifier("datasource1") DataSource dataSource1,
@Qualifier("datasource2") DataSource dataSource2) {
Map
接下来主要解析一下AbstractRoutingDataSource 这个类,这个类是Spring框架自带的一个动态切换数据源的类。
首先需要看一下我们刚刚在MybatisConfig中注册的DynamicDataSource这个Bean
温馨提示:DynamicDataSource也是DataSource的实现类
- 先是把两个数据源添加到targetDataSource这个Map集合中
- 接着实例化出DynamicDataSource这个类
- 设置dynamicDataSource的目标数据源(也就是我们在MybatisConfig中注册的两个数据源)
- 设置dynamicDataSource默认使用的数据源,也就是没有为ThreadLocal赋值的情况下默认使用的的数据源
-
返回dynamicDataSource,完成DataSource Bean的注册
注册多数据源的整合主要就是在dynamicDataSource的实现,接下来我们Ctrl+鼠标左键进入AbstractRoutingDataSource类的源码中
- 可以看到AbstractRoutingDataSource里面的变量,targetDataSources的值就是我们刚才在DynamicDataSource Bean中注入的目标数据源。
- defaultTargetDataSource则是我们刚才设置的默认数据源
afterPropertiesSet()是AbstractRoutingDataSource的核心方法,我们则从这个方法开始解析代码
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);
}
}
}
可以看到这句代码,如果目标数据源为空则会抛出IllegalArgumentException异常,而我们刚才已经在MybatisConfig类里面的dynamicDataSource()为其注入了两个数据源
this.targetDataSources == null
Map
如果this.targetDataSources不为null,afterPropertiesSet()方法,则会将targetDataSource转换为resolvedDataSources 。主要就是将targetDataSource中所有的value由Object对象转换为DataSource对象,两个map中的key存放的是我们自定义的DatabaseType 枚举类。这里的forEach使用的是lambda表达式,等同于foreach循环
private Map
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对象转换为DataSource对象。如果Object对象为DataSource的实例,则直接强转;如果Object对象为String的实例,则根据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会根据defaultTargetDataSource是否为空为resolvedDefaultDataSource赋值,resolvedDefaultDataSource是转变后的DataSource默认使用的数据源
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
多数据源的存储解析到此为止,接下来我们看看AbstractRoutingDataSource是如何切换数据源的。
这个方法则是AbstractRoutingDataSource切换数据源的关键,让我们来一步步解析该方法。
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");
这个方法为读取resolvedDataSources的key,用于获取对应的DataSource
Object lookupKey = this.determineCurrentLookupKey();
我们可以看到该类下的方法为抽象方法,而我们继承AbstractRoutingDataSource的DynamicDataSource类则实现了这个抽象方法,方法返回了当前线程所持有的线程变量,也就是DatabaseType其中的值。
protected abstract Object determineCurrentLookupKey();
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getDatabaseType();
}
}
determineTargetDataSource()根据实现类重写的determineCurrentLookupKey()方法获取到了当前线程所持有的线程变量,根据获取到的线程变量取出resolvedDataSources存储的DataSource。
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
如果从resolvedDataSources获取到的DataSource为空,且当前的线程变量为空、lenientFallback 为true的情况下会使用resolvedDefaultDataSource中的DataSource进行数据操作。lenientFallback 的意思是,当datasource这个变量为空的时候会使用resolvedDefaultDataSource进行数据源操作。
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那些方法就不进行解析了,以上所有对源代码的解读均为个人理解,如有错误欢迎指出。
Dao中切换数据源
只需要在执行SQL语句前调用setDatabaseType方法为线程本地变量contextHolder赋值即可
/**
* 使用数据源2
* @param id
* @return
*/
public User selectUserById(String id) {
DatabaseContextHolder.setDatabaseType(DatabaseType.DATASOURCE2);
return userMapper.selectByPrimaryKey(id);
}