Spring中AOP方式實現多數據源切換


作者:suroot

spring動態配置多數據源,即在大型應用中對數據進行切分,並且采用多個數據庫實例進行管理,這樣可以有效提高系統的水平伸縮性。而這樣的方案就會不同於常見的單一數據實例的方案,這就要程序在運行時根據當時的請求及系統狀態來動態的決定將數據存儲在哪個數據庫實例中,以及從哪個數據庫提取數據。
Spring2.x以后的版本中采用Proxy模式,就是我們在方案中實現一個虛擬的數據源,並且用它來封裝數據源選擇邏輯,這樣就可以有效地將數據源選擇邏輯從Client中分離出來。Client提供選擇所需的上下文(因為這是Client所知道的),由虛擬的DataSource根據Client提供的上下文來實現數據源的選擇。
實現
具體的實現就是,虛擬的DataSource僅需繼承AbstractRoutingDataSource實現determineCurrentLookupKey()在其中封裝數據源的選擇邏輯。

1.動態配置多數據源
數據源的名稱常量和一個獲得和設置上下文環境的類,主要負責改變上下文數據源的名稱

public class DataSourceContextHolder {

    public static final String DATA_SOURCE_A = "dataSourceA";
    public static final String DATA_SOURCE_B = "dataSourceB";
    public static final String DATA_SOURCE_C = "dataSourceC";
    public static final String DATA_SOURCE_D = "dataSourceD";
    // 線程本地環境
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    // 設置數據源類型
    public static void setDbType(String dbType) {
        // System.out.println("此時切換的數據源為:"+dbType);
        contextHolder.set(dbType);
    }
    // 獲取數據源類型 
    public static String getDbType() {
        return (contextHolder.get());
    }
    // 清除數據源類型 
    public static void clearDbType() {
        contextHolder.remove();
    }
}

2.建立動態數據源類,注意,這個類必須繼承AbstractRoutingDataSource,且實現方法determineCurrentLookupKey,該方法返回一個Object,一般是返回字符串:

public class DynamicDataSource extends AbstractRoutingDataSource {  
    @Override  
    protected Object determineCurrentLookupKey() {  
        System.out.println("此時獲取到的數據源為:"+DataSourceContextHolder.getDbType());
        return DataSourceContextHolder.getDbType();  
    }  
}

為了更好的理解為什么會切換數據源,我們來看一下AbstractRoutingDataSource.java源碼,源碼中確定數據源部分主要內容如下

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

上面這段源碼的重點在於determineCurrentLookupKey()方法,這是AbstractRoutingDataSource類中的一個抽象方法,而它的返回值是你所要用的數據源dataSource的key值,有了這個key值,resolvedDataSource(這是個map,由配置文件中設置好后存入的)就從中取出對應的DataSource,如果找不到,就用配置默認的數據源。
看完源碼,應該有點啟發了吧,沒錯!你要擴展AbstractRoutingDataSource類,並重寫其中的determineCurrentLookupKey()方法,來實現數據源的切換:

3.編寫spring的配置文件配置多個數據源

    <!-- 配置數據源  dataSourceA-->
    <bean name="dataSourceA"  class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc_url}" />
        <property name="username" value="${jdbc_username}" />
        <property name="password" value="${jdbc_password}" />

        <!-- 初始化連接大小 -->
        <property name="initialSize" value="20" />
        <!-- 連接池最大使用連接數量 -->
        <property name="maxActive" value="500" />
        <!-- 連接池最大空閑 -->
        <property name="maxIdle" value="20" />
        <!-- 連接池最小空閑 -->
        <property name="minIdle" value="0" />
        <!-- 獲取連接最大等待時間 -->
        <property name="maxWait" value="60000" />

        <property name="validationQuery" value="${validationQuery}" />
        <property name="testOnBorrow" value="true" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="25200000" />
        <!-- 打開removeAbandoned功能 -->
        <property name="removeAbandoned" value="true" />
        <!-- 1800秒,也就是30分鍾 -->
        <property name="removeAbandonedTimeout" value="1800" />
        <!-- 關閉abanded連接時輸出錯誤日志 -->
        <property name="logAbandoned" value="true" />

        <!-- 打開PSCache,並且指定每個連接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize" value="1000" />

        <!-- 監控數據庫 -->
        <!--<property name="filters" value="stat,log4j"/>-->
        <property name="filters" value="stat" /> 
    </bean>


    <!-- 配置數據源    dataSourceB    -->
    <bean name="dataSourceB"  class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc_new_url}" />
        <property name="username" value="${jdbc_new_username}" />
        <property name="password" value="${jdbc_new_password}" />
        <property name="initialSize" value="10" />
        <property name="maxActive" value="100" />
        <property name="maxIdle" value="10" />
        <property name="minIdle" value="0" />
        <property name="maxWait" value="10000" />
        <property name="validationQuery" value="${validationQuery3}" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <property name="minEvictableIdleTimeMillis" value="25200000" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="1800" />
        <property name="logAbandoned" value="true" />
        <property name="filters" value="stat" /> 
    </bean> 


     <!-- 配置數據源    dataSourceC  -->
     <bean name="dataSourceC"  class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc_mysql_url}" />
        <property name="username" value="${jdbc_mysql_username}" />
        <property name="password" value="${jdbc_mysql_password}" />
        <property name="initialSize" value="10" />
        <property name="maxActive" value="100" />
        <property name="maxIdle" value="10" />
        <property name="minIdle" value="0" />
        <property name="maxWait" value="60000" />
        <property name="validationQuery" value="${validationQuery2}" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <property name="minEvictableIdleTimeMillis" value="25200000" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="1800" />
        <property name="logAbandoned" value="true" />
        <property name="filters" value="stat" /> 
    </bean> 

    <!-- 多數據源配置 -->
     <bean id="dataSource" class="top.suroot.base.datasource.DynamicDataSource">  
        <!-- 默認使用dataSourceA的數據源 -->
        <property name="defaultTargetDataSource" ref="dataSourceA"></property>
        <property name="targetDataSources">  
            <map key-type="java.lang.String">  
                <entry value-ref="dataSourceA" key="dataSourceA"></entry>  
                <entry value-ref="dataSourceB" key="dataSourceB"></entry>
                <entry value-ref="dataSourceC" key="dataSourceC"></entry>
                <!--<entry value-ref="dataSourceD" key="dataSourceD"></entry>-->
            </map>  
        </property>  
   </bean>

