Spring AOP動態切換資料來源

Gin.p發表於2016-03-13

  現在稍微複雜一點的專案,一個資料庫也可能搞不定,可能還涉及分散式事務什麼的,不過由於現在我只是做一個介面整合的專案,所以分散式就先不用了,用Spring AOP來達到切換資料來源,查詢不同的資料庫就可以了。

  如果以前的我,可能就1個資料庫->1個資料來源->1個SessionFactory->1個事務管理,按照這樣的邏輯,操作一個資料庫是沒什麼問題的,但是兩個甚至多個這樣的相同配置,這不是要逼死強迫症患者的節奏嗎?

  Spring動態切換資料庫的原理是通過繼承AbstractRoutingDataSource重寫determineCurrentLookupKey()方法,來決定使用那個資料庫。在開啟事務之前,通過改變lookupKey來達到切換資料來源目的。

  先寫DataSourceHolder用來儲存當前執行緒的資料庫源。

public class DataSourceHolder {
    
    private static final ThreadLocal<String> datasourcce = new ThreadLocal<String>();
    
    public static void setCustomeType(String type){
        datasourcce.set(type);
    }
    
    public static String getCustomeType(){
        return datasourcce.get();
    }
    
    public static void remove(){
        datasourcce.remove();
    }
}
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource{

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getCustomeType();
    }

}

  ThreadLocal用作儲存資料庫源的key就可以了,相應的資料庫源會在切換的時候從AbstractRoutingDataSource的Map<Object, Object> targetDataSources中獲取。

<bean name="db1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
     <property name="url" value="${jdbc.db1.url}" />
     <property name="username" value="${jdbc.db1.username}" />
     <property name="password" value="${jdbc.db1.password}" />        
 </bean>
    
 <bean name="db2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${jdbc.db2.url}" />
        <property name="username" value="${jdbc.db2.username}" />
        <property name="password" value="${jdbc.db2.password}" />              
    </bean>
    
    <bean id="dataSource" class="com.test.dynamic.datasource.DynamicDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry key="db1" value-ref="db1" />
                <entry key="db2" value-ref="db2" />
            </map>
        </property>
        <property name="defaultTargetDataSource" ref="db1" />
    </bean>
    
    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="packagesToScan">
            <list>
                <value>${packagesToScan}</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
            </props>
        </property>
    </bean>
 
    <bean id="txManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>
    
    <aop:config>
        <aop:pointcut expression="${aop.expression}" id="bussinessService"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="bussinessService" order="2"/>

      <aop:aspect ref="dataSourceAspect" order="1">
        <aop:before method="changeDateSource" pointcut="@annotation(com.test.annotation.DataSource)"/>
      </aop:aspect>

    </aop:config>
    
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED" />  
        </tx:attributes>
    </tx:advice>

  這次使用的是@annotation的方式的AOP切面,當然也可以使用基於正則的AOP切面,接下來寫DataSourceAspect。

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
    public String name() default "";
}
import java.lang.reflect.Method;

import org.aspectj.lang.JoinPoint;
import org.springframework.stereotype.Component;

import com.test.annotation.DataSource;

@Component
public class DataSourceAspect {

    public void changeDateSource(JoinPoint jp){
        try{
            String methodName = jp.getSignature().getName();
            Class<?> targetClass = Class.forName(jp.getTarget().getClass().getName());
            for(Method method : targetClass.getMethods()){
                if(methodName.equals(method.getName())){
                    Class<?>[] args = method.getParameterTypes();
                    if(args.length == jp.getArgs().length){
                        DataSource ds = method.getAnnotation(DataSource.class);
                        DataSourceHolder.setCustomeType(ds.name());
                    }
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
}

  這個使用的時候很簡單,只要在需要切換資料來源上的方法加一個註解@DataSource(name="db1"),就可以了。由於我們做事務控制的在Service層,所以在Dao層上切換是不行的。只能在Controller層和Service做切換,而且在Service切換需要在切面上加order屬性,order屬性越小,就越先執行,只要切換的邏輯在開始事務前執行就可以了。

  1、那麼問題來了,可以在Service同一個方法上訪問兩個不同的資料庫嗎?

  不可以的。但是可以在Controller訪問Service的兩個不同方法。

  2、不同的資料庫方言要換嗎?

  其實是不用換的,方言不配置也可以(其實還沒試過,理論上-.-),經試驗,方言預設為預設資料來源的方言,由mysql切換為oracle需要注意。

  3、要注意什麼?

  注意hibernate掃面預設的資料來源就好了,hibernate.hbm2ddl.auto設定為validate,資料庫表手動建。  

 

  

相關文章