Spring MVC Mybatis 多數據源配置

 

1. 繼承AbstractRoutingDataSource

AbstractRoutingDataSource 是spring提供的一個多數據源抽象類。spring會在使用事務的地方來調用此類的determineCurrentLookupKey()方法來獲取數據源的key值。咱們繼承此抽象類並實現此方法:java

package com.ctitc.collect.manage.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * 
 * @author zongbo
 * 實現spring多路由配置,由spring調用
 */
public class DataSourceRouter extends AbstractRoutingDataSource {
 
 // 獲取數據源名稱
 protected Object determineCurrentLookupKey() {
  return HandleDataSource.getDataSource();
 }

}

2. 線程內部數據源處理類

DataSourceRouter 類中經過HandleDataSource.getDataSource()獲取數據源的key值。此方法應該和線程綁定。spring

package com.ctitc.collect.manage.datasource;
/**
 * 線程相關的數據源處理類
 * @author zongbo
 *
 */
public class HandleDataSource {
 // 數據源名稱線程池
 private static final ThreadLocal<String> holder = new ThreadLocal<String>();

 /**
  * 設置數據源
  * @param datasource 數據源名稱
  */
 public static void setDataSource(String datasource) {
  holder.set(datasource);
 }
 /**
  * 獲取數據源
  * @return 數據源名稱
  */
 public static String getDataSource() {
  return holder.get();
 }
 /**
  * 清空數據源
  */
 public static void clearDataSource() {
  holder.remove();
 }
}

3. 自定義數據源註解類

對於spring來講,註解即簡單方便且可讀性也高。因此,咱們也經過註解在service的方法前指定所用的數據源。咱們先定義本身的註解類,其中value爲數據源的key值。sql

package com.ctitc.collect.manage.datasource;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 數據源註解類
 * @author zongbo
 *
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value();
}

4. AOP 攔截service並切換數據源

指定註解之後,咱們能夠經過AOP攔截全部service方法,在方法執行以前獲取方法上的註解:即數據源的key值。數據庫

package com.ctitc.collect.manage.datasource;

import java.lang.reflect.Method;
import java.text.MessageFormat;

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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * 切換數據源(不一樣方法調用不一樣數據源)
 */
@Aspect
@Component
@Order(1) //請注意:這裏order必定要小於tx:annotation-driven的order,即先執行DataSourceAspect切面,再執行事務切面,才能獲取到最終的數據源
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DataSourceAspect {
    static Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

    /**
     * 切入點 service包及子孫包下的全部類
     */
    @Pointcut("execution(* com.ctitc.collect.service..*.*(..))")
    public void aspect() {
    }

    /**
     * 配置前置通知,使用在方法aspect()上註冊的切入點
     */
    @Before("aspect()")
    public void before(JoinPoint point) {
        Class<?> target = point.getTarget().getClass();
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod() ;
        DataSource dataSource = null ;
        //從類初始化
        dataSource = this.getDataSource(target, method) ;
        //從接口初始化
        if(dataSource == null){
            for (Class<?> clazz : target.getInterfaces()) {
                dataSource = getDataSource(clazz, method);
                if(dataSource != null){
                    break ;//從某個接口中一旦發現註解,再也不循環
                }
            }
        }
        
        if(dataSource != null && !StringUtils.isEmpty(dataSource.value()) ){
            HandleDataSource.setDataSource(dataSource.value());
        }
    }

    @After("aspect()")
    public void after(JoinPoint point) {
        //使用完記得清空
        HandleDataSource.setDataSource(null);
    }
    
    
    /**
     * 獲取方法或類的註解對象DataSource
     * @param target    類class
     * @param method    方法
     * @return DataSource
     */
    public DataSource getDataSource(Class<?> target, Method method){
        try {
            //1.優先方法註解
            Class<?>[] types = method.getParameterTypes();
            Method m = target.getMethod(method.getName(), types);
            if (m != null && m.isAnnotationPresent(DataSource.class)) {
                return m.getAnnotation(DataSource.class);
            }
            //2.其次類註解
            if (target.isAnnotationPresent(DataSource.class)) {
                return target.getAnnotation(DataSource.class);
            }
            
        } catch (Exception e) {
            e.printStackTrace();
            logger.error(MessageFormat.format("經過註解切換數據源時發生異常[class={0},method={1}]:"
                    , target.getName(), method.getName()),e)  ;
        }
        return null ;
    }
}

5. 數據源配置

假設我有兩個庫:業務庫和訂單庫。先要配置這兩個數據源併發

  • 業務數據源
