MyBatis配置多資料來源

RebeccaZhong發表於2019-04-28

分類

MyBatis的多資料來源配置分2種,

  1. 多資料來源配置:兩個庫業務互不相干,a方法使用a庫的資料,b方法使用b庫的資料;
  2. 動態資料來源配置:兩個庫業務有關聯,如讀寫分離庫。

第一種直接配置2個單獨的資料來源,不同模組引入不同的sqlSessionFactory即可;第二種需要配置可動態切換的資料來源。

準備工作

兩種方式都需要在pom.xml檔案引入不同資料庫需要的jar包,此處以MySQL和Oracle為例:

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.40</version>
</dependency>
<dependency>
  <groupId>com.oracle</groupId>
  <artifactId>ojdbc</artifactId>
  <version>7</version>
</dependency>
複製程式碼

此處Oracle的jar包因為版權問題,在maven中央倉庫中沒有,需要手動安裝。 安裝教程

多資料來源配置

在applicationContext.xml中配置兩套資料來源即可

<!-- ===============第一個資料來源的配置開始=============== -->
<!--MySQL的資料庫配置-->
<bean id="mysqlDtaSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="driverClassName" value="${mysql.driverClassName}"/>
    <property name="url" value="${mysql.url}"/>
    <property name="username" value="${mysql.username}"/>
    <property name="password" value="${mysql.password}"/>
    <!-- 初始化連線大小 -->
    <property name="initialSize" value="${mysql.druid.initialSize}"></property>
    <!-- 連線池最大數量 -->
    <property name="maxActive" value="${mysql.druid.maxActive}"></property>
    <!-- 連線池最大空閒 -->
    <property name="maxIdle" value="${mysql.druid.maxIdle}"></property>
    <!-- 連線池最小空閒 -->
    <property name="minIdle" value="${mysql.druid.minIdle}"></property>
    <!-- 獲取連線最大等待時間 -->
    <property name="maxWait" value="${mysql.druid.maxWait}"></property>
</bean>
<!--配置MySQL的SqlSessionFactory-->
<bean id="sqlSessionFactoryMySQL" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="mysqlDtaSource"/>
    <property name="configLocation" value="classpath*:mybatis-config.xml" />
    <!-- 自動掃描mapping.xml檔案 -->
    <property name="mapperLocations" value="classpath*:com/rebecca/mybatismutildb/mysql/**/dao/mapper/*Mapper.xml"/>
    <property name="typeAliasesPackage" value="com.rebecca.mybatismutildb.mysql.**"/>
</bean>
<!--告訴框架掃描指定包中的mapper介面,然後為其自動的生成代理物件-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.rebecca.mybatismutildb.mysql.**.dao"/>
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryMySQL"/>
</bean>
<!--配置事務管理器-->
<bean id="mysqlTxManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="mysqlDtaSource"/>
</bean>
<!--配置事務的傳播特性-->
<tx:advice id="txAdvice" transaction-manager="mysqlTxManager">
    <tx:attributes>
        <tx:method name="get*" read-only="true"/>
        <tx:method name="list*" read-only="true"/>
        <tx:method name="query*" read-only="true"/>
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>
<!-- ===============第一個資料來源的配置結束=============== -->

<!-- ===============第二個資料來源的配置開始=============== -->
<!--Oracle的資料庫配置-->
<bean id="oracleDataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${oracle.driverClassName}" />
    <property name="url" value="${oracle.url}" />
    <property name="username" value="${oracle.username}" />
    <property name="password" value="${oracle.password}" />
    <!-- 初始化連線大小 -->
    <property name="initialSize" value="${oracle.druid.initialSize}"></property>
    <!-- 連線池最大數量 -->
    <property name="maxActive" value="${oracle.druid.maxActive}"></property>
    <!-- 連線池最大空閒 -->
    <property name="maxIdle" value="${oracle.druid.maxIdle}"></property>
    <!-- 連線池最小空閒 -->
    <property name="minIdle" value="${oracle.druid.minIdle}"></property>
    <!-- 獲取連線最大等待時間 -->
    <property name="maxWait" value="${oracle.druid.maxWait}"></property>
