SpringBoot多数据源

多套数据源:

​ 定义:在项目中针对一个数据库都为其建立一套独立的数据处理逻辑,包括数据源(DataSource),会话工厂(SqlSessionFactory),连接,DAO操作。

SpringBoot多数据源_第1张图片

一、搭建springboot工程

​ 项目工程结构如下所示,主要从实体类和Mapper层开始进行划分,实际工作可从Service层进行划分。

├─config ---------------------------------- // 数据源配置
├─controller ------------------------------ // web服务
├─entity ---------------------------------- // 实体类
│ ├─master 
│ └─slave 
├─mapper ---------------------------------- // dao操作类
│ ├─master 
│ └─slave 
└─vo -------------------------------------- // 视图返回对象  
二、添加数据库配置文件

​ springboot默认的配置文件是application.properties。我们将数据库连接信息单独写一个配置文件jbdc.properties,放到Resource目录下,文件内容如下:

# master
spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.master.jdbc-url=jdbc:mysql://localhost:3306/mytest?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.master.username=root
spring.datasource.master.password=111111

# slave
spring.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.slave.jdbc-url=jdbc:mysql://localhost:3306/my_test1?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.slave.username=root
spring.datasource.slave.password=111111
三、配置数据源DataSource

​ 新增数据源配置类DatasourceConfig,读取配置文件信息,创建数据源Bean对象交给Spring容器管理。

@Configuration
@PropertySource("classpath:jdbc.properties")
public class DatasourceConfig {
    @Bean("master")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource(){
        return DataSourceBuilder.create().build();
    }

    @Bean("slave")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource(){
        return DataSourceBuilder.create().build();
    }
}

四、配置注入会话工厂对象

​ 分别为Master和Slave数据库添加配置类,用于创建SqlSessionFactory对象,交给Spring容器。

  1. MasterMybatisConfig.class:
@Configuration
@MapperScan(basePackages = "me.mason.demo.basicmultidatasource.mapper.master", sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterMybatisConfig {
    /**
     * 注意,此处需要使用MybatisSqlSessionFactoryBean,不是SqlSessionFactoryBean,
     * 否则,使用mybatis-plus的内置函数时就会报invalid bound statement (not found)异常
     */
    @Bean("masterSqlSessionFactory")
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("master") DataSource dataSource) throws Exception {
        // 设置数据源
        MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        mybatisSqlSessionFactoryBean.setDataSource(dataSource);
        //mapper的xml文件位置
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        String locationPattern = "classpath*:/mapper/master/*.xml";
        mybatisSqlSessionFactoryBean.setMapperLocations(resolver.getResources(locationPattern));
        //对应数据库的entity位置
        String typeAliasesPackage = "me.mason.demo.basicmultidatasource.entity.master";
        mybatisSqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage);
        return mybatisSqlSessionFactoryBean.getObject();
    }
}
  1. SlaveMybatisConfig.class:
@Configuration
@MapperScan(basePackages = "me.mason.demo.basicmultidatasource.mapper.slave", sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class SlaveMybatisConfig {
    /**
     * 注意,此处需要使用MybatisSqlSessionFactoryBean,不是SqlSessionFactoryBean,
     * 否则,使用mybatis-plus的内置函数时就会报invalid bound statement (not found)异常
     */
    @Bean("slaveSqlSessionFactory")
    public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slave") DataSource dataSource) throws Exception {
        // 设置数据源
        MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        mybatisSqlSessionFactoryBean.setDataSource(dataSource);
        //mapper的xml文件位置
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        String locationPattern = "classpath*:/mapper/slave/*.xml";
        mybatisSqlSessionFactoryBean.setMapperLocations(resolver.getResources(locationPattern));
        //对应数据库的entity位置
        String typeAliasesPackage = "me.mason.demo.basicmultidatasource.entity.slave";
        mybatisSqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage);
        return mybatisSqlSessionFactoryBean.getObject();
    }
}

​ 由于项目使用了MyBatis Plus,所以我们创建的会话工厂Bean对象是MybatisSqlSessionFactoryBean。

