SSM(八)動態(tài)切換數(shù)據(jù)源

0

前言

在現(xiàn)在開發(fā)的過程中應該大多數(shù)朋友都有遇到過切換數(shù)據(jù)源的需求脾还。比如現(xiàn)在常用的數(shù)據(jù)庫讀寫分離,或者就是有兩個數(shù)據(jù)庫的情況入愧,這些都需要用到切換數(shù)據(jù)源鄙漏。

手動切換數(shù)據(jù)源

使用SpringAbstractRoutingDataSource類來進行拓展多數(shù)據(jù)源。

該類就相當于一個dataSource的路由棺蛛,用于根據(jù)key值來進行切換對應的dataSource怔蚌。

下面簡單來看下AbstractRoutingDataSource類的幾段關鍵源碼:

    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }

    /**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
    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.
     */
    protected abstract Object determineCurrentLookupKey();

可以看到其中獲取鏈接的方法getConnection()調用的determineTargetDataSource則是關鍵方法。該方法用于返回我們使用的數(shù)據(jù)源旁赊。

其中呢又是determineCurrentLookupKey()方法來返回當前數(shù)據(jù)源的key值媚创。
之后通過該key值在resolvedDataSources這個map中找到對應的value(該value就是數(shù)據(jù)源)。

resolvedDataSources這個map則是在:

    @Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
        for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
            Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
            DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
            this.resolvedDataSources.put(lookupKey, dataSource);
        }
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }

這個方法通過targetDataSources這個map來進行賦值的彤恶。targetDataSources則是我們在配置文件中進行賦值的钞钙,下面會講到。

再來看看determineCurrentLookupKey()方法声离,從protected來修飾就可以看出是需要我們來進行重寫的芒炼。

DynamicDataSource 和 DataSourceHolder

于是我新增了DynamicDataSource類,代碼如下:

package com.crossoverJie.util;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * Function:
 *
 * @author chenjiec
 *         Date: 2017/1/2 上午12:22
 * @since JDK 1.7
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getDataSources();
    }
}

代碼很簡單术徊,繼承了AbstractRoutingDataSource類并重寫了其中的determineCurrentLookupKey()方法本刽。

這里直接用DataSourceHolder返回了一個數(shù)據(jù)源。

DataSourceHolder代碼如下:

package com.crossoverJie.util;

/**
 * Function:動態(tài)數(shù)據(jù)源
 *
 * @author chenjiec
 *         Date: 2017/1/2 上午12:19
 * @since JDK 1.7
 */
public class DataSourceHolder {
    private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();

    public static void setDataSources(String dataSource) {
        dataSources.set(dataSource);
    }

    public static String getDataSources() {
        return dataSources.get();
    }
}

這里我使用了ThreadLocal來保存了數(shù)據(jù)源赠涮,關于ThreadLocal的知識點可以查看以下這篇文章:
解密ThreadLocal

之后在Spring的配置文件中配置我們的數(shù)據(jù)源子寓,就是上文講到的為targetDataSources賦值

<bean id="ssm1DataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <!-- 指定連接數(shù)據(jù)庫的驅動 -->
        <property name="driverClassName" value="${jdbc.driverClass}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.user}" />
        <property name="password" value="${jdbc.password}" />
        <!-- 配置初始化大小、最小笋除、最大 -->
        <property name="initialSize" value="3" />
        <property name="minIdle" value="3" />
        <property name="maxActive" value="20" />
        <!-- 配置獲取連接等待超時的時間 -->
        <property name="maxWait" value="60000" />
        <!-- 配置間隔多久才進行一次檢測斜友,檢測需要關閉的空閑連接,單位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <!-- 配置一個連接在池中最小生存的時間垃它,單位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <property name="validationQuery" value="SELECT 'x'" />
        <property name="testWhileIdle" value="true" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <!-- 打開PSCache鲜屏,并且指定每個連接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
                  value="20" />
        <!-- 配置監(jiān)控統(tǒng)計攔截的filters,去掉后監(jiān)控界面sql無法統(tǒng)計 -->
        <property name="filters" value="stat" />
    </bean>

    <bean id="ssm2DataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <!-- 指定連接數(shù)據(jù)庫的驅動 -->
        <property name="driverClassName" value="${jdbc.driverClass}"/>
        <property name="url" value="${jdbc.url2}"/>
        <property name="username" value="${jdbc.user2}"/>
        <property name="password" value="${jdbc.password2}"/>
        <property name="initialSize" value="3"/>
        <property name="minIdle" value="3"/>
        <property name="maxActive" value="20"/>
        <!-- 配置獲取連接等待超時的時間 -->
        <property name="maxWait" value="60000"/>
        <!-- 配置間隔多久才進行一次檢測国拇,檢測需要關閉的空閑連接洛史,單位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000"/>
        <property name="validationQuery" value="SELECT 'x'"/>
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>
        <!-- 打開PSCache酱吝,并且指定每個連接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="true"/>
        <property name="maxPoolPreparedStatementPerConnectionSize"
                  value="20"/>
        <!-- 配置監(jiān)控統(tǒng)計攔截的filters也殖,去掉后監(jiān)控界面sql無法統(tǒng)計 -->
        <property name="filters" value="stat"/>
    </bean>
    <bean id="dataSource" class="com.crossoverJie.util.DynamicDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry key="ssm1DataSource" value-ref="ssm1DataSource"/>
                <entry key="ssm2DataSource" value-ref="ssm2DataSource"/>
            </map>
        </property>
        <!--默認數(shù)據(jù)源-->
        <property name="defaultTargetDataSource" ref="ssm1DataSource"/>
    </bean>