</bean>
<!--配置SqlSessionFactory-->
<bean id="sqlSessionFactoryOracle" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="oracleDataSource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml" />
    <!-- 自動掃描mapping.xml檔案 -->
    <property name="mapperLocations" value="classpath*:com/rebecca/mybatismutildb/oracle/**/dao/mapper/*Mapper.xml">
    </property>
    <property name="typeAliasesPackage" value="com.rebecca.mybatismutildb.oracle.**"/>
</bean>
<!--告訴框架掃描指定包中的mapper介面,然後為其自動的生成代理物件-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.rebecca.mybatismutildb.oracle.**.dao"/>
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryOracle"/>
</bean>

<!--配置事務管理器-->
<bean id="txManagerOracle" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="oracleDataSource"/>
</bean>

<!--配置事務的傳播特性-->
<tx:advice id="txAdviceOracle" transaction-manager="txManagerOracle">
    <tx:attributes>
        <tx:method name="get*" read-only="true"/>
        <tx:method name="list*" read-only="true"/>
        <tx:method name="query*" read-only="true"/>
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>
<!-- ===============第二個資料來源的配置結束=============== -->
複製程式碼

此處第一個資料來源指定去解析com/rebecca/mybatismutildb/mysql/**/dao/mapper包下的*Mapper.xml檔案,即指定這些dao的方法去連MySQL的資料來源;第二個資料來源指定去解析com/rebecca/mybatismutildb/oracle/**/dao/mapper包下的*Mapper.xml檔案,即指定這些dao的方法去連Oracle的資料來源。

applicationContext.xml配置好後,直接在指定的包內編碼即可。

動態資料來源配置

  1. 在applicationContext.xml中配置動態切換資料來源

<!--MySQL的資料庫配置-->
<bean id="mysqlDtaSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
    <property name="driverClassName" value="${mysql.driverClassName}"/>
    <property name="url" value="${mysql.url}"/>
    <property name="username" value="${mysql.username}"/>
    <property name="password" value="${mysql.password}"/>
    <!-- 初始化連線大小 -->
    <property name="initialSize" value="${mysql.druid.initialSize}"></property>
    <!-- 連線池最大數量 -->
    <property name="maxActive" value="${mysql.druid.maxActive}"></property>
    <!-- 連線池最大空閒 -->
    <property name="maxIdle" value="${mysql.druid.maxIdle}"></property>
    <!-- 連線池最小空閒 -->
    <property name="minIdle" value="${mysql.druid.minIdle}"></property>
    <!-- 獲取連線最大等待時間 -->
    <property name="maxWait" value="${mysql.druid.maxWait}"></property>
</bean>
<!--Oracle的資料庫配置-->
<bean id="oracleDataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${oracle.driverClassName}" />
    <property name="url" value="${oracle.url}" />
    <property name="username" value="${oracle.username}" />
    <property name="password" value="${oracle.password}" />
    <!-- 初始化連線大小 -->
    <property name="initialSize" value="${oracle.druid.initialSize}"></property>
    <!-- 連線池最大數量 -->
    <property name="maxActive" value="${oracle.druid.maxActive}"></property>
    <!-- 連線池最大空閒 -->
    <property name="maxIdle" value="${oracle.druid.maxIdle}"></property>
    <!-- 連線池最小空閒 -->
    <property name="minIdle" value="${oracle.druid.minIdle}"></property>
    <!-- 獲取連線最大等待時間 -->
    <property name="maxWait" value="${oracle.druid.maxWait}"></property>
</bean>
<!--告訴框架掃描指定包中的mapper介面,然後為其自動的生成代理物件-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.rebecca.mybatismutildb.mysql.**.dao,com.rebecca.mybatismutildb.oracle.**.dao"/>
</bean>
<!--配置事務管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!--配置事務的傳播特性-->
<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
        <tx:method name="get*" read-only="true"/>
        <tx:method name="list*" read-only="true"/>
        <tx:method name="query*" read-only="true"/>
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

