1. 程式人生 > >Spring中AOP方式實現多資料來源切換

Spring中AOP方式實現多資料來源切換

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
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出