SpringBoot动态切换数据源

SpringBoot动态切换数据源

1.需求

低代码服务需要给多套系统进行功能配置,要求表结构必须生成在对应系统的数据库中,所以表结构的生成需要动态的获取目标系统的数据库信息,切换当前数据源到目标数据库,然后再生成。

2.创建数据源配置类

java 复制代码
package com.tyq.datasource.config.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import com.tyq.datasource.config.Properties;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

/**
 * 向Spring容器中注入DruidConfiguration
 *
 * @author 谭永强
 */
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class DruidConfiguration {

    @Resource
    private Properties properties;

    /**
     * 数据源(默认)
     */
    public DataSource dataSourceToDefault() {
        DruidDataSource datasource = new DruidDataSource();
        try {
            datasource.setQueryTimeout(0);
            datasource.setUrl("jdbc:oracle:thin:@192.168.0.30:1521/test2");
            datasource.setUsername("SCPS");
            datasource.setPassword("yldtscps");
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return datasource;
    }

    @Bean
    @Primary
    public DataSource dataSource() {
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put("default", dataSourceToDefault());
        DynamicDatasource dynamicDatasource = new DynamicDatasource();
      	// 注入目标数据源,如果有多个数据源,直接加入map即可
        dynamicDatasource.setTargetDataSources(dataSourceMap);
        // 注入默认数据源
        dynamicDatasource.setDefaultTargetDataSource(dataSourceToDefault());
        return dynamicDatasource;
    }
}

3.切换数据源

SpringBoot动态切换数据源主要依靠AbstractRoutingDataSource类,这个抽象类中有一个属性为targetDataSources。该属性为Map结构,所有需要切换的数据源都存放在其中,根据指定的KEY进行切换。

在AbstractRoutingDataSource源码中,获取数据库连接是通过this.determineTargetDataSource().getConnection()去获取的,而this.determineTargetDataSource()方法中的DataSource则是通过determineCurrentLookupKey()方法返回的key值去前面的dynamicDatasource.setTargetDataSources(dataSourceMap);设置进去的map中查找的。

所以我们需要重写determineCurrentLookupKey()方法,如下:

java 复制代码
package com.tyq.datasource.config.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 动态数据源
 *
 * @author 谭永强
 * @date 2023-08-03
 */
public class DynamicDatasource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDatasourceHolder.getDataSource();
    }
}

4.切换数据源管理类

数据源属于公共资源,考虑到多线程的情况下,我们将数据源存储在【ThreadLocal】中,保证线程隔离。

java 复制代码
package com.tyq.datasource.config.datasource;

import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;

/**
 * 数据源切换管理
 *
 * @author 谭永强
 * @date 2023-08-03
 */
public class DynamicDatasourceHolder {
    /**
     * 保存数据源的映射
     */
    private final static ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    public static void setDataSource(String dataSourceKey) {
        CONTEXT_HOLDER.set(dataSourceKey);
    }

    public static void removeDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

5.使用案例

在Controller中通过DynamicDatasourceHolder指定当前需要使用哪个数据源,具体使用如下:

java 复制代码
@RequestMapping("findById")
public User findById(String userId) {
  //指定数据源
  DynamicDatasourceHolder.setDataSource("default");
  if (ObjectUtils.isEmpty(userId)) {
      throw new ParamValidateException("userId不能为空");
  }
  User user = userService.findById(userId);
  //移除当前数据源
  DynamicDatasourceHolder.removeDataSource();
  return user;
}

上述案例中已经实现了如果动态切换数据源的过程,但是在实际开发中还是太繁琐,比如每个接口都必须添加相关切换数据源的代码,对代码的侵入性太高,下面我们通过AOP的形式去优化切换数据源的过程。

5.AOP切面拦截

通过AOP切面对方法的前置和后置做切换数据源的操作,这样就降低了与业务代码耦合、

xml 复制代码
<!--AOP-->
<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.9.6</version>
</dependency>

注意:如果项目中无法导入@Aspect,则需要添加上述依赖。

java 复制代码
package com.tyq.datasource.aop;

import com.alibaba.druid.pool.DruidDataSource;
import com.tyq.datasource.config.Properties;
import com.tyq.datasource.config.datasource.DynamicDatasource;
import com.tyq.datasource.config.datasource.DynamicDatasourceHolder;
import com.tyq.datasource.config.exception.ParamValidateException;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

/**
 * 动态数据源切换AOP
 *
 * @author 谭永强
 * @date 2023-08-03
 */
@Aspect
@Component
public class DynamicDatasourceAop {

    @Autowired
    protected ApplicationContext applicationContext;
    @Resource
    private Properties properties;

    /**
     * 定义切点
     */
    @Pointcut("execution (* com.tyq.datasource.controller.*.*(..))")
    public void pointcut() {
    }

    /**
     * 前置处理
     */
    @Before(value = "pointcut()")
    public void beforeAdvice() {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (ObjectUtils.isEmpty(attributes)) {
            return;
        }
        HttpServletRequest request = attributes.getRequest();
        String dataSourceKey = request.getParameter("dataSourceKey");
        if (ObjectUtils.isEmpty(dataSourceKey)) {
            throw new ParamValidateException("dataSourceKey不能为空!");
        }
        //获取当前动态数据源
        DynamicDatasource dynamicDatasource = applicationContext.getBean(DynamicDatasource.class);
        //所有已连接的数据源集合
        Map<Object, DataSource> resolvedDataSources = dynamicDatasource.getResolvedDataSources();
        if (ObjectUtils.isEmpty(resolvedDataSources.get(dataSourceKey))) {
            //此处为模拟到数据库中查询数据源信息的过程,查询到数据源信息后,创建数据源并添加到动态数据源管理中
            //动态加入新的数据源
            DataSource dataSource = getDataSource();
            Map<Object, Object> dataSourceMap = new HashMap<>(resolvedDataSources.size() + 1);
            dataSourceMap.putAll(resolvedDataSources);
            dataSourceMap.put("three", dataSource);
            dynamicDatasource.setTargetDataSources(dataSourceMap);
        }
        //动态切换数据源
        DynamicDatasourceHolder.setDataSource(dataSourceKey);
    }

    /**
     * 后置处理
     */
    @AfterReturning(value = "pointcut()")
    public void afterAdvice() {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (ObjectUtils.isEmpty(attributes)) {
            return;
        }
        HttpServletRequest request = attributes.getRequest();
        String dataSourceKey = request.getParameter("dataSourceKey");
        if (ObjectUtils.isEmpty(dataSourceKey)) {
            throw new ParamValidateException("dataSourceKey不能为空!");
        }
        System.out.println("后置处理:" + dataSourceKey);
        //删除当前数据源
        DynamicDatasourceHolder.removeDataSource();
    }

    /**
     * 模拟成功查询到数据源并创建DataSource
     *
     * @return 数据源
     */
    public DataSource getDataSource() {
        DruidDataSource datasource = new DruidDataSource();
        try {
            datasource.setQueryTimeout(0);
            datasource.setUrl("jdbc:oracle:thin:@192.168.0.59:1526/qstest");
            datasource.setUsername("SCPS");
            datasource.setPassword("yldtscps");
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return datasource;
    }
}
相关推荐
一勺菠萝丶11 分钟前
深入浅出:Spring Boot 中 RestTemplate 的完整使用指南
java·spring boot·后端
努力的搬砖人.15 分钟前
Java 线程池原理
java·开发语言
有梦想的攻城狮1 小时前
SpEL(Spring Expression Language)使用详解
java·后端·spring·spel
极小狐1 小时前
如何从极狐GitLab 容器镜像库中删除容器镜像?
java·linux·开发语言·数据库·python·elasticsearch·gitlab
caihuayuan51 小时前
前端面试2
java·大数据·spring boot·后端·课程设计
黄雪超1 小时前
JVM——Java语法糖与Java编译器
java·开发语言·jvm
旷野本野1 小时前
【JavaWeb+后端常用部件】
java·开发语言
郭尘帅6662 小时前
SpringBoot学习(上) , SpringBoot项目的创建(IDEA2024版本)
spring boot·后端·学习
大G哥2 小时前
Rust 之 trait 与泛型的奥秘
java·开发语言·jvm·数据结构·rust
isyangli_blog2 小时前
(1-1)Java的JDK、JRE、JVM三者间的关系
java·开发语言·jvm