這里分別配置了兩個數(shù)據(jù)源:ssm1DataSourcessm2DataSource
之后再通過Spring的依賴注入方式將兩個數(shù)據(jù)源設置進targetDataSources务热。

接下來的用法相比大家也應該猜到了忆嗜。

就是在每次調用數(shù)據(jù)庫之前我們都要先通過DataSourceHolder來設置當前的數(shù)據(jù)源浪漠。看下demo:

    @Test
    public void selectByPrimaryKey() throws Exception {
        DataSourceHolder.setDataSources(Constants.DATASOURCE_TWO);
        Datasource datasource = dataSourceService.selectByPrimaryKey(7);
        System.out.println(JSON.toJSONString(datasource));
    }

詳見我的單測霎褐。

使用起來也是非常簡單。但是不知道大家注意到?jīng)]有该镣,這樣的做法槽點很多:

  1. 每次使用需要手動切換冻璃,總有一些人會忘記寫(比如我)。
  2. 如果是后期需求變了损合,查詢其他的表了還得一個個改回來省艳。

那有沒有什么方法可以自動的幫我們切換呢?

肯定是有的嫁审,大家應該也想得到跋炕。就是利用SpringAOP了。

自動切換數(shù)據(jù)源

首先要定義好我們的切面類DataSourceExchange:

package com.crossoverJie.util;

import org.aspectj.lang.JoinPoint;

/**
 * Function:攔截器方法
 *
 * @author chenjiec
 *         Date: 2017/1/3 上午12:34
 * @since JDK 1.7
 */
public class DataSourceExchange {
    
    /**
     *
     * @param point
     */
    public void before(JoinPoint point) {

        //獲取目標對象的類類型
        Class<?> aClass = point.getTarget().getClass();

        //獲取包名用于區(qū)分不同數(shù)據(jù)源
        String whichDataSource = aClass.getName().substring(25, aClass.getName().lastIndexOf("."));
        if ("ssmone".equals(whichDataSource)) {
            DataSourceHolder.setDataSources(Constants.DATASOURCE_ONE);
        } else {
            DataSourceHolder.setDataSources(Constants.DATASOURCE_TWO);
        }

    }


    /**
     * 執(zhí)行后將數(shù)據(jù)源置為空
     */
    public void after() {
        DataSourceHolder.setDataSources(null);
    }

}

邏輯也比較簡單律适,就是在執(zhí)行數(shù)據(jù)庫操作之前做一個切面辐烂。

  • 通過JoinPoint對象獲取目標對象。
  • 在目標對象中獲取包名來區(qū)分不同的數(shù)據(jù)源捂贿。
  • 根據(jù)不同數(shù)據(jù)源來進行賦值纠修。
  • 執(zhí)行完畢之后將數(shù)據(jù)源清空。

關于一些JoinPoint的API:

