Spring+Mybatis环境配置多数据源

栏目: Java · 发布时间: 6年前

内容简介:做一个积极的人编码、改bug、提升自己我有一个乐园,面向编程,春暖花开!

做一个积极的人

编码、改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环境配置多数据源

© 每天都在变得更好的阿飞云


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Building Web Reputation Systems

Building Web Reputation Systems

Randy Farmer、Bryce Glass / Yahoo Press / 2010 / GBP 31.99

What do Amazon's product reviews, eBay's feedback score system, Slashdot's Karma System, and Xbox Live's Achievements have in common? They're all examples of successful reputation systems that enable ......一起来看看 《Building Web Reputation Systems》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

URL 编码/解码
URL 编码/解码

URL 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换