現在稍微複雜一點的專案,一個資料庫也可能搞不定,可能還涉及分散式事務什麼的,不過由於現在我只是做一個介面整合的專案,所以分散式就先不用了,用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,資料庫表手動建。