1. 程式人生 > >AbstractRoutingDataSource動態資料來源切換,AOP實現動態資料來源切換

AbstractRoutingDataSource動態資料來源切換,AOP實現動態資料來源切換

AbstractRoutingDataSource動態資料來源切換

上週末,室友通宵達旦的敲程式碼處理他的多資料來源的問題,搞的非常的緊張,也和我聊了聊天,大概的瞭解了他的業務的需求。一般的情況下我們都是使用SSH或者SSM框架進行處理我們的資料來源的資訊。
操作資料一般都是在DAO層進行處理,可以選擇直接使用JDBC進行程式設計(http://blog.csdn.net/yanzi1225627/article/details/26950615/
或者是使用多個DataSource 然後建立多個SessionFactory,在使用Dao層的時候通過不同的SessionFactory進行處理,不過這樣的入侵性比較明顯,一般的情況下我們都是使用繼承HibernateSupportDao進行封裝了的處理,如果多個SessionFactory這樣處理就是比較的麻煩了,修改的地方估計也是蠻多的
最後一個,也就是使用AbstractRoutingDataSource的實現類通過AOP或者手動處理實現動態的使用我們的資料來源,這樣的入侵性較低,非常好的滿足使用的需求。比如我們希望對於讀寫分離或者其他的資料同步的業務場景

  • 下面看看圖片
    這裡寫圖片描述

  • 單資料來源的場景(一般的Web專案工程這樣配置進行處理,就已經比較能夠滿足我們的業務需求)

  • 多資料來源多SessionFactory這樣的場景,估計作為剛剛開始想象想處理在使用框架的情況下處理業務,配置多個SessionFactory,然後在Dao層中對於特定的請求,通過特定的SessionFactory即可處理實現這樣的業務需求,不過這樣的處理帶來了很多的不便之處,所有很多情況下我們寧願直接使用封裝的JDBC程式設計,或者使用Mybatis處理這樣的業務場景
  • 使用AbstractRoutingDataSource 的實現類,進行靈活的切換,可以通過AOP或者手動程式設計設定當前的DataSource,不用修改我們編寫的對於繼承HibernateSupportDao的實現類的修改,這樣的編寫方式比較好,至於其中的實現原理,讓我細細到來。我們想看看如何去應用,實現原理慢慢的說!

  • 編寫AbstractRoutingDataSource的實現類,HandlerDataSource就是提供給我們動態選擇資料來源的資料的資訊,我們這裡編寫一個根據當前執行緒來選擇資料來源,然後通過AOP攔截特定的註解,設定當前的資料來源資訊,也可以手動的設定當前的資料來源,在程式設計的類中。

package com.common.utils.manydatasource;

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

/**
 * descrption: 多資料來源的選擇
 * authohr: wangji
 * date: 2017-08-21 10:32
 */
public class MultipleDataSourceToChoose extends AbstractRoutingDataSource { /** * @desction: 根據Key獲取資料來源的資訊,上層抽象函式的鉤子 * @author: wangji * @date: 2017/8/21 * @param: * @return: */ @Override protected Object determineCurrentLookupKey() { return HandlerDataSource.getDataSource(); } }
  • 設定動態選擇的Datasource,這裡的Set方法可以留給AOP呼叫,或者留給我們的具體的Dao層或者Service層中手動呼叫,在執行SQL語句之前。
package com.common.utils.manydatasource;

/**
 * descrption: 根據當前執行緒來選擇具體的資料來源
 * authohr: wangji
 * date: 2017-08-21 10:36
 */
public class HandlerDataSource {

    private static ThreadLocal<String> handlerThredLocal = new ThreadLocal<String>();

    /**
     * @desction: 提供給AOP去設定當前的執行緒的資料來源的資訊
     * @author: wangji
     * @date: 2017/8/21
     * @param: [datasource]
     * @return: void
     */
    public static void putDataSource(String datasource) {
        handlerThredLocal.set(datasource);
    }

    /**
     * @desction: 提供給AbstractRoutingDataSource的實現類,通過key選擇資料來源
     * @author: wangji
     * @date: 2017/8/21
     * @param: []
     * @return: java.lang.String
     */
    public static String getDataSource() {
        return handlerThredLocal.get();
    }

    /**
     * @desction: 使用預設的資料來源
     */
    public static void clear() {
        handlerThredLocal.remove();
    }
}
  • 設定攔截資料來源的註解,可以設定在具體的類上,或者在具體的方法上,dataSource是當前資料來源的一個別名用於標識我們的資料來源的資訊。
package com.common.utils.manydatasource;

import java.lang.annotation.*;

/**
 * @description: 建立攔截設定資料來源的註解
 * Created by wangji on 2017/8/21.
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicSwitchDataSource {

    String dataSource() default "";
}
  • AOP攔截類的實現,通過攔截上面的註解,在其執行之前處理設定當前執行SQL的資料來源的資訊,HandlerDataSource.putDataSource(….),這裡的資料來源資訊從我們設定的註解上面獲取資訊,如果沒有設定就是用預設的資料來源的資訊。
package com.common.utils.manydatasource;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * descrption: 使用AOP攔截特定的註解去動態的切換資料來源
 * authohr: wangji
 * date: 2017-08-21 10:42
 */
@Aspect
@Slf4j
@Component
@Order(1)
public class HandlerDataSourceAop {
    //@within在類上設定
    //@annotation在方法上進行設定
    @Pointcut("@within(com.common.utils.manydatasource.DynamicSwitchDataSource)||@annotation(com.common.utils.manydatasource.DynamicSwitchDataSource)")
    public void pointcut() {}

    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint)
    {
        Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
        DynamicSwitchDataSource annotationClass = method.getAnnotation(DynamicSwitchDataSource.class);//獲取方法上的註解
        if(annotationClass == null){
            annotationClass = joinPoint.getTarget().getClass().getAnnotation(DynamicSwitchDataSource.class);//獲取類上面的註解
            if(annotationClass == null) return;
        }
        //獲取註解上的資料來源的值的資訊
        String dataSourceKey = annotationClass.dataSource();
        if(dataSourceKey !=null){
            //給當前的執行SQL的操作設定特殊的資料來源的資訊
            HandlerDataSource.putDataSource(dataSourceKey);
        }
        log.info("AOP動態切換資料來源,className"+joinPoint.getTarget().getClass().getName()+"methodName"+method.getName()+";dataSourceKey:"+dataSourceKey==""?"預設資料來源":dataSourceKey);
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        //清理掉當前設定的資料來源,讓預設的資料來源不受影響
        HandlerDataSource.clear();
    }

}
  • 配置資料來源在Spring 核心容器中配置
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/mybatis
jdbc.username=root
jdbc.password=root
jdbc2.url=jdbc:mysql://127.0.0.1:3306/datasource2

