zoukankan      html  css  js  c++  java
  • [教程] Spring+Mybatis环境配置多数据源

    一、简要概述

    在做项目的时候遇到需要从两个数据源获取数据,项目使用的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的时候都会执行该方法。

    AbstractRoutingDataSource afterPropertiesSet方法的实现。

    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配置多数据源涉及事务无法切换解决方案(绝对有效)


    如果您觉得这篇博文对你有帮助,请点赞或者喜欢,让更多的人看到,谢谢!

    如果帅气(美丽)、睿智(聪颖),和我一样简单善良的你看到本篇博文中存在问题,请指出,我虚心接受你让我成长的批评,谢谢阅读!
    祝你今天开心愉快!


    欢迎访问我的csdn博客和关注的个人微信公众号!

    愿你我在人生的路上能都变成最好的自己,能够成为一个独挡一面的人。

    不管做什么,只要坚持下去就会看到不一样!在路上,不卑不亢!

    博客首页 : http://blog.csdn.net/u010648555

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

  • 相关阅读:
    scrapy框架
    003-更改pip的源让下载安装更加快捷
    日志,序列反序列和正则表达式
    菜鸟的IT生活4
    IT菜鸟的3(for循环+分支语句)
    IT菜鸟的第2天(输入输出,数据类型,运算符的使用)
    IT菜鸟的第一天
    第二课笔记
    第一节课预习笔记
    第一节课作业
  • 原文地址:https://www.cnblogs.com/aflyun/p/10146548.html
Copyright © 2011-2022 走看看