<!--配置SqlSessionFactory(此處自定義實現SqlSessionFactoryBean,以支援mapperLocations的萬用字元配置)-->
<bean id="sqlSessionFactory" class="com.rebecca.mybatismutildb.db.PackagesSqlSessionFactoryBean" scope="prototype">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml" />
    <!-- 自動掃描mapping.xml檔案 -->
    <property name="mapperLocations">
        <list>
            <value>classpath*:com/rebecca/mybatismutildb/mysql/**/dao/mapper/*Mapper.xml</value>
            <value>classpath*:com/rebecca/mybatismutildb/oracle/**/dao/mapper/*Mapper.xml</value>
        </list>
    </property>
    <property name="typeAliasesPackage" value="com.rebecca.mybatismutildb.**"/>
</bean>
<!--動態配置資料來源-->
<bean id="dataSource" class="com.rebecca.mybatismutildb.db.DynamicDataSource" >
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <!--通過不同的key決定用哪個dataSource-->
            <entry value-ref="mysqlDtaSource" key="mysqlDtaSource"></entry>
            <entry value-ref="oracleDataSource" key="oracleDataSource"></entry>
        </map>
    </property>
    <!--設定預設的dataSource-->
    <property name="defaultTargetDataSource" ref="mysqlDtaSource">
    </property>
</bean>

<!--動態資料來源切換 -->
<bean id="dataSourceAspect" class="com.rebecca.mybatismutildb.db.DataSourceAspect" />
<!--動態資料來源切換 spring的aop切面-->
<aop:config>
    <!--請設定 @Order(0)。否則可能出現 資料來源切換失敗問題! 因為要在事務開啟之前就進行判斷,並進行切換資料來源!-->
    <aop:aspect ref="dataSourceAspect" order="0">
        <!--expression中要配置多個包時,注意用or連線,如:execution(* com.rebecca.mybatismutildb.mysql..dao..*.*(..)) or execution(* com.rebecca.mybatismutildb.oracle..dao..*.*(..)) ; 即攔截com.rebecca.mybatismutildb.mysql和com.rebecca.mybatismutildb.oracle下所有的dao包 -->
        <aop:pointcut id="dataSourcePointcut" expression="execution(* com.rebecca.mybatismutildb..dao..*.*(..)) "/>
        <aop:before pointcut-ref="dataSourcePointcut" method="changeDataSource" />
        <aop:after pointcut-ref="dataSourcePointcut" method="removeDataSource" />
    </aop:aspect>
</aop:config>

複製程式碼
  1. applicationContext.xml中用到的相關類
  • DataSource 註解
package com.rebecca.mybatismutildb.db;

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

/**
 * DataSource的註解
 * @Author: rebecca
 * @Date: Created in 2019/4/25 17:27
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DataSource {
    String value();
}
複製程式碼
  • DataSourceAspect 切面
package com.rebecca.mybatismutildb.db;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;

/**
 * 攔截所有帶有註解@DataSource的類和方法
 * @Author: rebecca
 * @Date: Created in 2019/4/25 17:28
 */
public class DataSourceAspect {
    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
    /**
     * @Title: changeDataSource
     * @Description: 攔截目標方法,獲取由@DataSource指定的資料來源標識,設定到執行緒儲存中以便切換資料來源
     * @param joinPoint
     * @return: void
     */
    public void changeDataSource(JoinPoint joinPoint){
        Class<?> target = joinPoint.getTarget().getClass();
        MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
        for(Class<?> clazz : target.getInterfaces()){
            resolveDataSource(clazz,methodSignature.getMethod());
        }
        resolveDataSource(target,methodSignature.getMethod());
        logger.debug("資料來源切換至--->", DataSourceContextHolder.getDbType());
    }

    private void resolveDataSource(Class<?> clazz, Method method){
        try {
            if(clazz.isAnnotationPresent(DataSource.class)){
                DataSource source = clazz.getAnnotation(DataSource.class);
                DataSourceContextHolder.setDbType(source.value());
            }

            Class<?>[] types = method.getParameterTypes();
            Method m = clazz.getDeclaredMethod(method.getName(), types);
            if(null != m && m.isAnnotationPresent(DataSource.class)){
                DataSource source = m.getAnnotation(DataSource.class);
                DataSourceContextHolder.setDbType(source.value());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void removeDataSource() {
        try {
            DataSourceContextHolder.clearDbType();
            logger.debug("資料來源已移除!");
        } catch (Exception e) {
            e.printStackTrace();
            logger.debug("資料來源移除報錯!", e);
        }
    }
}

複製程式碼
  • DataSourceContextHolder 類
package com.rebecca.mybatismutildb.db;

/**
 * @Author: rebecca
 * @Description:
 * @Date: Created in 2019/3/28 11:43
 * @Modified By:
 */
public class DataSourceContextHolder {
    // 注意此處的屬性值要與applicationContext.xml中配置的一致
    public static final String DATA_SOURCE_MYSQL = "mysqlDtaSource";
    public static final String DATA_SOURCE_ORACLE = "oracleDataSource";
    //用ThreadLocal來設定當前執行緒使用哪個dataSource
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    public static void setDbType(String customerType) {
        contextHolder.set(customerType);
    }

    public static String getDbType() {
        return contextHolder.get();
    }

    public static void clearDbType() {
        contextHolder.remove();
    }
}

複製程式碼
  • DynamicDataSource 類
package com.rebecca.mybatismutildb.db;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.logging.Logger;

/**
 * 動態切換資料來源
 * @Author: rebecca
 * @Description:
 * @Date: Created in 2019/3/28 11:42
 * @Modified By:
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDbType();
    }

    @Override
    public Logger getParentLogger() {
        return null;
    }
}
複製程式碼
  • PackagesSqlSessionFactoryBean 類(自定義實現SqlSessionFactoryBean,以支援mapperLocations的萬用字元配置)
package com.rebecca.mybatismutildb.db;

import org.apache.commons.lang.StringUtils;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * 支援包名的萬用字元解析
 * @Author: rebecca
 * @Description:
 * @Date: Created in 2019/3/28 15:30
 * @Modified By:
 */
public class PackagesSqlSessionFactoryBean extends SqlSessionFactoryBean {

    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    private Logger logger = LoggerFactory.getLogger(PackagesSqlSessionFactoryBean.class);

    @Override
    public void setTypeAliasesPackage(String typeAliasesPackage) {
        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
        typeAliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                ClassUtils.convertClassNameToResourcePath(typeAliasesPackage) + "/" + DEFAULT_RESOURCE_PATTERN;

        // 將載入多個絕對匹配的所有Resource
        // 將首先通過ClassLoader.getResource("META-INF")載入非模式路徑部分
        // 然後進行遍歷模式匹配
        try {
            List<String> result = new ArrayList<String>();
            Resource[] resources =  resolver.getResources(typeAliasesPackage);
            if(resources != null && resources.length > 0){
                MetadataReader metadataReader = null;
                for(Resource resource : resources){
                    if(resource.isReadable()){
                        metadataReader =  metadataReaderFactory.getMetadataReader(resource);
                        try {
                            result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            if(result.size() > 0) {
                super.setTypeAliasesPackage(StringUtils.join(result.toArray(), ","));
            }else{
                logger.warn("引數typeAliasesPackage:"+typeAliasesPackage+",未找到任何包");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼
  1. 在對應的dao的介面上,增加@DataSource(string value)註解,其中value的值是DataSourceContextHolder.DATA_SOURCE_MYSQLDataSourceContextHolder.DATA_SOURCE_ORACLE
    因為在applicationContext.xml中配置的預設資料庫連線是MySQL,所以在連MySQL庫的介面上可以不寫@DataSource(string value)註解。同樣,此註解也可以加在介面的方法上。

參考連結

mybatis配置多資料來源

mybatis配置動態資料來源

Spring aop:pointcut--expression--多個execution連線方法

相關文章