动态配置切换数据源的方式大概就以下几种:
- AOP
- Mybatis/Mybatis-plus(当然Mybatis-plus的@DS注解更方便)
- Springboot-data-jpa
- Hirika数据库连接池
- 以上这几种无一例外,要想切换数据源的话,就必须使用spring内置的抽象类AbstractRoutingDataSource 。动态修改数据源的方式很多,文章也很多,但是通过前端传递参数修改数据源并且应用到全局的文章很少,本篇就为大家来介绍一下通过前端传递一个对象的方式修改数据源,并且把它应用到全局;
1.引入依赖
```java
<!-- mysql数据源 -->
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.11</version>
</dependency>
<!--用于写入yaml文件-->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.29</version>
</dependency>
<!-- clickhouse数据源 -->
<dependency>
<groupId>ru.yandex.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.1.53</version>
</dependency>
<!-- 核心依赖,内置了spring-jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- 工具包 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>5.7.49</scope>
</dependency>
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请求也成功的从当前数据源里面查询到了数据



需要注意的是,在设置数据源的那里,不加修改mybatis的数据源的话,会导致请求切换数据源之后,只能在当前这个线程下去操作数据库。如果在这个请求接口之外操作切换的数据源的话,会导致不在同一个线程从而获取的还是默认的数据源;感兴趣的可以去试一试;
- 附:ThreadLocal的作用
创作不易,点个赞再走呗!
版权声明:本文为qq_46012880原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。