AbstractRoutingDataSource动态数据源切换

这篇文章就直接引用AbstractRoutingDataSource动态数据源切换:https://blog.csdn.net/u012881904/article/details/77449710,望博主看到多包涵,我已经给了原文地址啦!

操作数据一般都是在DAO层进行处理,可以选择直接使用JDBC进行编程

或者是使用多个DataSource 然后创建多个SessionFactory,在使用Dao层的时候通过不同的SessionFactory进行处理,不过这样的入侵性比较明显,一般的情况下我们都是使用继承HibernateSupportDao进行封装了的处理,如果多个SessionFactory这样处理就是比较的麻烦了,修改的地方估计也是蛮多的.

最后一个,也就是使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源,这样的入侵性较低,非常好的满足使用的需求。比如我们希望对于读写分离或者其他的数据同步的业务场景.

下面是对操作多数据库的几种方式:

方式1:

graph LR
Datasource-->SessionFactory
SessionFactory-->Dao层实现类
  • 单数据源的场景(一般的Web项目工程这样配置进行处理,就已经比较能够满足我们的业务需求)

方式2:

graph LR
Datasource1-->SessionFactory1
Datasource2-->SessionFactory2
SessionFactory1-->Dao层实现类
SessionFactory2-->Dao层实现类
  • 多数据源多SessionFactory这样的场景,估计作为刚刚开始想象想处理在使用框架的情况下处理业务,配置多个SessionFactory,然后在Dao层中对于特定的请求,通过特定的SessionFactory即可处理实现这样的业务需求,不过这样的处理带来了很多的不便之处,所有很多情况下我们宁愿直接使用封装的JDBC编程,或者使用Mybatis处理这样的业务场景

方式3:

graph LR
Datasource1-->AbstractRoutingDataSource每执行一次SQL操作就动态选择DataSource
Datasource2-->AbstractRoutingDataSource每执行一次SQL操作就动态选择DataSource
AbstractRoutingDataSource每执行一次SQL操作就动态选择DataSource-->SessionFactory
SessionFactory-->Dao层实现类
  • 使用AbstractRoutingDataSource 的实现类,进行灵活的切换,可以通过AOP或者手动编程设置当前的DataSource,不用修改我们编写的对于继承HibernateSupportDao的实现类的修改,这样的编写方式比较好,至于其中的实现原理,让我细细到来。我们想看看如何去应用,实现原理慢慢的说!

我们接下来就来详细讲解方式3啦:

接下来我们需要做的就是:编写AbstractRoutingDataSource的实现类,HandlerDataSource就是提供给我们动态选择数据源的数据的信息,我们这里编写一个根据当前线程来选择数据源,然后通过AOP拦截特定的注解,设置当前的数据源信息,也可以手动的设置当前的数据源,在编程的类中。

AbstractRoutingDataSource实现类

public class MultipleDataSourceToChoose extends AbstractRoutingDataSource {

    /**
     * @desction: 根据Key获取数据源的信息,上层抽象函数的钩子
     * @param:
     * @return:
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return HandlerDataSource.getDataSource();
    }
}

HandlerDataSource:代码是自己编写的一个动态选择数据源大道一个类

/**
 * descrption: 根据当前线程来选择具体的数据源
 */
public class HandlerDataSource {

    private static ThreadLocal<String> handlerThredLocal = new ThreadLocal<String>();

    /**
     * @desction: 提供给AOP去设置当前的线程的数据源的信息
     * @param: [datasource]
     * @return: void
     */
    public static void putDataSource(String datasource) {
        handlerThredLocal.set(datasource);
    }

    /**
     * @desction: 提供给AbstractRoutingDataSource的实现类,通过key选择数据源
     * @param: []
     * @return: java.lang.String
     */
    public static String getDataSource() {
        return handlerThredLocal.get();
    }

    /**
     * @desction: 使用默认的数据源
     */
    public static void clear() {
        handlerThredLocal.remove();
    }
}

接下来就有两种方案:

1.手动在程序中设置数据源。也就是通过手动调用HandlerDataSource中的putDataSource方法。

2.通过AOP编程(前置通知和后置通知来对我们需要更换数据源的方法进行代理增强,通过注解的方式辨别切换到哪个数据源)

我们这里就使用第二种方式,定义自定义注解:如果使用该注解的类或方法,就可以在在AOP编程的时候来判断注解的存在,从而来改变数据源。

/**
 * @description: 创建拦截设置数据源的注解
 * Created by wangji on 2017/8/21.
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicSwitchDataSource {

    String dataSource() default "";
}

我们已经定义了注解来判断切换哪个数据源,那么接下来,我们需要定义切面类,也就是增强类来对需要代理的类进行增强(也就是说对需要增强的类进行数据源的切换,至于切换哪个数据源,就通过扫描注解来确定)

/**
 * descrption: 使用AOP拦截特定的注解去动态的切换数据源
 */
@Aspect
@Component
public class HandlerDataSourceAop {
    
    @Pointcut("@within(com.common.utils.manydatasource.DynamicSwitchDataSource)||@annotation(com.common.utils.manydatasource.DynamicSwitchDataSource)")
    public void pointcut() {}

    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint)
    {
        Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
        DynamicSwitchDataSource annotationClass = method.getAnnotation(DynamicSwitchDataSource.class);//获取方法上的注解
        if(annotationClass == null){
            annotationClass = joinPoint.getTarget().getClass().getAnnotation(DynamicSwitchDataSource.class);//获取类上面的注解
            if(annotationClass == null) return;
        }
        //获取注解上的数据源的值的信息
        String dataSourceKey = annotationClass.dataSource();
        if(dataSourceKey !=null){
            //给当前的执行SQL的操作设置特殊的数据源的信息
            HandlerDataSource.putDataSource(dataSourceKey);
        }
        log.info("AOP动态切换数据源,className"+joinPoint.getTarget().getClass().getName()+"methodName"+method.getName()+";dataSourceKey:"+dataSourceKey==""?"默认数据源":dataSourceKey);
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        //清理掉当前设置的数据源,让默认的数据源不受影响
        HandlerDataSource.clear();
    }

}

