SSM(八)動態切換資料來源

crossoverJie發表於2017-01-04

前言

在現在開發的過程中應該大多數朋友都有遇到過切換資料來源的需求。比如現在常用的資料庫讀寫分離,或者就是有兩個資料庫的情況,這些都需要用到切換資料來源。

手動切換資料來源

使用SpringAbstractRoutingDataSource類來進行擴充多資料來源。

該類就相當於一個dataSource的路由,用於根據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則是關鍵方法。該方法用於返回我們使用的資料來源。

其中呢又是determineCurrentLookupKey()方法來返回當前資料來源的key值。
之後通過該key值在resolvedDataSources這個map中找到對應的value(該value就是資料來源)。

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返回了一個資料來源。

DataSourceHolder程式碼如下:

package com.crossoverJie.util;

/**
 * Function:動態資料來源
 *
 * @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來儲存了資料來源,關於ThreadLocal的知識點可以檢視以下這篇文章:
解密ThreadLocal

之後在Spring的配置檔案中配置我們的資料來源,就是上文講到的為targetDataSources賦值

<bean id="ssm1DataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <!-- 指定連線資料庫的驅動 -->
        <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" />
        <!-- 配置監控統計攔截的filters,去掉後監控介面sql無法統計 -->
        <property name="filters" value="stat" />
    </bean>

    <bean id="ssm2DataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <!-- 指定連線資料庫的驅動 -->
        <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"/>
        <!-- 配置監控統計攔截的filters,去掉後監控介面sql無法統計 -->
        <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>
        <!--預設資料來源-->
        <property name="defaultTargetDataSource" ref="ssm1DataSource"/>
    </bean>複製程式碼

這裡分別配置了兩個資料來源:ssm1DataSourcessm2DataSource
之後再通過Spring的依賴注入方式將兩個資料來源設定進targetDataSources

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

就是在每次呼叫資料庫之前我們都要先通過DataSourceHolder來設定當前的資料來源。看下demo:

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

詳見我的單測。

使用起來也是非常簡單。但是不知道大家注意到沒有,這樣的做法槽點很多:

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

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

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

自動切換資料來源

首先要定義好我們的切面類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();

        //獲取包名用於區分不同資料來源
        String whichDataSource = aClass.getName().substring(25, aClass.getName().lastIndexOf("."));
        if ("ssmone".equals(whichDataSource)) {
            DataSourceHolder.setDataSources(Constants.DATASOURCE_ONE);
        } else {
            DataSourceHolder.setDataSources(Constants.DATASOURCE_TWO);
        }

    }


    /**
     * 執行後將資料來源置為空
     */
    public void after() {
        DataSourceHolder.setDataSources(null);
    }

}複製程式碼

邏輯也比較簡單,就是在執行資料庫操作之前做一個切面。

  • 通過JoinPoint物件獲取目標物件。
  • 在目標物件中獲取包名來區分不同的資料來源。
  • 根據不同資料來源來進行賦值。
  • 執行完畢之後將資料來源清空。

關於一些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();       //返回被通知方法引數列表
    Signature getSignature();  //返回當前連線點簽名
    SourceLocation getSourceLocation();//返回連線點方法所在類檔案中的位置
    String getKind();        //連線點型別
    StaticPart getStaticPart(); //返回連線點靜態部分
}複製程式碼

為了通過包名來區分不同資料來源,我將目錄結構稍微調整了下:

SSM(八)動態切換資料來源
2

將兩個不同的資料來源的實現類放到不同的包中,這樣今後如果還需要新增其他資料來源也可以靈活的切換。

看下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"/>

        <!--所有資料庫操作的方法加入切面-->
        <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快取的基礎上進行修改的。
這樣快取和多資料來源都滿足了。

實際使用:

    @Test
    public void selectByPrimaryKey() throws Exception {
        Rediscontent rediscontent = rediscontentService.selectByPrimaryKey(30);
        System.out.println(JSON.toJSONString(rediscontent));
    }複製程式碼

SSM(八)動態切換資料來源
3

這樣看起來就和使用一個資料來源這樣簡單,再也不用關心切換的問題了。

總結

不過按照這樣的寫法是無法做到在一個事務裡控制兩個資料來源的。這個我還在學習中,有相關經驗的大牛不妨指點一下。

專案地址:github.com/crossoverJi…

個人部落格地址:crossoverjie.top

GitHub地址:github.com/crossoverJi…

相關文章