Spring AOP如何實現(xiàn)注解式的Mybatis多數(shù)據(jù)源切換詳解
一、為什么要使用多數(shù)據(jù)源切換?
多數(shù)據(jù)源切換是為了滿足什么業(yè)務(wù)場景?正常情況下,一個微服務(wù)或者說一個WEB項目,在使用Mybatis作為數(shù)據(jù)庫鏈接和操作框架的情況下通常只需要構(gòu)建一個系統(tǒng)庫,在該系統(tǒng)庫創(chuàng)建業(yè)務(wù)表來滿足需求,當(dāng)然也有分為測試庫和正式庫dev/prod,不過這倆庫的切換是使用配置文件進(jìn)行切分的,在項目啟動時或者打成maven JAR包指定environment-dev.properties或者environment-prod.properties。
那么當(dāng)程序運行過程中,比如一個controller中既需要查詢數(shù)據(jù)庫A,又需要查詢數(shù)據(jù)庫B,而且兩者都希望用entity(Mybatis中用于與表結(jié)構(gòu)保持一直的bean)來接收查詢結(jié)果,即都希望走M(jìn)ybatis的entity-mapper-mapper.xml這么一套框架。這個時候最原始的方法是在代碼中手動鏈接數(shù)據(jù)庫比如:
var conn:Connection = null try { Class.forName('com.mysql.jdbc.Driver') conn = DriverManager.getConnection('url','username','password') val statement = conn.createStatement() val result = statement.executeQuery('select * from **** where **** ') while(result.next()){ } }
本文所采用的是修改dao層context配置文件添加基于Spring事務(wù)和AOP方式的注解式數(shù)據(jù)源切換。最終實現(xiàn)的效果如下:
@Transactional //該注解表明該Service類開啟Spring事務(wù),事務(wù)的意思是指具有原子性的一個操作集合(本人理解),該事務(wù)做什么事在dao層的配置文件里配置,后面會講。 @Service //表明為Service類,使用Component也行,Spring在啟動時會掃描該類將該類所需要的bean全部構(gòu)建出來以供使用 @TargetDataSource(name = 'dataSource1') //重點,自定義的AOP注解,指定該TestService1類下的所有public方法都使用數(shù)據(jù)源dataSource1 class TestService1{ public void queryAllUser(){ UserMapper userMapper = new UserMapper() userMapper.queryAllUser(); System.out.println('使用數(shù)據(jù)源dataSource1查詢用戶信息') } } @Transactional @Service @TargetDataSource(name = 'dataSource2') class TestService2{ public void queryAllBook(){ BookMapper bookMapper = new BookMapper() bookMapper.queryAllBook(); System.out.println('使用數(shù)據(jù)源dataSource2查詢書籍信息') } }
在每一個需要切換數(shù)據(jù)源的Service層使用TargetDataSource(name= “***”)即可指定當(dāng)前線程的數(shù)據(jù)源,當(dāng)然別忘記@Transactional事務(wù)的添加,該事務(wù)用于Mybatis查詢數(shù)據(jù)時去獲取當(dāng)前線程的數(shù)據(jù)源為哪一個。如此在controller中正常調(diào)用Service中的方法就行了,如果需要查詢兩個數(shù)據(jù)庫那么分別調(diào)用兩個TestService中的方法即可。比如:
//本人目前使用scala語言作為開發(fā)語言,Java沒怎么寫了,還是習(xí)慣Scala,以下程序還是使用Scala語言規(guī)范哈 class testController{ @AutoWired TestService1 testService1; @AutoWired TestService2 testService2; @RequestMapping(value = Array('/test'), produces = Array('application/json;charset=UTF-8'), method = Array(RequestMethod.GET)) def test(): Unit = { val allUser = testService1.queryAllUser() println('使用TestService1查詢數(shù)據(jù)源1中的所有用戶') val allBook = testService2.queryAllBook('33287') println('使用TestService2查詢數(shù)據(jù)源2中的所有書籍信息') } }
二、如何實現(xiàn)
接下來就詳細(xì)講述如何在Spring MVC和Mybatis的單套數(shù)據(jù)源支持上擴(kuò)展多數(shù)據(jù)源切換能力。以下為雙數(shù)據(jù)源,三數(shù)據(jù)源的實現(xiàn)方式相同。
1.首先在配置文件中添加第二個數(shù)據(jù)源的鏈接信息。
environment-dev.properties #數(shù)據(jù)源1的鏈接信息 db1.jdbc.username=xxx db1.jdbc.password=xxxxx db1.jdbc.driverClassName=com.mysql.jdbc.Driver db1.jdbc.url=xxxx?useUnicode=true&characterEncoding=utf8 #新添加的數(shù)據(jù)源2的鏈接信息 db2.jdbc.username=xxx db2.jdbc.password=xxxxx db2.jdbc.driverClassName=com.mysql.jdbc.Driver db2.jdbc.url=xxxx?useUnicode=true&characterEncoding=utf8
2.在dao層的context.xml配置文件中添加基于注解的事務(wù)管理以及AOP切面配置
(1)在配置文件中添加雙數(shù)據(jù)源,如下:
<bean class='com.alibaba.druid.pool.DruidDataSource'> <property name='driverClassName' value='${db1.jdbc.driverClassName}'/> <property name='password' value='${db1.jdbc.password}'/> <property name='username' value='${db1.jdbc.username}'/> <property name='url' value='${db1.jdbc.url}'/> <property name='initialSize' value='5'/> <property name='maxActive' value='10'/> </bean> <bean class='com.alibaba.druid.pool.DruidDataSource'> <property name='driverClassName' value='${db2.jdbc.driverClassName}'/> <property name='password' value='${db2.jdbc.password}'/> <property name='username' value='${db2.jdbc.username}'/> <property name='url' value='${db2.jdbc.url}'/> <property name='initialSize' value='5'/> <property name='maxActive' value='10'/> </bean>
(2)使用AbstractRoutingDataSource實現(xiàn)動態(tài)數(shù)據(jù)源選擇
配置文件中添加
<bean class='common.dao.mysql.dataSourceManage.DynamicDataSource'> <property name='targetDataSources'> <map key-type='java.lang.String'> <entry key='dataSource1' value-ref='dataSource1' /> <entry key='dataSource2' value-ref='dataSource2' /> </map> </property> <!-- 默認(rèn)使用dataSource1的數(shù)據(jù)源 --> <property name='defaultTargetDataSource' ref='dataSource1' /> </bean>
在dao層創(chuàng)建dataSourceManage包,在包中創(chuàng)建如下類DynamicDataSource,DataSourceHolder。
類一:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceHolder.getDataSoure(); } }
類二:
public class DataSourceHolder { //線程本地環(huán)境 private static final ThreadLocal<String> dataSources = new ThreadLocal<String>(); //設(shè)置數(shù)據(jù)源 public static void setDataSource(String customerType) { dataSources.set(customerType); } //獲取數(shù)據(jù)源 public static String getDataSoure() { return (String) dataSources.get(); } //清除數(shù)據(jù)源 public static void clearDataSource() { dataSources.remove(); } }
Spring boot提供了AbstractRoutingDataSource 根據(jù)用戶定義的規(guī)則選擇當(dāng)前的數(shù)據(jù)源,這樣我們可以在執(zhí)行查詢之前,設(shè)置使用的數(shù)據(jù)源。實現(xiàn)可動態(tài)路由的數(shù)據(jù)源,在每次數(shù)據(jù)庫查詢操作前執(zhí)行。它的抽象方法 determineCurrentLookupKey() 決定使用哪個數(shù)據(jù)源。以上完成數(shù)據(jù)庫操作之前的數(shù)據(jù)源選擇,使用的是DataSourceHolder.getDataSoure();
(3)添加Spring事務(wù),確定在業(yè)務(wù)代碼中查詢數(shù)據(jù)庫時,由Spring事務(wù)去執(zhí)行以上對數(shù)據(jù)源的選擇,這樣既不影響業(yè)務(wù)代碼又能提供事務(wù)的性質(zhì)保證。
在配置文件中添加
<!-- 定義事務(wù)管理器(聲明式的事務(wù)) --> <bean class='org.springframework.jdbc.datasource.DataSourceTransactionManager'> <property name='dataSource' ref='dataSource' /> </bean> <!-- 將所有具有@Transactional注解的Bean自動配置為聲明式事務(wù)支持 --> <tx:annotation-driven transaction-manager='dataSourceTransactionManager' /> <bean class='org.mybatis.spring.SqlSessionFactoryBean'> <property name='dataSource' ref='dataSource'/> <property name='mapperLocations'> <list> <value>classpath:common/dao/mysql/mapper/*Mapper.xml</value> </list> </property> </bean>
注意配置sqlSessionFactory中使用的數(shù)據(jù)源需要和事務(wù)配置中的保持一直。以及配置文件的頂層bean需要添加 xmlns:tx='http://www.springframework.org/schema/tx'和xsi:schemaLocation中添加http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
(4)配置AOP提供Service層注解式聲明使用的數(shù)據(jù)源
首先在配置文件中添加AOP支持xmlns:aop='http://www.springframework.org/schema/aop',xsi:schemaLocation中添加http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
<!--配置切面的bean DataSourceExchange 自定義的切面類實現(xiàn)數(shù)據(jù)源切換--> <bean /> <!--配置AOP --> <aop:config> <!--配置切點表達(dá)式 定義dataSourceExchange中的攔截使用范圍--> <aop:pointcut expression='execution(* common.dao.mysql.service.*.*(..))'/> <aop:advisor advice-ref='dataSourceExchange' pointcut-ref='servicePointcut' order='1' /> </aop:config>
其中execution(* common.dao.mysql.service.*.*(..))為service下的所有類(指TestService1和TestService2)的所有public方法都加上切面代理即使用dataSourceExchange處理。
然后在dataSourceManage包下創(chuàng)建DataSourceExchange類實現(xiàn)AfterReturningAdvice,MethodBeforeAdvice兩個aop通知
import java.lang.reflect.Method; import org.springframework.aop.AfterReturningAdvice; import org.springframework.aop.MethodBeforeAdvice; public class DataSourceExchange implements MethodBeforeAdvice, AfterReturningAdvice { @Override public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable { DataSourceHolder.clearDataSource(); } @Override public void before(Method method, Object[] objects, Object o) throws Throwable { //這里TargetDataSource是自定義注解,method為查詢數(shù)據(jù)庫的方法比如一中的queryAllUser(),Objects為傳給該方法的參數(shù)數(shù)組,o為調(diào)用該方法的對象,比如val allUser =//testService1.queryAllUser()中的testService1 if (method.isAnnotationPresent(TargetDataSource.class)) { TargetDataSource dataSource = method.getAnnotation(TargetDataSource.class); DataSourceHolder.setDataSource(dataSource.name()); } else { if (o.getClass().isAnnotationPresent(TargetDataSource.class)) { TargetDataSource dataSource = o.getClass().getAnnotation(TargetDataSource.class); DataSourceHolder.setDataSource(dataSource.name()); } } } }
然后在dataSourceManage包下創(chuàng)建TargetDataSource注解類
import java.lang.annotation.*; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TargetDataSource { String name() default 'dataSource1'; }
以上配置完成之后即可達(dá)成一中的最終效果。
完整的dao配置文件內(nèi)容如下
<beans xmlns='http://www.springframework.org/schema/beans' xmlns:context='http://www.springframework.org/schema/context' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:tx='http://www.springframework.org/schema/tx' xmlns:aop='http://www.springframework.org/schema/aop' xsi:schemaLocation='http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/aophttps://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd '> <context:annotation-config/> <context:component-scan base-package='com.test.common.dao'/> <bean class='com.alibaba.druid.pool.DruidDataSource'> <property name='driverClassName' value='${db1.jdbc.driverClassName}'/> <property name='password' value='${db1.jdbc.password}'/> <property name='username' value='${db1.jdbc.username}'/> <property name='url' value='${db1.jdbc.url}'/> <property name='initialSize' value='5'/> <property name='maxActive' value='10'/> </bean> <bean class='com.alibaba.druid.pool.DruidDataSource'> <property name='driverClassName' value='${db2.jdbc.driverClassName}'/> <property name='password' value='${db2.jdbc.password}'/> <property name='username' value='${db2.jdbc.username}'/> <property name='url' value='${db2.jdbc.url}'/> <property name='initialSize' value='5'/> <property name='maxActive' value='10'/> </bean> <bean class='test.common.dao.mysql.dataSourceManage.DynamicDataSource'> <property name='targetDataSources'> <map key-type='java.lang.String'> <entry key='dataSource1' value-ref='dataSource1' /> <entry key='dataSource2' value-ref='dataSource2' /> </map> </property> <!-- 默認(rèn)使用dataSource1的數(shù)據(jù)源 --> <property name='defaultTargetDataSource' ref='dataSource1' /> </bean> <bean class='org.springframework.jdbc.datasource.DataSourceTransactionManager'> <property name='dataSource' ref='dataSource' /> </bean> <tx:annotation-driven transaction-manager='dataSourceTransactionManager' /> <bean class='org.mybatis.spring.SqlSessionFactoryBean'> <property name='dataSource' ref='dataSource'/> <property name='mapperLocations'> <list> <value>classpath:test/common/dao/mysql/mapper/*Mapper.xml</value> </list> </property> </bean> <!--配置可以批量執(zhí)行的sqlSession --> <!--配置切面的bean --> <bean /> <!--配置AOP --> <aop:config> <!--配置切點表達(dá)式 --> <aop:pointcut expression='execution(* test.common.dao.mysql.service.*.*(..))'/> <aop:advisor advice-ref='dataSourceExchange' pointcut-ref='servicePointcut' order='1' /> </aop:config> <bean class='org.mybatis.spring.mapper.MapperScannerConfigurer'> <property name='basePackage' value='test.common.dao'/> <property name='sqlSessionFactoryBeanName' value='sqlSessionFactory'/> </bean> </beans>
到此這篇關(guān)于Spring AOP如何實現(xiàn)注解式的Mybatis多數(shù)據(jù)源切換的文章就介紹到這了,更多相關(guān)Spring AOP注解式的Mybatis多數(shù)據(jù)源切換內(nèi)容請搜索好吧啦網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持好吧啦網(wǎng)!
相關(guān)文章:
