为您推荐:
精华内容
最热下载
问答
  • 5星
    923KB weixin_41581125 2021-01-22 23:04:17
  • 5星
    52.11MB weixin_41581125 2021-01-06 14:22:37
  • 5星
    12.18MB qq_44872173 2021-03-04 10:29:40
  • 5星
    190B Invoker123 2021-04-16 23:48:04
  • 5星
    916KB feijiaogu7393 2021-03-01 11:20:47
  • 5星
    2.55MB silence0989 2021-07-16 16:22:47
  • 5星
    20KB qq_27595745 2021-07-10 16:07:56
  • 5星
    15.45MB xipengbozai 2021-03-13 19:47:45
  • 5星
    10.46MB xipengbozai 2021-03-22 00:43:45
  • 5星
    2.37MB qq_45799793 2021-05-10 22:21:33
  • 在使用Dao层的时候通过不同的SessionFactory进行处理,一般的情况下我们都是使用Mybatis,配置个DataSource,每个DataSource扫描不同的Mapper、注入到不同SqlSessionFactory实现的多数据源。 2. **使用...

    一.前言

    普及知识

    • 一个数据源,也就代表一个数据库,源=数据的源头
    • 数据源实例:一个数据库连接,就代表一个数据源实例对象;
    • 多数据源实例:多个数据库连接对象

    什么是动态多数据源

    • 简单来说,可以在应用运行中,将数据源动态生成并可以切换使用。

    动态多数据源好处

    • 省去了创建大量的 Bean 的操作;
    • 可以在运行时添加数据源;

    实现方式一:

    • 多DataSource + 多SqlSessionFactory 在使用Dao层的时候通过不同的SessionFactory进行处理,一般的情况下我们都是使用Mybatis,配置多个DataSource,每个DataSource扫描不同的Mapper、注入到不同SqlSessionFactory实现的多数据源。

    实现方式二:

    要求:
    公司云平台项目每个商户一个数据库,所以在写后台代码时,需要根据应用层传递过来的商户id,进行动态切换数据源。

    • 因此方式一就不能满足这个需求,所以就引出了方式二
    • 使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源, 基于这种方式,不仅可是实现真正意义上的多数据源的动态切换,还可以实现在程序的运行过程中,实现动态加载一个或多个新的数据源。
      在这里插入图片描述

    本文主要讲如何通过继承AbstractRoutingDataSource类实现多数据源的动态切换与加载

    二.抽象类-AbstractRoutingDataSource

    1.概述

    SpringBoot提供了AbstractRoutingDataSource类,可以根据用户定义的规则选择当前的数据源,这样我们可以在每次数据库查询操作前执之前,设置使用的数据源。从而实现动态加载、切换数据源。

    • 该类位于 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
    • 它的抽象方法 determineCurrentLookupKey() 方法可以让用户根据自己定义的规则在某一个SQL执行之前动态地选择想要的数据源

    2.源码分析

    分析一下AbstractRoutingDataSource抽象类的源码

    public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
        private Map<Object, Object> targetDataSources;
        private Object defaultTargetDataSource;
        private boolean lenientFallback = true;
        private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
        private Map<Object, DataSource> resolvedDataSources;
        private DataSource resolvedDefaultDataSource;
    
        public AbstractRoutingDataSource() {
        }
    	
    	//关键:以Map的结构存储的我们配置的多个数据源的键值对 key为数据源名称,value为对应数据源
        public void setTargetDataSources(Map<Object, Object> targetDataSources) {
            this.targetDataSources = targetDataSources;
        }
    	//关键: 设置默认数据源
        public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
            this.defaultTargetDataSource = defaultTargetDataSource;
        }
    
        public void setLenientFallback(boolean lenientFallback) {
            this.lenientFallback = lenientFallback;
        }
    
        public void setDataSourceLookup(DataSourceLookup dataSourceLookup) {
            this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
        }
    	
    	//关键:数据源重新赋值
        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);
                }
    
            }
        }
    
        protected Object resolveSpecifiedLookupKey(Object lookupKey) {
            return lookupKey;
        }
    
        protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
            if (dataSource instanceof DataSource) {
                return (DataSource)dataSource;
            } else if (dataSource instanceof String) {
                return this.dataSourceLookup.getDataSource((String)dataSource);
            } else {
                throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
            }
        }
    
        public Connection getConnection() throws SQLException {
            return this.determineTargetDataSource().getConnection();
        }
    
        public Connection getConnection(String username, String password) throws SQLException {
            return this.determineTargetDataSource().getConnection(username, password);
        }
    
        public <T> T unwrap(Class<T> iface) throws SQLException {
            return iface.isInstance(this) ? this : this.determineTargetDataSource().unwrap(iface);
        }
    
        public boolean isWrapperFor(Class<?> iface) throws SQLException {
            return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
        }
    	//关键:如何选择数据源
        protected DataSource determineTargetDataSource() {
            Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
            //关键:就是在这里调用我们实现的determineCurrentLookupKey方法返回的名称
            Object lookupKey = this.determineCurrentLookupKey();
            DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
            if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
                dataSource = this.resolvedDefaultDataSource;
            }
    
            if (dataSource == null) {
                throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
            } else {
                return dataSource;
            }
        }
    	//关键:需要重写的方法,就是根据当前方法的名称查找对应的数据源的,查询不到则使用默认数据源
    	// 	用当前返回的Object去作为key去targetDataSources中查找相应的值,如果查找到相对应的DataSource,那么就使用此DataSource获取数据库连接。没有就使用默认数据源。
        protected abstract Object determineCurrentLookupKey();
    }
    

    对于该抽象类,了解两组变量即可:

    • Map<Object, Object> targetDataSources和Object defaultTargetDataSource

    • Map<Object, DataSource> resolvedDataSources和DataSource resolvedDefaultDataSource,这两组变量是相互对应的。

      • 当有多个数据源的时候,需要手动调用``setDefaultTargetDataSource方法指定一个作为默认数据源

      • targetDataSources 是暴露给外部用来赋值的,而 resolvedDataSources 是程序内部执行时的依据,因此resolvedDataSources会有一个赋值操作,如下图所示:
        在这里插入图片描述

        • 每次执行afterPropertiesSet()方法时,都会遍历targetDataSources内的所有元素赋值给resolvedDataSources
        • 因此在外部每新增一个新的数据源,需要手动调用afterPropertiesSet(),从而实现数据源的动态加载
    • 继承该抽象类的时候,必须实现一个抽象方法:protected abstract Object determineCurrentLookupKey(),用于指定到底需要使用哪一个数据源。
      在这里插入图片描述

    实现动态数据源逻辑

    1. 自定义DynamicDataSource类继承AbstractRoutingDataSource类并实现determineCurrentLookupKey方法(具体逻辑是从当前线程的ThreadLocal中获取我们在某一个SQL执行之前通过AOP切面动态指定的数据源名称);

    2. application.yml中配置多个数据源;

    3. 解析在application.yml中配置的多个数据源,然后生成DynamicDataSource实例,并设置默认数据源(defaultTargetDataSource)和其他数据源(targetDataSources),然后通过afterPropertiesSet()方法将数据源分别进行复制resolvedDataSourcesresolvedDefaultDataSource中。

    4. 定义SqlSessionFactory并注入DynamicDataSource以及MapperLocations

    5. 调用AbstractRoutingDataSource的#getConnection的方法的时候,会先调用determineTargetDataSource方法获取具体的数据源,而在这个方法中会进一步调用我们在DynamicDataSource类中自定义的determineCurrentLookupKey方法,最后在返回DataSource后再进行getConnection的调用。剩下就是具体的SQL逻辑执行了。

    6. 启动类或者配置类使用@MapperScan注解 扫描Mapper接口以及引用sqlSessionFactory

    三.具体实现

    1.引入依赖

    	   <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            
            <!--关键: aop框架-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    
            <!--关键: 持久化框架-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.4.2</version>
            </dependency>
            
            <!--关键: mysql驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
            
            <!--关键: druid数据库连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.1.10</version>
            </dependency>
            
            <!--自动生成代码工具-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.11</version>
            </dependency>
            
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context-support</artifactId>
            </dependency>
    

    2.配置文件application.yml

    2.1.修改配置文件

    一个d01一个db02,我这边只配2个 需要多个的 增加即可

    spring:
      datasource:
        db01:
          driverClassName: com.mysql.cj.jdbc.Driver
          password: root
          url: jdbc:mysql://localhost:3306/springboot_quartz1?characterEncoding=utf-8&serverTimezone=UTC
          username: root
        db02:
          driverClassName: com.mysql.cj.jdbc.Driver
          password: root
          url: jdbc:mysql://localhost:3306/springboot_quartz2?characterEncoding=utf-8&serverTimezone=UTC
          username: root
    

    2.2.增加测试数据库1

    CREATE DATABASE /*!32312 IF NOT EXISTS*/`springboot_quartz1` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */;
    
    USE `springboot_quartz1`;
    
    DROP TABLE IF EXISTS `sys_task`;
    
    CREATE TABLE `sys_task` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `job_name` varchar(255) DEFAULT NULL COMMENT '任务名',
      `description` varchar(255) DEFAULT NULL COMMENT '任务描述',
      `cron_expression` varchar(255) DEFAULT NULL COMMENT 'cron表达式',
      `bean_class` varchar(255) DEFAULT NULL COMMENT '任务执行时调用哪个类的方法 包名+类名',
      `job_status` varchar(255) DEFAULT NULL COMMENT '任务状态',
      `job_group` varchar(255) DEFAULT NULL COMMENT '任务分组',
      `create_user` varchar(64) DEFAULT NULL COMMENT '创建者',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      `update_user` varchar(64) DEFAULT NULL COMMENT '更新者',
      `update_time` datetime DEFAULT NULL COMMENT '更新时间',
      PRIMARY KEY (`id`)
    ) ENGINE=MyISAM AUTO_INCREMENT=33 DEFAULT CHARSET=utf8;
    
    
    insert  into `sys_task`(`id`,`job_name`,`description`,`cron_expression`,`bean_class`,`job_status`,`job_group`,`create_user`,`create_time`,`update_user`,`update_time`) values 
    (1,'1111','1111','1111','1111','1111','1111','1111','2021-04-15 17:04:55','1111','2021-04-11 17:04:59');
    

    2.3.增加测试数据库2

    CREATE DATABASE /*!32312 IF NOT EXISTS*/`springboot_quartz2` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */;
    
    USE `springboot_quartz2`;
    
    DROP TABLE IF EXISTS `sys_task`;
    
    CREATE TABLE `sys_task` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `job_name` varchar(255) DEFAULT NULL COMMENT '任务名',
      `description` varchar(255) DEFAULT NULL COMMENT '任务描述',
      `cron_expression` varchar(255) DEFAULT NULL COMMENT 'cron表达式',
      `bean_class` varchar(255) DEFAULT NULL COMMENT '任务执行时调用哪个类的方法 包名+类名',
      `job_status` varchar(255) DEFAULT NULL COMMENT '任务状态',
      `job_group` varchar(255) DEFAULT NULL COMMENT '任务分组',
      `create_user` varchar(64) DEFAULT NULL COMMENT '创建者',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      `update_user` varchar(64) DEFAULT NULL COMMENT '更新者',
      `update_time` datetime DEFAULT NULL COMMENT '更新时间',
      PRIMARY KEY (`id`)
    ) ENGINE=MyISAM AUTO_INCREMENT=33 DEFAULT CHARSET=utf8;
    
    
    insert  into `sys_task`(`id`,`job_name`,`description`,`cron_expression`,`bean_class`,`job_status`,`job_group`,`create_user`,`create_time`,`update_user`,`update_time`) values 
    (1,'2222','2222','2222','2222','2222','2222','2222','2021-04-15 17:04:55','2222','2021-04-11 17:04:59');
    

    2.4.增加测试数据库3

    CREATE DATABASE /*!32312 IF NOT EXISTS*/`springboot_quartz2` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */;
    
    USE `springboot_quartz3`;
    
    DROP TABLE IF EXISTS `sys_task`;
    
    CREATE TABLE `sys_task` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `job_name` varchar(255) DEFAULT NULL COMMENT '任务名',
      `description` varchar(255) DEFAULT NULL COMMENT '任务描述',
      `cron_expression` varchar(255) DEFAULT NULL COMMENT 'cron表达式',
      `bean_class` varchar(255) DEFAULT NULL COMMENT '任务执行时调用哪个类的方法 包名+类名',
      `job_status` varchar(255) DEFAULT NULL COMMENT '任务状态',
      `job_group` varchar(255) DEFAULT NULL COMMENT '任务分组',
      `create_user` varchar(64) DEFAULT NULL COMMENT '创建者',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      `update_user` varchar(64) DEFAULT NULL COMMENT '更新者',
      `update_time` datetime DEFAULT NULL COMMENT '更新时间',
      PRIMARY KEY (`id`)
    ) ENGINE=MyISAM AUTO_INCREMENT=33 DEFAULT CHARSET=utf8;
    
    
    insert  into `sys_task`(`id`,`job_name`,`description`,`cron_expression`,`bean_class`,`job_status`,`job_group`,`create_user`,`create_time`,`update_user`,`update_time`) values 
    (1,'3333','3333','3333','3333','3333','3333','3333','2021-04-15 17:04:55','3333','2021-04-11 17:04:59');
    

    3.动态切换数据源的上下文

    自定义一个动态数据源上下文类,该类通过ThreadLocal的静态常量存储当前线程是需要访问哪一个数据源。方便在同一个线程上下文中共享数据源名称

    public class DataSourceContextHolder {
        private static final ThreadLocal<String> contextHolder = new ThreadLocal();
    
        public static String get() {
            return contextHolder.get();
        }
    
        public static void set(String dataSourceType) {
            contextHolder.set(dataSourceType);
        }
    
        public static void clear() {
            contextHolder.remove();
        }
    }
    

    4.动态数据源

    创建一个动态数据源,继承自Spring的org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource类,并重写determineCurrentLookupKey方法

    • 该方法唯一需要做的事情就是获取我们在某一个SQL执行之前通过AOP切面动态指定的数据源名称。
    public class DynamicDataSource extends AbstractRoutingDataSource {
        private final Logger log = LoggerFactory.getLogger(getClass());
    
        //缓存添加的数据源
        private Map<Object, Object> dynamicTargetDataSources = new HashMap<>();
        //缓存默认数据源
        private Object dynamicDefaultTargetDataSource = null;
    
        /**
         * 重新加载数据源
         */
        @Override
        public void setTargetDataSources(Map<Object, Object> targetDataSources) {
            super.setTargetDataSources(targetDataSources);
            dynamicTargetDataSources.putAll(targetDataSources);
            super.afterPropertiesSet();// 必须添加该句,否则新添加数据源无法识别到
        }
    
        /**
         * 设置默认数据源
         */
        @Override
        public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
            super.setDefaultTargetDataSource(defaultTargetDataSource);
            this.dynamicDefaultTargetDataSource = defaultTargetDataSource;
            super.afterPropertiesSet();// 必须添加该句,否则新添加数据源无法识别到
        }
    
    
        /**
         * 获取默认数据源
         */
        public Object getDynamicDefaultTargetDataSource() {
            return dynamicDefaultTargetDataSource;
        }
    
        /**
         * 获取已加载的数据源
         */
        public Map<Object, Object> getDynamicTargetDataSources() {
            return dynamicTargetDataSources;
        }
    
        /**
         * 重写至AbstractRoutingDataSource,运行时就是在这里根据名称动态切换数据源的
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceContextHolder.get();
        }
    }
    
    

    5.动态数据源配置类

    解析在application.yml中配置的多个数据源,并初始化动态数据源实体类DynamicDataSource 以及sqlSessionFactory

    @Configuration
    //sqlSessionFactoryRef 表示定义了 key ,表示一个唯一 SqlSessionFactory 实例
    @MapperScan(basePackages = {DynamicDataSourceConfiguration.MAPPER_INTERFACE_PACKAGE}, sqlSessionTemplateRef = "sqlSessionTemplate")
    public class DynamicDataSourceConfiguration {
        /**
         * 扫描的mapper接口路径
         * 	注意:com.oyjp.ds3.mapper是我项目里的Mapper接口全路径(这里需要改成你们项目中的全路径)
         */
        public static final String MAPPER_INTERFACE_PACKAGE = "com.oyjp.ds3.mapper";
        /**
         * 扫描的mapper文件路径
         * 注意:classpath*:com/oyjp/ds3/mapper/*.xml是我项目里的xml的路径(这里需要改成你们项目中xml的路径)
         */
        public static final String MAPPER_XML_PACKAGE = "classpath*:com/oyjp/ds3/mapper/*.xml";
    
    
        //一个一个的将属性注入
        /*
        @Value("${spring.datasource.db01.url}")
        private String db01DBUrl;
        @Value("${spring.datasource.db01.username}")
        private String db01DBUser;
        @Value("${spring.datasource.db01.password}")
        private String db01DBPassword;
        @Value("${spring.datasource.db01.driverClassName}")
        private String db01DriverClassName;
    
        @Value("${spring.datasource.db02.url}")
        private String db02DBUrl;
        @Value("${spring.datasource.db02.username}")
        private String db02DBUser;
        @Value("${spring.datasource.db02.password}")
        private String db02DBPassword;
        @Value("${spring.datasource.db02.driverClassName}")
        private String db02DriverClassName;
        */
    
        /**
         * 初始化db01数据源
         */
        @Bean(name = "db01DataSource")
        //根据配置前缀将属性注入到对象属性中,数据源db01的配置前缀为spring.datasource.db01
        @ConfigurationProperties(prefix = "spring.datasource.db01")
        public DataSource db01DataSource() {
            DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
            dataSource.setName("db01");//数据源名称
            return dataSource;
        }
    
        /**
         * 初始化db02数据源
         */
        @Bean(name = "db02DataSource")
        //根据配置前缀将属性注入到对象属性中,数据源db01的配置前缀为spring.datasource.db02
        @ConfigurationProperties(prefix = "spring.datasource.db02")
        public DataSource db02DataSource() {
            DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
            dataSource.setName("db02");//数据源名称
            return dataSource;
        }
    
        /**
         * 初始化动态数据源,并注入db01,db02,以及设置默认数据源为db01
         */
        @Bean
        public DynamicDataSource dynamicDataSource(@Qualifier("db01DataSource") DataSource db01DataSource, @Qualifier("db02DataSource") DataSource db02DataSource) {
            DynamicDataSource dynamicDataSource = new DynamicDataSource;
            Map<Object, Object> map = new HashMap<>();
            map.put("db01", db01DataSource);
            map.put("db02", db02DataSource);
            //默认数据源
            dynamicDataSource.setDefaultTargetDataSource(db01DataSource);
            //数据源列表,与名称绑定
            dynamicDataSource.setTargetDataSources(map);
    
            return dynamicDataSource;
        }
    
       /**
         * 配置 SqlSessionFactoryBean
         * 将 MyBatis 的 mapper 位置和持久层接口的别名设置到 Bean 的属性中,如果没有使用 *.xml 则可以不用该配置,否则将会产生 invalid bond statement 异常
         */
        @Bean
        public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
             // 配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource 作为数据源则不能实现切换
            bean.setDataSource(dynamicDataSource);
             //此处设置为了解决找不到mapper文件的问题
            bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_XML_PACKAGE));
            return bean.getObject();
        }
    
        /**
         * 设置动态数据源DynamicDataSource到会话工厂
         */
        @Bean(name = "sqlSessionTemplate")
        public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    
        /**
         * 将动态数据源添加到事务管理器中,并生成新的bean
         *
         * 切库与事务注意:
         *   1.有@Transactional注解的方法,方法内部不可以做切换数据库 操作
         *   2.在同一个service其他方法调用带@Transactional的方法,事务不起作用
         *   3.在应用中因为使用了 DAO 层的切面切换数据源,所以 @Transactional 注解不能加在类上,只能用于方法;有 @Trasactional注解的方法无法切换数据源
         * @return 事务管理实例
         */
        @Bean
        public PlatformTransactionManager platformTransactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
            return new DataSourceTransactionManager(dynamicDataSource);
        }
    }
    
    • dynamicDataSource.setDefaultTargetDataSource(db01DataSource); 这一行代码表示没有指定数据源时,默认使用默认数据源。

    3.自定义注解

    在实际的项目开发中,不可能总是在访问数据库之前,调用DataSourceContextHolder .set设置数据源,这样不好维护、繁琐、代码可阅读性也不好。所以,可以自定义一个注解,用于标识当前方法是要切换到数据源,然后用一个切面(AOP)在进入方法前指定数据源、退出方法前清空数据源。

    import java.lang.annotation.*;
    /**
     * 数据源注解
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Documented
    public @interface DS {
        /**
         * 没有设置数据源名称,默认取default名称数据源
         */
        String value() default "default";
    }
    

    7.动态数据源切面类(多数据源动态切换)

    通过AOP+注解实现数据源的动态切换,在调用切入点下面的方法时,会先进入前置通知方法中,将注解@DS配置数据源名称设置到DataSourceContextHolder的ThreadLocal中,在方法返回前清空ThreadLocal设置的数据源

    • 其中@Order是很重要的,必须确保DynamicDataSourceAspect 的执行优先于TranctionInterceptor。不然数据源的指定就无法生效(数据源的指定在数据库连接的获取之后!!)
    /**
     * 动态数据源AOP切面
     */
    @Aspect
    @Order(-1)
    @Component
    public class DynamicDataSourceAspect {
    
        private Logger logger = LoggerFactory.getLogger(this.getClass());
    
        //切点
        @Pointcut("execution(* com.oyjp.ds3.service..*(..))")
        public void aspect() {
        }
    
        //调用方法前结束后,根据注解@DS设置数据源
        @Before("aspect()")
        private void before(JoinPoint point) {
            Object target = point.getTarget();
            String method = point.getSignature().getName();
            Class<?> classz = target.getClass();// 获取目标类
            Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
                    .getMethod().getParameterTypes();
            try {
                Method m = classz.getMethod(method, parameterTypes);
                if (m != null && m.isAnnotationPresent(DS.class)) {
                    DS data = m.getAnnotation(DS.class);
                    logger.info("method :{},datasource:{}", m.getName(), data.value());
                    DataSourceContextHolder.set(data.value());// 数据源放到当前线程中
                }
            } catch (Exception e) {
                logger.error("get datasource error ", e);
                //默认选择master
                DataSourceContextHolder.set("master");// 数据源放到当前线程中
            }
        }
    
        //调用方法结束后,清空数据源
        @AfterReturning("aspect()")
        public void after(JoinPoint point) {
            DataSourceContextHolder.clear();
        }
    }
    
    

    8.启动类取消自动配置数据源

    //指定aop事务执行顺序,已保证在切换数据源的后面
    @EnableTransactionManagement(order = 2)
    //排除数据源自动配置
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})
    

    9.测试

    在需要切换数据源的service的方法上面标注自定义注解@DS即可

    mapper接口

    @Mapper
    public interface SysTaskMapper {
        /**
         * 通过ID查询单条数据
         *
         * @param id 主键
         * @return 实例对象
         */
        SysTask queryById(@Param("id") Integer id);
    }
    

    Mapper文件

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.oyjp.ds3.mapper.SysTaskMapper">
    
        <resultMap type="com.oyjp.ds3.bean.SysTask" id="SysTaskMap">
            <result property="id" column="id" jdbcType="INTEGER"/>
            <result property="jobName" column="job_name" jdbcType="VARCHAR"/>
            <result property="description" column="description" jdbcType="VARCHAR"/>
            <result property="cronExpression" column="cron_expression" jdbcType="VARCHAR"/>
            <result property="beanClass" column="bean_class" jdbcType="VARCHAR"/>
            <result property="jobStatus" column="job_status" jdbcType="VARCHAR"/>
            <result property="jobGroup" column="job_group" jdbcType="VARCHAR"/>
            <result property="createUser" column="create_user" jdbcType="VARCHAR"/>
            <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
            <result property="updateUser" column="update_user" jdbcType="VARCHAR"/>
            <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
        </resultMap>
    
        <!--查询单个-->
        <select id="queryById" resultMap="SysTaskMap">
            select   * from sys_task  where id = #{id}
        </select>
    </mapper>
    
    

    Service接口

    public interface SysTaskService {
        /**
         * 查询db01数据源
         *
         * @param id
         * @return
         */
        SysTask queryById1(Integer id);
    
        /**
         * 查询db02数据源
         *
         * @param id
         * @return
         */
        SysTask queryById2(Integer id);
    
        /**
         * 查询db03数据源
         *
         * @param id
         * @return
         */
        SysTask queryById3(Integer id);
        /**
         * 通过ID查询单条数据
         *
         * @param id 主键
         * @return 实例对象
         */
        SysTask queryById(Integer id);
    
    }
    
    

    Service接口实现类

    @Service("SysTaskService")
    public class SysTaskServiceImpl implements SysTaskService {
        @Resource
        private SysTaskMapper SysTaskMapper;
    
        /**
         *查询单条数据源,没有指定数据源,查询默认数据db01的数据
         */
        @Override
        public SysTask queryById(Integer id) {
            return this.SysTaskMapper.queryById(id);
        }
    
        /**
         * 查询db01数据源原
         */
        @Override
        @DS("db01")
        public SysTask queryById1(Integer id) {
            return this.SysTaskMapper.queryById(id);
        }
    
        /**
         * 查询db02数据源原
         */
        @Override
        @DS("db02")
        public SysTask queryById2(Integer id) {
            return this.SysTaskMapper.queryById(id);
        }
    
        /**
         * 查询db03数据源原
         */
        @Override
        @DS("db03")
        public SysTask queryById3(Integer id) {
            return this.SysTaskMapper.queryById(id);
        }
    }
    

    伪代码-controller

    @RestController
    public class SysTaskController {
        Logger log = LoggerFactory.getLogger(SysTaskController.class);
        /**
         * 服务对象
         */
        @Resource
        private SysTaskService sysTaskService;
    
        /**
         * 切换到数据源db01
         */
        @GetMapping("/tasks1/{id}")
        public SysTask selectOne1(@PathVariable("id") Integer id) {
            return this.sysTaskService.queryById1(id);
        }
    
        /**
         * 切换到数据源db02
         */
        @GetMapping("/tasks2/{id}")
        public SysTask selectOne2(@PathVariable("id") Integer id) {
            return this.sysTaskService.queryById2(id);
        }
    
        /**
         * 切换到数据源db03,没有配置此数据源,因此默认查询的是db01的数据
         */
        @GetMapping("/tasks3/{id}")
        public SysTask selectOne3(@PathVariable("id") Integer id) {
            return this.sysTaskService.queryById3(id);
        }
     }   
    

    也可以在方法内通过DataSourceContextHolder.set()方法修改数据源名称,从而在进行数据源操作时触发DynamicDataSource.determineCurrentLookupKey获取到我们修改后的数据源名称

        /**
         * 切换到db01数据源
         */
        @GetMapping("/tasks4/{id}")
        public SysTask selectOne4(@PathVariable("id") Integer id) {
            DataSourceContextHolder.set("db01");
            return this.sysTaskService.queryById(id);
        }
    
        /**
         * 切换到db02数据源
         */
        @GetMapping("/tasks5/{id}")
        public SysTask selectOne5(@PathVariable("id") Integer id) {
            DataSourceContextHolder.set("db02");
            return this.sysTaskService.queryById(id);
        }
    
        /**
         * 切换到db03数据源,没有配置此数据源,因此默认查询的是db01的数据
         */
        @GetMapping("/tasks6/{id}")
        public SysTask selectOne6(@PathVariable("id") Integer id) {
            DataSourceContextHolder.set("db03");
            return this.sysTaskService.queryById(id);
        }
    

    10.动态加载数据源

    基于上面的代码进行新增

    10.1.新增获取Spring容器工具类

    • 主要是用于获取Spring容器中的动态数据源DynamicDataSource 实例
    /**
     * 实现ApplicationContextAware接口设置applicationContext
     * 提供static方法供调用者使用,不要求使用者受spring容器管理
     */
    @Component
    public class SpringContext implements ApplicationContextAware {
    
        public static ApplicationContext applicationContext;
    
        public static ApplicationContext getInstance() {
            return SpringContext.applicationContext;
        }
    
        public static Object getBean(String name) {
            return getInstance().getBean(name);
        }
    
        public static <T> T getBean(Class<T> clazz) {
            return getInstance().getBean(clazz);
        }
    
        public static <T> T getBean(String name, Class<T> clazz) {
            return getInstance().getBean(name, clazz);
        }
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            SpringContext.applicationContext = applicationContext;
        }
    }
    

    10.2.动态数据源服务类

    动态数据源服务类,提供一些动态添加数据源、删除数据源方法

    @Service
    public class DynamicDataSourceService {
        private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceService.class);
        //可重入锁
        private final Lock lock = new ReentrantLock();
    
        /**
         * 删除数据源
         *
         * @param dsName 数据源名称
         * @return
         */
        public boolean delDataSources(String dsName) {
            lock.lock();
            try {
                //获取Spring容器动态数据源对象
                DynamicDataSource dynamicDataSource = SpringContext.getInstance().getBean(DynamicDataSource.class);
                //获取已加载的所有数据源
                Map<Object, Object> dynamicTargetDataSources = dynamicDataSource.getDynamicTargetDataSources();
    
                //返回结果
                boolean result = false;
                //如果存在当前数据源
                if (dynamicTargetDataSources.containsKey(dsName)) {
                    //获取Druid管理下的数据源
                    Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
                    for (DruidDataSource druidDataSource : druidDataSourceInstances) {
                        if (druidDataSource.getName().equals(dsName)) {
                            //删除数据源监控列表
                            DruidDataSourceStatManager.removeDataSource(druidDataSource);
                            //删除数据源
                            dynamicTargetDataSources.remove(dsName);
                            //将删除后的数据源集合赋值给父类的TargetDataSources
                            dynamicDataSource.setTargetDataSources(dynamicTargetDataSources);
                            log.info(dsName + "数据源删除成功");
                            result = true;
                            break;
                        }
                    }
                }
                return result;
            } finally {
                lock.unlock();
            }
        }
    
    
        /**
         * 创建Druid数据源
         *
         * @param dsName     数据源名称
         * @param driveClass 数据源驱动
         * @param url        数据库url
         * @param username   用户名
         * @param password   密码
         * @return
         */
        public boolean addDataSource(String dsName, String driveClass, String url, String username, String password) {
            lock.lock();
            try {
                //测试当前连接
                if (!testDatasource(dsName, driveClass, url, username, password)) return false;
    
                //获取Spring容器动态数据源对象
                DynamicDataSource dynamicDataSource = SpringContext.getInstance().getBean(DynamicDataSource.class);
                //获取已加载的所有数据源
                Map<Object, Object> dynamicTargetDataSources = dynamicDataSource.getDynamicTargetDataSources();
    
                //创建Druid数据源
                DruidDataSource druidDataSource = new DruidDataSource();
                druidDataSource.setName(dsName);
                druidDataSource.setDriverClassName(driveClass);
                druidDataSource.setUrl(url);
                druidDataSource.setUsername(username);
                druidDataSource.setPassword(password);
                druidDataSource.init();
    
                //添加数据源
                dynamicTargetDataSources.put(dsName, druidDataSource);
                //将删除后的数据源集合赋值给父类的TargetDataSources
                dynamicDataSource.setTargetDataSources(dynamicTargetDataSources);
                log.info(dsName + "数据源初始化成功");
                log.info(dsName + "数据源的概况:" + druidDataSource.dump());
                return true;
    
            } catch (Exception e) {
                log.error(dsName + "数据源初始化异常:{}",e.getMessage());
                return false;
            } finally {
                lock.unlock();
            }
        }
    
    
        /**
         * 测试数据源是否能够连接
         *
         * @param dsName     数据源名称
         * @param driveClass 驱动
         * @param url        url
         * @param username   账号
         * @param password   密码
         * @return true可以 false不可以
         */
        public boolean testDatasource(String dsName, String driveClass, String url, String username, String password) {
            try {
                Class.forName(driveClass);
                //设置连接数据库的等待时间,单位秒
                DriverManager.setLoginTimeout(3);
                DriverManager.getConnection(url, username, password);
                log.info(dsName + "数据源建立连接正常");
                return true;
            } catch (Exception e) {
                log.info(dsName + "数据源建立连接异常:{}", e.getMessage());
                return false;
            }
        }
    }
    

    10.4.测试

    @RestController
    public class SysTaskController {
        Logger log = LoggerFactory.getLogger(SysTaskController.class);
        /**
         * 服务对象
         */
        @Resource
        private SysTaskService sysTaskService;
        @Resource
        private DBService dbService;
        @Resource
        private DynamicDataSourceService dynamicDataSourceService;
    
        /**
         * 切换到数据源db01-aop
         *
         * @param id 主键
         * @return 单条数据
         */
        @GetMapping("/tasks1/{id}")
        public SysTask selectOne1(@PathVariable("id") Integer id) {
            return this.sysTaskService.queryById1(id);
        }
    
        /**
         * 切换到数据源db02-aop
         *
         * @param id 主键
         * @return 单条数据
         */
        @GetMapping("/tasks2/{id}")
        public SysTask selectOne2(@PathVariable("id") Integer id) {
            return this.sysTaskService.queryById2(id);
        }
    
    
        /**
         * 切换到数据源db02-aop
         *
         * @param id 主键
         * @return 单条数据
         */
        @GetMapping("/tasks3/{id}")
        public SysTask selectOne3(@PathVariable("id") Integer id) {
            return this.sysTaskService.queryById3(id);
        }
    
        /**
         * 动态加载数据源
         */
        @GetMapping("/tasks/addDataSource")
        public String addDs() {
            String driverClassName = "com.mysql.jdbc.Driver";
            String url = "jdbc:mysql://127.0.0.1:3306/springboot_quertz3?characterEncoding=utf-8&serverTimezone=UTC";
            String username = "root";
            String password = "root";
            String dbName = "db03";
    
            //动态加载数据源
            if (dynamicDataSourceService.addDataSource(dbName, driverClassName, url, username, password)) {
                return "success";
            }
            return "fail";
        }
    
    
        /**
         * 动态加载数据源
         */
        @GetMapping("/tasks/delDataSource/{dsName}")
        public String delDs(@PathVariable("dsName") String dsName) {
            //删除数据源
            if (dynamicDataSourceService.delDataSources(dsName)) {
                return "success";
            }
            return "fail";
        }
    }
    
    

    11. 关于事务

    11.1.单个数据源测试

    AbstractRoutingDataSource 只支持单库事务,也就是说切换数据源要在开启事务之前执行

    • Spring DataSourceTransactionManager进行事务管理,开启事务,会将数据源缓存到DataSourceTransactionObject对象中进行后续的commit rollback等事务操作。
      在这里插入图片描述

    出现多数据源动态切换失败的原因是因为在事务开启后,数据源就不能再进行随意切换了,也就是说,一个事务对应一个数据源

    • 传统的Spring管理事务是放在·Service业务层操作的,所以更换数据源的操作要放在这个操作之前进行。也就是切换数据源操作放在Controller层,可是这样操作会造成Controller层代码混乱的结果。

      • 故而想到的解决方案是将事务管理在数据持久 (Dao层) 开启,切换数据源的操作放在业务层进行操作,就可在事务开启之前顺利进行数据源切换,不会再出现切换失败了。

    Springboot 从数据库读取数据库配置信息,动态切换多数据源 最详细实战教程
    Springboot 整合druid+mybatis+jta分布式事务+多数据源aop注解动态切换 (一篇到位)
    在使用 Spring Boot 和 MyBatis 动态切换数据源时遇到的问题以及解决方法

    展开全文
    qq877728715 2021-04-02 15:40:52
  • 上两遍已经描述了动态多数据源的原理和基础实现了,上面两篇都是从applition.yml中配置多数据源的,这里再拓展补充一下其他场景,如何读取数据源不从applition.yml中配置,实现从数据库中读取数据源配置并动态切换...

    上两遍已经描述了动态多数据源的原理和基础实现了,前面的数据源配置都是从application.yml中配置多数据源的,这里再拓展补充一下其他场景,如何读取数据源不从application.yml中配置,实现从数据库中读取数据源配置并动态切换数据源。

    一、回顾上篇的动态多数据源配置

    上篇:springboot动态多数据源配置和使用(二)

    1. 继承AbstractRoutingDataSource,重写抽象方法determineCurrentLookupKey()
    /**
     * 多数据源
     *
     */
    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            return DynamicContextHolder.peek();
        }
    
    }
    
    1. 注入spring容器
        @Bean
        public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            //设置多个数据源的map
            dynamicDataSource.setTargetDataSources(getDynamicDataSource())
            //默认数据源
            DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
            dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
            return dynamicDataSource;
        }
    
        private Map<Object, Object> getDynamicDataSource(){
            Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource();
            Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size());
            dataSourcePropertiesMap.forEach((k, v) -> {
                DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v);
                targetDataSources.put(k, druidDataSource);
            });
    
            return targetDataSources;
        }
    
    
    1. 可以看到多数据源配置是从application.yml读取出来,然后设置到DynamicDataSource对象里的targetDataSources属性
      dynamicDataSource.setTargetDataSources();

    分析

    从上面分析可以知道,重要的是targetDataSources这个存放多数据源的map属性。
    那么我们只要把targetDataSources这个map由配置文件获取创建dataSource然后放入map改写成由数据读读取出来的配置,再创建dataSource再放入targetDataSources这个map变量就可以实现我们想要的功能了。

    二、从数据库获取配置创建数据源

    这一步说难也不难,就把数据库的配置保存在数据库的表里面,在切面类切换数据源时读取数据库的配置,然后创建数据源,把创建的数据源通过put方法放入targetDataSources这个map即可,最后在切面类DynamicContextHolder.push(key)改变数据源

    但是这样子就很没效率,每次都从数据库读取配置,然后创建dataSource数据源。所以实际上我们是懒加载的模式,再用一个数据源缓存池pool来保存dataSource,如果缓存有了dataSource就不再从数据库读取了,直接从数据源缓存池的pool来获取数据源。

    具体实现

    1.创建表

    CREATE TABLE `oa_quick_knife_datasource` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
      `datasource_name` varchar(20) DEFAULT '' COMMENT '数据源名称',
      `datasource_url` varchar(200) DEFAULT '' COMMENT '数据源url',
      `datasource_account` varchar(50) DEFAULT '' COMMENT '数据源帐号',
      `datasource_password` varchar(64) DEFAULT '' COMMENT '数据源密码',
      `remark` varchar(200) DEFAULT NULL COMMENT '备注',
      `is_show_type` tinyint(1) DEFAULT NULL COMMENT '数据源可见类型(1-全部人可见,2-部分人可见)',
      `datasource_type` tinyint(1) DEFAULT NULL COMMENT '默认mysql,暂时只支持mysql',
      `update_name` varchar(20) DEFAULT '' COMMENT '更新人',
      `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
      `create_code` int(11) unsigned DEFAULT '0' COMMENT '创建人工号',
      `create_name` varchar(20) DEFAULT '' COMMENT '创建人',
      `update_code` int(11) unsigned DEFAULT '0' COMMENT '更新人工号',
      `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
      `deleted_flag` tinyint(1) DEFAULT '0' COMMENT '是否删除',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='数据源 ';
    

    注:datasource_password这个字段我们不要明文保存数据库密码,我们加密后再放入这个字段里面

    2.数据源缓存池

    数据源缓存池的类代码

    /**
     * 数据源缓存池
     */
    public class DataSourceCachePool {
        /** 数据源连接池缓存【本地 class缓存 - 不支持分布式】 */
        private static Map<String, DruidDataSource> dbSources = new HashMap<>();
        private static RedisTemplate<String, Object> redisTemplate;
    
        private static RedisTemplate<String, Object> getRedisTemplate() {
            if (redisTemplate == null) {
                redisTemplate = (RedisTemplate<String, Object>) SpringContextUtils.getBean("redisTemplate");
            }
            return redisTemplate;
        }
    
        /**
         * 获取多数据源缓存
         *
         * @param dbKey
         * @return
         */
        public static DynamicDataSourceModel getCacheDynamicDataSourceModel(String dbKey) {
            String redisCacheKey = ConfigConstant.SYS_DYNAMICDB_CACHE + dbKey;
            if (getRedisTemplate().hasKey(redisCacheKey)) {
                String model = (String)getRedisTemplate().opsForValue().get(redisCacheKey);
                return  JSON.parseObject(model,DynamicDataSourceModel.class);
            }
            DatasourceDao datasourceDao = (DatasourceDao)SpringContextUtils.getBean("datasourceDao");
            DynamicDataSourceModel dbSource = datasourceDao.getDynamicDbSourceByCode(dbKey);
            try{
                dbSource.setDbPassword(AesUtil.decryptBySalt(dbSource.getDbPassword(),dbSource.getId()));
            }catch (Exception e){
                throw new RRException("动态数据源密钥解密失败,dbKey:"+dbKey);
            }
    
            if (dbSource != null) {
                getRedisTemplate().opsForValue().set(redisCacheKey, JSONObject.toJSONString(dbSource));
            }
            return dbSource;
        }
    
        public static DruidDataSource getCacheBasicDataSource(String dbKey) {
            return dbSources.get(dbKey);
        }
    
        /**
         * put 数据源缓存
         *
         * @param dbKey
         * @param db
         */
        public static void putCacheBasicDataSource(String dbKey, DruidDataSource db) {
            dbSources.put(dbKey, db);
        }
    
        /**
         * 清空数据源缓存
         */
        public static void cleanAllCache() {
            //关闭数据源连接
            for(Map.Entry<String, DruidDataSource> entry : dbSources.entrySet()){
                String dbkey = entry.getKey();
                DruidDataSource druidDataSource = entry.getValue();
                if(druidDataSource!=null && druidDataSource.isEnable()){
                    druidDataSource.close();
                }
                //清空redis缓存
                getRedisTemplate().delete(ConfigConstant.SYS_DYNAMICDB_CACHE + dbkey);
            }
            //清空缓存
            dbSources.clear();
        }
    
        public static void removeCache(String dbKey) {
            //关闭数据源连接
            DruidDataSource druidDataSource = dbSources.get(dbKey);
            if(druidDataSource!=null && druidDataSource.isEnable()){
                druidDataSource.close();
            }
            //清空redis缓存
            getRedisTemplate().delete(ConfigConstant.SYS_DYNAMICDB_CACHE + dbKey);
            //清空缓存
            dbSources.remove(dbKey);
        }
    
    }
    

    上面的数据源缓存池主要代码是下面getCacheDynamicDataSourceModel方法的这段

    这个方法的逻辑是先从redis缓存数据源配置,redis没有则从数据库获取,以及获取的配置的数据库密码是加密的,所以这里还要再解密

        /**
         * 获取多数据源缓存配置
         *
         * @param dbKey
         * @return
         */
        public static DynamicDataSourceModel getCacheDynamicDataSourceModel(String dbKey) {
            String redisCacheKey = ConfigConstant.SYS_DYNAMICDB_CACHE + dbKey;
            if (getRedisTemplate().hasKey(redisCacheKey)) {
                String model = (String)getRedisTemplate().opsForValue().get(redisCacheKey);
                return  JSON.parseObject(model,DynamicDataSourceModel.class);
            }
            DatasourceDao datasourceDao = (DatasourceDao)SpringContextUtils.getBean("datasourceDao");
            DynamicDataSourceModel dbSource = datasourceDao.getDynamicDbSourceByCode(dbKey);
            try{
                dbSource.setDbPassword(AesUtil.decryptBySalt(dbSource.getDbPassword(),dbSource.getId()));
            }catch (Exception e){
                throw new RRException("动态数据源密钥解密失败,dbKey:"+dbKey);
            }
    
            if (dbSource != null) {
                getRedisTemplate().opsForValue().set(redisCacheKey, JSONObject.toJSONString(dbSource));
            }
            return dbSource;
        }
    

    还有一个重要的方法,把数据源放入缓存池的dbSource这个map属性里面

        /**
         * put 数据源缓存
         *
         * @param dbKey
         * @param db
         */
        public static void putCacheBasicDataSource(String dbKey, DruidDataSource db) {
            dbSources.put(dbKey, db);
        }
    

    3.再写一个工具类DynamicDBUtil 操作数据源缓存池

    这个类的核心方法是getDbSourceByDbKey(),先判断缓存池有没有对应key的数据源,没有则读取数据源配置(先从redis读配置,没有再从数据库读配置),根据配置创建DruidDataSource数据源,再把数据源放入缓存池

    getDbSourceByDbKey这个方法的dbKey是指能根据这个key找到数据库对应的记录,这里指该表的id

    /**
     * Spring JDBC 实时数据库访问
     *
     */
    @Slf4j
    public class DynamicDBUtil {
    
        /**
         * 通过 dbKey ,获取数据源
         *
         * @param dbKey
         * @return
         */
        public static DruidDataSource getDbSourceByDbKey(final String dbKey) {
    
            //先判断缓存中是否存在数据库链接
            DruidDataSource cacheDbSource = DataSourceCachePool.getCacheBasicDataSource(dbKey);
            if (cacheDbSource != null && !cacheDbSource.isClosed()) {
                log.debug("--------getDbSourceBydbKey------------------从缓存中获取DB连接-------------------");
                return cacheDbSource;
            } else {
                //获取多数据源配置
                DynamicDataSourceModel dbSource = DataSourceCachePool.getCacheDynamicDataSourceModel(dbKey);
                DruidDataSource dataSource = getJdbcDataSource(dbSource);
                if(dataSource!=null && dataSource.isEnable()){
                    DataSourceCachePool.putCacheBasicDataSource(dbKey, dataSource);
                }else{
                    throw new RRException("动态数据源连接失败,dbKey:"+dbKey);
                }
                log.info("--------getDbSourceBydbKey------------------创建DB数据库连接-------------------");
                return dataSource;
            }
        }
    
        /**
         * 获取数据源【最底层方法,不要随便调用】
         *
         * @param dbSource
         * @return
         */
        private static DruidDataSource getJdbcDataSource(final DynamicDataSourceModel dbSource) {
            DruidDataSource dataSource = new DruidDataSource();
    
            String driverClassName = dbSource.getDbDriver();
            String url = dbSource.getDbUrl();
            String dbUser = dbSource.getDbUsername();
            String dbPassword = dbSource.getDbPassword();
            dataSource.setDriverClassName(driverClassName);
            dataSource.setUrl(url);
            //dataSource.setValidationQuery("SELECT 1 FROM DUAL");
            dataSource.setTestWhileIdle(true);
            dataSource.setTestOnBorrow(false);
            dataSource.setTestOnReturn(false);
            dataSource.setBreakAfterAcquireFailure(true);
            dataSource.setConnectionErrorRetryAttempts(0);
            dataSource.setUsername(dbUser);
            dataSource.setMaxWait(60000);
            dataSource.setPassword(dbPassword);
    
            log.info("******************************************");
            log.info("*                                        *");
            log.info("*====【"+dbSource.getCode()+"】=====Druid连接池已启用 ====*");
            log.info("*                                        *");
            log.info("******************************************");
            return dataSource;
        }
    
        /**
         * 关闭数据库连接池
         *
         * @param dbKey
         * @return
         */
        public static void closeDbKey(final String dbKey) {
            DruidDataSource dataSource = getDbSourceByDbKey(dbKey);
            try {
                if (dataSource != null && !dataSource.isClosed()) {
                    dataSource.getConnection().commit();
                    dataSource.getConnection().close();
                    dataSource.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    
    
        private static JdbcTemplate getJdbcTemplate(String dbKey) {
            DruidDataSource dataSource = getDbSourceByDbKey(dbKey);
            return new JdbcTemplate(dataSource);
        }
    
        /**
         * 获取连接
         * @param url
         * @param username
         * @param password
         * @param driverName
         * @return
         */
        public static Connection getConn(String url,String username,String password,String driverName) {
            Connection conn = null;
            try {
                Class.forName(driverName);
                conn = DriverManager.getConnection(url, username, password);
            } catch (Exception e) {
                throw new RRException("无法连接,问题:"+e.getMessage(), e);
            }
    
            return conn;
        }
    
        /**
         * 关闭数据库连接
         * @param
         */
        public static void closeConnection(Connection conn) {
            try {
                if(conn!=null){
                    conn.close();
                }
            } catch (SQLException e) {
                throw new RRException("close connection failure", e);
            }
        }
    
    }
    

    4. 继承继承AbstractRoutingDataSource,重写抽象方法determineCurrentLookupKey()

    这里比上一篇的DynamicDataSource新增了targetDataSources静态变量和setDataSource()静态方法。

    targetDataSources这个属性用于存放多数据源

    /**
     * 多数据源
     *
     */
    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        public static Map<Object, Object> targetDataSources = new ConcurrentHashMap<>(10);
    
        @Override
        protected Object determineCurrentLookupKey() {
            return DynamicContextHolder.peek();
        }
    
        public static void setDataSource(String dbKey) throws Exception{
            if(!DynamicDataSource.targetDataSources.containsKey(dbKey)){
                DruidDataSource dataSource = DynamicDBUtil.getDbSourceByDbKey(dbKey);
                DynamicDataSource.targetDataSources.put(dbKey,dataSource);
            }
            //切换动态多数据源的dbKey
            DynamicContextHolder.push(dbKey);
            DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringContextUtils.getBean("dynamicDataSource");
            //使得修改后的targetDataSources生效
            dynamicDataSource.afterPropertiesSet();
        }
    
    }
    

    5. 数据源配置类

    下面代码通过dynamicDataSource.setTargetDataSources(DynamicDataSource.targetDataSources)把值引用赋值给dynamicDataSource对象(即指向同一块内存,修改了静态变量targetDataSources,就相当于修改了dynamicDataSource对象里面的targetDataSources属性)

    @Configuration
    @EnableConfigurationProperties(DynamicDataSourceProperties.class)
    public class DynamicDataSourceConfig {
        @Autowired
        private DynamicDataSourceProperties properties;
    
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.druid")
        public DataSourceProperties dataSourceProperties() {
            return new DataSourceProperties();
        }
    
        @Bean
        public DruidDataSource defaultDataSource(DataSourceProperties dataSourceProperties) {
            //默认数据源,通过配置获取创建
            DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
            return defaultDataSource;
        }
    
        @Bean
        @Primary
        @DependsOn({"defaultDataSource"})
        public DynamicDataSource dynamicDataSource(DruidDataSource defaultDataSource) {
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            //设置targetDataSources(通过数据库配置获取,首次创建没有数据源)
            dynamicDataSource.setTargetDataSources(DynamicDataSource.targetDataSources);
    
            //默认数据源
            dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
            return dynamicDataSource;
        }
    
    }
    

    6. 再写一个注解以及实现这个注解的切面类就可以了

    /**
     * 多数据源注解
     *
     */
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface DataSource {
        String value() default "";
    }
    
    /**
     * 多数据源,切面处理类
     *
     */
    @Aspect
    @Component
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class DataSourceAspect {
        protected Logger logger = LoggerFactory.getLogger(getClass());
    
        @Pointcut("@annotation(io.renren.datasource.annotation.DataSource) " +
                "|| @within(io.renren.datasource.annotation.DataSource)")
        public void dataSourcePointCut() {
    
        }
    
        @Around("dataSourcePointCut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
            MethodSignature signature = (MethodSignature) point.getSignature();
            Class targetClass = point.getTarget().getClass();
            Method method = signature.getMethod();
    
            DataSource targetDataSource = (DataSource)targetClass.getAnnotation(DataSource.class);
            DataSource methodDataSource = method.getAnnotation(DataSource.class);
            if(targetDataSource != null || methodDataSource != null){
                String value;
                if(methodDataSource != null){
                    value = methodDataSource.value();
                }else {
                    value = targetDataSource.value();
                }
                //根据dbKey动态设置数据源
                DynamicDataSource.setDataSource(dbKey);
                logger.debug("set datasource is {}", value);
            }
    
            try {
                return point.proceed();
            } finally {
                DynamicContextHolder.poll();
                logger.debug("clean datasource");
            }
        }
    }
    

    到这一步就已完成了,然后把DataSource注解加到service的类或方法上,即可实现操作指定的多数据源。

    三、通过接口的入参来指定数据源

    上面的注解DataSource的value是写死在代码里面的,但是我们有这样的需求,前端根据接口入参来操作指定数据源的数据。

    所以我们在上面的基础上,再改造一下
    再写两个注解

    /**
     * 多数据源注解-注解数据源的dbKey
     *
     * @author ZhangXinLin
     */
    @Target({ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface DbKey {
    }
    

    这个自定义注解DbKey,是作用在参数上的,标志该参数是用来指定数据源的dbKey

    /**
     * 多数据源注解
     *
     * @author ZhangXinLin
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface DynamicDataSource {
    
    }
    
    

    DynamicDataSource的自定义注解是用在controller的方法上

    DynamicDataSource注解的切面类

    这个切面类是根据方法的入参dbKey来动态切换数据源,核心代码是调用这行代码
    //根据dbKey动态设置数据源
    DynamicDataSource.setDataSource(dbKey);

    /**
     * @Description: 动态加载多数据源(启动后加载)
     **/
    @Aspect
    @Component
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class DynamicDataSourceAspect {
        protected Logger logger = LoggerFactory.getLogger(getClass());
    
        @Pointcut("@annotation(io.renren.datasource.annotation.DynamicDataSource) " +
                "|| @within(io.renren.datasource.annotation.DynamicDataSource)")
        public void dynamicdataSourcePointCut() {
    
        }
    
        @Around("dynamicdataSourcePointCut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
            //获取参数,根据参数获取数据源
            String dbKey = getDbKey(point);
    
            if( dbKey != null){
                //根据dbKey动态设置数据源
                DynamicDataSource.setDataSource(dbKey);
            }
            try {
                return point.proceed();
            } finally {
                DynamicContextHolder.poll();
                logger.debug("clean datasource");
            }
        }
    
    
        /**
         * 根据@DbKey注解获取数据源的dbKey
         * @param point
         * @return
         */
        private String getDbKey(ProceedingJoinPoint point) {
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            Object[] args = point.getArgs();
            String value = null;
            //参数注解,1维是参数,2维是注解
            Annotation[][] annotations = method.getParameterAnnotations();
            for (int i = 0; i < annotations.length; i++) {
                Object param = args[i];
                Annotation[] paramAnn = annotations[i];
                //参数为空,直接下一个参数
                if (param == null || paramAnn.length == 0) {
                    continue;
                }
                for (Annotation pAnnotation : paramAnn) {
                    if (pAnnotation.annotationType().equals(DbKey.class)) {
                        value =  param.toString();
                        break;
                    }
                }
            }
            return value;
        }
    }
    

    然后在controller的方法上加上注解@DynamicDataSource,以及入参加上注解@Dbkey

        /**
         * 查看数据源的所有表列表
         * @param id
         * @return
         */
        @DynamicDataSource
        @RequestMapping("/getTableList/{id}")
        public R getTableList(@PathVariable("id") @DbKey Integer id){
            List<Map<String, Object>> list = datasourceService.queryTableList(id);
            return R.ok().put("list", list);
        }
    

    看看实际效果

    1. 首先我们表的数据源的增删改写一个页面
      在这里插入图片描述

    在这里插入图片描述

    1. 在页面调用getTableList接口来切换数据源的数据

    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述
    可以看到,前端页面选择不同的数据库,后端接口就会根据dbKey的入参来动态切换数据源,从而查询出不同数据源的表名列表



    源码

    源码在一个还没有写完的快速开发平台的项目里面(功能可以在线编写模版,线上配置数据源,不用改代码就可以编写开发模版,生成不同系统的基础代码);
    这个项目还没写完,后面写完也会开源出来,所以这里的源码暂时没有

    展开全文
    qq_21187515 2021-11-29 16:33:13
  • @SpringBoot使用多数据源添加事务后多数据源不生效问题 SpringBoot使用多数据源添加事务后多数据源不生效问题 在网上找了好多解决方案,但是都好繁琐,我这个应该不是最佳解决方案,但应该是最简单的解决方案 一、 ...

    @SpringBoot使用多数据源添加事务后多数据源不生效问题

    SpringBoot使用多数据源添加事务后多数据源不生效问题

    在网上找了好多解决方案,但是都好繁琐,我这个应该不是最佳解决方案,但应该是最简单的解决方案
    一、
    springboot项目
    我的需求是: 将A库中未同步的数据 找出来 存到B库中,存储成功后 将A库中该条数据改为已同步
    二、
    我遇到的问题:因为有添加操作 所以需要加事务注解@Transactional,但是加上注解后多数据源就不生效,默认都走的主数据源也就是默认的数据源

    三、
    解决方法:将查询和添加分为两个方法,A方法查询,B方法添加,查询出来后调用B方法,重点就是调B方法的方式 ,事务注解要加在B方法上,也就是要加添加操作的方法上,详见代码

       public void A() {
    
            ApasinfoJYGLEntity sel = apasinfoJYGLMapper.selByPrimaryKey("1");
            System.out.println(sel);
            if(sel!=null){
     		// 这个调B方法的方式是重点,大概意思就是说将这个方法放在springBoot容器中管理
                SpringContextUtil.getBean(ApasinfoJYGLServiceImpl.class).B(sel);
    //            B(sel);
            }
    
        }
    
    
    
    //    @Scheduled(fixedDelay = 600000)
    @Transactional
        public void B(ApasinfoJYGLEntity entity ) {
    
            System.out.println("=============BB============");
    
            ApasinfoEntity apasinfoEntity = new ApasinfoEntity();
            apasinfoEntity.setProjid(entity.getProjid());
            apasinfoEntity.setTongid(entity.getTongid());
            apasinfoMapper.insertSelective(apasinfoEntity);
    
    		// 添加成功会修改状态
            entity.setExtend2("0000000000");
            apasinfoJYGLMapper.updateId(entity);
    		// 手动报异常,有异常就回滚
            int i1 = 1/0;
    
        }
    

    希望以上对你有帮助,感谢ys提供的解决方案,如果没有
    SpringContextUtil.getBean(***.class).B(sel);这个东西百度下看看怎么添加,获取spring上下文的工具

    展开全文
    lc2019715 2021-01-17 11:35:12
  • 由于本人使用的是eladmin框架,在进行集成springboot+jpa多数据源时报错导致项目无法启动,现改用以下方式进行多数据源的应用集成到eladmin框架里 1.首先在yml文件配置第一个数据源 2,配置第二个数据源 3.创建两...

    **

    一、前言

    由于本人使用的是eladmin框架,在进行集成springboot+jpa多数据源时报错导致项目无法启动,现改用以下方式进行多数据源的应用集成到eladmin框架里

    1.首先在yml文件配置第一个数据源
    在这里插入图片描述
    2,配置第二个数据源
    在这里插入图片描述
    3.创建两个配置文件
    在这里插入图片描述

    DataSourceConfig 配置类

    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.boot.jdbc.DataSourceBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;

    import javax.sql.DataSource;

    @Configuration
    public class DataSourceConfig {

    /***
     * 配置主数据源
     * @return
     */
    @Bean(name = "fistDS")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.fist")     ##与配置的第一个数据源名称对应
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    /****
     * 配置数据源
     * @return
     */
    @Bean(name = "sqlserverDS")
    @ConfigurationProperties(prefix = "spring.datasource.sqlserver")  ##与配置的第二个数据源名称对应
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    

    }

    JdbcTemplateConfig 配置类

    package me.zhengjie.modules.head.util;

    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.jdbc.core.JdbcTemplate;

    import javax.sql.DataSource;

    @Configuration
    public class JdbcTemplateConfig {

    @Bean
    public JdbcTemplate jdbcTemplatefalse(@Qualifier("fistDS") DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }
    
    @Bean
    public JdbcTemplate jdbcTemplatesqlserver(@Qualifier("sqlserverDS") DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }
    

    }

    3,第三步在你需要用到的类进行引用(注意,引用的类名与JdbcTemplateConfig 配置类里的方法名相对应,即可引用到对应的数据源)
    在这里插入图片描述

    4,引用后就可以进行增删改查操作
    在这里插入图片描述

    展开全文
    qq_41541310 2021-07-25 15:10:02
  • qq_42852587 2021-07-18 17:57:04
  • mvcVSstruts 2021-11-25 16:02:40
  • gork_mc 2021-12-01 09:40:05
  • qq_30060089 2021-01-04 16:09:08
  • weixin_44195615 2021-09-08 11:56:08
  • DreamsArchitects 2021-05-20 10:56:47
  • DreamsArchitects 2021-05-20 10:45:07
  • qq_36175946 2021-01-18 11:24:57
  • qq_40386113 2021-03-26 10:40:48
  • qq_19636353 2021-04-05 22:12:47
  • mayf666 2021-03-20 18:18:46
  • weixin_43968855 2020-12-21 15:36:22
  • u012613276 2021-02-26 14:28:00
  • weixin_32348697 2021-02-04 20:50:40
  • weixin_39930144 2021-07-08 01:23:16
  • weixin_36085895 2021-03-10 06:37:06
  • SharingOfficer 2021-11-21 14:40:07
  • yangmeng1111 2021-04-11 12:38:17
  • weixin_30045091 2021-01-19 02:18:13
  • weixin_29774037 2021-03-07 08:11:57
  • weixin_42500454 2021-01-19 04:03:43
  • weixin_39527078 2021-01-18 20:12:54
  • liuerchong 2021-07-21 15:26:44
  • weixin_33622555 2021-03-16 10:13:38
  • lindexi_gd 2021-05-22 16:01:20
  • weixin_38681967 2021-04-22 15:03:39

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 502,831
精华内容 201,132
关键字:

多数据源引用