一.由最核心的类说起
在实现动态数据源的过程中,最核心的一个类在我的代码中如下:
package com.example.common;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 取得当前使用哪个数据源
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
可以看到,这里是直接继承了AbstractRoutingDataSource,然后重写了他的抽象方法determineCurrentLookupKey(),剩下的就是交由Spring容器根据AOP或者注解在程序运行时动态切换数据源了.
二.org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
看下com.example.config.MybatisPlusConfig.java类中的代码:
package com.example.config;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.MybatisConfiguration;
import com.baomidou.mybatisplus.entity.GlobalConfiguration;
import com.baomidou.mybatisplus.mapper.LogicSqlInjector;
import com.baomidou.mybatisplus.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.plugins.PerformanceInterceptor;
import com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean;
import com.example.common.DBTypeEnum;
import com.example.common.DynamicDataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
@MapperScan({"com.example.mapper*"})
public class MybatisPlusConfig {
/**
* mybatis-plus分页插件<br>
* 文档:http://mp.baomidou.com<br>
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
//paginationInterceptor.setLocalPage(true);// 开启 PageHelper 的支持
return paginationInterceptor;
}
/**
* mybatis-plus SQL执行效率插件【生产环境可以关闭】
*/
@Bean
public PerformanceInterceptor performanceInterceptor() {
return new PerformanceInterceptor();
}
@Bean(name = "db1")
@ConfigurationProperties(prefix = "spring.datasource.druid.db1" )
public DataSource db1 () {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "db2")
@ConfigurationProperties(prefix = "spring.datasource.druid.db2" )
public DataSource db2 () {
return DruidDataSourceBuilder.create().build();
}
/**
* 动态数据源配置
* @return
*/
@Bean
@Primary
public DataSource multipleDataSource (@Qualifier("db1") DataSource db1,
@Qualifier("db2") DataSource db2 ) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map< Object, Object > targetDataSources = new HashMap<>();
targetDataSources.put(DBTypeEnum.db1.getValue(), db1 );
targetDataSources.put(DBTypeEnum.db2.getValue(), db2);
dynamicDataSource.setTargetDataSources(targetDataSources);
//设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(db1);
return dynamicDataSource;
}
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
sqlSessionFactory.setDataSource(multipleDataSource(db1(),db2()));
//sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*/*Mapper.xml"));
MybatisConfiguration configuration = new MybatisConfiguration();
//configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sqlSessionFactory.setConfiguration(configuration);
sqlSessionFactory.setPlugins(new Interceptor[]{ //PerformanceInterceptor(),OptimisticLockerInterceptor()
paginationInterceptor()
});
sqlSessionFactory.setGlobalConfig(globalConfiguration());
return sqlSessionFactory.getObject();
}
@Bean
public GlobalConfiguration globalConfiguration() {
GlobalConfiguration conf = new GlobalConfiguration(new LogicSqlInjector());
conf.setLogicDeleteValue("-1");
conf.setLogicNotDeleteValue("1");
conf.setIdType(0);
conf.setMetaObjectHandler(new MyMetaObjectHandler());
conf.setDbColumnUnderline(true);
conf.setRefresh(true);
return conf;
}
}
在 multipleDataSource (@Qualifier(“db1”) DataSource db1,@Qualifier(“db2”) DataSource db2 ),既然是多数据源,那么对于javax.sql.DataSource就会有多个实现,可以看到方法参数中用@Qualifier标明了不同实现在容器中的唯一beanName.
@Primary和@Qualifier的不同一句话总结:
- @Primary: 意思是在众多相同的bean中,优先使用用@Primary注解的bean
- @Qualifier : 这个注解则指定某个bean有没有资格进行注入
我们看下这两句代码:
dynamicDataSource.setTargetDataSources(targetDataSources);
//设置默认数据源
dynamicDataSource.setDefaultTargetDataSource(db1);
然后看下对应的AbstractRoutingDataSource.java对应的field:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
....
}
AbstractRoutingDataSource位于org.springframework.jdbc.datasource.lookup包下,这里又可以牵扯出Spring中lookup-method方法注入,暂且不讨论.
代码中将多数据源存储到了Map
@Bean(name = "db1")
@ConfigurationProperties(prefix = "spring.datasource.druid.db1" )
public DataSource db1 () {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "db2")
@ConfigurationProperties(prefix = "spring.datasource.druid.db2" )
public DataSource db2 () {
return DruidDataSourceBuilder.create().build();
}
同时设置了默认数据源defaultTargetDataSource.
接下来是核心方法determineCurrentLookupKey(),在实现中由DbContextHolder.getDbType()返回当前线程所持有的DataSource对应的key,DbContextHolder是一个由ThreadLocal实现的线程安全的数据源容器.debug进入AbstractRoutingDataSource类中,
@Nullable
protected abstract Object determineCurrentLookupKey();
这里是用到了模板设计模式,Spring中大量用到了这种模式.既然是个模板方法,那么在这个抽象类构成的整体算法骨架下肯定是用到了这个方法,会找到如下的方法:
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;
}
这里才是真正动态切换数据源的地方.首先获取到当前线程持有的数据源beanName,由beanName找到容器中的DataSource,接着会在如下代码中就能拿到数据库连接了.
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
下面补充看下如何拿到DataSource?
关键一句:DataSource dataSource = this.resolvedDataSources.get(lookupKey);
首先,他的声明如下:
@Nullable
private Map<Object, DataSource> resolvedDataSources;
也是一个Map,那就好办,看看是哪里给他塞了entry进去,找到了如下方法:
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
实现了org.springframework.beans.factory.InitializingBean中的afterPropertiesSet()方法,这个方法有什么作用呢?这里涉及到Spring中bean的初始化方式.spring为bean提供了两种初始化bean的方式,实现InitializingBean接口,实现afterPropertiesSet方法,或者在配置文件中同过init-method指定,两种方式可以同时使用.实现InitializingBean接口是直接调用afterPropertiesSet方法,比通过反射调用init-method指定的方法效率相对来说要高点。但是init-method方式消除了对spring的依赖,如果调用afterPropertiesSet方法时出错,则不调用init-method指定的方法.
有个疑问?既然在targetDataSources中已经存好了数据源,为什么还要再弄个resolvedDataSources呢?看下两个Map的参数泛型,一个是Map
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);
}
}
这是一个protected方法,所以他是可以被子类重写的.在这个方法中,首先判断targetDataSources中的value是不是DataSource实例,是的话就直接返回了.接着看下面的判断,如果targetDataSources中的value是String类型, 则从dataSourceLookup中返回DataSource.否则就抛出异常.这里也可以看出,默认存储的value要么是DataSource实例,要么是String类型.那dataSourceLookup又是什么呢?
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();