前端传递参数动态修改数据源应用到全局(Springboot+mybatis+spring-jdbc实现)

动态配置切换数据源的方式大概就以下几种:

  1. AOP
  2. Mybatis/Mybatis-plus(当然Mybatis-plus的@DS注解更方便)
  3. Springboot-data-jpa
  4. Hirika数据库连接池
  • 以上这几种无一例外,要想切换数据源的话,就必须使用spring内置的抽象类AbstractRoutingDataSource 。动态修改数据源的方式很多,文章也很多,但是通过前端传递参数修改数据源并且应用到全局的文章很少,本篇就为大家来介绍一下通过前端传递一个对象的方式修改数据源,并且把它应用到全局;

1.引入依赖


```java

        
        
            com.alibaba
            druid-spring-boot-starter
            1.2.11
        
        
        
            org.yaml
            snakeyaml
            1.29
        
        
        
            ru.yandex.clickhouse
            clickhouse-jdbc
            0.1.53
        
        
        
            org.springframework.boot
            spring-boot-starter-jdbc
        
         
            org.springframework.boot
            spring-boot-starter-web
        
   
        org.mybatis.spring.boot
        mybatis-spring-boot-starter
        2.2.2
    
    
        
            org.apache.commons
            commons-lang3
            3.12.0
        

        mysql
        mysql-connector-java
        5.7.49
    

2.编写数据库连接对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class DataSourceVo {
    private String key; // 给数据源的命名
    private String url;  // IP地址
    private String port; //端口号
    private String dataName; //数据库名称
    private String username; // 用户名
    private String password; // 密码
    private String dataType; //数据库类型 目前支持取值mysql/oracle/sqlserver/postgresql
}

3.编写通用yaml文件

server:
port: 8081

spring:
aop:
proxy-target-class: true #true为使用CGLIB代理,AOP动态代理
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306?test
username: root
password: 663970
dataType: mysql # 可选值 mysql oracle sqlserver postgresql
type: com.alibaba.druid.pool.DruidDataSource
druid:
test-while-idle: false
validation-query: select 1

四种数据源的配置 这里目前只配置两种

server:
  port: 8081

spring:
  aop:
    proxy-target-class: true  #true为使用CGLIB代理,AOP动态代理
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306?test
    username: root
    password: ****
    dataType: mysql  # 可选值 mysql oracle sqlserver postgresql
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      test-while-idle: false
      validation-query: select 1
datatype:
  mysql:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://{IP}:{port}/{database}
  clickhouse:
    driverClassName: ru.yandex.clickhouse.ClickHouseDriver
    url: jdbc:clickhouse://{IP}:{port}/{database}
  sqlserver:
    driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
    url: jdbc:sqlserver://{IP}:{port};DatabaseName={database}
  postgresql:
    driverClassName: org.postgresql.Driver
    url: jdbc:postgresql://{IP}:{port}/{database}

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3.编写数据源配置类

/**
 * @Author: Mr.Lee
 * @Date: 2022/8/13 11:05
 * @param null
 * @Description:
 *  动态数据源配置类
 */
@Configuration
//事务管理,数据库连接这里涉及到事务的提交
@EnableTransactionManagement   
public class DataSourceConfig {
    // 动态注入数据库信息
    @Value("${spring.datasource.url}")
    private String url;
    @Value("${spring.datasource.username}")
    private String username;
    @Value("${spring.datasource.password}")
    private String password;
    @Value("${spring.datasource.driverClassName}")
    private String driverClassName;
    @Value("${spring.datasource.dataType}")
    private String dataType;

    // 创建DynamicDataSource的bean交给SpringIOC容器管理
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource dataSource() {
        // 配置默认数据源
        DruidDataSource datasource = new DruidDataSource();
        datasource.setUrl(url);
        datasource.setUsername(username);
        datasource.setPassword(password);
        datasource.setDriverClassName(driverClassName);
        datasource.setTestWhileIdle(false);
        datasource.setName("default");

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("defaultDataSource",datasource);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        // 将该数据源设置成默认数据源
        dynamicDataSource.setDefaultTargetDataSource(datasource);
        return dynamicDataSource;
    }

    // 获取数据源的驱动信息
    public Map<String,Object> getDataBaseConfig() {
        Yaml yaml = new Yaml();
        Map<String,Object> map;
        try {
            map = yaml.load(new FileInputStream(System.getProperty("user.dir") + "\\src\\main\\resources\\application.yml"));
        } catch (IOException e) {
            throw new RuntimeException("SYS_PATH_ERROR");
        }
        //这里通过Map的方式获取到yaml文件里面的dataType是哪一个
        Map<String,Object> dataBaseConfig = (Map<String, Object>) map.get("datatype");
        return dataBaseConfig;
    }

4.编写切换数据源的核心配置类,并且修改mybatis的数据源应用全局,保证所有线程均可使用;(其中SpringContextUtils是获取spring容器中bean的工具类)

/**
 * @Author: Mr.Lee
 * @Date: 2022/8/13 11:11
 * @param null
 * @Description:
 * 切换数据源的核心配置类
 */

//涉及到数据源一定要加事务管理注解
@EnableTransactionManagement
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Autowired
    private DataSourceConfig dataSourceConfig;

    // 通过ThreadLocal线程隔离的优势线程存储线程,当前线程只能操作当前线程的局部变量
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    // 把已有的数据源封装在Map里
    private Map<Object, Object> dynamicTargetDataSources = new HashMap<>();

    //通过重写AbstractRoutingDataSource的内置函数,来通过当前连接的数据源的key,进行数据源的获取
    @Override
    protected Object determineCurrentLookupKey() {
        if (StringUtils.isEmpty(getDataSource())) {
            return "default";
        }
        return getDataSource();
    }

    // 设置默认数据源(必须要有,否则无法启动)
    @Override
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
    }

    // 通过设置数据源
    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
    }

    // 切换数据源,更改ThreadLocal中的局部变量
    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    // 获取数据源
    public static String getDataSource() {
        return contextHolder.get();
    }

    // 删除数据源(每次切换数据源的时候都应先移除已有数据源)
    public static void clearDataSource() {
        contextHolder.remove();
    }
	//创建一个新的数据源连接,并且设置此数据源为我们要用的数据源
    public boolean createDataSource(DataSourceVo dataSourceVo) throws NoSuchFieldException, IllegalAccessException {
        // 获取配置在Yaml文件中的所有数据源信息
        Map<String, Object> dataBaseConfig = dataSourceConfig.getDataBaseConfig();
        // 根据数据库类型获取数据源信息
        Map<String, String> dataConfig = (Map<String, String>) dataBaseConfig.get(dataSourceVo.getDataType());
        if(dataConfig == null) {
            // 不支持此类数据源
            throw new RuntimeException("不支持此数据源!");
        }
        String driveName = dataConfig.get("driverClassName");
        // url  替换其中的占位符
        String url = dataConfig.get("url").replaceAll("\\{IP}", dataSourceVo.getUrl())
                .replaceAll("\\{port}", dataSourceVo.getPort())
                .replaceAll("\\{database}",dataSourceVo.getDataName());
        // 测试连接
        testConnection(driveName, url, dataSourceVo.getUsername(), dataSourceVo.getPassword());

        // 通过Druid数据库连接池连接数据库
        DruidDataSource dataSource = new DruidDataSource();
        //接收前端传递的参数并且注入进去
        dataSource.setName(dataSourceVo.getDataName());
        dataSource.setUrl(url);
        dataSource.setUsername(dataSourceVo.getUsername());
        dataSource.setPassword(dataSourceVo.getPassword());
        dataSource.setDriverClassName(driveName);
        // 设置最大连接等待时间
        dataSource.setMaxWait(4000);

        // 数据源初始化
        try {
            dataSource.init();
        } catch (SQLException e) {
            // 创建失败则抛出异常
            throw new RuntimeException();
        }
        //获取当前数据源的键值对存入Map
        this.dynamicTargetDataSources.put(dataSourceVo.getKey(), dataSource);
        // 设置数据源
        this.setTargetDataSources(this.dynamicTargetDataSources);
        // 解析数据源
        super.afterPropertiesSet();
        // 切换数据源
        setDataSource(dataSourceVo.getKey());
        /**
         ** 修改mybatis的数据源
         * !!!重要,不修改mybatis的数据源的话,
         * 即使切换了数据源之后还是会出现默认数据源的情况
         */
        SqlSessionFactory SqlSessionFactory = (SqlSessionFactory) SpringContextUtils.getBean(SqlSessionFactory.class);
        Environment environment =SqlSessionFactory.getConfiguration().getEnvironment();
        Field dataSourceField = environment.getClass().getDeclaredField("dataSource");
        //跳过检验
        dataSourceField.setAccessible(true);
        //修改mybatis的数据源
        dataSourceField.set(environment,dataSource);
        //修改完成后所有线程使用此数据源
        return true;
    }
    // 测试数据源连接的方法
    public void testConnection(String driveClass, String url, String username, String password) {
        try {
            Class.forName(driveClass);
            DriverManager.getConnection(url, username, password);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }
}

5.SpringContextUtils工具类

百度一搜一堆,我这里贴上其中一个


```java
/*
 * 获取spring容器中bean工具类
 */
//这个注解的目的是为了把的SpringContextUtils的实例化交给Spring容器管理
@Component("springContextUtils")
@Configuration
public class SpringContextUtils implements ApplicationContextAware{
    private static ApplicationContext applicationContext = null;

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @SuppressWarnings("unchecked")
    public static <T> T getBean(String beanId) {
        return (T) applicationContext.getBean(beanId);
    }

    public static <T> T getBean(Class<T> requiredType) {
        return (T) applicationContext.getBean(requiredType);
    }

    /*
     * Spring容器启动后,会把 applicationContext 给自动注入进来,然后我们把 applicationContext 赋值到静态变量中,方便后续拿到容器对象
     * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }
}

6.编写接口测试连接

/**
 * @Author: Mr.Lee
 * @Date: 2022/8/13 12:13
 * @param null
 * @Description:
 *  数据源切换测试连接
 */
@RestController
@RequestMapping("/datasource")
public class DynamicDataSourceController {
    @Autowired
    private DynamicDataSource dynamicDataSource;
    @Resource
    private TestMapper testMapper;

    /**
     * 测试数据源的切换
     * @param dataSourceVo
     * @return
     * @throws SQLException
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    @PostMapping("/link")
    public JsonData link(@RequestBody DataSourceVo dataSourceVo) throws SQLException, NoSuchFieldException, IllegalAccessException {
        //切换数据源之前先清空
        DynamicDataSource.clearDataSource();
        //切换数据源
        dynamicDataSource.createDataSource(dataSourceVo);

        System.out.println("当前数据源:"+dynamicDataSource.getConnection());

        return JsonData.buildSuccess("当前数据源:"+dynamicDataSource.getConnection());
    }

    /**
     * 测试切换不同数据源之后的查询是否能成功
     * @return
     * @throws SQLException
     */
    @GetMapping("/test")
    public JsonData test() throws SQLException {
        System.out.println("当前数据源为:"+dynamicDataSource.getConnection());
        List<TStockSum> tStockSums = testMapper.selectList(new QueryWrapper<TStockSum>(null));
        return JsonData.buildSuccess(tStockSums);
    }
}
  • 我们可以看到数据源成功切换,对应的test请求也成功的从当前数据源里面查询到了数据
    前端传递参数动态修改数据源应用到全局(Springboot+mybatis+spring-jdbc实现)_第1张图片
    前端传递参数动态修改数据源应用到全局(Springboot+mybatis+spring-jdbc实现)_第2张图片
    前端传递参数动态修改数据源应用到全局(Springboot+mybatis+spring-jdbc实现)_第3张图片

需要注意的是,在设置数据源的那里,不加修改mybatis的数据源的话,会导致请求切换数据源之后,只能在当前这个线程下去操作数据库。如果在这个请求接口之外操作切换的数据源的话,会导致不在同一个线程从而获取的还是默认的数据源;感兴趣的可以去试一试;


  • 附:ThreadLocal的作用
    创作不易,点个赞再走呗!

你可能感兴趣的:(Java,mybatis,spring,boot,spring)