javascript
Spring AOP之四:利用AOP实现动态数据源切换
2019獨角獸企業重金招聘Python工程師標準>>>
簡介和依賴
項目的前提是安裝了MySQL數據庫,并且建立了2個數據庫一個是master,一個是slave,并且這2個數據庫都有一個user表,表導出語句如下:
DROP TABLE IF EXISTS `user`; CREATE TABLE `user` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT,`name` varchar(20) NOT NULL COMMENT '用戶名,字母數字中文',`password` char(64) NOT NULL COMMENT '密碼,sha256加密',`nick_name` varchar(20) DEFAULT '' COMMENT '昵稱',`portrait` varchar(30) DEFAULT '' COMMENT '頭像,使用相對路徑',`status` enum('valid','invalid') DEFAULT 'valid' COMMENT 'valid有效,invalid無效',PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用戶表';數據庫中有一個密碼為123456的tim用戶對這2個庫有讀寫權限。你可以在配置文件中修改用戶名和密碼,也可以執行下面的語句授權給tim用戶:
grant select,insert,update,delete on slave.* to tim identified by '123456' grant select,insert,update,delete on master.* to tim identified by '123456'或者給tim全部權限(除了grant):
grant all privileges on slave.* to tim identified by '123456' grant all privileges on master.* to tim identified by '123456'還是來一張工程目錄的圖片吧:
Spring AbstractRoutingDataSource分析
我們使用org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource做動態數據源。
既然是數據源肯定直接或者間接的實現了javax.sql.DataSource接口,所以直接找getConnection方法就可以了。
我們可以看到AbstractRoutingDataSource#getConnection方法:
@Overridepublic Connection getConnection() throws SQLException {return determineTargetDataSource().getConnection();}沒有什么說的,直接看AbstractRoutingDataSource#determineTargetDataSource:
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;}determineCurrentLookupKey是一個抽象方法:
protected abstract Object determineCurrentLookupKey();resolvedDataSources是一個HashMap<String,Object> resolvedDefaultDataSource是一個DataSource
所以整個的邏輯就非常清楚了: AbstractRoutingDataSource是一個抽象類,我們只需要繼承它就可以了,然后提供一個包含多個數據源的HashMap,還可以提供一個默認的數據源resolvedDefaultDataSource,然后實現determineCurrentLookupKey返回一個String類型的key,通過這個key來找到一個對應的數據源。如果沒有找到就使用默認的數據源。
接下來我們就來通過繼承AbstractRoutingDataSource來實現一個動態數據源。
AbstractRoutingDataSource實現
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DataSourceKeyThreadHolder.getDataSourceKey();} } import org.springframework.util.Assert;public class DataSourceKeyThreadHolder {// 同一個線程持有相同的keyprivate static final ThreadLocal<String> dataSourcesKeyHolder = new ThreadLocal<String>();public static void setDataSourceKey(String customerType) {Assert.notNull(customerType, "DataSourceKey cannot be null");dataSourcesKeyHolder.set(customerType);}public static String getDataSourceKey() {return dataSourcesKeyHolder.get();}public static void clearDataSourceKey() {dataSourcesKeyHolder.remove();} }其實完全沒有必要拆分為2個類,雖然這樣可以讓DynamicDataSource的邏輯清晰一些,但是對于整個的來說并不一定是更加清晰的。
使用ThreadLocal讓每一個線程持有一個key也只是一種手段,也可以通過其他的方式實現。
下面我們來看一下數據源的配置文件,讓配置文件和DynamicDataSource對應起來:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:tx="http://www.springframework.org/schema/tx"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx-3.0.xsd"><bean id="design" abstract="true" class="com.alibaba.druid.pool.DruidDataSource"><property name="driverClassName" value="${common.driver}" /><!-- 配置初始化大小、最小、最大 --><property name="initialSize" value="10" /><property name="minIdle" value="10" /><property name="maxActive" value="60" /><!-- 從池中取連接的最大等待時間,單位ms --><property name="maxWait" value="3000" /><!-- 配置一個連接在池中最小生存的時間,單位是毫秒 --><property name="minEvictableIdleTimeMillis" value="300000" /><!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒 --><property name="timeBetweenEvictionRunsMillis" value="60000" /><!-- 測試語句 --><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="false" /><property name="maxPoolPreparedStatementPerConnectionSize" value="20" /><!-- 配置監控統計攔截的filters --><property name="filters" value="stat" /><!-- 打開removeAbandoned功能 --><property name="removeAbandoned" value="true" /><!-- 1800秒,也就是30分鐘 --><property name="removeAbandonedTimeout" value="1800" /><!-- 關閉abanded連接時輸出錯誤日志 --><property name="logAbandoned" value="true" /></bean><bean id="master" parent="design"><property name="username" value="${master.username}" /><property name="password" value="${master.password}" /><property name="url" value="${master.url}"/></bean><bean id="slave" parent="design"><property name="username" value="${slave.username}" /><property name="password" value="${slave.password}" /><property name="url" value="${slave.url}"/></bean><!-- 動態數據源 --><bean id="dynamicDataSource" class="cn.freemethod.datasource.DynamicDataSource"><property name="targetDataSources"><map key-type="java.lang.String"><entry key="master" value-ref="master"/><entry key="slave" value-ref="slave"/></map></property><!-- 默認數據源 --><property name="defaultTargetDataSource" ref="master"/></bean><bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><property name="dataSource" ref="dynamicDataSource" /><property name="configLocation" value="classpath:design-config.xml"/><property name="mapperLocations" value="classpath:mapper/design/*.xml"/></bean><bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate" ><constructor-arg index="0" ref="sqlSessionFactory" /></bean><!-- 配置mapper的映射掃描器 根據包中定義的接口自動生成dao的實現類--><bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"><property name="basePackage" value="cn.freemethod.dao.mapper"/></bean></beans>我們先看一下id為design這個bean,注意這個bean配置了abstract="true",表明這個bean是不會實例化的是用來被其他bean繼承的。這個bean使用的是com.alibaba.druid.pool.DruidDataSource。
接下來配置了2個數據源一個master一個slave,這2個bean繼承了design。
重點看id為dynamicDataSource的bean,這個就是我們繼承了AbstractRoutingDataSource的類,看到在屬性為targetDataSources的Map中注入了2個數據源master和slave,key也是master和slave,默認的數據源defaultTargetDataSource配置的是master。
所以在我們的DynamicDataSource的determineCurrentLookupKey中如果返回的是master就是使用的是master數據源,如果返回的是slave使用的就是slave數據源。
上面說到的都是和動態數據源有關的沒有使用到AOP啊,下面我們就介紹一下把AOP應用上。
利用AOP切換數據源
既然是利用AOP,那當然得有一個切面了,我們就先來看一下切面的代碼吧。
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component;@Component @Aspect public class DataSourceAspect {@Pointcut("@annotation(cn.freemethod.datasource.DataSourceKey)") // @Pointcut("this(cn.freemethod.service.UserService)")public void dataSourceKey() {}@Before("dataSourceKey() && @annotation(dataSourceKey)")public void doBefore(JoinPoint point,DataSourceKey dataSourceKey) { // MethodSignature signature = (MethodSignature) point.getSignature(); // Method method = signature.getMethod(); // DataSourceKey datasource = method.getAnnotation(DataSourceKey.class);if (dataSourceKey != null) {String sourceKey = DataSourceKey.master;if (dataSourceKey.value().equals(DataSourceKey.master)) {sourceKey = DataSourceKey.master;} else if (dataSourceKey.value().equals(DataSourceKey.slave)) {sourceKey = DataSourceKey.slave;}DataSourceKeyThreadHolder.setDataSourceKey(sourceKey);}}@After("dataSourceKey()")public void doAfter(JoinPoint point) {DataSourceKeyThreadHolder.clearDataSourceKey();} }首先我們要明確連接點,就是要在什么地方進行切換數據源操作,這里很明確了我們要在有DataSourceKey注解的方法上進行切換數據源。
根據我們前面學習的Pointcut表達式,我們很容易的就能寫出下面的表達式:
@Pointcut("@annotation(cn.freemethod.datasource.DataSourceKey)")通知邏輯也很簡單,我們只需要把線程的上下文的key設置為方法注解上獲取的數據源的key就可以了。方法執行之后再設置為之前的數據源。這樣在方法執行的過程中如果使用的數據源獲取到的就是方法注解上的配置對應的數據源。
看一下下面的實例怎樣使用吧:
@DataSourceKey("master")@Overridepublic int saveUserMaster(UserBean user) {return userBeanMapper.insertSelective(user);}測試代碼:
import javax.annotation.Resource;import org.apache.commons.codec.digest.DigestUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import cn.freemethod.config.AspectConfig; import cn.freemethod.dao.bean.design.UserBean; import cn.freemethod.service.UserService; import cn.freemethod.util.DataGenerateUtil; //@ContextConfiguration(locations = {"classpath:spring-base.xml"}) @ContextConfiguration(classes={AspectConfig.class}) @RunWith(SpringJUnit4ClassRunner.class) public class UserServiceImplTest {@ResourceUserService userServiceImpl;@Testpublic void testSaveUserMaster() {UserBean userBean = getUser();int actual = userServiceImpl.saveUserMaster(userBean);Assert.assertEquals(1, actual);}@Testpublic void testSaveUserSlave() {UserBean userBean = getUser();int actual = userServiceImpl.saveUserSlave(userBean);Assert.assertEquals(1, actual);}@Testpublic void testGetUser(){UserBean user = userServiceImpl.getUser(2);System.out.println(user);}private UserBean getUser(){UserBean userBean = new UserBean();userBean.setName(DataGenerateUtil.getAlphabet(3));userBean.setPassword(DigestUtils.sha256Hex(DataGenerateUtil.getAlnum(6)));return userBean;}}完整的代碼請下載參考中的完整工程代碼鏈接,這里之所以把測試類貼出來是因為有一個非常糾結的問題要講,你應該也搜不到相關的資料。所以如果感興趣的話最好把源碼下載下來,然后對比著測試一下。
細心的同學可能已經注意到了切面類DataSourceAspect中注釋的代碼:
// MethodSignature signature = (MethodSignature) point.getSignature(); // Method method = signature.getMethod(); // DataSourceKey datasource = method.getAnnotation(DataSourceKey.class);這里有2個矛盾的地方,一個是Spring中注入的類型只能是接口類型的如測試中的:
@Resource UserService userServiceImpl;如果替換為:
@Resource UserServiceImpl userServiceImpl;就會注入失敗。
但是在Spring AOP(使用AspectJ)中通過下面的代碼:
MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod();獲取的不是實際委派的方法,也就是說UserService注入的實際的類型是UserServiceImpl,但是通過UserService調用方法上面得到的簽名Method的是UserService的方法簽名。這個有什么影響呢?在上面的例子中最直接的影響就是userServiceImpl中的方法上的注解公共上面的方法獲取不到。
最開始我也是弄的我一愣一愣的,弄了很久,最后還是通過Advice 參數把注解注入到通知當中的。如下:
@Before("dataSourceKey() && @annotation(dataSourceKey)")關于Advice的Parameter可以參考后面的Spring AOP 之三:通知(Advice)方法參數這篇文章。
參考
項目碼云鏈接
完整工程代碼
Spring AOP 之一:基本概念與流程
Spring AOP 之二:Pointcut注解表達式
Spring AOP 之三:通知(Advice)方法參數
轉載于:https://my.oschina.net/u/2474629/blog/1083552
總結
以上是生活随笔為你收集整理的Spring AOP之四:利用AOP实现动态数据源切换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ❀❀ selenium 学习网站 ★★
- 下一篇: 三个简单的问题,让你顺势而为