現在稍微復雜一點的項目,一個數據庫也可能搞不定,可能還涉及分布式事務什么的,不過由於現在我只是做一個接口集成的項目,所以分布式就先不用了,用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,數據庫表手動建。