在现在开发的过程中应该大多数朋友都有遇到过切换数据源的需求。比如现在常用的数据库读写分离,或者就是有两个数据库的情况,这些都需要用到切换数据源。
使用 Spring
的 AbstractRoutingDataSource
类来进行拓展多数据源。
该类就相当于一个 dataSource
的路由,用于根据 key
值来进行切换对应的 dataSource
。
下面简单来看下 AbstractRoutingDataSource
类的几段关键源码:
@Override publicConnectiongetConnection()throwsSQLException{ return determineTargetDataSource().getConnection(); } @Override publicConnectiongetConnection(String username, String password)throwsSQLException{ 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() */ protectedDataSourcedetermineTargetDataSource(){ 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. */ protectedabstractObjectdetermineCurrentLookupKey();
可以看到其中获取链接的方法 getConnection()
调用的 determineTargetDataSource
则是关键方法。该方法用于返回我们使用的数据源。
其中呢又是 determineCurrentLookupKey()
方法来返回当前数据源的 key
值。
之后通过该key值在 resolvedDataSources
这个map中找到对应的 value
(该value就是数据源)。
resolvedDataSources
这个map则是在:
@Override publicvoidafterPropertiesSet(){ 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
类,代码如下:
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 classDynamicDataSourceextendsAbstractRoutingDataSource{ @Override protectedObjectdetermineCurrentLookupKey(){ 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 classDataSourceHolder{ private static final ThreadLocal<String> dataSources = new ThreadLocal<String>(); publicstaticvoidsetDataSources(String dataSource){ dataSources.set(dataSource); } publicstaticStringgetDataSources(){ return dataSources.get(); } }
这里我使用了 ThreadLocal
来保存了数据源,关于 ThreadLocal
的知识点可以查看以下这篇文章:
之后在 Spring
的配置文件中配置我们的数据源,就是上文讲到的 为targetDataSources赋值
:
<beanid="ssm1DataSource"class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- 指定连接数据库的驱动 --> <propertyname="driverClassName"value="${jdbc.driverClass}"/> <propertyname="url"value="${jdbc.url}"/> <propertyname="username"value="${jdbc.user}"/> <propertyname="password"value="${jdbc.password}"/> <!-- 配置初始化大小、最小、最大 --> <propertyname="initialSize"value="3"/> <propertyname="minIdle"value="3"/> <propertyname="maxActive"value="20"/> <!-- 配置获取连接等待超时的时间 --> <propertyname="maxWait"value="60000"/> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <propertyname="timeBetweenEvictionRunsMillis"value="60000"/> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <propertyname="minEvictableIdleTimeMillis"value="300000"/> <propertyname="validationQuery"value="SELECT 'x'"/> <propertyname="testWhileIdle"value="true"/> <propertyname="testOnBorrow"value="false"/> <propertyname="testOnReturn"value="false"/> <!-- 打开PSCache,并且指定每个连接上PSCache的大小 --> <propertyname="poolPreparedStatements"value="true"/> <propertyname="maxPoolPreparedStatementPerConnectionSize" value="20" /> <!-- 配置监控统计拦截的filters,去掉后监控界面sql无法统计 --> <propertyname="filters"value="stat"/> </bean> <beanid="ssm2DataSource"class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- 指定连接数据库的驱动 --> <propertyname="driverClassName"value="${jdbc.driverClass}"/> <propertyname="url"value="${jdbc.url2}"/> <propertyname="username"value="${jdbc.user2}"/> <propertyname="password"value="${jdbc.password2}"/> <propertyname="initialSize"value="3"/> <propertyname="minIdle"value="3"/> <propertyname="maxActive"value="20"/> <!-- 配置获取连接等待超时的时间 --> <propertyname="maxWait"value="60000"/> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <propertyname="timeBetweenEvictionRunsMillis"value="60000"/> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <propertyname="minEvictableIdleTimeMillis"value="300000"/> <propertyname="validationQuery"value="SELECT 'x'"/> <propertyname="testWhileIdle"value="true"/> <propertyname="testOnBorrow"value="false"/> <propertyname="testOnReturn"value="false"/> <!-- 打开PSCache,并且指定每个连接上PSCache的大小 --> <propertyname="poolPreparedStatements"value="true"/> <propertyname="maxPoolPreparedStatementPerConnectionSize" value="20"/> <!-- 配置监控统计拦截的filters,去掉后监控界面sql无法统计 --> <propertyname="filters"value="stat"/> </bean> <beanid="dataSource"class="com.crossoverJie.util.DynamicDataSource"> <propertyname="targetDataSources"> <mapkey-type="java.lang.String"> <entrykey="ssm1DataSource"value-ref="ssm1DataSource"/> <entrykey="ssm2DataSource"value-ref="ssm2DataSource"/> </map> </property> <!--默认数据源--> <propertyname="defaultTargetDataSource"ref="ssm1DataSource"/> </bean>
这里分别配置了两个数据源: ssm1DataSource
和 ssm2DataSource
。
之后再通过 Spring
的依赖注入方式将两个数据源设置进 targetDataSources
。
接下来的用法相比大家也应该猜到了。
就是在每次调用数据库之前我们都要先通过 DataSourceHolder
来设置当前的数据源。看下demo:
@Test publicvoidselectByPrimaryKey()throwsException{ DataSourceHolder.setDataSources(Constants.DATASOURCE_TWO); Datasource datasource = dataSourceService.selectByPrimaryKey(7); System.out.println(JSON.toJSONString(datasource)); }
详见我的单测。
使用起来也是非常简单。但是不知道大家注意到没有,这样的做法槽点很多:
那有没有什么方法可以自动的帮我们切换呢?
肯定是有的,大家应该也想得到。就是利用 Spring
的 AOP
了。
首先要定义好我们的切面类 DataSourceExchange
:
package com.crossoverJie.util; import org.aspectj.lang.JoinPoint; /** * Function:拦截器方法 * * @author chenjiec * Date: 2017/1/3 上午12:34 * @since JDK 1.7 */ public classDataSourceExchange{ /** * * @param point */ publicvoidbefore(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); } } /** * 执行后将数据源置为空 */ publicvoidafter(){ DataSourceHolder.setDataSources(null); } }
逻辑也比较简单,就是在执行数据库操作之前做一个切面。
JoinPoint
对象获取目标对象。 关于一些 JoinPoint
的API:
package org.aspectj.lang; import org.aspectj.lang.reflect.SourceLocation; public interfaceJoinPoint{ StringtoString(); //连接点所在位置的相关信息 StringtoShortString(); //连接点所在位置的简短相关信息 StringtoLongString(); //连接点所在位置的全部相关信息 ObjectgetThis(); //返回AOP代理对象 ObjectgetTarget(); //返回目标对象 Object[] getArgs(); //返回被通知方法参数列表 SignaturegetSignature(); //返回当前连接点签名 SourceLocationgetSourceLocation();//返回连接点方法所在类文件中的位置 StringgetKind(); //连接点类型 StaticPartgetStaticPart(); //返回连接点静态部分 }
为了通过 包名
来区分不同数据源,我将目录结构稍微调整了下:
将两个不同的数据源的实现类放到不同的包中,这样今后如果还需要新增其他数据源也可以灵活的切换。
看下 Spring
的配置:
<beanid="dataSourceExchange"class="com.crossoverJie.util.DataSourceExchange"/> <!--配置切面拦截方法 --> <aop:configproxy-target-class="false"> <!--将com.crossoverJie.service包下的所有select开头的方法加入拦截 去掉select则加入所有方法 --> <aop:pointcutid="controllerMethodPointcut"expression=" execution(* com.crossoverJie.service.*.select*(..))"/> <aop:pointcutid="selectMethodPointcut"expression=" execution(* com.crossoverJie.dao..*Mapper.select*(..))"/> <aop:advisoradvice-ref="methodCacheInterceptor"pointcut-ref="controllerMethodPointcut"/> <!--所有数据库操作的方法加入切面--> <aop:aspectref="dataSourceExchange"> <aop:pointcutid="dataSourcePointcut"expression="execution(* com.crossoverJie.service.*.*(..))"/> <aop:beforepointcut-ref="dataSourcePointcut"method="before"/> <aop:afterpointcut-ref="dataSourcePointcut"method="after"/> </aop:aspect> </aop:config>
这是在我们上一篇整合redis缓存的基础上进行修改的。
这样缓存和多数据源都满足了。
实际使用:
@Test publicvoidselectByPrimaryKey()throwsException{ Rediscontent rediscontent = rediscontentService.selectByPrimaryKey(30); System.out.println(JSON.toJSONString(rediscontent)); }
这样看起来就和使用一个数据源这样简单,再也不用关心切换的问题了。
不过按照这样的写法是无法做到在一个事务里控制两个数据源的。这个我还在学习中,有相关经验的大牛不妨指点一下。
项目地址: https://github.com/crossoverJie/SSM.git
个人博客地址: http://crossoverjie.top 。
GitHub地址: https://github.com/crossoverJie 。