五、建立多套实体、Mapper和xml
  1. 实体:

    @Data
    @TableName("test_user")
    public class MasterTestUser implements Serializable {
        private static final long serialVersionUID = 1L;
        /** id */
        private Long id;
        /** 姓名 */
        private String name;
        ...
    }
    
  2. Mapper:

    @Repository
    public interface MasterTestUserMapper extends BaseMapper<MasterTestUser> {
        /**
         * 自定义查询
         * @param wrapper 条件构造器
         */
        List<MasterTestUser> selectAll(@Param(Constants.WRAPPER)Wrapper<MasterTestUser> wrapper);
    }
    

    slave数据库对应的Mapper同上类似。

  3. xml文件:

    <mapper namespace="me.mason.demo.basicmultidatasource.mapper.master.MasterTestUserMapper">
        <select id="selectAll" resultType="masterTestUser">
            select * from test_user
            <if test="ew!=null">
              ${ew.customSqlSegment}
            if>
        select>
    mapper>
    
六、使用:
@RestController
@RequestMapping("/user")
public class TestUserController {

    @Autowired
    private MasterTestUserMapper masterTestUserMapper;
    @Autowired
    private SlaveTestUserMapper slaveTestUserMapper;
    /**
     * 查询全部
     */
    @GetMapping("/listall")
    public Object listAll() {
        //master库,自定义接口查询
        QueryWrapper<MasterTestUser> queryWrapper = new QueryWrapper<>();
        List<MasterTestUser> resultData = masterTestUserMapper.selectAll(queryWrapper.isNotNull("name"));
        //slave库,mp内置接口
        List<SlaveTestUser> resultDataSlave = slaveTestUserMapper.selectList(null);
        //返回
        Map<String, Object> result = new HashMap<>();
        result.put("master" , resultData);
        result.put("slave" , resultDataSlave);
        return ResponseResult.success(result);
    }

}

​ **总结:**多套数据源符合开闭原则,当我们新增或者删除数据库时,只要修改对应的即可,不会影响到原有数据库。在使用时,我们需要注入对应的mapper,再调用其对应的方法,对于很多方法,代码实现逻辑是一致的,只是选择的数据源不同,这就造成了代码存在一定的冗余,缺乏灵活性。另外,对于一主多从的情况,若需要对多个从库进行负载均衡,相对比较麻烦。

动态数据源:

​ 定义:确定数量的多个数据源共用一个会话工厂,根据条件动态选取数据源进行连接、SQL 操作。

SpringBoot多数据源_第2张图片

​ 相比于多套数据源项目,动态数据源不再是为每个数据库建立一套独立的数据处理逻辑,而是根据实际业务需求来统一逻辑,再需要切换数据源的实际进行处理即可,所以我们不需要在Service层或者Mapper层就对数据库进行划分。

​ SpringBoot动态数据源的本质是将多个DataSource存储在一个Map集合中,当需要用到某个数据源时,从Map中获取此数据源进行处理。Spring提供了抽象类AbstractRoutingDataSource,实现了此功能,所以我们实现动态数据源时继承它,实现自己的获取数据源的逻辑即可。

一、搭建springboot工程
├─annotation ---- // 自定义注解
├─aop ----------- // 切面
├─config -------- // 数据源配置
├─constants ----- // 常用注解
├─context ------- // 自定义上下文
├─controller ---- // 访问接口
├─entity -------- // 实体
├─mapper -------- // 数据库dao操作
├─service ------- // 服务类
└─vo ------------ // 视图返回数据

二、添加数据库配置文件

jdbc.properties :

# master
spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.master.jdbc-url=jdbc:mysql://localhost:3306/mytest?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.master.username=root
spring.datasource.master.password=111111

# slave
spring.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.slave.jdbc-url=jdbc:mysql://localhost:3306/my_test1?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8
spring.datasource.slave.username=root
spring.datasource.slave.password=111111

三、配置数据源DataSource

DynamicDataSourceConfig.class :

@Configuration
@PropertySource("classpath:jdbc.properties")
@MapperScan(basePackages = "me.mason.demo.dynamicdatasource.mapper")
public class DynamicDataSourceConfig {
    @Bean(DataSourceConstants.DS_KEY_MASTER)
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(DataSourceConstants.DS_KEY_SLAVE)
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }  
}

四、配置注入会话工厂对象

​ 我们不需要为每个数据库创建SqlSessionFactory对象,直接使用SpringBoot自动配置的SqlSessionFactory 即可。

五、动态数据源配置
  1. 添加jdbc起步依赖:

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-jdbcartifactId>
    dependency>
    
    
  2. 添加动态数据源类:

    public class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            // 此处暂时返回固定 master 数据源, 后面按动态策略修改
            return DataSourceConstants.DS_KEY_MASTER;
        }
    }
    
    
  3. DynamicDataSourceConfig.class设置动态数据源:

    使用集合保存多个DataSource,并设置到动态数据源对象中,且设置动态数据源的优先级最高。

    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());
        dataSourceMap.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());
        //设置动态数据源
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
    
        return dynamicDataSource;
    }
    
    
  4. DynamicDataSourceConfig.class 中,排除 DataSourceAutoConfiguration 的自动配置:

    @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
    
    
  5. 完整的DynamicDataSourceConfig.class配置如下:

    @Configuration
    @PropertySource("classpath:jdbc.properties")
    @MapperScan(basePackages = "me.mason.demo.dynamicdatasource.mapper")
    @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class })
    public class DynamicDataSourceConfig {
        @Bean(DataSourceConstants.DS_KEY_MASTER)
        @ConfigurationProperties(prefix = "spring.datasource.master")
        public DataSource masterDataSource() {
            return DataSourceBuilder.create().build();
        }
    
        @Bean(DataSourceConstants.DS_KEY_SLAVE)
        @ConfigurationProperties(prefix = "spring.datasource.slave")
        public DataSource slaveDataSource() {
            return DataSourceBuilder.create().build();
        }  
        
        @Bean
        @Primary
        public DataSource dynamicDataSource() {
            Map<Object, Object> dataSourceMap = new HashMap<>(2);
            dataSourceMap.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());
            dataSourceMap.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());
            //设置动态数据源
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            dynamicDataSource.setTargetDataSources(dataSourceMap);
            dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
    
            return dynamicDataSource;
        }
    }
    
    
六、动态选择数据源:
  1. 数据源key的上下文:

    ​ 前面AbstractRoutingDataSource的实现类DynamicDataSource,固定写了一个数据源路由策略,总是返回 master,显然不是我们想要的。我们想要的是在需要的地方,想切换就切换。因此,需要有一个动态获取数据源 key 的地方(我们称为上下文),对于 web 应用,访问以线程为单位,使用 ThreadLocal 就比较合适

    public class DynamicDataSourceContextHolder {
        /**
         * 动态数据源名称上下文
         */
        private static final ThreadLocal<String> DATASOURCE_CONTEXT_KEY_HOLDER = new ThreadLocal<>();
        /**
         * 设置/切换数据源
         */
        public static void setContextKey(String key){
            DATASOURCE_CONTEXT_KEY_HOLDER.set(key);
        }
        /**
         * 获取数据源名称
         */
        public static String getContextKey(){
            String key = DATASOURCE_CONTEXT_KEY_HOLDER.get();
            return key == null?DataSourceConstants.DS_KEY_MASTER:key;
        }
    
        /**
         * 删除当前数据源名称
         */
        public static void removeContextKey(){
            DATASOURCE_CONTEXT_KEY_HOLDER.remove();
        } 
    
    
  2. 设置DynamicDataSource路由策略:

    public class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return DynamicDataSourceContextHolder.getContextKey();
        }
    }
    
    
  3. 基本使用:

    @RestController
    @RequestMapping("/user")
    public class TestUserController {
    
        @Autowired
        private TestUserMapper testUserMapper;
    
        /**
         * 查询全部
         */
        @GetMapping("/listall")
        public Object listAll() {
            int initSize = 2;
            Map<String, Object> result = new HashMap<>(initSize);
            //默认master查询
            QueryWrapper<TestUser> queryWrapper = new QueryWrapper<>();
            List<TestUser> resultData = testUserMapper.selectAll(queryWrapper.isNotNull("name"));
            result.put(DataSourceConstants.DS_KEY_MASTER, resultData);
    
            //切换数据源,在slave查询
            DynamicDataSourceContextHolder.setContextKey(DataSourceConstants.DS_KEY_SLAVE);
            List<TestUser> resultDataSlave = testUserMapper.selectList(null);
            result.put(DataSourceConstants.DS_KEY_SLAVE, resultDataSlave);
            //恢复数据源
            DynamicDataSourceContextHolder.removeContextKey();
            //返回数据
            return ResponseResult.success(result);
        }
    }
    
    
七、使用AOP切换数据源

​ 上面我们通过setContextKeyremoveContextKey进行数据源的切换,每次切换都需要我们重复手动调用方法,造成了代码冗余,下面我们通过配置切面和自定义注解,实现添加注解实现切换。

  1. 添加自定义注解DS

    @Target({ElementType.METHOD,ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DS {
        /**
         * 数据源名称
         */
        String value() default DataSourceConstants.DS_KEY_MASTER;
    }
    
    
  2. 添加aop起步依赖:

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-aopartifactId>
    dependency>
    
    
  3. 定义切面:

    • 注解 Pointcut 使用 annotation 指定注解
    • 注解 Around 使用环绕通知处理,使用上下文进行对使用注解 DS 的值进行数据源切换,处理完后,恢复数据源。
    @Aspect
    @Component
    public class DynamicDataSourceAspect {
        @Pointcut("@annotation(me.mason.demo.dynamicdatasource.annotation.DS)")
        public void dataSourcePointCut(){
    
        }
    
        @Around("dataSourcePointCut()")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            String dsKey = getDSAnnotation(joinPoint).value();
            DynamicDataSourceContextHolder.setContextKey(dsKey);
            try{
                return joinPoint.proceed();
            }finally {
                DynamicDataSourceContextHolder.removeContextKey();
            }
        }
    
        /**
         * 根据类或方法获取数据源注解
         */
        private DS getDSAnnotation(ProceedingJoinPoint joinPoint){
            Class<?> targetClass = joinPoint.getTarget().getClass();
            DS dsAnnotation = targetClass.getAnnotation(DS.class);
            // 先判断类的注解,再判断方法注解
            if(Objects.nonNull(dsAnnotation)){
                return dsAnnotation;
            }else{
                MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
                return methodSignature.getMethod().getAnnotation(DS.class);
            }
        }
    }
    
    
  4. 基本使用:

    创建TestUserService类,定义两个方法,分别是从 master 和 slave 中获取数据,使用了注解DS

    /**
     * 查询master库User
     */
    @DS(DataSourceConstants.DS_KEY_MASTER)
    public List<TestUser> getMasterUser() {
        QueryWrapper<TestUser> queryWrapper = new QueryWrapper<>();
        return testUserMapper.selectAll(queryWrapper.isNotNull("name"));
    }
    
    /**
     * 查询slave库User
     */
    @DS(DataSourceConstants.DS_KEY_SLAVE)
    public List<TestUser> getSlaveUser() { 
        return testUserMapper.selectList(null); 
    }
    
    

    在controller层调用不同的方法:

    @GetMapping("/listall")
    public Object listAll() {
    	int initSize = 2;
    	Map<String, Object> result = new HashMap<>(initSize);
    	//默认master数据源查询
    	List<TestUser> masterUser = testUserService.getMasterUser();
    	result.put(DataSourceConstants.DS_KEY_MASTER, masterUser);
    	//从slave数据源查询
    	List<TestUser> slaveUser = testUserService.getSlaveUser();
    	result.put(DataSourceConstants.DS_KEY_SLAVE, slaveUser);
    	//返回数据
    	return ResponseResult.success(result);
    }
    
    
八、MyBatis-Plus动态数据源插件使用:
  1. 引入依赖:

    <dependency>
      <groupId>com.baomidougroupId>
      <artifactId>dynamic-datasource-spring-boot-starterartifactId>
      <version>${version}version>
    dependency>
    
    
  2. 配置数据源:

    spring:
      datasource:
        dynamic:
          primary: master #设置默认的数据源或者数据源组,默认值即为master
          strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
          datasource:
            master:
              url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
              username: root
              password: 123456
              driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
            slave_1:
              url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
              username: root
              password: 123456
              driver-class-name: com.mysql.jdbc.Driver
            slave_2:
              url: ENC(xxxxx) # 内置加密,使用请查看详细文档
              username: ENC(xxxxx)
              password: ENC(xxxxx)
              driver-class-name: com.mysql.jdbc.Driver
    
    
  3. 使用@DS切换数据源:

    @DS可注解在方法或者类上,同时存在就近原则,方法上注解优先于类上注解。

注意:

DruidDataSourceAutoConfigure会注入一个DataSourceWrapper,其会在原生的spring.datasource下找url,username,password等。而我们动态数据源的配置路径是变化的,所以需要排除:

spring:
  autoconfigure:
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure

或者在启动类上排除:

@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)

​ **总结:**上面手动编码自定义注解和配置切面实现了数据源的切换,MyBatis-Plus也提供了相应的插件,基本实现原理都差不多,实际开发中,直接引入插件起步依赖,并配置数据源即可。但是不管是我们手动编码实现还是集成MyBatis-Plus插件,都仅仅只是实现了数据源的切换,没有涉及事务相关的处理,当我们定义的方法中嵌套使用不同数据源进行操作时,spring提供的事务是无效的,虽然我们在开发中应该尽量避免跨库事务,但如果避免不了,则需要使用分布式事务。

​ 不管是多套数据源还是动态数据源,数据源本身在编码阶段就已经确定,无法实时添加数据源并进行切换。

参数化变更数据源:

​ 定义:根据参数添加数据源,并进行数据源切换,数据源数量不确定。通常用于对多个数据库的管理工作。

​ 思路:SpringBoot动态数据源的本质是将多个DataSource存储在一个Map集合中,当需要用到某个DataSource时,从Map中获取此数据源进行处理。Spring提供了抽象类AbstractRoutingDataSource,实现了此功能。我们可以把新的数据源添加到SpringBoot保存数据源的集合中,以供我们切换使用。但是从AbstractRoutingDataSource源码可看出,存放数据源的 Map targetDataSources 是 private 的,而且并没有提供对此 Map 本身的操作,它提供的是两个关键操作:setTargetDataSourcesafterPropertiesSet 。其中 setTargetDataSources 设置整个 Map 目标数据源,afterPropertiesSet 则是对 Map 目标数据源进行解析,形成最终使用的 resolvedDataSources,可见以下源码:

SpringBoot多数据源_第3张图片

因此,为实现动态添加数据源到 Map 的功能,我们可以根据这两个关键操作进行处理。

一、动态数据源配置:
  1. 添加动态数据源:

    • 添加了自定义的 backupTargetDataSources 作为原 targetDataSources 的拷贝
    • 自定义构造函数,把需要保存的目标数据源拷贝到自定义的 Map 中
    • 添加新数据源时,依然使用 setTargetDataSourcesafterPropertiesSet 完成新数据源添加。
    • 注意:afterPropertiesSet 的作用很重要,它负责解析成可用的目标数据源。
    public class DynamicDataSource extends AbstractRoutingDataSource {
        private Map<Object, Object> backupTargetDataSources;
    
        /**
         * 自定义构造函数
         */
        public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSource){
            backupTargetDataSources = targetDataSource;
            super.setDefaultTargetDataSource(defaultDataSource);
            super.setTargetDataSources(backupTargetDataSources);
            super.afterPropertiesSet();
        }
    
        /**
         * 添加新数据源
         */
        public void addDataSource(String key, DataSource dataSource){
            this.backupTargetDataSources.put(key,dataSource);
            super.setTargetDataSources(this.backupTargetDataSources);
            super.afterPropertiesSet();
        }
    
        @Override
        protected Object determineCurrentLookupKey() {
            return DynamicDataSourceContextHolder.getContextKey();
        }
    }
    
    
  2. 动态数据源配置:

    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put(DataSourceConstants.DS_KEY_MASTER, masterDataSource());
        dataSourceMap.put(DataSourceConstants.DS_KEY_SLAVE, slaveDataSource());
        // 有参构造函数
        return new DynamicDataSource(masterDataSource(), dataSourceMap);
    }
    
    
二、添加数据源工具类:
  1. 添加Spring上下文工具类:

    @Component
    public class SpringContextHolder implements ApplicationContextAware {
        private static ApplicationContext applicationContext;
        
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            SpringContextHolder.applicationContext = applicationContext;
        }
        
        /**
         * 返回上下文
         */
        public static ApplicationContext getContext(){
            return SpringContextHolder.applicationContext;
        }
    }
    
    
  2. 添加数据源操作工具类:

    public class DataSourceUtil {
        /**
         * 创建新的数据源,注意:此处只针对 MySQL 数据库
         */
        public static DataSource makeNewDataSource(DbInfo dbInfo){
            String url = "jdbc:mysql://"+dbInfo.getIp() + ":"+dbInfo.getPort()+"/"+dbInfo.getDbName()
                    +"?useSSL=false&serverTimezone=GMT%2B8&characterEncoding=UTF-8";
            String driveClassName = StringUtils.isEmpty(dbInfo.getDriveClassName())? "com.mysql.cj.jdbc.Driver":dbInfo.getDriveClassName();
            return DataSourceBuilder.create().url(url)
                    .driverClassName(driveClassName)
                    .username(dbInfo.getUsername())
                    .password(dbInfo.getPassword())
                    .build();
        }
    
        /**
         * 添加数据源到动态源中
         */
        public static void addDataSourceToDynamic(String key, DataSource dataSource){
            DynamicDataSource dynamicDataSource = SpringContextHolder.getContext().getBean(DynamicDataSource.class);
            dynamicDataSource.addDataSource(key,dataSource);
        }
    
        /**
         * 根据数据库连接信息添加数据源到动态源中
         * @param key
         * @param dbInfo
         */
        public static void addDataSourceToDynamic(String key, DbInfo dbInfo){
            DataSource dataSource = makeNewDataSource(dbInfo);
            addDataSourceToDynamic(key,dataSource);
        }
    }
    
    
三、基本使用:
  1. 添加Mapper:

    @Repository
    public interface TableMapper extends BaseMapper<TestUser> {
        /**
         * 查询表信息
         */
        @Select("select table_name, table_comment, create_time, update_time " +
                " from information_schema.tables " +
                " where table_schema = (select database())")
        List<Map<String,Object>> selectTableList();
    }
    
    
  2. 定义数据库连接信息对象:

    @Data
    public class DbInfo {
        private String ip;
        private String port;
        private String dbName;
        private String driveClassName;
        private String username;
        private String password;
    }
    
    
  3. 添加数据源并使用:

    /**
     * 根据数据库连接信息获取表信息
     */
    @GetMapping("table")
    public Object findWithDbInfo(DbInfo dbInfo) throws Exception {
        //数据源key
        String newDsKey = System.currentTimeMillis()+"";
        //添加数据源
        DataSourceUtil.addDataSourceToDynamic(newDsKey,dbInfo);
        DynamicDataSourceContextHolder.setContextKey(newDsKey);
        //查询表信息
        List<Map<String, Object>> tables = tableMapper.selectTableList();
        DynamicDataSourceContextHolder.removeContextKey();
        return ResponseResult.success(tables);
    }
    
    
四、动态代理优化代码:

​ 上面实现参数化变更数据源的功能,但是仍需要手动的调用setContextKeyremoveContextKey方法进行数据源切换变更,下面采用JDK动态代理的方法,取消模板代码。

  1. 添加动态代理类:

    public class JdkParamDsMethodProxy implements InvocationHandler {
        // 代理对象及相应参数
        private String dataSourceKey;
        private DbInfo dbInfo;
        private Object targetObject;
        public JdkParamDsMethodProxy(Object targetObject, String dataSourceKey, DbInfo dbInfo) {
            this.targetObject = targetObject;
            this.dataSourceKey = dataSourceKey;
            this.dbInfo = dbInfo;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //切换数据源
            DataSourceUtil.addDataSourceToDynamic(dataSourceKey, dbInfo);
            DynamicDataSourceContextHolder.setContextKey(dataSourceKey);
            //调用方法
            Object result = method.invoke(targetObject, args);
            DynamicDataSourceContextHolder.removeContextKey();
            return result;
        }
    
        /**
         * 创建代理
         */
        public static Object createProxyInstance(Object targetObject, String dataSourceKey, DbInfo dbInfo) throws Exception {
            return Proxy.newProxyInstance(targetObject.getClass().getClassLoader()
                    , targetObject.getClass().getInterfaces(), new JdkParamDsMethodProxy(targetObject, dataSourceKey, dbInfo));
        }
    }
    
    
  2. 使用:

    @GetMapping("table")
    public Object findWithDbInfo(DbInfo dbInfo) throws Exception {
        //数据源key
        String newDsKey = System.currentTimeMillis()+"";
        //使用代理切换数据源
        TableMapper tableMapperProxy = (TableMapper)JdkParamDsMethodProxy.createProxyInstance(tableMapper, newDsKey, dbInfo);
        List<Map<String, Object>> tables = tableMapperProxy.selectTableList();
        return ResponseResult.success(tables);
    }
    
    

你可能感兴趣的:(Java笔记,spring,boot,java,spring)