Spring boot多數(shù)據(jù)源實現(xiàn)動態(tài)切換

概述

日常的業(yè)務開發(fā)項目中只會配置一套數(shù)據(jù)源店量,如果需要獲取其他系統(tǒng)的數(shù)據(jù)往往是通過調(diào)用接口公壤, 或者是通過第三方工具比如kettle將數(shù)據(jù)同步到自己的數(shù)據(jù)庫中進行訪問堤舒。

但是也會有需要在項目中引用多數(shù)據(jù)源的場景踢京。比如如下場景:

  • 自研數(shù)據(jù)遷移系統(tǒng)誉碴,至少需要新、老兩套數(shù)據(jù)源瓣距,從老庫讀取數(shù)據(jù)寫入新庫
  • 自研讀寫分離中間件黔帕,系統(tǒng)流量增加,單庫響應效率降低蹈丸,引入讀寫分離方案成黄,寫入數(shù)據(jù)是一個數(shù)據(jù)源,讀取數(shù)據(jù)是另一個數(shù)據(jù)源

環(huán)境說明

  • spring boot
  • mysql
  • mybatis-plus
  • spring-aop

項目目錄結(jié)構(gòu)

  • controller: 存放接口類
  • service: 存放服務類
  • mapper: 存放操作數(shù)據(jù)庫的mapper接口
  • entity: 存放數(shù)據(jù)庫表實體類
  • vo: 存放返回給前端的視圖類

  • context: 存放持有當前線程數(shù)據(jù)源key類
  • constants: 存放定義數(shù)據(jù)源key常量類
  • config: 存放數(shù)據(jù)源配置類
  • annotation: 存放動態(tài)數(shù)據(jù)源注解
  • aop: 存放動態(tài)數(shù)據(jù)源注解切面類
  • resources.config: 項目配置文件
  • resources.mapper: 數(shù)據(jù)庫xml文件

關鍵類說明

忽略掉controller/service/entity/mapper/xml介紹逻杖。

  • jdbc.properties: 數(shù)據(jù)源配置文件奋岁。雖然可以配置到Spring boot的默認配置文件application.properties/application.yml文件當中,但是如果數(shù)據(jù)源比較多的話荸百,根據(jù)實際使用闻伶,最佳的配置方式還是獨立配置比較好。
  • DynamicDataSourceConfig: 數(shù)據(jù)源配置類
  • DynamicDataSource: 動態(tài)數(shù)據(jù)源配置類
  • DataSourceRouting: 動態(tài)數(shù)據(jù)源注解
  • DynamicDataSourceAspect: 動態(tài)數(shù)據(jù)源設置切面
  • DynamicDataSourceContextHolder: 當前線程持有的數(shù)據(jù)源key
  • DataSourceConstants: 數(shù)據(jù)源key常量類

開發(fā)流程

動態(tài)數(shù)據(jù)源流程

Spring Boot 的動態(tài)數(shù)據(jù)源管搪,本質(zhì)上是把多個數(shù)據(jù)源存儲在一個 Map 中虾攻,當需要使用某個數(shù)據(jù)源時,從 Map 中獲取此數(shù)據(jù)源進行處理更鲁。

在 Spring 中已提供了抽象類 AbstractRoutingDataSource 來實現(xiàn)此功能霎箍,繼承AbstractRoutingDataSource類并覆寫其determineCurrentLookupKey()方法即可,該方法只需要返回數(shù)據(jù)源key即可澡为,也就是存放數(shù)據(jù)源的Map的key漂坏。

因此,我們在實現(xiàn)動態(tài)數(shù)據(jù)源的,只需要繼承它顶别,實現(xiàn)自己的獲取數(shù)據(jù)源邏輯即可谷徙。AbstractRoutingDataSource頂級繼承了DataSource,所以它也是可以做為數(shù)據(jù)源對象驯绎,因此項目中使用它作為主數(shù)據(jù)源完慧。

AbstractRoutingDataSource原理

AbstractRoutingDataSource中有一個重要的屬性:

  • targetDataSources: 目標數(shù)據(jù)源,即項目啟動的時候設置的需要通過AbstractRoutingDataSource管理的數(shù)據(jù)源剩失。
  • defaultTargetDataSource: 默認數(shù)據(jù)源屈尼,項目啟動的時候設置的默認數(shù)據(jù)源,如果沒有指定數(shù)據(jù)源拴孤,默認返回改數(shù)據(jù)源脾歧。
  • resolvedDataSources: 也是存放的數(shù)據(jù)源,是對targetDataSources進行處理后進行存儲的演熟”拗矗可以看一下源碼。
  • resolvedDefaultDataSource: 對默認數(shù)據(jù)源進行了二次處理芒粹,源碼如上圖最后的兩行代碼兄纺。

AbstractRoutingDataSource中所有的方法和屬性:

比較重要的是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;
}

/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Nullable
protected abstract Object determineCurrentLookupKey();

這個方法主要就是返回一個DataSource對象是辕,主要邏輯就是先通過方法determineCurrentLookupKey獲取一個Object對象的lookupKey囤热,然后通過這個lookupKey到resolvedDataSources中獲取數(shù)據(jù)源(resolvedDataSources就是一個Map猎提,上面已經(jīng)提到過了)获三;如果沒有找到數(shù)據(jù)源,就返回默認的數(shù)據(jù)源锨苏。determineCurrentLookupKey就是程序員配置動態(tài)數(shù)據(jù)源需要自己實現(xiàn)的方法疙教。


問題

配置多數(shù)據(jù)源后啟動項目報錯:Property 'sqlSessionFactory' or 'sqlSession Template' are required。

翻譯過來就是:需要屬性“sqlSessionFactory”或“sqlSessionTemplate”伞租。也就是說 注入數(shù)據(jù)源的時候需要這兩個數(shù)據(jù)贞谓,但是這兩個屬性在啟動容器中沒有找到。

當引入mybatis-plus依賴mybatis-plus-boot-starter后葵诈,會添加一個自動配置類 MybatisPlusAutoConfiguration 裸弦。 其中有兩個方法sqlSessionFactory()sqlSessionTemplate()作喘。這兩個方法就是 給容器中注入“sqlSessionFactory”或“sqlSessionTemplate”兩個屬性理疙。


@Configuration(
        proxyBeanMethods = false
)
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }

        this.applyConfiguration(factory);
        if (this.properties.getConfigurationProperties() != null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }

        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }

        if (this.databaseIdProvider != null) {
            factory.setDatabaseIdProvider(this.databaseIdProvider);
        }

        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }

        if (this.properties.getTypeAliasesSuperType() != null) {
            factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
        }

        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }

        if (!ObjectUtils.isEmpty(this.typeHandlers)) {
            factory.setTypeHandlers(this.typeHandlers);
        }

        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            factory.setMapperLocations(this.properties.resolveMapperLocations());
        }

        Objects.requireNonNull(factory);
        this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory);
        Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
        if (!ObjectUtils.isEmpty(this.languageDrivers)) {
            factory.setScriptingLanguageDrivers(this.languageDrivers);
        }

        Optional var10000 = Optional.ofNullable(defaultLanguageDriver);
        Objects.requireNonNull(factory);
        var10000.ifPresent(factory::setDefaultScriptingLanguageDriver);
        this.applySqlSessionFactoryBeanCustomizers(factory);
        GlobalConfig globalConfig = this.properties.getGlobalConfig();
        Objects.requireNonNull(globalConfig);
        this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
        this.getBeansThen(IKeyGenerator.class, (i) -> {
            globalConfig.getDbConfig().setKeyGenerators(i);
        });
        Objects.requireNonNull(globalConfig);
        this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
        Objects.requireNonNull(globalConfig);
        this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
        factory.setGlobalConfig(globalConfig);
        return factory.getObject();
    }

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
    }
}    

這里主要關注配置類上面的注解,詳細如下:

