一、先说怎么用AbstractRoutingDataSource配置多数据源自动切换
1、先配置多个数据源
@Configuration
@MapperScan(basePackages = "com.example.demo.mappers", sqlSessionTemplateRef = "sqlSessionTemplate")
public class MultiDataSourceConfig {
/**
* 创建Mysql DataSrouce数据源
*
* @return DataSource
*/
@Bean("mysqlDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid.mysql")
public DataSource dataSource() {
return DruidDataSourceBuilder.create().build();
}
/**
* 创建SQL Server数据源
*
* @return DataSource
*/
@Bean(name = "sqlServerDataSource")
@ConfigurationProperties(prefix = "spring.datasource.druid.sqlserver")
public DataSource sqlServerDataSource3() {
return DruidDataSourceBuilder.create().build();
}
/**
* 动态数据源配置
*
* @param sqlServerDataSource sqlServerDataSource
* @param mysqlDataSource mysqlDataSource
* @return DataSource
*/
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource(@Qualifier("sqlServerDataSource") DataSource sqlServerDataSource, @Qualifier("mysqlDataSource") DataSource mysqlDataSource) {
DynamicDataSource multipleDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("sqlServerDataSource", sqlServerDataSource);
targetDataSources.put("mysqlDataSource", mysqlDataSource);
//设置默认数据源
multipleDataSource.setDefaultTargetDataSource(mysqlDataSource);
//添加数据源
multipleDataSource.setTargetDataSources(targetDataSources);
return multipleDataSource;
}
/**
* @param dataSource dataSource
* @return SqlSessionFactory
* @throws Exception Exception
*/
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("mybatis-config.xml"));
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mappers/**.xml"));
return bean.getObject();
}
/**
* 替代线程不安全的defaultSqlSessionFactory
*
* @param sqlSessionFactory sqlSessionFactory
* @return SqlSessionTemplate
*/
@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
2、配置动态数据源对象
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return null;
}
}
3、配置数据源保持器
public class DataSourceHolder {
private static final ThreadLocal<String> HOLDER = new ThreadLocal<String>() {
@Override
protected String initialValue() {
HOLDER.set("mysqlDataSource");
return HOLDER.get();
}
};
/**
* @param name name
*/
public static void setDataSource(String name) {
HOLDER.set(name);
}
/**
* @return dbName
*/
public static String getDataSource() {
return HOLDER.get();
}
/**
*
*/
public static void destroy() {
HOLDER.remove();
}
/**
* @param name name
* @return boolean
*/
public static boolean contain(String name) {
return name != null && name.equals(HOLDER.get());
}
}
4、设置切点、编写切面逻辑,使用springaop动态切换数据源
@Aspect
@Component
public class DataSourceAspect {
private static final Logger LOGGER = LogManager.getLogger(DataSourceAspect.class);
/**
*
*/
@Pointcut(value = "@annotation(com.example.demo.annotation.DataSourceSelectAnnotation)")
public void pointcut() {
}
/**
* @param joinPoint joinPoint
*/
@Before(value = "pointcut()")
public void before(JoinPoint joinPoint) {
DataSourceSelectAnnotation dataSourceSelect = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(DataSourceSelectAnnotation.class);
String dbName = dataSourceSelect.name();
if (!StringUtils.isEmpty(dbName)) {
DataSourceHolder.setDataSource(dbName);
} else {
LOGGER.error("dbName is null");
}
}
/**
* @param joinPoint joinPoint
*/
@After(value = ("pointcut()"), argNames = "joinPoint")
public void doAfterAdvice(JoinPoint joinPoint) {
DataSourceHolder.destroy();
}
}
5、只要在需要切换的mapper接口方法上加上下面这个注解,程序会自动根据注解设置的name属性值切换到对应数据源
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface DataSourceSelectAnnotation {
String name() default "mysqlDataSource";
}
二、动态数据源实现的原理分析 在上面的配置中,我们为mybatis的sqlSessionFactory对象注入的是名称为dynamicDataSource,也就是我们的动态数据源管理对象。看如下代码
/**
* @param dataSource dataSource
* @return SqlSessionFactory
* @throws Exception Exception
*/
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("mybatis-config.xml"));
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mappers/**.xml"));
return bean.getObject();
}
mybatis在执行数据库操作的过程中,会调用Excutor执行器,Excutor执行器在执行sql语句之前会调用sqlSessionFactory里面的dataSource来获取连接,也就是调用我们配置dynamicDataSource对象的getConnection()方法,代码如下图:
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
其中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;
}
这个方法的作用就是选择正确的数据源返回给上级方法调用,这个方法的逻辑流程大致如下:
1、首先通过determineCurrentLookupKey()方法获取我们在AOP里面的设置好的将要使用的数据源的名称;
2、通过数据源名称到名称为resolvedDataSources的map类型的成员变量里面去取的对应的数据源(后面会介绍这些变量的初始化)
3、如果数据源为空并且lenientFallback变量为真或者lookupkey为空则使用默认数据源,否则抛出异常,如果数据源不为空,则返回当前获取到的数据源。
大致流程就是这样,但是还需要搞清楚两个问题就能把整个数据源的选择过程串联起来了。
1、determineCurrentLookupKey()方法是如何得到我们在AOP逻辑里面设置的数据源名称的呢?
2、我们是通过lookupkey到resolvedDataSources这个map里面去取数据源对象,那么这些数据源对象是如何设置到resolvedDataSources这个map里面的呢?
determineCurrentLookupKey()方法其实就是我们配置的DynamicDataSource类里面的方法,这个方法是我们自己写的,我们跟进到determineCurrentLookupKey()方法里面,代码如下:
@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.getDataSource();
}
它调用的是DataSourceHolder的getDataSource()方法:
public static String getDataSource() {
return HOLDER.get();
}
private static final ThreadLocal<String> HOLDER = new ThreadLocal<String>() {
@Override
protected String initialValue() {
HOLDER.set("mysqlDataSource");
return HOLDER.get();
}
};
HOLDER其实就是一个ThreadLocal,既然是从ThreadLocal里面取的,那么肯定是当前线程在这个时刻之前的某个时间节点set进去的。我们可以看到默认设置的是mysqlDataSource,那么还有什么地方可以设置呢?还记得我们配置的切面类的before方法吗?它通过DataSourceHolder.setDataSource(dbName)调用将注解的属性值设置到了ThreadLocal里面。如下代码:
/**
* @param joinPoint joinPoint
*/
@Before(value = "pointcut()")
public void before(JoinPoint joinPoint) {
DataSourceSelectAnnotation dataSourceSelect = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(DataSourceSelectAnnotation.class);
String dbName = dataSourceSelect.name();
if (!StringUtils.isEmpty(dbName)) {
DataSourceHolder.setDataSource(dbName);
} else {
LOGGER.error("dbName is null");
}
}
把这个流程串连起来就是:我们通过AOP技术将代码织入到mapper接口的实现方法之前,代码的逻辑就是将DataSourceSelectAnnotation注解的name属性值设置到DataSourceHoler的ThreadLocal里面,当mybatis调用mapper接口对应的实现方法时候会通过sessionFactory获取连接,sessionFactory会调用DynamicDataSource的getConnection方法获取连接,getConnection会调用determineTargetDataSource方法,determineTargetDataSource方法会调用determineCurrentLookupKey方法获取数据源的名称,determineCurrentLookupKey方法会到DataSourceHolder的ThreadLocal里面取当前线程在这个时间节点之前设置进去的数据源名称。
到这里数据源名称的来龙去脉就搞清楚了,那么根据数据源名称获取数据源是到名称为resolvedDataSources的map里面去取的,那么这个map又是怎么来的呢?
还记得我们在初始化动态数据源的时候,把一个默认数据源和一个目标数据源设置进去了,进入对应的方法:
@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);
}
}
然后在成员变量被设置了值后,会调用这个方法把数据源相关键值对解析到了resolvedDataSources这个map里面。
通过上面的分析我们了解到多数据源的实现核心逻辑其实就是在mybatis通过sessionFactory获取连接之前将即将要访问的数据源放在指定的地方,让它去取就可以了。