前言
在現(xiàn)在開發(fā)的過程中應該大多數(shù)朋友都有遇到過切換數(shù)據(jù)源的需求脾还。比如現(xiàn)在常用的數(shù)據(jù)庫讀寫分離,或者就是有兩個數(shù)據(jù)庫的情況入愧,這些都需要用到切換數(shù)據(jù)源鄙漏。
手動切換數(shù)據(jù)源
使用Spring
的AbstractRoutingDataSource
類來進行拓展多數(shù)據(jù)源。
該類就相當于一個dataSource
的路由棺蛛,用于根據(jù)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
則是關鍵方法。該方法用于返回我們使用的數(shù)據(jù)源旁赊。
其中呢又是determineCurrentLookupKey()
方法來返回當前數(shù)據(jù)源的key
值媚创。
之后通過該key值在resolvedDataSources
這個map中找到對應的value
(該value就是數(shù)據(jù)源)。
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
返回了一個數(shù)據(jù)源。
DataSourceHolder
代碼如下:
package com.crossoverJie.util;
/**
* Function:動態(tài)數(shù)據(jù)源
*
* @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
來保存了數(shù)據(jù)源赠涮,關于ThreadLocal
的知識點可以查看以下這篇文章:
解密ThreadLocal
之后在Spring
的配置文件中配置我們的數(shù)據(jù)源子寓,就是上文講到的為targetDataSources賦值
:
<bean id="ssm1DataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<!-- 指定連接數(shù)據(jù)庫的驅動 -->
<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" />
<!-- 配置監(jiān)控統(tǒng)計攔截的filters,去掉后監(jiān)控界面sql無法統(tǒng)計 -->
<property name="filters" value="stat" />
</bean>
<bean id="ssm2DataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<!-- 指定連接數(shù)據(jù)庫的驅動 -->
<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"/>
<!-- 配置監(jiān)控統(tǒng)計攔截的filters也殖,去掉后監(jiān)控界面sql無法統(tǒng)計 -->
<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>
<!--默認數(shù)據(jù)源-->
<property name="defaultTargetDataSource" ref="ssm1DataSource"/>
</bean>
這里分別配置了兩個數(shù)據(jù)源:ssm1DataSource
和ssm2DataSource
。
之后再通過Spring
的依賴注入方式將兩個數(shù)據(jù)源設置進targetDataSources
务热。
接下來的用法相比大家也應該猜到了忆嗜。
就是在每次調用數(shù)據(jù)庫之前我們都要先通過
DataSourceHolder
來設置當前的數(shù)據(jù)源浪漠。看下demo:
@Test
public void selectByPrimaryKey() throws Exception {
DataSourceHolder.setDataSources(Constants.DATASOURCE_TWO);
Datasource datasource = dataSourceService.selectByPrimaryKey(7);
System.out.println(JSON.toJSONString(datasource));
}
詳見我的單測霎褐。
使用起來也是非常簡單。但是不知道大家注意到?jīng)]有该镣,這樣的做法槽點很多:
- 每次使用需要手動切換冻璃,總有一些人會忘記寫(比如我)。
- 如果是后期需求變了损合,查詢其他的表了還得一個個改回來省艳。
那有沒有什么方法可以自動的幫我們切換呢?
肯定是有的嫁审,大家應該也想得到跋炕。就是利用Spring
的AOP
了。
自動切換數(shù)據(jù)源
首先要定義好我們的切面類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();
//獲取包名用于區(qū)分不同數(shù)據(jù)源
String whichDataSource = aClass.getName().substring(25, aClass.getName().lastIndexOf("."));
if ("ssmone".equals(whichDataSource)) {
DataSourceHolder.setDataSources(Constants.DATASOURCE_ONE);
} else {
DataSourceHolder.setDataSources(Constants.DATASOURCE_TWO);
}
}
/**
* 執(zhí)行后將數(shù)據(jù)源置為空
*/
public void after() {
DataSourceHolder.setDataSources(null);
}
}
邏輯也比較簡單律适,就是在執(zhí)行數(shù)據(jù)庫操作之前做一個切面辐烂。
- 通過
JoinPoint
對象獲取目標對象。 - 在目標對象中獲取包名來區(qū)分不同的數(shù)據(jù)源捂贿。
- 根據(jù)不同數(shù)據(jù)源來進行賦值纠修。
- 執(zhí)行完畢之后將數(shù)據(jù)源清空。
關于一些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(); //返回被通知方法參數(shù)列表
Signature getSignature(); //返回當前連接點簽名
SourceLocation getSourceLocation();//返回連接點方法所在類文件中的位置
String getKind(); //連接點類型
StaticPart getStaticPart(); //返回連接點靜態(tài)部分
}
為了通過包名
來區(qū)分不同數(shù)據(jù)源厂僧,我將目錄結構稍微調整了下:
將兩個不同的數(shù)據(jù)源的實現(xiàn)類放到不同的包中扣草,這樣今后如果還需要新增其他數(shù)據(jù)源也可以靈活的切換。
看下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"/>
<!--所有數(shù)據(jù)庫操作的方法加入切面-->
<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緩存的基礎上進行修改的颜屠。
這樣緩存和多數(shù)據(jù)源都滿足了辰妙。
實際使用:
@Test
public void selectByPrimaryKey() throws Exception {
Rediscontent rediscontent = rediscontentService.selectByPrimaryKey(30);
System.out.println(JSON.toJSONString(rediscontent));
}
這樣看起來就和使用一個數(shù)據(jù)源這樣簡單,再也不用關心切換的問題了甫窟。
總結
不過按照這樣的寫法是無法做到在一個事務里控制兩個數(shù)據(jù)源的密浑。這個我還在學習中,有相關經(jīng)驗的大牛不妨指點一下粗井。
個人博客地址:http://crossoverjie.top肴掷。
GitHub地址:https://github.com/crossoverJie。