Spring MVC+Mybatis 多數(shù)據(jù)源配置

隨著業(yè)務(wù)的不斷擴(kuò)張,應(yīng)用壓力逐漸增大,特別是數(shù)據(jù)庫(kù)。不論從讀寫(xiě)分離還是分庫(kù)的方法來(lái)提高應(yīng)用的性能,都需要涉及到多數(shù)據(jù)源問(wèn)題。本文主要介紹在Spring MVC+Mybatis下的多數(shù)據(jù)源配置。主要通過(guò)Spring提供的AbstractRoutingDataSource來(lái)實(shí)現(xiàn)多數(shù)據(jù)源。

1. 繼承AbstractRoutingDataSource

AbstractRoutingDataSource 是spring提供的一個(gè)多數(shù)據(jù)源抽象類。spring會(huì)在使用事務(wù)的地方來(lái)調(diào)用此類的determineCurrentLookupKey()方法來(lái)獲取數(shù)據(jù)源的key值。我們繼承此抽象類并實(shí)現(xiàn)此方法:

package com.ctitc.collect.manage.datasource;

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

}

2. 線程內(nèi)部數(shù)據(jù)源處理類

DataSourceRouter 類中通過(guò)HandleDataSource.getDataSource()獲取數(shù)據(jù)源的key值。此方法應(yīng)該和線程綁定。

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

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

3. 自定義數(shù)據(jù)源注解類

對(duì)于spring來(lái)說(shuō),注解即簡(jiǎn)單方便且可讀性也高。所以,我們也通過(guò)注解在service的方法前指定所用的數(shù)據(jù)源。我們先定義自己的注解類,其中value為數(shù)據(jù)源的key值。

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;

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

4. AOP 攔截service并切換數(shù)據(jù)源

指定注解以后,我們可以通過(guò)AOP攔截所有service方法,在方法執(zhí)行之前獲取方法上的注解:即數(shù)據(jù)源的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;

/**
 * 切換數(shù)據(jù)源(不同方法調(diào)用不同數(shù)據(jù)源)
 */
@Aspect
@Component
@Order(1) //請(qǐng)注意:這里order一定要小于tx:annotation-driven的order,即先執(zhí)行DataSourceAspect切面,再執(zhí)行事務(wù)切面,才能獲取到最終的數(shù)據(jù)源
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DataSourceAspect {
    static Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

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

    /**
     * 配置前置通知,使用在方法aspect()上注冊(cè)的切入點(diǎn)
     */
    @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 ;//從某個(gè)接口中一旦發(fā)現(xiàn)注解,不再循環(huán)
                }
            }
        }
        
        if(dataSource != null && !StringUtils.isEmpty(dataSource.value()) ){
            HandleDataSource.setDataSource(dataSource.value());
        }
    }

    @After("aspect()")
    public void after(JoinPoint point) {
        //使用完記得清空
        HandleDataSource.setDataSource(null);
    }
    
    
    /**
     * 獲取方法或類的注解對(duì)象DataSource
     * @param target    類class
     * @param method    方法
     * @return DataSource
     */
    public DataSource getDataSource(Class<?> target, Method method){
        try {
            //1.優(yōu)先方法注解
            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("通過(guò)注解切換數(shù)據(jù)源時(shí)發(fā)生異常[class={0},method={1}]:"
                    , target.getName(), method.getName()),e)  ;
        }
        return null ;
    }
}

5. 數(shù)據(jù)源配置

假設(shè)我有兩個(gè)庫(kù):業(yè)務(wù)庫(kù)和訂單庫(kù)。先要配置這兩個(gè)數(shù)據(jù)源

  • 業(yè)務(wù)數(shù)據(jù)源
