1. 程式人生 > 程式設計 >Spring AOP如何實現註解式的Mybatis多資料來源切換詳解

Spring AOP如何實現註解式的Mybatis多資料來源切換詳解

一、為什麼要使用多資料來源切換?

多資料來源切換是為了滿足什麼業務場景?正常情況下,一個微服務或者說一個WEB專案,在使用Mybatis作為資料庫連結和操作框架的情況下通常只需要構建一個系統庫,在該系統庫建立業務表來滿足需求,當然也有分為測試庫和正式庫dev/prod,不過這倆庫的切換是使用配置檔案進行切分的,在專案啟動時或者打成maven JAR包指定environment-dev.properties或者environment-prod.properties。

那麼當程式執行過程中,比如一個controller中既需要查詢資料庫A,又需要查詢資料庫B,而且兩者都希望用entity(Mybatis中用於與表結構保持一直的bean)來接收查詢結果,即都希望走Mybatis的entity-mapper-mapper.xml這麼一套框架。這個時候最原始的方法是在程式碼中手動連結資料庫比如:

 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事務和AOP方式的註解式資料來源切換。最終實現的效果如下:

 @Transactional //該註解表明該Service類開啟Spring事務,事務的意思是指具有原子性的一個操作集合(本人理解),該事務做什麼事在dao層的配置檔案裡配置,後面會講。


 @Service //表明為Service類,使用Component也行,Spring在啟動時會掃描該類將該類所需要的bean全部構建出來以供使用


 @TargetDataSource(name = "dataSource1") //重點,自定義的AOP註解,指定該TestService1類下的所有public方法都使用資料來源dataSource1
 class TestService1{
 public void queryAllUser(){
  UserMapper userMapper = new UserMapper()
  userMapper.queryAllUser();
  System.out.println("使用資料來源dataSource1查詢使用者資訊")
 }
 }

 @Transactional 
 @Service 
 @TargetDataSource(name = "dataSource2") 
 class TestService2{

 public void queryAllBook(){
  BookMapper bookMapper = new BookMapper()
  bookMapper.queryAllBook();
  System.out.println("使用資料來源dataSource2查詢書籍資訊")
 }
 }

在每一個需要切換資料來源的Service層使用TargetDataSource(name= “***”)即可指定當前執行緒的資料來源,當然別忘記@Transactional事務的新增,該事務用於Mybatis查詢資料時去獲取當前執行緒的資料來源為哪一個。如此在controller中正常呼叫Service中的方法就行了,如果需要查詢兩個資料庫那麼分別呼叫兩個TestService中的方法即可。比如:

 //本人目前使用scala語言作為開發語言,Java沒怎麼寫了,還是習慣Scala,以下程式還是使用Scala語言規範哈

 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查詢資料來源1中的所有使用者")
  val allBook = testService2.queryAllBook("33287")
  println("使用TestService2查詢資料來源2中的所有書籍資訊")
  }
 }

二、如何實現

接下來就詳細講述如何在Spring MVC和Mybatis的單套資料來源支援上擴充套件多資料來源切換能力。以下為雙資料來源,三資料來源的實現方式相同。

1.首先在配置檔案中新增第二個資料來源的連結資訊。

 environment-dev.properties
 #資料來源1的連結資訊
 db1.jdbc.username=xxx
 db1.jdbc.password=xxxxx
 db1.jdbc.driverClassName=com.mysql.jdbc.Driver
 db1.jdbc.url=xxxx?useUnicode=true&characterEncoding=utf8

 #新新增的資料來源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配置檔案中新增基於註解的事務管理以及AOP切面配置

(1)在配置檔案中新增雙資料來源,如下:

 <bean id="dataSource1" 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 id="dataSource2" 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實現動態資料來源選擇

配置檔案中新增

 <bean id="dataSource" 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>
  <!-- 預設使用dataSource1的資料來源 --> 
  <property name="defaultTargetDataSource" ref="dataSource1" />
 </bean>

在dao層建立dataSourceManage包,在包中建立如下類DynamicDataSource,DataSourceHolder。

類一:

 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
 public class DynamicDataSource extends AbstractRoutingDataSource {
 @Override
 protected Object determineCurrentLookupKey() {
  return DataSourceHolder.getDataSoure();
 }
 }

類二:

 public class DataSourceHolder {

 //執行緒本地環境
 private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();

 //設定資料來源
 public static void setDataSource(String customerType) {
  dataSources.set(customerType);
 }

 //獲取資料來源
 public static String getDataSoure() {
  return (String) dataSources.get();
 }

 //清除資料來源
 public static void clearDataSource() {
  dataSources.remove();
 }
 }

Spring boot提供了AbstractRoutingDataSource 根據使用者定義的規則選擇當前的資料來源,這樣我們可以在執行查詢之前,設定使用的資料來源。實現可動態路由的資料來源,在每次資料庫查詢操作前執行。它的抽象方法 determineCurrentLookupKey() 決定使用哪個資料來源。以上完成資料庫操作之前的資料來源選擇,使用的是DataSourceHolder.getDataSoure();

(3)新增Spring事務,確定在業務程式碼中查詢資料庫時,由Spring事務去執行以上對資料來源的選擇,這樣既不影響業務程式碼又能提供事務的性質保證。

在配置檔案中新增

 <!-- 定義事務管理器(宣告式的事務) -->
 <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
 <property name="dataSource" ref="dataSource" />
 </bean>
 <!-- 將所有具有@Transactional註解的Bean自動配置為宣告式事務支援 --> 
 <tx:annotation-driven transaction-manager="dataSourceTransactionManager" />

 <bean id="sqlSessionFactory" 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中使用的資料來源需要和事務配置中的保持一直。以及配置檔案的頂層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層註解式宣告使用的資料來源

首先在配置檔案中新增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 自定義的切面類實現資料來源切換-->
 <bean id="dataSourceExchange" class="common.dao.mysql.datasource.DataSourceExchange" />
 <!--配置AOP -->
 <aop:config>
  <!--配置切點表示式 定義dataSourceExchange中的攔截使用範圍-->
  <aop:pointcut id="servicePointcut" 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包下建立DataSourceExchange類實現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 o) throws Throwable {

  //這裡TargetDataSource是自定義註解,method為查詢資料庫的方法比如一中的queryAllUser(),Objects為傳給該方法的引數陣列,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包下建立TargetDataSource註解類

 import java.lang.annotation.*;

 @Target({ElementType.METHOD,ElementType.TYPE})
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 public @interface TargetDataSource {
 String name() default "dataSource1";
 }

以上配置完成之後即可達成一中的最終效果。

完整的dao配置檔案內容如下

 <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/aop    
    https://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 id="dataSource1" 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 id="dataSource2" 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 id="dataSource" 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>
   <!-- 預設使用dataSource1的資料來源 --> 
  <property name="defaultTargetDataSource" ref="dataSource1" />
 </bean>

 <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
 </bean>
 <tx:annotation-driven transaction-manager="dataSourceTransactionManager" />
 <bean id="sqlSessionFactory" 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>

 <!--配置可以批量執行的sqlSession -->
 <!--配置切面的bean -->
 <bean id="dataSourceExchange" class="test.common.dao.mysql.datasource.DataSourceExchange" />
 <!--配置AOP -->
 <aop:config>
 <!--配置切點表示式 -->
  <aop:pointcut id="servicePointcut" 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>

到此這篇關於Spring AOP如何實現註解式的Mybatis多資料來源切換的文章就介紹到這了,更多相關Spring AOP註解式的Mybatis多資料來源切換內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!