<!-- 配置資料來源 -->
    <bean id="dataSource0" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close" init-method="init">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="maxActive" value="10"/>
    </bean>
    <bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close" init-method="init">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc2.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="maxActive" value="10"/>
    </bean>
  • 配置之前我們實現的資料來源選擇的中間層AbstractRoutingDataSource的實現類,這裡的key就是資料來源資訊的別名,通過這個key可以選擇到資料來源的資訊。MultipleDataSourceToChoose就是上面寫的資料來源選擇器的實現類
bean id="dataSource" class="com.common.utils.manydatasource.MultipleDataSourceToChoose" lazy-init="true">
        <description>資料來源</description>
        <property name="targetDataSources">
            <map key-type="java.lang.String" value-type="javax.sql.DataSource">
                <entry key="datasource0" value-ref="dataSource0" />
                <entry key="datasource1" value-ref="dataSource1" />
            </map>
        </property>
        <!-- 設定預設的目標資料來源 -->
        <property name="defaultTargetDataSource" ref="dataSource0" />
    </bean>
  • SessionFactory的配置還是照舊,使用以前的配置,只不過當前選擇的資料來源是datasource,也就是資料來源選擇的中間層MultipleDataSourceToChoose,因為當前的中間層中實現了DataSource這個介面,所以可以看做為DataSource的是實現類啦,所以配置不會出現問題。
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!--指定Hibernate屬性 -->
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                <prop key="hibernate.show_sql">false</prop>
                <prop key="hibernate.format_sql">false</prop>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
                <prop key="hibernate.autoReconnect">true</prop>
                <prop key="hibernate.jdbc.batch_size">50</prop>
              <prop key="hibernate.connection.autocommit">false</prop>
              <prop key="hibernate.connection.release_mode">after_transaction</prop>
              <prop key="hibernate.bytecode.use_reflection_optimizer">false</prop>
            </props>
        </property>
        <property name="packagesToScan">
            <list>
                <value>com.module</value>
            </list>
        </property>
    </bean>
  • 簡單的使用AOP進行測試一下,這裡測試的結果時不同的,所以是生效的,使用了不同的資料來源,但是底層的實現沒有進行任何的修改處理。
