现在稍微复杂一点的项目,一个数据库也可能搞不定,可能还涉及分布式事务什么的,不过由于现在我只是做一个接口集成的项目,所以分布式就先不用了,用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,数据库表手动建。