SSM(八)动态切换数据源

0

前言

在现在开发的过程中应该大多数朋友都有遇到过切换数据源的需求。比如现在常用的数据库读写分离,或者就是有两个数据库的情况,这些都需要用到切换数据源。

手动切换数据源

使用SpringAbstractRoutingDataSource类来进行拓展多数据源。

该类就相当于一个dataSource的路由,用于根据key值来进行切换对应的dataSource

下面简单来看下AbstractRoutingDataSource类的几段关键源码:

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

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }

    /**
     * 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");
        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;
    }

    /**
     * Determine the current lookup key. This will typically be
     * implemented to check a thread-bound transaction context.
     * <p>Allows for arbitrary keys. The returned key needs
     * to match the stored lookup key type, as resolved by the
     * {@link #resolveSpecifiedLookupKey} method.
     */
    protected abstract Object determineCurrentLookupKey();

可以看到其中获取链接的方法getConnection()调用的determineTargetDataSource则是关键方法。该方法用于返回我们使用的数据源。

其中呢又是determineCurrentLookupKey()方法来返回当前数据源的key值。
之后通过该key值在resolvedDataSources这个map中找到对应的value(该value就是数据源)。

resolvedDataSources这个map则是在:

    @Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
        for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
            Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
            DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
            this.resolvedDataSources.put(lookupKey, dataSource);
        }
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }

这个方法通过targetDataSources这个map来进行赋值的。targetDataSources则是我们在配置文件中进行赋值的,下面会讲到。

再来看看determineCurrentLookupKey()方法,从protected来修饰就可以看出是需要我们来进行重写的。

DynamicDataSource 和 DataSourceHolder

于是我新增了DynamicDataSource类,代码如下:

package com.crossoverJie.util;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * Function:
 *
 * @author chenjiec
 *         Date: 2017/1/2 上午12:22
 * @since JDK 1.7
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getDataSources();
    }
}

代码很简单,继承了AbstractRoutingDataSource类并重写了其中的determineCurrentLookupKey()方法。

这里直接用DataSourceHolder返回了一个数据源。

DataSourceHolder代码如下:

package com.crossoverJie.util;

/**
 * Function:动态数据源
 *
 * @author chenjiec
 *         Date: 2017/1/2 上午12:19
 * @since JDK 1.7
 */
public class DataSourceHolder {
    private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();

    public static void setDataSources(String dataSource) {
        dataSources.set(dataSource);
    }

    public static String getDataSources() {
        return dataSources.get();
    }
}

这里我使用了ThreadLocal来保存了数据源,关于ThreadLocal的知识点可以查看以下这篇文章:
解密ThreadLocal

之后在Spring的配置文件中配置我们的数据源,就是上文讲到的为targetDataSources赋值

<bean id="ssm1DataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <!-- 指定连接数据库的驱动 -->
        <property name="driverClassName" value="${jdbc.driverClass}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.user}" />
        <property name="password" value="${jdbc.password}" />
        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="3" />
        <property name="minIdle" value="3" />
        <property name="maxActive" value="20" />
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="60000" />
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <property name="validationQuery" value="SELECT 'x'" />
        <property name="testWhileIdle" value="true" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
                  value="20" />
        <!-- 配置监控统计拦截的filters,去掉后监控界面sql无法统计 -->
        <property name="filters" value="stat" />
    </bean>

    <bean id="ssm2DataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <!-- 指定连接数据库的驱动 -->
        <property name="driverClassName" value="${jdbc.driverClass}"/>
        <property name="url" value="${jdbc.url2}"/>
        <property name="username" value="${jdbc.user2}"/>
        <property name="password" value="${jdbc.password2}"/>
        <property name="initialSize" value="3"/>
        <property name="minIdle" value="3"/>
        <property name="maxActive" value="20"/>
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="60000"/>
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000"/>
        <property name="validationQuery" value="SELECT 'x'"/>
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>
        <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="true"/>
        <property name="maxPoolPreparedStatementPerConnectionSize"
                  value="20"/>
        <!-- 配置监控统计拦截的filters,去掉后监控界面sql无法统计 -->
        <property name="filters" value="stat"/>
    </bean>
    <bean id="dataSource" class="com.crossoverJie.util.DynamicDataSource">
        <property name="targetDataSources">
            <map key-type="java.lang.String">
                <entry key="ssm1DataSource" value-ref="ssm1DataSource"/>
                <entry key="ssm2DataSource" value-ref="ssm2DataSource"/>
            </map>
        </property>
        <!--默认数据源-->
        <property name="defaultTargetDataSource" ref="ssm1DataSource"/>
    </bean>

这里分别配置了两个数据源:ssm1DataSourcessm2DataSource
之后再通过Spring的依赖注入方式将两个数据源设置进targetDataSources

接下来的用法相比大家也应该猜到了。

