动态配置切换数据源的方式大概就以下几种:
- AOP
- Mybatis/Mybatis-plus(当然Mybatis-plus的@DS注解更方便)
- Springboot-data-jpa
- Hirika数据库连接池
```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
@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
}
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
/**
* @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;
}
/**
* @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();
}
}
}
百度一搜一堆,我这里贴上其中一个
```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;
}
}
/**
* @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);
}
}
需要注意的是,在设置数据源的那里,不加修改mybatis的数据源的话,会导致请求切换数据源之后,只能在当前这个线程下去操作数据库。如果在这个请求接口之外操作切换的数据源的话,会导致不在同一个线程从而获取的还是默认的数据源;感兴趣的可以去试一试;