【教程】Spring+Mybatis環境配置多資料來源

阿飛雲發表於2019-05-09

做一個積極的人

編碼、改bug、提升自己

我有一個樂園,面向程式設計,春暖花開!

一、簡要概述

在做專案的時候遇到需要從兩個資料來源獲取資料,專案使用的Spring + Mybatis環境,看到網上有一些關於多資料來源的配置,自己也整理學習一下,然後自動切換實現從不同的資料來源獲取資料功能。

二、程式碼詳解

2.1 DataSourceConstants 資料來源常量類

/**
 * 資料來源名稱常量類
 * 對應 application.xml 中 bean multipleDataSource
 * @author:dufy
 * @version:1.0.0
 * @date 2018/12/17
 */
public class DataSourceConstants {
    /**
     * 資料來源1,預設資料來源配置
     */
    public static final String DATASOURCE_1 = "dataSource1";
    /**
     * 資料來源2
     */
    public static final String DATASOURCE_2 = "dataSource2";


}
複製程式碼

2.2 DataSourceType 自定義資料來源註解

/**
 * 自定義資料來源型別註解
 *
 * @author:dufy
 * @version:1.0.0
 * @date 2018/12/17
 */

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceType {

    String value() default DataSourceConstants.DATASOURCE_1;

}
複製程式碼

2.3 MultipleDataSource 多資料來源配置類

MultipleDataSource 繼承 AbstractRoutingDataSource 類,為什麼繼承這個類就可以了?請看 第五章 :實現原理。

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 自定義多資料來源配置類
 *
 * @author:dufy
 * @version:1.0.0
 * @date 2018/12/17
 */
public class MultipleDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> dataSourceHolder = new ThreadLocal<String>();

    /**
     * 設定資料來源
     * @param dataSource 資料來源名稱
     */
    public static void setDataSource(String dataSource){
        dataSourceHolder.set(dataSource);
    }

    /**
     * 獲取資料來源
     * @return
     */
    public static String getDatasource() {
        return dataSourceHolder.get();
    }

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


    @Override
    protected Object determineCurrentLookupKey() {
        return dataSourceHolder.get();
    }
}

複製程式碼

2.4 MultipleDataSourceAop 多資料來源自動切換通知類

注意:請設定 @Order(0)。否則可能出現 資料來源切換失敗問題! 因為要在事務開啟之前就進行判斷,並進行切換資料來源!

/**
 * 多資料來源自動切換通知類<br>
 * <p>
 * 首先判斷當前類是否被該DataSourceType註解進行註釋,如果沒有指定註解,則採用預設的資料來源配置; <br>
 * 如果有,則讀取註解中的value值,將資料來源切到value指定的資料來源
 *
 * @author:dufy
 * @version:1.0.0
 * @date 2018/12/17
 */

@Aspect    // for aop
@Component // for auto scan
@Order(0)  // execute before @Transactional
public class MultipleDataSourceAop {

    private final Logger logger = Logger.getLogger(MultipleDataSourceAop.class);

    /**
     * 攔截 com.**.servicee中所有的方法,根據配置情況進行資料來源切換
     * com.jiuling.tz.service
     * com.jiuling.web.service
     * @param joinPoint
     * @throws Throwable
     */
    @Before("execution(* com.dufy.*.service.*.*(..))")
    public void changeDataSource(JoinPoint joinPoint) throws Throwable {

        try {
            // 攔截的實體類,就是當前正在執行的service
            Class<?> clazz = joinPoint.getTarget().getClass();
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            // 提取目標物件方法註解和型別註解中的資料來源標識
            Class<?>[] types = method.getParameterTypes();
            if (clazz.isAnnotationPresent(DataSourceType.class)) {
                DataSourceType source = clazz.getAnnotation(DataSourceType.class);
                MultipleDataSource.setDataSource(source.value());
                logger.info("Service Class 資料來源切換至--->" + source.value());
            }

            Method m = clazz.getMethod(method.getName(), types);
            if (m != null && m.isAnnotationPresent(DataSourceType.class)) {
                DataSourceType source = m.getAnnotation(DataSourceType.class);
                MultipleDataSource.setDataSource(source.value());
                logger.info("Service Method 資料來源切換至--->" + source.value());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 方法結束後
     */
    @After("execution(* com.dufy.*.service.*.*(..))")
    public void afterReturning() throws Throwable {
        try {
            MultipleDataSource.clearDataSource();
            logger.debug("資料來源已移除!");
        } catch (Exception e) {
            e.printStackTrace();
            logger.debug("資料來源移除報錯!");
        }

    }
}
複製程式碼

三、配置詳情

applicationContext.xml 中配置詳情

<?xml version="1.0" encoding="UTF-8"?>
<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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:p="http://www.springframework.org/schema/p"
       xmlns:util="http://www.springframework.org/schema/util" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xsi:schemaLocation="
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd
    http://www.springframework.org/schema/jdbc
    http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
    http://www.springframework.org/schema/cache
    http://www.springframework.org/schema/cache/spring-cache.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd
    http://www.springframework.org/schema/util
    http://www.springframework.org/schema/util/spring-util.xsd">

    <!-- 自動掃描包 ,將帶有註解的類 納入spring容器管理 -->
    <context:component-scan base-package="com.dufy"></context:component-scan>

    <!-- 引入配置檔案 -->
    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath*:jdbc.properties</value>
            </list>
        </property>
    </bean>

    <!-- dataSource1 配置 -->
    <bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 基本屬性 url、user、password -->
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${ds.initialSize}"/>
        <property name="minIdle" value="${ds.minIdle}"/>
        <property name="maxActive" value="${ds.maxActive}"/>

        <!-- 配置獲取連線等待超時的時間 -->
        <property name="maxWait" value="${ds.maxWait}"/>

        <!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連線,單位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${ds.timeBetweenEvictionRunsMillis}"/>

        <!-- 配置一個連線在池中最小生存的時間,單位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${ds.minEvictableIdleTimeMillis}"/>

        <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"/>
    </bean>

    <!-- dataSource2 配置-->
    <bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 基本屬性 url、user、password -->
        <property name="url" value="${jd.jdbc.url}"/>
        <property name="username" value="${jd.jdbc.username}"/>
        <property name="password" value="${jd.jdbc.password}"/>
		<!-- 其他配置省略 -->
    </bean>


    <!--多資料來源配置-->
    <bean id="multipleDataSource" class="com.jiuling.core.ds.MultipleDataSource">
        <property name="defaultTargetDataSource" ref="dataSource1" />
        <property name="targetDataSources">
            <map key-type = "java.lang.String">
                <entry key="dataSource1" value-ref="dataSource1"/>
                <entry key="dataSource2" value-ref="dataSource2"/>
                <!-- 這裡還可以加多個dataSource -->
            </map>
        </property>
    </bean>

    <!-- mybatis檔案配置,掃描所有mapper檔案 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" p:dataSource-ref="multipleDataSource"
          p:configLocation="classpath:mybatis-config.xml"
          p:mapperLocations="classpath:com/dufy/*/dao/*.xml"/>

    <!-- spring與mybatis整合配置,掃描所有dao -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" p:basePackage="com.dufy.*.dao"
          p:sqlSessionFactoryBeanName="sqlSessionFactory"/>

    <!-- 對dataSource 資料來源進行事務管理 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
          p:dataSource-ref="multipleDataSource"/>

    <!-- 事務管理 通知 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- 對insert,update,delete 開頭的方法進行事務管理,只要有異常就回滾 -->
            <tx:method name="insert*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <tx:method name="delete*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
            <!-- select,count開頭的方法,開啟只讀,提高資料庫訪問效能 -->
            <tx:method name="select*" read-only="true"/>
            <tx:method name="count*" read-only="true"/>
            <!-- 對其他方法 使用預設的事務管理 -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- 事務 aop 配置 -->
    <aop:config>
       <aop:pointcut id="serviceMethods" expression="execution(* com.dufy.*.service..*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/>
    </aop:config>

    <!-- 配置使Spring採用CGLIB代理 -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>

    <!-- 啟用對事務註解的支援 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
  
</beans>

複製程式碼

jdbc.properties 配置內容

##-------------mysql資料庫連線配置 ---------------------###
# dataSource1
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.1.110:3306/jdsc?useUnicode=true&characterEncoding=utf-8
jdbc.username=test
jdbc.password=123456

#配置初始化大小、最小、最大
ds.initialSize=1
ds.minIdle=1
ds.maxActive=20
#配置獲取連線等待超時的時間 
ds.maxWait=60000

#配置間隔多久才進行一次檢測,檢測需要關閉的空閒連線,單位是毫秒
ds.timeBetweenEvictionRunsMillis=60000

#配置一個連線在池中最小生存的時間,單位是毫秒
ds.minEvictableIdleTimeMillis=300000

# dataSource2
jd.jdbc.url=jdbc:mysql://192.168.1.120:3306/jdsc?useUnicode=true&characterEncoding=utf-8
jd.jdbc.username=root
jd.jdbc.password=123456

複製程式碼

四、測試切換

注意:測試服務類的包路徑,因為只有被AOP攔截的到的指定的Service才會進行資料來源的切換。

package com.dufy.web.service.impl.Data1ServiceImpl;

/**
 * 使用 dataSourc1 ,配置dataSourc1的資料來源
 * @author:duf
 * @version:1.0.0
 * @date 2018/12/17
 */
@Service
@DataSourceType(value = DataSourceConstants.DATASOURCE_1)
public class Data1ServiceImpl implements Data1Service {

    @Resource
    private Data1Mapper data1Mapper;

    @Override
    public List<String> selectCaseByUpdateTime(String name) {
        List<String> data1 = data1Mapper.selectData1(name);
        return data1;
    }

}

package com.dufy.web.service.impl.Data2ServiceImpl;
/**
 * 使用 dataSourc2 ,配置dataSourc2的資料來源
 * @author:duf
 * @version:1.0.0
 * @date 2018/12/17
 */
@Service
@DataSourceType(value = DataSourceConstants.DATASOURCE_2)
public class Data2ServiceImpl implements Data2Service {

    @Resource
    private Data2Mapper data2Mapper;

    @Override
    public List<String> selectCaseByUpdateTime(String name) {
        List<String> data2 = data2Mapper.selectData2(name);
        return data2;
    }

}

複製程式碼

通過測試後發現,兩個Service伺服器分別呼叫自己的配置的資料來源進行資料的獲取!

五、實現原理

基於AbstractRoutingDataSource 實現 多資料來源配置,通過AOP來進行資料來源的靈活切換。AOP的相關原理這裡不做說明,就簡單說一下 AbstractRoutingDataSource ,它是如何切換資料來源的!

首先我們繼承 AbstractRoutingDataSource 它是一個抽象類,然後要實現它裡面的一個抽象方法。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean 
複製程式碼

實現了InitializingBean,InitializingBean介面為bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是繼承該介面的類,在初始化bean的時候都會執行該方法。

AbstractRoutingDataSourceafterPropertiesSet方法的實現。

public void afterPropertiesSet() {
    if (this.targetDataSources == null) {
        throw new IllegalArgumentException("Property 'targetDataSources' is required");
    } else {
        this.resolvedDataSources = new HashMap(this.targetDataSources.size());
        Iterator var1 = this.targetDataSources.entrySet().iterator();

        while(var1.hasNext()) {
            Entry<Object, Object> entry = (Entry)var1.next();
            Object lookupKey = this.resolveSpecifiedLookupKey(entry.getKey());
            DataSource dataSource = this.resolveSpecifiedDataSource(entry.getValue());
            this.resolvedDataSources.put(lookupKey, dataSource);
        }

        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }

    }
}

複製程式碼

afterPropertiesSet方法將配置在 applicationContext.xml中的 targetDataSources 解析並構造出一個HashMap。

然後在實際過程中當需要訪問資料庫的時候,會首先獲取一個Connection,下面看一下獲取 Connection 的方法。

public Connection getConnection() throws SQLException {
    return this.determineTargetDataSource().getConnection();
}

public Connection getConnection(String username, String password) throws SQLException {
    return this.determineTargetDataSource().getConnection(username, password);
}

protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    // 這裡需要注意,通過子類的 determineCurrentLookupKey 方法 獲取 lookupKey
    Object lookupKey = this.determineCurrentLookupKey();
    // 轉換為對應的 DataSource 資料來源
    DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
        // 如果符合上述條件,則獲取預設的資料來源,也就是在 ApplicationContext.xml 配置的預設資料來源
        dataSource = this.resolvedDefaultDataSource;
    }

    if (dataSource == null) {
        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    } else {
        return dataSource;
    }
}
// 抽象方法,用於子類實現
protected abstract Object determineCurrentLookupKey();
複製程式碼

知道了 AbstractRoutingDataSource 的抽象方法後,通過AOP攔截,將Service上面配置不同的資料來源進行裝配到當前請求的ThreadLocal中, 最後 在獲取Connection的時候,就能通過determineCurrentLookupKey方法獲取到 設定的資料來源。

@Override
protected Object determineCurrentLookupKey() {
    return dataSourceHolder.get();
}
複製程式碼

再次強調必須保證切換資料來源的Aspect必須在@Transactional這個Aspect之前執行,使用@Order(0)來保證切換資料來源先於@Transactional執行)

六、參考文章

Spring, MyBatis 多資料來源的配置和管理

spring配置多資料來源涉及事務無法切換解決方案(絕對有效)


謝謝你的閱讀,如果您覺得這篇博文對你有幫助,請點贊或者喜歡,讓更多的人看到!祝你每天開心愉快!



不管做什麼,只要堅持下去就會看到不一樣!在路上,不卑不亢!

願你我在人生的路上能都變成最好的自己,能夠成為一個獨擋一面的人

【教程】Spring+Mybatis環境配置多資料來源

© 每天都在變得更好的阿飛雲

相關文章