基于SpringBoot1.5&Mybatis实现DAO层多数据源切换

背景

多数据源是一个比较普遍的需求,如读写库分离、数据散落在多个库等场景中,项目必须具备多数据源访问能力。本文基于工作中一次实现略作探讨,分为:实现原理、实践两大部分。

目前普遍的JavaWeb项目为Controller-Service-Dao(Mapper)三层结构,最理想的方案是能在DAO(Mapper)层方法粒度实现数据源自动切换,这样能最大限度降低耦合,让service层及以上无感知。而且为了支持动态扩展,使用注解标记是一个比较好的方案。

实现原理

我们知道Java中访问数据库最底层的标准是JDBC,还记得当初每执行一条sql,都需要写一堆DriveManager.getConnection()、PreparedStatement、ResultSet、close的处理逻辑吗?
Spring、Mybatis等框架做的事情,就是把所有流程化的代码全部封装,让开发者只需关注Sql、对象处理等业务逻辑,其他流程全部透明化。实现细节此处不展开,但我们要知道一个结论:Spring帮我们干了这些活。
要想实现访问数据库的能力,我们必须配置DataSource对象。通过它获得数据库链。

//DataSource,其定位就是提供Connectiion
package javax.sql;  
public interface DataSource  extends CommonDataSource, Wrapper {  

    Connection getConnection() throws SQLException;  
    
    Connection getConnection(String username, String password)  
    throws SQLException;  
}

在Spring框架中,一切Bean都由Spring管理,所以Mybatis在需要DataSource时,会向Spring申请DataSource对象来获取Connection。而Spring也是在这个节点,提供动态切换数据源的能力:AbstractRoutingDataSource

AbstractRoutingDataSource简介

AbstractRoutingDataSource实现了javax.sql.DataSource接口,也就是说AbstractRoutingDataSource本身就是一个数据源,但为何其具有动态切换功能。而我们常见的如DruidDataSourceHikariDataSource数据源则不具备呢?我们可以看下其getConnection()方法的实现:

//代码有精简
@Override  
public Connection getConnection() throws SQLException {  
   return determineTargetDataSource().getConnection();  
}
protected DataSource determineTargetDataSource() {   
  Object lookupKey = determineCurrentLookupKey(); 
  
  DataSource dataSource = this.resolvedDataSources.get(lookupKey);  
  
  return dataSource;  
}

通过源码可知,AbstractRoutingDataSource其实是个不干活的,其内部维护了一个DataSource的Map,当外部调用如getConnection时,其会根据一个Key从自身Map中获取一个数据源,然后把活都交给它干。
此时有两个关键问题:
1、Map中的数据源从何而来?
答案是我们给它。我们在配置AbstractRoutingDataSource时,需要将真正的数据源(多个)put进Map中
2、我们如何告诉Spring,何时该用何Key呢?
答案在 determineCurrentLookupKey()方法。这是一个抽象方法,所以必须由子类实现,此方法会返回查询数据源的Key。当然,要和当初put时key保持一致,不然永远也拿不到DataSource。

到目前为止,我们大致清楚了Spring支持动态数据源切换的实现原理:
1、使用AbstractRoutingDataSource数据源,在其内部通过Map维护我们要使用的多个真正数据源
2、实现动态返回Key的逻辑

实践

配置AbstractRoutingDataSource和相关对象

要使用AbstractRoutingDataSource,我们需提供一个实现类,实现determineCurrentLookupKey()方法

public class MyDynamicDataSource extends AbstractRoutingDataSource {

@Override  
protected Object determineCurrentLookupKey() {  
 //动态返回Key逻辑
}

通过Configuration类,告诉Spring使用MyDynamicDataSource
首先需要禁用SpringBoot对DataSource的自动配置:@SpringBootApplication(exclude ={DataSourceAutoConfiguration.class}),然后配置自己的MyDynamicDataSource

//key1,key2,dataSource1,dataSource2 可以通过@Autowired或者其他方式注入
@Bean  
public DynamicDataSource dynamicDataSource() {  
    DynamicDataSource dataSource = new DynamicDataSource();  
    dataSource.setDefaultTargetDataSource(默认数据源);  
    Map dataSourceMap = new HashMap<>(2);   
    dataSourceMap.put(key1, dataSource1);  
    dataSourceMap.put(key2, dataSource2);  
    dataSource.setTargetDataSources(dataSourceMap);  
    return dataSource;  
}

还需要配置Mybatis框架对象。在SpringBoot中,Mybatis自动配置类为:

//重点:@AutoConfigureAfter

@AutoConfigureAfter({DataSourceAutoConfiguration.class}) 

public class MybatisAutoConfiguration {
}

我们禁用了DataSourceAutoConfiguration配置类,MybatisAutoConfiguration自然不会生效,Mybatis所需要的SqlSessionFactorySqlSessionTemplate等对象,都需要我们手动补齐了。

@Bean
public SqlSessionFactory(){
}
动态返回key实现

完成上面的配置后,此时项目已经能运行且能访问数据库。接下来的重点,就是如何正确实现 "动态返回key"的逻辑。
一开始我们说过,在Mapper的方法粒度,通过注解来控制数据源切换,是个比较好的方案。
常用的方案是利用 ThreadLocal来传递key,通过AOP拦截标记了注解的方法,并在调用之前完成key的正确设置。
不过由于Mybatis的实现,Mapper接口的实现类中并没有注解信息,这样就导致AOP失效。而如过拦截Mapper所有方法,又会达不到 注解标记的目的。
Mapper层方法注解拦截:Mybatis实现Mapper后,也会向Spring注册对象。我们可以在此时,利用Map记录所有标记了注解的方法,然后在AOP中拦截所有Mapper方法,然后判断是否命中Map,来实现设置key的目的。

//记录所有需要切换的方法
public class MultipleDataSourceAspect implements BeanPostProcessor {
    @Override  
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

        Field mapperInterfaceField = MapperFactoryBean.class.getDeclaredField("mapperInterface");  
        mapperInterfaceField.setAccessible(true);  
        //获取Mybatis代理的接口 - 遍历方法拿到带切换数据源的方法 - 塞到map中  
        Class mapperInterfaceClazz = (Class) mapperInterfaceField.get(bean);  
        DataSource classDataSource = (DataSource) mapperInterfaceClazz.getDeclaredAnnotation(DataSource.class);  
        Method[] daoMethods = mapperInterfaceClazz.getDeclaredMethods();
        //foreach daoMethods
        if(method 标记注解){
            map.put(Method,key1/key2)
        }
    }
    
    
    @Around
    public Object changeDataSource(ProceedingJoinPoint pjp, Object obj) throws Throwable{
        if( pjp.getSignature().getMethod 在Map中){
            //切换ThreadLocal中的标记值
        }
    } 
}

你可能感兴趣的:(基于SpringBoot1.5&Mybatis实现DAO层多数据源切换)