package org.aspectj.lang;
import org.aspectj.lang.reflect.SourceLocation;
public interface JoinPoint {
    String toString();         //連接點所在位置的相關信息
    String toShortString();     //連接點所在位置的簡短相關信息
    String toLongString();     //連接點所在位置的全部相關信息
    Object getThis();         //返回AOP代理對象
    Object getTarget();       //返回目標對象
    Object[] getArgs();       //返回被通知方法參數(shù)列表
    Signature getSignature();  //返回當前連接點簽名
    SourceLocation getSourceLocation();//返回連接點方法所在類文件中的位置
    String getKind();        //連接點類型
    StaticPart getStaticPart(); //返回連接點靜態(tài)部分
}

為了通過包名來區(qū)分不同數(shù)據(jù)源厂僧,我將目錄結構稍微調整了下:

2

將兩個不同的數(shù)據(jù)源的實現(xiàn)類放到不同的包中扣草,這樣今后如果還需要新增其他數(shù)據(jù)源也可以靈活的切換。

看下Spring的配置:

    <bean id="dataSourceExchange" class="com.crossoverJie.util.DataSourceExchange"/>
    <!--配置切面攔截方法 -->
    <aop:config proxy-target-class="false">
        <!--將com.crossoverJie.service包下的所有select開頭的方法加入攔截
        去掉select則加入所有方法
        -->
        <aop:pointcut id="controllerMethodPointcut" expression="
        execution(* com.crossoverJie.service.*.select*(..))"/>

        <aop:pointcut id="selectMethodPointcut" expression="
        execution(* com.crossoverJie.dao..*Mapper.select*(..))"/>

        <aop:advisor advice-ref="methodCacheInterceptor" pointcut-ref="controllerMethodPointcut"/>

        <!--所有數(shù)據(jù)庫操作的方法加入切面-->
        <aop:aspect ref="dataSourceExchange">
            <aop:pointcut id="dataSourcePointcut" expression="execution(* com.crossoverJie.service.*.*(..))"/>
            <aop:before pointcut-ref="dataSourcePointcut" method="before"/>
            <aop:after pointcut-ref="dataSourcePointcut" method="after"/>
        </aop:aspect>
    </aop:config>

這是在我們上一篇整合redis緩存的基礎上進行修改的颜屠。
這樣緩存和多數(shù)據(jù)源都滿足了辰妙。

實際使用:

    @Test
    public void selectByPrimaryKey() throws Exception {
        Rediscontent rediscontent = rediscontentService.selectByPrimaryKey(30);
        System.out.println(JSON.toJSONString(rediscontent));
    }
3

這樣看起來就和使用一個數(shù)據(jù)源這樣簡單,再也不用關心切換的問題了甫窟。

總結

不過按照這樣的寫法是無法做到在一個事務里控制兩個數(shù)據(jù)源的密浑。這個我還在學習中,有相關經(jīng)驗的大牛不妨指點一下粗井。

項目地址:https://github.com/crossoverJie/SSM.git

個人博客地址:http://crossoverjie.top肴掷。

GitHub地址:https://github.com/crossoverJie

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末背传,一起剝皮案震驚了整個濱河市呆瞻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌径玖,老刑警劉巖痴脾,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異梳星,居然都是意外死亡赞赖,警方通過查閱死者的電腦和手機滚朵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來前域,“玉大人辕近,你說我怎么就攤上這事∧渎ⅲ” “怎么了移宅?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長椿疗。 經(jīng)常有香客問我漏峰,道長,這世上最難降的妖魔是什么届榄? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任浅乔,我火速辦了婚禮,結果婚禮上铝条,老公的妹妹穿的比我還像新娘靖苇。我一直安慰自己,他們只是感情好班缰,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布顾复。 她就那樣靜靜地躺著,像睡著了一般鲁捏。 火紅的嫁衣襯著肌膚如雪芯砸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天给梅,我揣著相機與錄音假丧,去河邊找鬼。 笑死动羽,一個胖子當著我的面吹牛包帚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播运吓,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼渴邦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拘哨?” 一聲冷哼從身側響起谋梭,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎倦青,沒想到半個月后瓮床,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年隘庄,在試婚紗的時候發(fā)現(xiàn)自己被綠了踢步。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡丑掺,死狀恐怖获印,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情街州,我是刑警寧澤兼丰,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站菇肃,受9級特大地震影響,放射性物質發(fā)生泄漏取募。R本人自食惡果不足惜琐谤,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望玩敏。 院中可真熱鬧斗忌,春花似錦、人聲如沸旺聚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽砰粹。三九已至唧躲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間碱璃,已是汗流浹背弄痹。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嵌器,地道東北人肛真。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像爽航,于是被迫代替她去往敵國和親蚓让。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

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