基於SpringBoot1.5&Mybatis實現DAO層多數據源切換

背景

多數據源是一個比較廣泛的需求,如讀寫庫分離、數據散落在多個庫等場景中,項目必須具有多數據源訪問能力。本文基於工做中一次實現略做探討,分爲:實現原理、實踐兩大部分。java

目前廣泛的JavaWeb項目爲Controller-Service-Dao(Mapper)三層結構,最理想的方案是能在DAO(Mapper)層方法粒度實現數據源自動切換,這樣能最大限度下降耦合,讓service層及以上無感知。並且爲了支持動態擴展,使用註解標記是一個比較好的方案。sql

實現原理

咱們知道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也是在這個節點,提供動態切換數據源的能力:AbstractRoutingDataSourceapp

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中獲取一個數據源,而後把活都交給它幹。
此時有兩個關鍵問題:
一、Map中的數據源從何而來?
答案是咱們給它。咱們在配置AbstractRoutingDataSource時,須要將真正的數據源(多個)put進Map中
二、咱們如何告訴Spring,什麼時候該用何Key呢?
答案在 determineCurrentLookupKey()方法。這是一個抽象方法,因此必須由子類實現,此方法會返回查詢數據源的Key。固然,要和當初put時key保持一致,否則永遠也拿不到DataSource。ide

到目前爲止,咱們大體清楚了Spring支持動態數據源切換的實現原理:
一、使用AbstractRoutingDataSource數據源,在其內部經過Map維護咱們要使用的多個真正數據源
二、實現動態返回Key的邏輯post

實踐

配置AbstractRoutingDataSource和相關對象

要使用AbstractRoutingDataSource,咱們需提供一個實現類,實現determineCurrentLookupKey()方法ui

public class MyDynamicDataSource extends AbstractRoutingDataSource {

@Override  
protected Object determineCurrentLookupKey() {  
 //動態返回Key邏輯
}

經過Configuration類,告訴Spring使用MyDynamicDataSource
首先須要禁用SpringBoot對DataSource的自動配置:@SpringBootApplication(exclude ={DataSourceAutoConfiguration.class}),而後配置本身的MyDynamicDataSourcethis

//key1,key2,dataSource1,dataSource2 能夠經過@Autowired或者其餘方式注入
@Bean  
public DynamicDataSource dynamicDataSource() {  
    DynamicDataSource dataSource = new DynamicDataSource();  
    dataSource.setDefaultTargetDataSource(默認數據源);  
    Map<Object, Object> 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中的標記值
        }
    } 
}
相關文章
相關標籤/搜索