<bean id="busiDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
  <description>業(yè)務(wù)數(shù)據(jù)源</description>
     <!-- 數(shù)據(jù)庫(kù)基本信息配置 -->
        <property name="driverClassName" value="${busi.driverClassName}" />
        <property name="url" value="${busi.url}" />
        <property name="username" value="${busi.username}" />
        <property name="password" value="${busi.password}" />        
        <!-- 初始化連接數(shù)量 -->
        <property name="initialSize" value="${druid.initialSize}" />
        <!-- 最大并發(fā)連接數(shù) -->
        <property name="maxActive" value="${druid.maxActive}" />
        <!-- 最小空閑連接數(shù) -->
        <property name="minIdle" value="${druid.minIdle}" />
        <!-- 配置獲取連接等待超時(shí)的時(shí)間 -->     
        <property name="maxWait" value="${druid.maxWait}" />
        <!-- 超過(guò)時(shí)間限制是否回收 -->
        <property name="removeAbandoned" value="${druid.removeAbandoned}" />
        <!-- 超過(guò)時(shí)間限制多長(zhǎng); -->
        <property name="removeAbandonedTimeout" value="${druid.removeAbandonedTimeout}" />
        <!-- 配置間隔多久才進(jìn)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" />
        <!-- 配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" />
        <!-- 用來(lái)檢測(cè)連接是否有效的sql,要求是一個(gè)查詢語(yǔ)句-->
        <property name="validationQuery" value="${druid.validationQuery}" />
        <!-- 申請(qǐng)連接的時(shí)候檢測(cè) -->
        <property name="testWhileIdle" value="${druid.testWhileIdle}" />
        <!-- 申請(qǐng)連接時(shí)執(zhí)行validationQuery檢測(cè)連接是否有效,配置為true會(huì)降低性能 -->
        <property name="testOnBorrow" value="${druid.testOnBorrow}" />
        <!-- 歸還連接時(shí)執(zhí)行validationQuery檢測(cè)連接是否有效,配置為true會(huì)降低性能  -->
        <property name="testOnReturn" value="${druid.testOnReturn}" />
        <!-- 打開(kāi)PSCache,并且指定每個(gè)連接上PSCache的大小 -->
        <property name="poolPreparedStatements" value="${druid.poolPreparedStatements}" />     
        <property name="maxPoolPreparedStatementPerConnectionSize" value="${druid.maxPoolPreparedStatementPerConnectionSize}" />
        <!--屬性類型是字符串,通過(guò)別名的方式配置擴(kuò)展插件,常用的插件有:                 
                監(jiān)控統(tǒng)計(jì)用的filter:stat
                日志用的filter:log4j
               防御SQL注入的filter:wall -->
        <property name="filters" value="${druid.filters}" />  
 </bean>  
  • 訂單數(shù)據(jù)源
<bean id="orderDataSource" class="com.alibaba.druid.pool.DruidDataSource"  parent="busiDataSource">   
     <description>訂單數(shù)據(jù)源</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 則是剛剛實(shí)現(xiàn)的DataSourceRouter,且需要指定此類的 targetDataSources屬性和 defaultTargetDataSource屬性。

targetDataSources :數(shù)據(jù)源列表,key-value形式,即上面配置的兩個(gè)數(shù)據(jù)源
defaultTargetDataSource:默認(rèn)數(shù)據(jù)源,如果未指定數(shù)據(jù)源 或者指定的數(shù)據(jù)源不存在的話 默認(rèn)使用這個(gè)數(shù)據(jù)源

<bean id="dataSource" class="com.ctitc.collect.manage.datasource.DataSourceRouter" lazy-init="true">
  <description>多數(shù)據(jù)源路由</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>
  <!-- 默認(rèn)數(shù)據(jù)源,如果未指定數(shù)據(jù)源 或者指定的數(shù)據(jù)源不存在的話 默認(rèn)使用這個(gè)數(shù)據(jù)源 -->
  <property name="defaultTargetDataSource" ref="busiDataSource" />
  
 </bean>

6. AOP的順序問(wèn)題

由于我使用的注解式事務(wù),和我們的AOP數(shù)據(jù)源切面有一個(gè)順序的關(guān)系。數(shù)據(jù)源切換必須先執(zhí)行,數(shù)據(jù)庫(kù)事務(wù)才能獲取到正確的數(shù)據(jù)源。所以要明確指定 注解式事務(wù)和 我們AOP數(shù)據(jù)源切面的先后順序。

  • 我們數(shù)據(jù)源切換的AOP是通過(guò)注解來(lái)實(shí)現(xiàn)的,只需要在AOP類上加上一個(gè)order(1)注解即可,其中1代表順序號(hào)。
  • 注解式事務(wù)的是通過(guò)xml配置啟動(dòng)
<tx:annotation-driven transaction-manager="transactionManager"
  proxy-target-class="true" order="2" />

7. 示例Demo

在每個(gè)service方法前使用@DataSource("數(shù)據(jù)源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();
 }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,695評(píng)論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,288評(píng)論 6 342
  • application的配置屬性。 這些屬性是否生效取決于對(duì)應(yīng)的組件是否聲明為Spring應(yīng)用程序上下文里的Bea...
    新簽名閱讀 5,545評(píng)論 1 27
  • 我覺(jué)得他總結(jié)得很好,尤其是心態(tài)和方法,非常值得學(xué)習(xí)。對(duì)正在找工作的同學(xué)非常有用。 本譯文的作者是方應(yīng)杭,禁止「饑人...
    最大化閱讀 612評(píng)論 0 4
  • 第一章 夢(mèng)回唐朝 我從加拿大回來(lái)后在機(jī)場(chǎng)看到一戴黑色鴨舌帽的男生,隨著他走來(lái),熙熙攘攘的人群涌來(lái):“?。⊥?..
    TFBOYS墨籽閱讀 636評(píng)論 6 17

友情鏈接更多精彩內(nèi)容