Spring AOP动态切换数据源

作者:chenhaozjnubit 分类: 发布于:2018-2-8 15:13 ė1336次浏览 60条评论

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

 

本文出自 爱奕乐,转载时请注明出处及相应链接。

分享本文至:

发表评论

电子邮件地址不会被公开。必填项已用*标注