SpringBoot 動態數據源


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"
  }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM