SpringBoot 實現動態數據源切換
Spring Boot + Mybatis Plus + Druid + MySQL 實現動態數據源切換及動態 SQL 語句執行。
項目默認加載 application.yml 中配置的數據源,只有在調用數據源切換時創建數據連接。
Druid 實現動態數據源切換
相關依賴
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
application.yml Druid 配置
spring:
#Druid 連接池通用配置
datasource:
url: jdbc:mysql://127.0.0.1:3306/demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&useSSL=false
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 下面為連接池的補充設置,應用到上面所有數據源中
# 初始化大小,最小,最大
initial-size: 5
min-idle: 5
max-active: 20
# 配置獲取連接等待超時的時間
max-wait: 60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
min-evictable-idle-time-millis: 300000
# sql 校驗
validation-query: select count(1) from sys.objects Where type='U' And type_desc='USER_TABLE'
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 打開PSCache,並且指定每個連接上PSCache的大小
pool-prepared-statements: true
# 配置監控統計攔截的filters,去掉后監控界面sql無法統計,'wall'用於防火牆
max-pool-prepared-statement-per-connection-size: 20
filters: stat # wall 若開啟 wall,會把 if 中的 and 判斷為注入進行攔截
use-global-data-source-stat: true
# 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 指定當連接超過廢棄超時時間時,是否立刻刪除該連接
remove-abandoned: true
# 指定連接應該被廢棄的時間
remove-abandoned-timeout: 60000
# 是否追蹤廢棄statement或連接,默認為: false
log-abandoned: false
Druid 配置
package com.demo.utils.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import javax.servlet.Servlet;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName: DruidConfig.java
* @Description: Druid配置
* @Author: tanyp
* @Date: 2022/2/18 10:29
**/
@Configuration
public class DruidConfig {
@Value("${spring.datasource.type}")
private String db_type;
// @Value("${spring.datasource.driver-class-name}")
// private String db_driver_name;
@Value("${spring.datasource.url}")
private String db_url;
@Value("${spring.datasource.username}")
private String db_user;
@Value("${spring.datasource.password}")
private String db_pwd;
// 連接池初始化大小
@Value("${spring.datasource.druid.initial-size}")
private int initialSize;
// 連接池最小值
@Value("${spring.datasource.druid.min-idle}")
private int minIdle;
// 連接池最大值
@Value("${spring.datasource.druid.max-active}")
private int maxActive;
// 配置獲取連接等待超時的時間
@Value("${spring.datasource.druid.max-wait}")
private int maxWait;
// 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒
@Value("${spring.datasource.druid.time-between-eviction-runs-millis}")
private int timeBetweenEvictionRunsMillis;
// 配置一個連接在池中最小生存的時間,單位是毫秒
@Value("${spring.datasource.druid.min-evictable-idle-time-millis}")
private int minEvictableIdleTimeMillis;
// 用來驗證數據庫連接的查詢語句,這個查詢語句必須是至少返回一條數據的SELECT語句
@Value("${spring.datasource.druid.validation-query}")
private String validationQuery;
// 檢測連接是否有效
@Value("${spring.datasource.druid.test-while-idle}")
private boolean testWhileIdle;
// 申請連接時執行validationQuery檢測連接是否有效。做了這個配置會降低性能。
@Value("${spring.datasource.druid.test-on-borrow}")
private boolean testOnBorrow;
// 歸還連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能
@Value("${spring.datasource.druid.test-on-return}")
private boolean testOnReturn;
// 是否緩存preparedStatement,也就是PSCache。
@Value("${spring.datasource.druid.pool-prepared-statements}")
private boolean poolPreparedStatements;
// 指定每個連接上PSCache的大小。
@Value("${spring.datasource.druid.max-pool-prepared-statement-per-connection-size}")
private int maxPoolPreparedStatementPerConnectionSize;
// 配置監控統計攔截的filters,去掉后監控界面sql無法統計,'wall'用於防火牆
@Value("${spring.datasource.druid.filters}")
private String filters;
// 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
@Value("${spring.datasource.druid.connect-properties}")
private String connectionProperties;
// 指定當連接超過廢棄超時時間時,是否立刻刪除該連接
@Value("${spring.datasource.druid.remove-abandoned}")
private boolean removeAbandoned;
// 指定連接應該被廢棄的時間
@Value("${spring.datasource.druid.remove-abandoned-timeout}")
private int removeAbandonedTimeout;
// 使用DBCP connection pool,是否追蹤廢棄statement或連接,默認為: false
@Value("${spring.datasource.druid.log-abandoned}")
private boolean logAbandoned;
@Bean
public DynamicDataSource druidDataSource() {
Map<Object, Object> map = new HashMap<>();
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
DruidDataSource defaultDataSource = new DruidDataSource();
// defaultDataSource.setDriverClassName(db_driver_name);
defaultDataSource.setUrl(db_url);
defaultDataSource.setUsername(db_user);
defaultDataSource.setPassword(db_pwd);
defaultDataSource.setInitialSize(initialSize);
defaultDataSource.setMinIdle(minIdle);
defaultDataSource.setMaxActive(maxActive);
defaultDataSource.setMaxWait(maxWait);
defaultDataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
defaultDataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
defaultDataSource.setValidationQuery(validationQuery);
defaultDataSource.setTestWhileIdle(testWhileIdle);
defaultDataSource.setTestOnBorrow(testOnBorrow);
defaultDataSource.setTestOnReturn(testOnReturn);
defaultDataSource.setPoolPreparedStatements(poolPreparedStatements);
defaultDataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
defaultDataSource.setRemoveAbandoned(removeAbandoned);
defaultDataSource.setRemoveAbandonedTimeout(removeAbandonedTimeout);
defaultDataSource.setLogAbandoned(logAbandoned);
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
map.put("default", defaultDataSource);
dynamicDataSource.setTargetDataSources(map);
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
return dynamicDataSource;
}
@Bean
public ServletRegistrationBean<Servlet> druid() {
// 現在要進行druid監控的配置處理操作
ServletRegistrationBean<Servlet> servletRegistrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
// 白名單,多個用逗號分割, 如果allow沒有配置或者為空,則允許所有訪問
servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
// 黑名單,多個用逗號分割 (共同存在時,deny優先於allow)
//servletRegistrationBean.addInitParameter("deny", "127.0.0.1");
// 控制台管理用戶名
servletRegistrationBean.addInitParameter("loginUsername", "admin");
// 控制台管理密碼
servletRegistrationBean.addInitParameter("loginPassword", "admin");
// 是否可以重置數據源,禁用HTML頁面上的“Reset All”功能
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean<Filter> filterRegistrationBean() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new WebStatFilter());
// 所有請求進行監控處理
filterRegistrationBean.addUrlPatterns("/*");
// 添加不需要忽略的格式信息
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.css,/druid/*");
return filterRegistrationBean;
}
}
數據源上下文
package com.demo.utils.datasource;
/**
* @ClassName: DataSourceContextHolder.java
* @Description: 數據源上下文
* @Author: tanyp
* @Date: 2022/2/18 10:04
**/
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
/**
* @MonthName: setDBType
* @Description: 設置當前線程持有的數據源
* @Author: tanyp
* @Date: 2022/2/18 10:07
* @Param: [dbType]
* @return: void
**/
public static synchronized void setDBType(String dbType) {
contextHolder.set(dbType);
}
/**
* @MonthName: getDBType
* @Description: 獲取當前線程持有的數據源
* @Author: tanyp
* @Date: 2022/2/18 10:07
* @Param: []
* @return: java.lang.String
**/
public static String getDBType() {
return contextHolder.get();
}
/**
* @MonthName: clearDBType
* @Description: 清除當前線程持有的數據源
* @Author: tanyp
* @Date: 2022/2/18 10:07
* @Param: []
* @return: void
**/
public static void clearDBType() {
contextHolder.remove();
}
}
數據源信息
package com.demo.utils.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName: DynamicDataSource.java
* @Description: 數據源信息
* @Author: tanyp
* @Date: 2022/2/18 10:26
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static DynamicDataSource instance;
private static byte[] lock = new byte[0];
private static Map<Object, Object> dataSourceMap = new HashMap<>();
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
dataSourceMap.putAll(targetDataSources);
super.afterPropertiesSet();
}
public Map<Object, Object> getDataSourceMap() {
return dataSourceMap;
}
public static synchronized DynamicDataSource getInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new DynamicDataSource();
}
}
}
return instance;
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDBType();
}
}
切換數據源
以數據庫 ip + 端口 + 數據庫名作為 key 和數據庫連接的映射關系。
package com.demo.utils;
import com.alibaba.druid.pool.DruidDataSource;
import com.demo.utils.datasource.DataSourceContextHolder;
import com.demo.utils.datasource.DynamicDataSource;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.Objects;
/**
* @ClassName: DruidDataSourceUtil.java
* @Description: 用於查找並切換數據源
* @Author: tanyp
* @Date: 2022/2/18 10:34
**/
@Slf4j
public class DruidDataSourceUtil {
/**
* @MonthName: addOrChangeDataSource
* @Description: 切換數據源
* @Author: tanyp
* @Date: 2022/2/18 10:38
* @Param: dbip:IP地址
* dbport:端口號
* dbname:數據庫名稱
* dbuser:用戶名稱
* dbpwd:密碼
* @return: void
**/
public static void addOrChangeDataSource(String dbip, String dbport, String dbname, String dbuser, String dbpwd) {
try {
DataSourceContextHolder.setDBType("default");
// 數據庫連接key:ip + 端口 + 數據庫名
String key = "db" + dbip + dbport + dbname;
// 創建動態數據源
Map<Object, Object> dataSourceMap = DynamicDataSource.getInstance().getDataSourceMap();
if (!dataSourceMap.containsKey(key + "master") && Objects.nonNull(key)) {
String url = "jdbc:mysql://" + dbip + ":" + dbport + "/" + dbname + "?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&useSSL=false";
log.info("插入新數據庫連接信息為:{}", url);
DruidDataSource dynamicDataSource = new DruidDataSource();
// dynamicDataSource.setDriverClassName("com.mysql.jdbc.Driver");
dynamicDataSource.setUsername(dbuser);
dynamicDataSource.setUrl(url);
dynamicDataSource.setPassword(dbpwd);
dynamicDataSource.setInitialSize(50);
dynamicDataSource.setMinIdle(5);
dynamicDataSource.setMaxActive(1000);
dynamicDataSource.setMaxWait(500); // 如果失敗,當前的請求可以返回
dynamicDataSource.setTimeBetweenEvictionRunsMillis(60000);
dynamicDataSource.setMinEvictableIdleTimeMillis(300000);
dynamicDataSource.setValidationQuery("SELECT 1 FROM DUAL");
dynamicDataSource.setTestWhileIdle(true);
dynamicDataSource.setTestOnBorrow(false);
dynamicDataSource.setTestOnReturn(false);
dynamicDataSource.setPoolPreparedStatements(true);
dynamicDataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
dynamicDataSource.setRemoveAbandoned(true);
dynamicDataSource.setRemoveAbandonedTimeout(180);
dynamicDataSource.setLogAbandoned(true);
dynamicDataSource.setConnectionErrorRetryAttempts(0); // 失敗后重連的次數
dynamicDataSource.setBreakAfterAcquireFailure(true); // 請求失敗之后中斷
dataSourceMap.put(key + "master", dynamicDataSource);
DynamicDataSource.getInstance().setTargetDataSources(dataSourceMap);
// 切換為動態數據源實例
DataSourceContextHolder.setDBType(key + "master");
} else {
// 切換為動態數據源實例
DataSourceContextHolder.setDBType(key + "master");
}
} catch (Exception e) {
log.error("=====創建據庫連接異常:{}", e);
}
}
}
以上動態數據源加載及切換已完成。
使用 MyBatis Plus 動態執行 SQL 語句
加載動態數據源執行 SQL (增、刪、改、查)
package com.demo.service.impl;
import com.demo.constants.Constants;
import com.demo.mapper.DynamicSqlMapper;
import com.demo.service.DynamicDataSourceService;
import com.demo.utils.DataUtils;
import com.demo.utils.DruidDataSourceUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName: DynamicDataSourceServiceImpl.java
* @Description: 動態數據源
* @Author: tanyp
* @Date: 2022/2/18 10:43
**/
@Slf4j
@Service("dynamicDataSourceService")
public class DynamicDataSourceServiceImpl implements DynamicDataSourceService {
@Autowired
private DynamicSqlMapper dynamicSqlMapper;
/**
* @MonthName: dynamicExecutive
* @Description: 加載動態數據源執行SQL
* @Author: tanyp
* @Date: 2022/2/28 10:46
* @Param: {
* "dbip":"IP地址",
* "dbport":"端口號",
* "dbname":"數據庫名稱",
* "dbuser":"用戶名稱",
* "dbpwd":"密碼",
* "type":"執行類型:SELECT、INSERT、UPDATE、DELETE",
* "paramSQL":"需要執行的SQL",
* "param":{} // SQL中的參數
* }
* @return: java.util.Map<java.lang.String, java.lang.Object>
**/
@Override
public Map<String, Object> dynamicExecutive(Map<String, Object> params) {
Map<String, Object> result = null;
try {
DruidDataSourceUtil.addOrChangeDataSource(
String.valueOf(params.get("dbip")),
String.valueOf(params.get("dbport")),
String.valueOf(params.get("dbname")),
String.valueOf(params.get("dbuser")),
String.valueOf(params.get("dbpwd"))
);
} catch (Exception e) {
log.error("=====創建據庫連接異常:{}", e);
result.put("data", "創建據庫連接異常,請檢查連接信息是否有誤!");
}
try {
// 執行動態SQL
Object data = null;
String type = String.valueOf(params.get("type"));
String paramSQL = String.valueOf(params.get("paramSQL"));
Map<String, Object> param = (HashMap) params.get("param");
// 參數替換
String sql = DataUtils.strRreplace(paramSQL, param);
log.info("======請求SQL語句:{}======", sql);
switch (type) {
case Constants.SELECT:
data = dynamicSqlMapper.dynamicsSelect(sql);
break;
case Constants.INSERT:
data = dynamicSqlMapper.dynamicsInsert(sql);
break;
case Constants.UPDATE:
data = dynamicSqlMapper.dynamicsUpdate(sql);
break;
case Constants.DELETE:
data = dynamicSqlMapper.dynamicsDelete(sql);
break;
default:
data = "請求參數【type】有誤,請核查!";
break;
}
result = new HashMap<>();
result.put("data", data);
} catch (Exception e) {
log.error("=====執行SQL異常:{}", e);
result.put("data", "執行SQL異常,請檢查SQL語句是否有誤!");
}
return result;
}
}
動態 SQL 執行器
package com.demo.mapper;
import org.apache.ibatis.annotations.*;
import java.util.List;
import java.util.Map;
/**
* @ClassName: DynamicSqlMapper.java
* @Description: 動態SQL執行器
* @Author: tanyp
* @Date: 2022/2/28 10:21
**/
@Mapper
public interface DynamicSqlMapper {
@Select({"${sql}"})
@ResultType(Object.class)
List<Map<String, Object>> dynamicsSelect(@Param("sql") String sql);
@Insert({"${sql}"})
@ResultType(Integer.class)
Integer dynamicsInsert(@Param("sql") String sql);
@Update({"${sql}"})
@ResultType(Integer.class)
Integer dynamicsUpdate(@Param("sql") String sql);
@Delete({"${sql}"})
@ResultType(Integer.class)
Integer dynamicsDelete(@Param("sql") String sql);
}
SQL 占位符處理
package com.demo.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @ClassName: DataUtils.java
* @Description: 數據處理
* @Author: tanyp
* @Date: 2022/2/28 9:21
**/
@Slf4j
public class DataUtils {
private static final Pattern pattern = Pattern.compile("\\#\\{(.*?)\\}");
private static Matcher matcher;
/**
* @MonthName: strRreplace
* @Description: 字符串站位處理
* @Author: tanyp
* @Date: 2022/2/28 9:21
* @Param: [content, param]
* @return: java.lang.String
**/
public static String strRreplace(String content, Map<String, Object> param) {
if (Objects.isNull(param)) {
return null;
}
try {
matcher = pattern.matcher(content);
while (matcher.find()) {
String key = matcher.group();
String keyclone = key.substring(2, key.length() - 1).trim();
boolean containsKey = param.containsKey(keyclone);
if (containsKey && Objects.nonNull(param.get(keyclone))) {
String value = "'" + param.get(keyclone) + "'";
content = content.replace(key, value);
}
}
return content;
} catch (Exception e) {
log.error("字符串站位處理:{}", e);
return null;
}
}
}
測試
POST 請求接口
http://127.0.0.1:8001/dynamicExecutive
請求參數
{
"dbip":"127.0.0.1",
"dbport":"3306",
"dbname":"demo",
"dbuser":"root",
"dbpwd":"root",
"type":"SELECT",
"paramSQL":"SELECT id, code, name, path, message, status, classify, params, icon, update_time, create_time FROM component where id = #{id}",
"param":{
"id":"611fb3e553371b9d42f8583391cc8478"
}
}
正常返回值
{
"code": 200,
"message": "操作成功",
"result": {
"code": 200,
"message": "操作成功!",
"result": {
"data": [
{
"path": "127.0.0.1",
"classify": "8ab3f21e1607a0374fb2d82f7fcaee98",
"update_time": "2022-03-08 17:59:11",
"code": "dynamicDataSourceService",
"create_time": "2022-03-07 14:51:15",
"name": "動態數據源",
"icon": "Rank",
"id": "611fb3e553371b9d42f8583391cc8478",
"message": "加載動態數據源執行SQL",
"status": 0
}
]
},
"dateTime": "2022-03-11T09:56:31.87"
}
}