就是在每次调用数据库之前我们都要先通过DataSourceHolder来设置当前的数据源。看下demo:

    @Test
    public void selectByPrimaryKey() throws Exception {
        DataSourceHolder.setDataSources(Constants.DATASOURCE_TWO);
        Datasource datasource = dataSourceService.selectByPrimaryKey(7);
        System.out.println(JSON.toJSONString(datasource));
    }

详见我的单测。

使用起来也是非常简单。但是不知道大家注意到没有,这样的做法槽点很多:

  1. 每次使用需要手动切换,总有一些人会忘记写(比如我)。
  2. 如果是后期需求变了,查询其他的表了还得一个个改回来。

那有没有什么方法可以自动的帮我们切换呢?

肯定是有的,大家应该也想得到。就是利用SpringAOP了。

自动切换数据源

首先要定义好我们的切面类DataSourceExchange:

package com.crossoverJie.util;

import org.aspectj.lang.JoinPoint;

/**
 * Function:拦截器方法
 *
 * @author chenjiec
 *         Date: 2017/1/3 上午12:34
 * @since JDK 1.7
 */
public class DataSourceExchange {
    
    /**
     *
     * @param point
     */
    public void before(JoinPoint point) {

        //获取目标对象的类类型
        Class<?> aClass = point.getTarget().getClass();

        //获取包名用于区分不同数据源
        String whichDataSource = aClass.getName().substring(25, aClass.getName().lastIndexOf("."));
        if ("ssmone".equals(whichDataSource)) {
            DataSourceHolder.setDataSources(Constants.DATASOURCE_ONE);
        } else {
            DataSourceHolder.setDataSources(Constants.DATASOURCE_TWO);
        }

    }


    /**
     * 执行后将数据源置为空
     */
    public void after() {
        DataSourceHolder.setDataSources(null);
    }

}

逻辑也比较简单,就是在执行数据库操作之前做一个切面。

  • 通过JoinPoint对象获取目标对象。
  • 在目标对象中获取包名来区分不同的数据源。
  • 根据不同数据源来进行赋值。
  • 执行完毕之后将数据源清空。

关于一些JoinPoint的API:

package org.aspectj.lang;
import org.aspectj.lang.reflect.SourceLocation;
public interface JoinPoint {
    String toString();         //连接点所在位置的相关信息
    String toShortString();     //连接点所在位置的简短相关信息
    String toLongString();     //连接点所在位置的全部相关信息
    Object getThis();         //返回AOP代理对象
    Object getTarget();       //返回目标对象
    Object[] getArgs();       //返回被通知方法参数列表
    Signature getSignature();  //返回当前连接点签名
    SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置
    String getKind();        //连接点类型
    StaticPart getStaticPart(); //返回连接点静态部分
}

为了通过包名来区分不同数据源,我将目录结构稍微调整了下:

2

将两个不同的数据源的实现类放到不同的包中,这样今后如果还需要新增其他数据源也可以灵活的切换。

看下Spring的配置:

    <bean id="dataSourceExchange" class="com.crossoverJie.util.DataSourceExchange"/>
    <!--配置切面拦截方法 -->
    <aop:config proxy-target-class="false">
        <!--将com.crossoverJie.service包下的所有select开头的方法加入拦截
        去掉select则加入所有方法
        -->
        <aop:pointcut id="controllerMethodPointcut" expression="
        execution(* com.crossoverJie.service.*.select*(..))"/>

        <aop:pointcut id="selectMethodPointcut" expression="
        execution(* com.crossoverJie.dao..*Mapper.select*(..))"/>

        <aop:advisor advice-ref="methodCacheInterceptor" pointcut-ref="controllerMethodPointcut"/>

        <!--所有数据库操作的方法加入切面-->
        <aop:aspect ref="dataSourceExchange">
            <aop:pointcut id="dataSourcePointcut" expression="execution(* com.crossoverJie.service.*.*(..))"/>
            <aop:before pointcut-ref="dataSourcePointcut" method="before"/>
            <aop:after pointcut-ref="dataSourcePointcut" method="after"/>
        </aop:aspect>
    </aop:config>

这是在我们上一篇整合redis缓存的基础上进行修改的。
这样缓存和多数据源都满足了。

实际使用:

    @Test
    public void selectByPrimaryKey() throws Exception {
        Rediscontent rediscontent = rediscontentService.selectByPrimaryKey(30);
        System.out.println(JSON.toJSONString(rediscontent));
    }
3

这样看起来就和使用一个数据源这样简单,再也不用关心切换的问题了。

总结

不过按照这样的写法是无法做到在一个事务里控制两个数据源的。这个我还在学习中,有相关经验的大牛不妨指点一下。

项目地址:https://github.com/crossoverJie/SSM.git

个人博客地址:http://crossoverjie.top

GitHub地址:https://github.com/crossoverJie

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

推荐阅读更多精彩内容