@Service
@Slf4j
public class UserInfoService implements IUserInfoService {


    @Resource
    private UserDao userDao;
    @Autowired
    private CommonHibernateDao commonDao;

    @TestValidateParam
    public User getUserInfoById(Integer id) {
        return userDao.findById(id);
    }

    @DynamicSwitchDataSource(dataSource = "datasource0")
    public void save(User user) {
        userDao.save(user);
    }

    @DynamicSwitchDataSource(dataSource = "datasource1")
    public List<User> findAll(){
        String sql = "select u.userName as name,u.userAge as age,u.userAddress as address,u.id from user u";
        List<User> list =commonDao.findListBySQL(sql,User.class);
        return list;
    }

}
  • 也可以不適用AOP,直接在程式設計中實現,通過測試,結果分別為兩個資料庫中的資訊
  public void test(){
        HandlerDataSource.putDataSource("datasource1");
        String sql = "select u.userName as name,u.userAge as age,u.userAddress as address,u.id from user u";
        List<User> list =commonDao.findListBySQL(sql,User.class);

        HandlerDataSource.putDataSource("datasource0");
        commonDao.deleteById("2",User.class);
    }
  • 實現原理,MultipleDataSourceToChoose的繼承結構圖,之前說過他是DataSource的子類,由於無論我們是使用Mybatis還是使用Hibernate進行SQL操作的時候總會執行getConnection(),無論我們的資料來源是否使用了資料庫連線池,因為資料庫連線池的主要作用就是保持一堆的Connection不進行關閉的處理,節省我們的關閉和開啟連線的開銷。http://blog.csdn.net/shuaihj/article/details/14223015/ 淺談資料庫連線池說的簡單易懂。 Connection getConnection() throws SQLException;所以這句話總是要執行的,只是AbstractRoutingDataSource這個類給我們進行了一些中介的處理,在獲取Connection的時候會去尋找儲存的DataSource的引用,到底是選擇哪個DataSource進行處理,看程式碼!

這裡寫圖片描述
- 配置的引數

 <bean id="dataSource" class="com.common.utils.manydatasource.MultipleDataSourceToChoose" lazy-init="true">
        <description>資料來源</description>
        <property name="targetDataSources">
            <map key-type="java.lang.String" value-type="javax.sql.DataSource">
                <entry key="datasource0" value-ref="dataSource0" />
                <entry key="datasource1" value-ref="dataSource1" />
            </map>
        </property>
        <!-- 設定預設的目標資料來源 -->
        <property name="defaultTargetDataSource" ref="dataSource0" />
    </bean>

targetDataSources,是一個Map對於資料來源的引用

public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

對於實現SQL的Connection getConnection() throws SQLException的實現,其實就是代理模式找到之前Map的引用,通過key,而這個key就是我們靈活配置的key,通過這個key就可以尋找到這個值。

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

這裡說的非常的詳細,通過鉤子函式讓子類去實現,尋找特定的key,然後選擇DataSource 的時候就可以很靈活的使用啦!

/**
     * 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;
    }

這個就是模板方法模式中常見的鉤子函式,在HttpServlet中也有類似的使用鉤子,非常的棒,不過這個是必須實現,httpServlet不是必須實現,只是新增一些補充。由於每次執行資料庫的呼叫,總會執行這個getConnection方法,每次都檢視AOP中是否設定了當前的資料來源,然後找到Map的引用的代理的資料來源的Connection方法,原理沒有變化的。

/**
     * 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();

這裡就是我們的實現的資料來源的選擇哦!

/**
 * descrption: 多資料來源的選擇
 * authohr: wangji
 * date: 2017-08-21 10:32
 */
public class MultipleDataSourceToChoose extends AbstractRoutingDataSource {

    /**
     * @desction: 根據Key獲取資料來源的資訊,上層抽象函式的鉤子
     * @author: wangji
     * @date: 2017/8/21
     * @param:
     * @return:
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return HandlerDataSource.getDataSource();
    }
}