<bean id="busiDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
  <description>業務數據源</description>
     <!-- 數據庫基本信息配置 -->
        <property name="driverClassName" value="${busi.driverClassName}" />
        <property name="url" value="${busi.url}" />
        <property name="username" value="${busi.username}" />
        <property name="password" value="${busi.password}" />        
        <!-- 初始化鏈接數量 -->
        <property name="initialSize" value="${druid.initialSize}" />
        <!-- 最大併發鏈接數 -->
        <property name="maxActive" value="${druid.maxActive}" />
        <!-- 最小空閒鏈接數 -->
        <property name="minIdle" value="${druid.minIdle}" />
        <!-- 配置獲取鏈接等待超時的時間 -->     
        <property name="maxWait" value="${druid.maxWait}" />
        <!-- 超過期間限制是否回收 -->
        <property name="removeAbandoned" value="${druid.removeAbandoned}" />
        <!-- 超過期間限制多長; -->
        <property name="removeAbandonedTimeout" value="${druid.removeAbandonedTimeout}" />
        <!-- 配置間隔多久才進行一次檢測,檢測須要關閉的空閒鏈接,單位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" />
        <!-- 配置一個鏈接在池中最小生存的時間,單位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" />
        <!-- 用來檢測鏈接是否有效的sql,要求是一個查詢語句-->
        <property name="validationQuery" value="${druid.validationQuery}" />
        <!-- 申請鏈接的時候檢測 -->
        <property name="testWhileIdle" value="${druid.testWhileIdle}" />
        <!-- 申請鏈接時執行validationQuery檢測鏈接是否有效,配置爲true會下降性能 -->
        <property name="testOnBorrow" value="${druid.testOnBorrow}" />
        <!-- 歸還鏈接時執行validationQuery檢測鏈接是否有效,配置爲true會下降性能  -->
        <property name="testOnReturn" value="${druid.testOnReturn}" />
        <!-- 打開PSCache,而且指定每一個鏈接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="${druid.poolPreparedStatements}" />     
        <property name="maxPoolPreparedStatementPerConnectionSize" value="${druid.maxPoolPreparedStatementPerConnectionSize}" />
        <!--屬性類型是字符串,經過別名的方式配置擴展插件,經常使用的插件有:                 
                監控統計用的filter:stat
                日誌用的filter:log4j
               防護SQL注入的filter:wall -->
        <property name="filters" value="${druid.filters}" />  
 </bean>  
  • 訂單數據源
<bean id="orderDataSource" class="com.alibaba.druid.pool.DruidDataSource"  parent="busiDataSource">   
     <description>訂單數據源</description>
        <property name="driverClassName" value="${order.driverClassName}" />
        <property name="url" value="${order.url}" />
        <property name="username" value="${order.username}" />
        <property name="password" value="${order.password}" />        
        
 </bean>  
  • dataSource 則是剛剛實現的DataSourceRouter,且須要指定此類的 targetDataSources屬性和 defaultTargetDataSource屬性。

targetDataSources :數據源列表,key-value形式,即上面配置的兩個數據源
defaultTargetDataSource:默認數據源,若是未指定數據源 或者指定的數據源不存在的話 默認使用這個數據源app

<bean id="dataSource" class="com.ctitc.collect.manage.datasource.DataSourceRouter" lazy-init="true">
  <description>多數據源路由</description>
  <property name="targetDataSources">
   <map key-type="java.lang.String" value-type="javax.sql.DataSource">
    <!-- write -->
    <entry key="busi" value-ref="busiDataSource" />
    <entry key="order" value-ref="orderDataSource" />
   </map>
  </property>
  <!-- 默認數據源,若是未指定數據源 或者指定的數據源不存在的話 默認使用這個數據源 -->
  <property name="defaultTargetDataSource" ref="busiDataSource" />
  
 </bean>

6. AOP的順序問題

因爲我使用的註解式事務,和咱們的AOP數據源切面有一個順序的關係。數據源切換必須先執行,數據庫事務才能獲取到正確的數據源。因此要明確指定 註解式事務和 咱們AOP數據源切面的前後順序。ide

  • 咱們數據源切換的AOP是經過註解來實現的,只須要在AOP類上加上一個order(1)註解便可,其中1表明順序號。
  • 註解式事務的是經過xml配置啓動
<tx:annotation-driven transaction-manager="transactionManager"
  proxy-target-class="true" order="2" />

 

 

 

 
 

7. 示例Demo

在每一個service方法前使用@DataSource("數據源key")註解便可。性能

@Override
 @DataSource("busi")
 @Transactional(readOnly = true, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
 public List<BasVersion> test1() {
  // TODO Auto-generated method stub
  return coreMapper.getVersion();
 }
 
 @Override
 @DataSource("order")
 @Transactional(readOnly = true, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
 public List<BasVersion> test2() {
  // TODO Auto-generated method stub
  return coreMapper.getVersion();
 }
 
相關文章
相關標籤/搜索