@Configuration(
        proxyBeanMethods = false
)
// 當類路徑下有SqlSessionFactory.class泞坦、SqlSessionFactoryBean.class時才生效
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
// 容器中只能有一個符合條件的DataSource
// 因為容器中有3個數(shù)據(jù)源窖贤,且沒有指定主數(shù)據(jù)源,這個條件不通過,就不會初始化這個配置類了
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {

}

因為MybatisPlusAutoConfiguration不滿足@ConditionalOnSingleCandidate(DataSource.class) 條件赃梧,所以自動配置不會生效滤蝠,就不會執(zhí)行sqlSessionFactory()sqlSessionTemplate()兩個方法授嘀。 容器中就不會有sqlSessionFactory物咳、sqlSessionTemplate 兩個bean對象,因此會報錯蹄皱。

所以在數(shù)據(jù)源配置類中的動態(tài)數(shù)據(jù)源配置上添加@Primary注解即可所森。

@Configuration
@PropertySource("classpath:config/jdbc.properties")
@MapperScan(basePackages = {"com.xinxing.learning.datasource.mapper"})
public class DynamicDataSourceConfig {
    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());
        targetDataSource.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());

        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSource);
        dataSource.setDefaultTargetDataSource(masterDataSource());

        return dataSource;
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市夯接,隨后出現(xiàn)的幾起案子焕济,更是在濱河造成了極大的恐慌,老刑警劉巖盔几,帶你破解...
    沈念sama閱讀 219,188評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晴弃,死亡現(xiàn)場離奇詭異,居然都是意外死亡逊拍,警方通過查閱死者的電腦和手機上鞠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芯丧,“玉大人芍阎,你說我怎么就攤上這事∮Ш悖” “怎么了谴咸?”我有些...
    開封第一講書人閱讀 165,562評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長骗露。 經(jīng)常有香客問我岭佳,道長,這世上最難降的妖魔是什么萧锉? 我笑而不...
    開封第一講書人閱讀 58,893評論 1 295
  • 正文 為了忘掉前任珊随,我火速辦了婚禮,結(jié)果婚禮上柿隙,老公的妹妹穿的比我還像新娘叶洞。我一直安慰自己,他們只是感情好禀崖,可當我...
    茶點故事閱讀 67,917評論 6 392
  • 文/花漫 我一把揭開白布衩辟。 她就那樣靜靜地躺著,像睡著了一般帆焕。 火紅的嫁衣襯著肌膚如雪惭婿。 梳的紋絲不亂的頭發(fā)上不恭,一...
    開封第一講書人閱讀 51,708評論 1 305
  • 那天,我揣著相機與錄音财饥,去河邊找鬼换吧。 笑死,一個胖子當著我的面吹牛钥星,可吹牛的內(nèi)容都是我干的沾瓦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼谦炒,長吁一口氣:“原來是場噩夢啊……” “哼贯莺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起宁改,我...
    開封第一講書人閱讀 39,342評論 0 276
  • 序言:老撾萬榮一對情侶失蹤缕探,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后还蹲,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爹耗,經(jīng)...
    沈念sama閱讀 45,801評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,976評論 3 337
  • 正文 我和宋清朗相戀三年谜喊,在試婚紗的時候發(fā)現(xiàn)自己被綠了潭兽。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,115評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡斗遏,死狀恐怖山卦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情诵次,我是刑警寧澤账蓉,帶...
    沈念sama閱讀 35,804評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站藻懒,受9級特大地震影響剔猿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嬉荆,卻給世界環(huán)境...
    茶點故事閱讀 41,458評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酷含。 院中可真熱鬧鄙早,春花似錦、人聲如沸椅亚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呀舔。三九已至弥虐,卻和暖如春扩灯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背霜瘪。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評論 1 272
  • 我被黑心中介騙來泰國打工珠插, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人颖对。 一個月前我還...
    沈念sama閱讀 48,365評論 3 373
  • 正文 我出身青樓捻撑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親缤底。 傳聞我的和親對象是個殘疾皇子顾患,可洞房花燭夜當晚...
    茶點故事閱讀 45,055評論 2 355

推薦閱讀更多精彩內(nèi)容