上面我们已经对数据源切换的AOP编程代码进行了实现啦,接下来,我们就需要进行配置文件的配置啦!


配置文件需要配置的项有:数据源(如果需要切换哪些数据源,就需要把需要切换的数据源都配置出来)、数据源的切换配置(也就是上面我们定义的继承AbstractRoutingDataSource的MultipleDataSourceToChoose的类)、SessionFactory的配置就和以前一样,没有任何变化(根据Hibernate和Mybatis的SessionFactory配置)。

<!-- 配置数据源 -->
    <bean id="dataSource0" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close" init-method="init">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="maxActive" value="10"/>
    </bean>
    <bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close" init-method="init">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc2.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="maxActive" value="10"/>
    </bean>
    <!-- 切换数据源-->
    bean id="dataSource" class="com.common.utils.manydatasource.MultipleDataSourceToChoose" lazy-init="true">
        <description>数据源</description>
        <property name="targetDataSources">
            <map key-type="java.lang.String" value-type="javax.sql.DataSource">
                <entry key="datasource0" value-ref="dataSource0" />
                <entry key="datasource1" value-ref="dataSource1" />
            </map>
        </property>
        <!-- 设置默认的目标数据源 -->
        <property name="defaultTargetDataSource" ref="dataSource0" />
    </bean>
    <!--SessionFactory的配置,这里就以Hibernate为例-->
    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <!-- 注意这里的DataSource数据源就是切换的数据源,因为它会给我们提供一个代理的数据源,至于是提供那个数据源跟我们代码中的选择-->
        <property name="dataSource" ref="dataSource"/>
        <!--指定Hibernate属性 -->
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                <prop key="hibernate.show_sql">false</prop>
                <prop key="hibernate.format_sql">false</prop>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
                <prop key="hibernate.autoReconnect">true</prop>
                <prop key="hibernate.jdbc.batch_size">50</prop>
              <prop key="hibernate.connection.autocommit">false</prop>
              <prop key="hibernate.connection.release_mode">after_transaction</prop>
              <prop key="hibernate.bytecode.use_reflection_optimizer">false</prop>
            </props>
        </property>
        <property name="packagesToScan">
            <list>
                <value>com.module</value>
            </list>
        </property>
    </bean>
    <!-- 事务的配置我们就省略啦,和之前的配置一样,只是数据源依然是切换数据源的id-->

接下来我们就进行测试:测试我们先对AOP动态代理切换数据源的方式进行测试,然后对手动切换数据源进行测试

AOP动态代理切换数据源测试

@Service
@Slf4j
public class UserInfoService implements IUserInfoService {


    @Resource
    private UserDao userDao;
    @Autowired
    private CommonHibernateDao commonDao;

    @TestValidateParam
    public User getUserInfoById(Integer id) {
        return userDao.findById(id);
    }
    
    //切换到dataSource0
    @DynamicSwitchDataSource(dataSource = "datasource0")
    public void save(User user) {
        userDao.save(user);
    }
    
    //切换到dataSource1
    @DynamicSwitchDataSource(dataSource = "datasource1")
    public List<User> findAll(){
        String sql = "select u.userName as name,u.userAge as age,u.userAddress as address,u.id from user u";
        List<User> list =commonDao.findListBySQL(sql,User.class);
        return list;
    }

}

手动方式切换数据源

    public void test(){
        HandlerDataSource.putDataSource("datasource1");
        String sql = "select u.userName as name,u.userAge as age,u.userAddress as address,u.id from user u";
        List<User> list =commonDao.findListBySQL(sql,User.class);

        HandlerDataSource.putDataSource("datasource0");
        commonDao.deleteById("2",User.class);
    }

实现原理

实现原理,MultipleDataSourceToChoose的继承结构图,之前说过他是DataSource的子类,由于无论我们是使用Mybatis还是使用Hibernate进行SQL操作的时候总会执行getConnection(),无论我们的数据源是否使用了数据库连接池,因为数据库连接池的主要作用就是保持一堆的Connection不进行关闭的处理,节省我们的关闭和打开连接的开销。这里提供一个浅谈数据库连接池说的简单易懂 。 Connection getConnection() throws SQLException;所以这句话总是要执行的,只是AbstractRoutingDataSource这个类给我们进行了一些中介的处理,在获取Connection的时候会去寻找保存的DataSource的引用,到底是选择哪个DataSource进行处理,看代码!

下面就给出继承图:

graph LR
MultipleDataSourceToChoose-->AbstractRoutingDataSource
AbstractRoutingDataSource-->InitializingBean
AbstractRoutingDataSource-->AbstractDataSource
AbstractDataSource-->DataSource

第一:我们去看targetDataSource的源码看究竟是什么

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    private Map<Object, Object> targetDataSources;

    private Object defaultTargetDataSource;

通过源码我们发现targetDataSource就是一个Map对象。

第二:我们去看小获取连接getConnection的源代码,看是怎么获取的

    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

通过源代码,我们发现,是determineTargetDataSource()方法返回一个DataSource,然后就可以获取连接啦!

第三:关键的一步,我们去看下determineTargetDataSource()方法是如何获取DataSource的

    /**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        //determineCurrentLookupKey()方法就是我们重写的一个方法,他就是为targetDataSources的Map对象返回一个key
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

通过这里我们应该就很清楚整个流程啦!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容