多數據源是一個比較廣泛的需求,如讀寫庫分離、數據散落在多個庫等場景中,項目必須具有多數據源訪問能力。本文基於工做中一次實現略做探討,分爲:實現原理、實踐兩大部分。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也是在這個節點,提供動態切換數據源的能力:AbstractRoutingDataSource
。app
AbstractRoutingDataSource
實現了javax.sql.DataSource
接口,也就是說AbstractRoutingDataSource
自己就是一個數據源,但爲什麼其具備動態切換功能。而咱們常見的如DruidDataSource
、HikariDataSource
數據源則不具有呢?咱們能夠看下其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,咱們需提供一個實現類,實現determineCurrentLookupKey()方法ui
public class MyDynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { //動態返回Key邏輯 }
經過Configuration
類,告訴Spring使用MyDynamicDataSource
首先須要禁用SpringBoot對DataSource的自動配置:@SpringBootApplication(exclude ={DataSourceAutoConfiguration.class})
,而後配置本身的MyDynamicDataSource
:this
//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所須要的SqlSessionFactory
、SqlSessionTemplate
等對象,都須要咱們手動補齊了。
@Bean public SqlSessionFactory(){ }
完成上面的配置後,此時項目已經能運行且能訪問數據庫。接下來的重點,就是如何正確實現 "動態返回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中的標記值 } } }