在這個配置中第一個property屬性配置目標數據源,<map key-type="java.lang.String">中的key-type必須要和靜態鍵值對照類DataSourceConst中的值的類型相 同;<entry key="User" value-ref="userDataSource"/>中key的值必須要和靜態鍵值對照類中的值相同,如果有多個值,可以配置多個< entry>標簽。第二個property屬性配置默認的數據源。

4.動態切換是數據源

DataSourceContextHolder.setDbType(DataSourceContextHolder.DATA_SOURCE_B);

以上講的是怎么動態切換數據源,下面要說下自定義注解、aop方式自動切換數據源 ,感興趣的可以繼續往下看看
5.配置自定義注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DynamicDataSourceAnnotation {
    //dataSource 自定義注解的參數
    String dataSource() default DataSourceContextHolder.DATA_SOURCE_A;
}

6.配置切面類

@Aspect
@Component
@Order(1) 
public class DynamicDataSourceAspect {


    @Before("@annotation(top.suroot.base.aop.DynamicDataSourceAnnotation)") //前置通知
    public void testBefore(JoinPoint point){
        //獲得當前訪問的class
        Class<?> className = point.getTarget().getClass();
        DynamicDataSourceAnnotation dataSourceAnnotation = className.getAnnotation(DynamicDataSourceAnnotation.class);
        if (dataSourceAnnotation != null ) {
            //獲得訪問的方法名
            String methodName = point.getSignature().getName();
            //得到方法的參數的類型
            Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
            String dataSource = DataSourceContextHolder.DATA_SOURCE_A;
            try {
                Method method = className.getMethod(methodName, argClass);
                if (method.isAnnotationPresent(DynamicDataSourceAnnotation.class)) {
                    DynamicDataSourceAnnotation annotation = method.getAnnotation(DynamicDataSourceAnnotation.class);
                    dataSource = annotation.dataSource();
                    System.out.println("DataSource Aop ====> "+dataSource);
                }
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            DataSourceContextHolder.setDbType(dataSource);
        }

    }

    @After("@annotation(top.suroot.base.aop.DynamicDataSourceAnnotation)")   //后置通知
    public void testAfter(JoinPoint point){
        //獲得當前訪問的class
        Class<?> className = point.getTarget().getClass();
        DynamicDataSourceAnnotation dataSourceAnnotation = className.getAnnotation(DynamicDataSourceAnnotation.class);
        if (dataSourceAnnotation != null ) {
            //獲得訪問的方法名
            String methodName = point.getSignature().getName();
            //得到方法的參數的類型
            Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
            String dataSource = DataSourceContextHolder.DATA_SOURCE_A;
            try {
                Method method = className.getMethod(methodName, argClass);
                if (method.isAnnotationPresent(DynamicDataSourceAnnotation.class)) {
                    DynamicDataSourceAnnotation annotation = method.getAnnotation(DynamicDataSourceAnnotation.class);
                    dataSource = annotation.dataSource();
                }
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            if(dataSource != null && !DataSourceContextHolder.DATA_SOURCE_A.equals(dataSource)) DataSourceContextHolder.clearDbType();
        }
    }
}

7.在切入點添加自定義的注解

@Service("baseService")
@DynamicDataSourceAnnotation
public class BaseServiceImpl implements BaseService {

    @DynamicDataSourceAnnotation(dataSource = DataSourceContextHolder.DATA_SOURCE_B)
    public void changeDataSource() {
        System.out.println("切換數據源serviceImple");
    }
}

8.當然注解掃描、和aop代理一定要在配置文件中配好

    <!-- 自動掃描(bean注入) --> <context:component-scan base-package="top.suroot.*" /> <!-- AOP自動代理功能 --> <aop:aspectj-autoproxy proxy-target-class="true"/>

大功告成,當調用changeDataSource方法的時候會進入切面類中切換數據源,方法調用完畢會把數據源切換回來。

作者:suroot
鏈接:http://www.jianshu.com/p/ddebf4ae57c1
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM