一、SpringBoo中Mybatis多数据源动态切换
myzbx 2025-06-30 18:50 46 浏览
我们以一个实例来详细说明一下如何在SpringBoot中动态切换MyBatis的数据源。
一、需求
1、用户可以界面新增数据源相关信息,提交后,保存到数据库
2、保存后的数据源需要动态生效,并且可以由用户动态切换选择使用哪个数据源
3、数据库保存了多个数据源的相关记录后,要求在系统启动时把这些个数据源创建出来,用户在使用时可以自由选择切换
二、项目准备
创建项目的基础骨架
建项目
项目名:dds
改pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xiaoxie</groupId>
<artifactId>dds</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>dds</name>
<description>dds</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</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>3.0.3</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
修改yml
server:
port: 8888
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/dds?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis:
mapper-locations: classpath:mapper/**.xml
configuration:
map-underscore-to-camel-case: true主启动类
@SpringBootApplication
public class DdsApplication {
public static void main(String[] args) {
SpringApplication.run(DdsApplication.class, args);
}
}做完成上面就是不带任何业务类的一个基础项目框架。
数据库准备
新增一个数据库dds,其中有两个数据表,一个是用来存储用户提交的数据源信息的(ds),一个是后续我们测试效果用的(test)。
CREATE TABLE `ds` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '数据源名称',
`url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'url',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'username',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'password',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `test` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `dds`.`test` (`id`, `name`) VALUES (1, '王二麻子');
新增一个测试库test,其中有一个测试数据表,这个表的结构保持与dds库中的test表一致,但数据不一样。
CREATE TABLE `test` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `test`.`test` (`id`, `name`) VALUES (1, '张三');三、处理与前端的交互
后端接口
新增一个Controller类,这个类中添加一个处理器方法:AddSourceController
@Controller
@Slf4j
@RequiredArgsConstructor
public class AddSourceController {
private final DsService dsService;
@GetMapping("/toAddSource")
public String addSource(){
return "add_source";
}
}这样的话当我们请求项目的/toAddSource接口时,跳转到add_source.html,在这个页面中我们进行用户数据的提交动作。
前端页面
html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>添加数据源</title>
<link rel="stylesheet" th:href="@{/css/style.css}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<script th:src="@{/js/jquery-3.6.0.min.js}"></script>
<script th:src="@{/js/crypto-js-4.1.1.min.js}"></script>
<script th:src="@{/js/add_source.js}"></script>
</head>
<body>
<h1>添加数据源(MySQL)</h1>
<form id="dataSourceForm" action="#" method="post">
<div class="form-group">
<label for="dataSourceName">数据源名称:</label>
<div class="input-container">
<input type="text" id="dataSourceName" name="dataSourceName" required onblur="checkDataSourceName()">
<i class="fas fa-check valid-icon" id="sourceNameValid" style="display: none;"></i>
</div>
<div class="error" id="dataSourceNameError"></div>
</div>
<div class="form-group">
<label for="url">URL:</label>
<div class="input-container">
<input type="text" id="url" name="url" required onblur="checkUrl()">
<i class="fas fa-check valid-icon" id="urlValid" style="display: none;"></i>
</div>
<div class="error" id="urlError"></div>
</div>
<div class="form-group">
<label for="username">连接账号:</label>
<input type="text" id="username" name="username" required>
<div class="error" id="usernameError"></div>
</div>
<div class="form-group">
<label for="password">连接密码:</label>
<input type="password" id="password" name="password" required>
<div class="error" id="passwordError"></div>
</div>
<button type="button" onclick="testConnection()">测试</button>
<button type="submit" onclick="submitForm()">提交</button>
</form>
</body>
</html>css
页面css文件,js文件如下:style.css
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
button {
padding: 10px 20px;
margin-right: 10px;
}
.error {
color: red;
font-size: 0.9em;
}
.valid-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: green;
/*display: none;*/
}
.input-container {
position: relative;
display: inline-block;
width: 100%;
}
.input-container input {
padding-right: 25px; /* 为对勾图标留出空间 */
}js
const secretKey = CryptoJS.enc.Latin1.parse("aab11e66fa232e32");
const iv = CryptoJS.enc.Latin1.parse("aab11e66fa232e32");
let isValid = true;
/**
* 测试连通性
*/
function testConnection() {
const dataSourceName = $('#dataSourceName').val().trim();
const url = $('#url').val().trim();
const username = $('#username').val().trim();
const password = $('#password').val().trim();
checkDataSourceName();
checkUrl();
if (username === '') {
$('#usernameError').text('连接账号不能为空');
isValid = false;
} else {
$('#usernameError').text('');
}
if (password === '') {
$('#passwordError').text('连接密码不能为空');
isValid = false;
} else {
$('#passwordError').text('');
}
if(isValid) {
// 禁用提交按钮
$('#dataSourceForm button[type="submit"]').prop('disabled', true);
let encryptedPassword = CryptoJS.AES.encrypt(password, secretKey, {iv: iv, mode:CryptoJS.mode.CBC,
padding:CryptoJS.pad.ZeroPadding}).toString();
const data = {
name: dataSourceName,
url: url,
username: username,
password: encryptedPassword
}
// 向后端发送ajax请求
$.ajax({
url: '/testConnection',
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json',
success: function(response) {
if(response.code === 200) {
alert('测试连接成功!');
} else {
alert('测试连接失败!');
}
},
error: function() {
alert('测试连接时发生错误!');
},
complete: function() {
// 无论成功还是失败,都重新启用提交按钮
$('#dataSourceForm button[type="submit"]').prop('disabled', false);
}
});
}
}
/**
* 校验数据源名称是否已经在数据库中记录表中已存在,如果已经存在,则提示用户,否则继续执行。
*/
function checkDataSourceName() {
const dataSourceName = $('#dataSourceName').val().trim();
if (dataSourceName === '') {
$('#dataSourceNameError').text('数据源名称不能为空!')
isValid = false;
}
/* 向后端发送ajax请求,密码是否已经存在 */
$.ajax({
url: '/checkDataSourceName',
type: 'POST',
data: JSON.stringify({dataSourceName: dataSourceName}),
contentType: 'application/json',
success: function(response) {
if(response.code === 200) {
if(response.msg === '存在') {
$('#dataSourceNameError').text('数据源名称已存在,不可重复哦!');
$('#sourceNameValid').hide(); // 隐藏对勾图标
isValid = false;
} else if(response.msg === '不存在') {
$('#dataSourceNameError').text('');
$('#sourceNameValid').show(); // 显示对勾图标
isValid = true;
} else {
$('#dataSourceNameError').text('检查数据源名称时发生错误!')
$('#sourceNameValid').hide(); // 隐藏对勾图标
isValid = false;
}
} else {
$('#dataSourceNameError').text('检查数据源名称时发生错误!')
$('#sourceNameValid').hide(); // 隐藏对勾图标
isValid = false;
}
},
error: function() {
$('#dataSourceNameError').text('检查数据源名称时发生错误!')
$('#sourceNameValid').hide(); // 隐藏对勾图标
isValid = false;
}
})
}
/**
* 检查输入的url是否满足mysql连接字符串的规则
*/
function checkUrl() {
const url = $('#url').val().trim();
if (url === '') {
$('#urlError').text('URL连接字符串不能为空');
}
const urlRegex = /^jdbc:mysql:\/\/[a-zA-Z0-9.-]+(:\d+)?(\/[a-zA-Z0-9._-]+)?(\?[^=]+=.*)?$/;
if(!urlRegex.test(url)) {
$('#urlError').text('URL连接字符串格式不正确,请重新输入');
isValid = false;
} else {
$('#urlError').text('');
$('#urlValid').show();
isValid = true;
}
}
/**
* 提交数据源数据
*/
function submitForm() {
const dataSourceName = $('#dataSourceName').val().trim();
const url = $('#url').val().trim();
const username = $('#username').val().trim();
const password = $('#password').val().trim();
if (checkDataSourceName() && checkUrl()) {
isValid = true;
}
if (username === '') {
$('#usernameError').text('连接账号不能为空');
isValid = false;
} else {
$('#usernameError').text('');
}
if (password === '') {
$('#passwordError').text('连接密码不能为空');
isValid = false;
} else {
$('#passwordError').text('');
}
if(isValid) {
// 防止表单多次重复提交
$('#dataSourceForm button[type="submit"]').prop('disabled', true);
let encryptedPassword = CryptoJS.AES.encrypt(password, secretKey, {iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.ZeroPadding}).toString();
const data = {
name: dataSourceName,
url: url,
username: username,
password: encryptedPassword
};
$.ajax({
url: '/addDataSource',
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json',
success: function(response) {
if(response.code === 200) {
alert("添加成功!")
} else {
alert("添加失败!")
}
},
error: function() {
alert("添加时发生错误!")
},
complete: function() {
// 无论成功还是失败,都重新启用提交按钮
$('#dataSourceForm button[type="submit"]').prop('disabled', false);
}
});
}
}这个js要注意一下我们使用了CryptoJS,对于前端传到后端的数据库连接密码进行加密,其中需要有key和iv,我们当前设置为:aab11e66fa232e32,后端到时解密时也需要使用相同的值,所以我们把这个值配置到后端项目的application.yml当中
crypto:
secret: aab11e66fa232e32四、后端处理接口
在上面js当中有多个地方需要使用ajax调用后端的接口,所以后端要完善这些接口,在完善它们之前我们要在后端要把实体类、统一的json返回这些基础搭建好。
实体类
Ds
@Data
public class Ds implements Serializable {
/**
* 主键
*/
private Long id;
/**
* 数据源名称
*/
private String name;
/**
* url
*/
private String url;
/**
* username
*/
private String username;
/**
* password
*/
private String password;
/**
* 创建时间
*/
private Date createTime;
private static final long serialVersionUID = 1L;
}Test
@Data
public class Test {
private Long id;
private String name;
}DsDto
@Data
public class DsDto {
private String name;
private String url;
private String username;
private String password;
}统一返回
ReturnCode枚举
public enum ReturnCode {
SUCCESS(200, "成功"),
FAIL(400, "失败"),
INTERNAL_SERVER_ERROR(500, "服务器内部错误");
private Integer code;
private String msg;
ReturnCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}ResultData
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultData {
private Integer code;
private String msg;
private Object data;
public static ResultData ok(Object data) {
return new ResultData(ReturnCode.SUCCESS.getCode(), ReturnCode.SUCCESS.getMsg(), data);
}
public static ResultData ok(String msg) {
return new ResultData(ReturnCode.SUCCESS.getCode(), msg, null);
}
public static ResultData ok(String msg, Object data) {
return new ResultData(ReturnCode.SUCCESS.getCode(), msg, data);
}
public static ResultData ok(ReturnCode returnCode) {
return new ResultData(returnCode.getCode(), returnCode.getMsg(), null);
}
public static ResultData ok(ReturnCode returnCode,Object data) {
return new ResultData(returnCode.getCode(), returnCode.getMsg(), data);
}
public static ResultData fail(Object data) {
return new ResultData(ReturnCode.FAIL.getCode(), ReturnCode.FAIL.getMsg(), data);
}
public static ResultData fail(String msg) {
return new ResultData(ReturnCode.FAIL.getCode(), msg, null);
}
public static ResultData fail(Integer code,String msg) {
return new ResultData(code, msg, null);
}
public static ResultData fail(String msg, Object data) {
return new ResultData(ReturnCode.FAIL.getCode(), msg, data);
}
public static ResultData fail(ReturnCode returnCode) {
return new ResultData(returnCode.getCode(), returnCode.getMsg(),null);
}
public static ResultData fail(ReturnCode returnCode,Object data) {
return new ResultData(returnCode.getCode(), returnCode.getMsg(), data);
}
public static ResultData fail(ReturnCode returnCode,String msg) {
return new ResultData(returnCode.getCode(), msg, null);
}
}相关接口
/checkDataSourceName
这个接口的目的是检测数据源的名称是否在数据库已经存在,我们动态切换数据源的时候是按这个名称来切换的,所以要求这里的名称要保持一致!
@PostMapping("/checkDataSourceName")
@ResponseBody
public ResultData checkDataSourceName(@RequestBody Map<String,String> params){
// dsService.switchDataSource("defaultDataSource");
String name = params.get("dataSourceName");
log.info("检查数据源名称开始:" + name);
List<Ds> dsList = dsService.selectByName(name);
if (!dsList.isEmpty()){
return ResultData.ok("存在");
}
return ResultData.ok("不存在");
}这里我们要调service,所以新增service接口及对应的接口
public interface DsService {
List<Ds> selectByName(String name);
}@Service
@RequiredArgsConstructor
public class DsServiceImpl implements DsService {
private final DsMapper dsMapper;
@Override
public List<Ds> selectByName(String name) {
return dsMapper.selectByName(name);
}
}service中需要依赖mybatis查询数据库,新增mapper接品及对应的sql映射文件
@Mapper
public interface DsMapper {
List<Ds> selectByName(@Param("name") String name);
List<Ds> selectAll();
}<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaoxie.dds.mapper.DsMapper">
<resultMap id="BaseResultMap" type="com.xiaoxie.dds.domain.Ds">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="url" jdbcType="VARCHAR" property="url" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
</resultMap>
<sql id="Base_Column_List">
id, `name`, url, username, `password`, create_time
</sql>
<select id="selectByName" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from ds where name = #{name}
</select>
</mapper>/testConnection
这个接口是获取用户提供的值,根据这些值我们来测试是否可以正常连上数据库
controller中新增方法
@PostMapping("/testConnection")
@ResponseBody
public ResultData testConnection(@RequestBody DsDto dsDto) throws Exception {
boolean result = dsService.testConnection(dsDto);
return result ? ResultData.ok("连接成功") : ResultData.fail("连接失败");
}dsService中新增方法testConnection(),service接口和实现类如下:
boolean testConnection(DsDto dsDto) throws Exception; @Value("${crypto.secret}")
private String secretKey;
@Override
public boolean testConnection(DsDto dsDto) {
try {
String password = AESUtils.decrypt(dsDto.getPassword(), secretKey);
return testConnectDB(dsDto.getUrl(), dsDto.getUsername(), password);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 测试数据库是否可连接
* @param url
* @param username
* @param password
* @return
*/
private boolean testConnectDB(String url, String username, String password) {
try{
Connection connection = DriverManager.getConnection(url, username, password);
return connection != null && !connection.isClosed();
} catch (Exception e) {
return false;
}
}这里注意一下,我们从前端传过来的是密码是加过密的,所以对于dto中的密码我们是要进行解密的,所以我们提供一个工具类:AESUtils
public class AESUtils {
public static String decrypt(String content, String key) throws Exception{
try {
byte[] encrypted1 = Base64.getDecoder().decode(content);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(key.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
byte[] original = cipher.doFinal(encrypted1);
// 去掉补充的字符
String originalString = new String(original).trim();
return originalString;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}/addDataSource
用户在界面点击提交后,我们需要把数据库连接信息保存到数据库当中,使用这个接口来完成,这个接口除了保存这些信息外,还有一个本次最要的工作要做,就是所保存这个数据库连接相关的信息动态创建一个数据源,以便供后续可以动态切换使用!!
Controller中新增如下代码:
@PostMapping("/addDataSource")
@ResponseBody
public ResultData addDataSource(@RequestBody DsDto dsDto){
System.out.println(dsDto);
boolean result = dsService.addDs(dsDto);
return result ? ResultData.ok("添加成功") : ResultData.fail("添加失败");
}service接口及实现类
boolean addDs(DsDto dsDto); private final DynamicDataSourceManager dynamicDataSourceManager;
private final Map<Object, Object> dataSourceMap = new ConcurrentHashMap<>();
@Override
@Transactional
public boolean addDs(DsDto dsDto){
//1、把数据库连接信息保存到数据库
Ds ds = new Ds();
BeanUtils.copyProperties(dsDto, ds);
ds.setCreateTime(new Date());
dsMapper.insertSelective(ds);
//2、动态添加新的数据源
try {
String password = AESUtils.decrypt(dsDto.getPassword(), secretKey);
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(dsDto.getUrl());
dataSource.setUsername(dsDto.getUsername());
dataSource.setPassword(password);
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSourceMap.put(dsDto.getName(), dataSource);
dynamicDataSourceManager.setTargetDataSources(dataSourceMap);
dynamicDataSourceManager.afterPropertiesSet();
} catch (Exception e) {
throw new RuntimeException(e);
}
return true;
}要完成第一个功能简单,调用mapper中的方法来向数据库中插入数据
mpper接口中方法:
int insertSelective(Ds record);sql映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaoxie.dds.mapper.DsMapper">
<resultMap id="BaseResultMap" type="com.xiaoxie.dds.domain.Ds">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="url" jdbcType="VARCHAR" property="url" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
</resultMap>
<sql id="Base_Column_List">
id, `name`, url, username, `password`, create_time
</sql>
<insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.xiaoxie.dds.domain.Ds" useGeneratedKeys="true">
insert into ds
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="name != null">
`name`,
</if>
<if test="url != null">
url,
</if>
<if test="username != null">
username,
</if>
<if test="password != null">
`password`,
</if>
<if test="createTime != null">
create_time,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="name != null">
#{name,jdbcType=VARCHAR},
</if>
<if test="url != null">
#{url,jdbcType=VARCHAR},
</if>
<if test="username != null">
#{username,jdbcType=VARCHAR},
</if>
<if test="password != null">
#{password,jdbcType=VARCHAR},
</if>
<if test="createTime != null">
#{createTime,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>
</mapper>接下来重点介绍如何动态创建数据源、切换数据源
五、数据源动态添加及切换
第一步:新增数据源理类,用来动态创建切换数据源
这里使用了ThreadLoacal
/**
* 动态数据源管理类,用于管理和切换数据源
*/
public class DynamicDataSourceManager extends AbstractRoutingDataSource {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public DynamicDataSourceManager(DruidDataSource druidDataSource,
Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(druidDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return contextHolder.get();
}
public static void setDataSource(String dataSourceKey) {
contextHolder.set(dataSourceKey);
}
public static void clearDataSource() {
contextHolder.remove();
}
}第二步:配置默认数据源
/**
* 配置类用来管理动态数据源
*/
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.druid.url}")
private String url;
@Value("${spring.datasource.druid.username}")
private String username;
@Value("${spring.datasource.druid.password}")
private String password;
@Value("${spring.datasource.druid.driver-class-name}")
private String driverClassName;
// 注意:这里要使用Autowired,不要使用Bean,否则启动后mybatis的mapper不会加载
@Autowired
public DruidDataSource defaultDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClassName);
// System.out.println(dataSource.getUrl());
return dataSource;
}
@Bean
public DynamicDataSourceManager dynamicDataSourceManager() {
Map<Object,Object> targetDataSources = new HashMap<>();
targetDataSources.put("defaultDs", defaultDataSource());
return new DynamicDataSourceManager(defaultDataSource(), targetDataSources);
}
}从上面的代码可以看出来我们默认的数据源是"defaultDs"
在项目启动的时候就会去构造它这个默认的数据源。
注意:我们实际创建的的方法上使用的注解是@Autowired,不能使用@Bean,是因为我们要让它存在依赖关系而不是直接创建一个Bean
第三步:我们在项目启动的时候就加载数据中所有的数据连接信息创建好数据源
@Component
@RequiredArgsConstructor
@Slf4j
public class DataSourceInitializer implements CommandLineRunner {
private final DynamicDataSourceManager dynamicDataSourceManager;
private final DsService dsService;
@Value("${crypto.secret}")
private String secretKey;
@Override
public void run(String... args) throws Exception {
log.info("数据源初始化开始...");
// 获取所有数据源
List<Ds> dsList = dsService.selectAll();
// 创建数据源map
Map<Object,Object> dataSourceMap = new HashMap<>();
for (Ds ds : dsList) {
String password = AESUtils.decrypt(ds.getPassword(), secretKey);
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(ds.getUrl());
dataSource.setUsername(ds.getUsername());
dataSource.setPassword(password);
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSourceMap.put(ds.getName(), dataSource);
}
// 设置目标数据源
dynamicDataSourceManager.setTargetDataSources(dataSourceMap);
dynamicDataSourceManager.afterPropertiesSet();
log.info("数据源初始化完成...");
}
}
六、测试
新增一个controller来测试动态数据源的测试
@RestController
@RequiredArgsConstructor
public class TestController {
private final TestMapper testMapper;
private final DsService dsService;
@GetMapping("/test")
public List<Test> getAllTest(@RequestParam("ds") String datasourceName){
// System.out.println("datasourceName:"+datasourceName);
// DynamicDataSourceManager.setDataSource("test");
dsService.switchDataSource(datasourceName);
List<Test> tests = testMapper.selectAll();
dsService.clearDataSource();
return tests;
}
}
其中service接口及实现类
void switchDataSource(String dataSourceName);
void clearDataSource();
List<Ds> selectAll(); @Override
public void switchDataSource(String dataSourceName) {
DynamicDataSourceManager.setDataSource(dataSourceName);
}
@Override
public void clearDataSource() {
DynamicDataSourceManager.clearDataSource();
}
@Override
public List<Ds> selectAll() {
return dsMapper.selectAll();
}动态数据源管理类中实现如下:
public static void setDataSource(String dataSourceKey) {
contextHolder.set(dataSourceKey);
}
public static void clearDataSource() {
contextHolder.remove();
}mapper及sql映射比较简单就是查询一下test表中的记录
@Mapper
public interface TestMapper {
List<Test> selectAll();
}<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaoxie.dds.mapper.TestMapper">
<select id="selectAll" resultType="com.xiaoxie.dds.domain.Test">
select * from test
</select>
</mapper>接下来我们看看我们ds表示的记录:
启动项目:
1、访问项目时使用test数据源
2、访问项目使用ds1数据源
3、使用默认数据源defaultDs
相关推荐
- 如何设计一个优秀的电子商务产品详情页
-
加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...
- 怎么在JS中使用Ajax进行异步请求?
-
大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...
- 中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革
-
前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...
- 前端监控 SDK 开发分享_前端监控系统 开源
-
一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...
- Ajax 会被 fetch 取代吗?Axios 怎么办?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...
- 前端面试题《AJAX》_前端面试ajax考点汇总
-
1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...
- Ajax 详细介绍_ajax
-
1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...
- 6款可替代dreamweaver的工具_替代powerdesigner的工具
-
dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...
- 我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊
-
接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...
- 福斯《死侍》发布新剧照 "小贱贱"韦德被改造前造型曝光
-
时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...
- 2021年超详细的java学习路线总结—纯干货分享
-
本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...
- 不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!
-
Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...
- 2025 年 Python 爬虫四大前沿技术:从异步到 AI
-
作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...
- 最贱超级英雄《死侍》来了!_死侍超燃
-
死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...
- 停止javascript的ajax请求,取消axios请求,取消reactfetch请求
-
一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)
