SpringBoot3动态切换数据源

打印 上一主题 下一主题

主题 689|帖子 689|积分 2067

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
     背景

        随着公司业务战略的发展,相关的软件服务也逐步的向多元化变革,之前是单纯的拿项目,赚人工钱,现在开始向产物化\服务化变革。迩来雷袭又接到一项新的挑战:相识SAAS模型,思量怎么将公司的产物转换成多租户架构。
        经过一番百度,雷袭对多租户架构总算有了一番相识,以下是整理的笔记。
        多租户架构是一种软件架构,用于实现多用户环境下,使用相同的系统或步伐组件时,能保证用户之间数据的隔离性。简单说就是使用共用的数据中心,通过单一系统架构与服务提供多数客户端相同甚至可定制化的服务,并且保障客户的数据隔离。一个支持多租户架构的系统需要在设计上对它的数据和设置举行虚拟分区,从而使系统的每个租户或组织都能够使用一个单独的系统实例,每个租户都可以根据本身的需求对租用的系统实例举行个性化设置。

        多租户技能的实现重点在于差异租户间应用步伐环境的隔离以及数据的隔离,使得差异租户间应用步伐不会相互干扰。应用步伐可通过历程隔离大概多种运维工具实现,数据存储上的隔离方案则是有三种:
        1、独立数据库:优点是独立性最高,缺点是数据库较多,购置和维护成本高。
        2、共享数据库,隔离数据架构:同一个数据库实例内多个用户/schema来对应多个租户,优点是单实例可以支持更多租户,缺点是数据恢复比较困难。
        3、共享数据库,共享数据布局:物理分表,表分区,大概在表中通过字段区分,优点是成本最低,实现难度低,缺点是数据隔离水平低。
        第三种其实雷袭已经试过了,之前的博客里就提到了表分区,分表的实现方式,这里不多缀述,今天雷袭想试试前面两种,因此不得不办理的一个问题:如何实现同一个项目中,数据源的动态切换?

     代码实践

        雷袭在网上查阅了许多资料,最终找到了两种合适的方式实现,一种是通过AOP来实现,另一种是通过Filter实现,以下是实现的方式说明。
       一、通过切面实现

        1、准备工作,创建数据库模式,添加测试数据:
  1. --先创建三个用户,设置密码
  2. SAAS_MASTER   leixi123
  3. SAAS_DEV   leixi123
  4. SAAS_UAT   leixi123
  5. --再用sysdba给用户授权
  6. grant dba to SAAS_MASTER;
  7. grant resource to SAAS_MASTER;
  8. grant dba to SAAS_DEV;
  9. grant resource to SAAS_DEV;
  10. grant dba to SAAS_UAT;
  11. grant resource to SAAS_UAT;
  12. CREATE TABLE SAAS_MASTER."sys_db_info"
  13. (
  14.     "id" VARCHAR2(32) NOT NULL,
  15.     "url" VARCHAR2(255) NOT NULL,
  16.     "username" VARCHAR2(255) NOT NULL,
  17.     "password" VARCHAR2(255) NOT NULL,
  18.     "driver_class_name" VARCHAR2(255) NOT NULL,
  19.     "db_name" VARCHAR2(255) NOT NULL,
  20.     "db_key" VARCHAR2(255) NOT NULL,
  21.     "status" INT DEFAULT '0' NOT NULL,
  22.     "remark" VARCHAR2(255) DEFAULT NULL,
  23.      PRIMARY KEY("id")) ;
  24. COMMENT ON TABLE SAAS_MASTER."sys_db_info" IS '数据库配置信息表';
  25. COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."db_key" IS '数据库key,即保存Map中的key(保证唯一,并且和DataSourceType中的枚举项保持一致,包括大小写)';
  26. COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."db_name" IS '数据库名称';
  27. COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."driver_class_name" IS '数据库驱动';
  28. COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."id" IS '主键ID';
  29. COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."password" IS '密码';
  30. COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."remark" IS '备注说明';
  31. COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."status" IS '是否停用';
  32. COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."url" IS '数据库URL';
  33. COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."username" IS '用户名';
  34. --添加数据源信息
  35. insert into SAAS_MASTER."sys_db_info" ("id","url","username","password","driver_class_name","db_name","db_key","status","remark")
  36. values ('1', 'jdbc:dm://127.0.0.1:5236/SAAS_DEV', 'SAAS_DEV', 'leixi123', 'dm.jdbc.driver.DmDriver', 'DEV', 'DEV', 0, '连接DEV数据库');
  37. insert into SAAS_MASTER."sys_db_info" ("id","url","username","password","driver_class_name","db_name","db_key","status","remark")
  38. values ('2', 'jdbc:dm://127.0.0.1:5236/SAAS_UAT', 'SAAS_UAT', 'leixi123', 'dm.jdbc.driver.DmDriver', 'UAT', 'UAT', 0, '连接UAT数据库');
  39. --添加测试数据库
  40. CREATE TABLE SAAS_MASTER.leixi_test (
  41.   id VARCHAR2(32) NOT NULL,
  42.   name VARCHAR2(255) NOT NULL,
  43.   PRIMARY KEY (id)
  44. ) ;
  45. CREATE TABLE SAAS_DEV.leixi_test (
  46.     id VARCHAR2(32) NOT NULL,
  47.     name VARCHAR2(255) NOT NULL,
  48.     PRIMARY KEY (id)
  49. ) ;
  50. CREATE TABLE SAAS_UAT.leixi_test (
  51.      id VARCHAR2(32) NOT NULL,
  52.      name VARCHAR2(255) NOT NULL,
  53.      PRIMARY KEY (id)
  54. ) ;
  55. insert into SAAS_MASTER.leixi_test(id, name) values('', '这里是leixi_test 的MASTER库数据');
  56. insert into SAAS_DEV.leixi_test(id, name) values('1', '这里是leixi_test 的DEV库数据');
  57. insert into SAAS_UAT.leixi_test(id, name) values('1', '这里是leixi_test 的UAT数据');
复制代码
        2、创建一个springboot项目,项目环境为JDK17,以下是相关设置和代码:
        pom.xml
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <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">
  3.     <modelVersion>4.0.0</modelVersion>
  4.     <packaging>jar</packaging>
  5.     <parent>
  6.         <groupId>org.springframework.boot</groupId>
  7.         <artifactId>spring-boot-starter-parent</artifactId>
  8.         <version>3.3.2</version> <!-- lookup parent from repository -->
  9.     </parent>
  10.     <groupId>com.leixi.hub.saasdb</groupId>
  11.     <artifactId>leixi-saas-db</artifactId>
  12.     <version>1.0-SNAPSHOT</version>
  13.     <name>leixi-saas-db</name>
  14.     <description>用于动态切换数据源</description>
  15.     <properties>
  16.         <maven.compiler.source>17</maven.compiler.source>
  17.         <maven.compiler.target>17</maven.compiler.target>
  18.         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  19.         <hutool.version>5.8.15</hutool.version>
  20.         <mysql.version>8.0.28</mysql.version>
  21.         <druid.version>1.2.16</druid.version>
  22.         <mybatis-plus.version>3.5.3.1</mybatis-plus.version>
  23.         <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
  24.     </properties>
  25.     <dependencies>
  26.         <!-- Lombok -->
  27.         <dependency>
  28.             <groupId>org.projectlombok</groupId>
  29.             <artifactId>lombok</artifactId>
  30.             <!--编译测试环境,不打包在lib-->
  31.             <scope>provided</scope>
  32.         </dependency>
  33.         <!-- 允许使用Lombok的Java Bean类中使用MapStruct注解 (Lombok 1.18.20+) -->
  34.         <dependency>
  35.             <groupId>org.projectlombok</groupId>
  36.             <artifactId>lombok-mapstruct-binding</artifactId>
  37.             <version>${lombok-mapstruct-binding.version}</version>
  38.             <scope>provided</scope>
  39.         </dependency>
  40.         <!--    hutool工具包    -->
  41.         <dependency>
  42.             <groupId>cn.hutool</groupId>
  43.             <artifactId>hutool-all</artifactId>
  44.             <version>${hutool.version}</version>
  45.         </dependency>
  46.         <!-- web支持 -->
  47.         <dependency>
  48.             <groupId>org.springframework.boot</groupId>
  49.             <artifactId>spring-boot-starter-web</artifactId>
  50.         </dependency>
  51.         <!-- aop -->
  52.         <dependency>
  53.             <groupId>org.springframework.boot</groupId>
  54.             <artifactId>spring-boot-starter-aop</artifactId>
  55.         </dependency>
  56.         <!-- DM驱动 -->
  57.         <dependency>
  58.             <groupId>com.dameng</groupId>
  59.             <artifactId>DmJdbcDriver18</artifactId>
  60.             <version>8.1.1.193</version>
  61.         </dependency>
  62.         <!--    阿里druid工具包    -->
  63.         <dependency>
  64.             <groupId>com.alibaba</groupId>
  65.             <artifactId>druid-spring-boot-starter</artifactId>
  66.             <version>${druid.version}</version>
  67.         </dependency>
  68.         <dependency>
  69.             <groupId>com.alibaba</groupId>
  70.             <artifactId>fastjson</artifactId>
  71.             <version>2.0.40</version>
  72.         </dependency>
  73.         <!-- mybatis-plus -->
  74.         <dependency>
  75.             <groupId>com.baomidou</groupId>
  76.             <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
  77.             <version>3.5.5</version>
  78.         </dependency>
  79.     </dependencies>
  80.     <build>
  81.         <plugins>
  82.             <plugin>
  83.                 <groupId>org.springframework.boot</groupId>
  84.                 <artifactId>spring-boot-maven-plugin</artifactId>
  85.             </plugin>
  86.             <plugin>
  87.                 <groupId>org.apache.maven.plugins</groupId>
  88.                 <artifactId>maven-archetype-plugin</artifactId>
  89.                 <version>3.0.0</version>
  90.             </plugin>
  91.         </plugins>
  92.     </build>
  93. </project>
复制代码
        application.yml
  1. server:
  2.   port: 19200
  3.   servlet:
  4.     context-path: /leixi
  5. spring:
  6.   jackson:
  7.     ## 默认序列化时间格式
  8.     date-format: yyyy-MM-dd HH:mm:ss
  9.     ## 默认序列化时区
  10.     time-zone: GMT+8
  11.   datasource:
  12.     type: com.alibaba.druid.pool.DruidDataSource
  13.     driver-class-name: dm.jdbc.driver.DmDriver
  14.     url: jdbc:dm://127.0.0.1:5236/SAAS_MASTER
  15.     username: SAAS_MASTER
  16.     password: leixi123
  17.     druid:
  18.       slave: false
  19.       initial-size: 15
  20.       min-idle: 15
  21.       max-active: 200
  22.       max-wait: 60000
  23.       time-between-eviction-runs-millis: 60000
  24.       min-evictable-idle-time-millis: 300000
  25.       validation-query: ""
  26.       test-while-idle: true
  27.       test-on-borrow: false
  28.       test-on-return: false
  29.       pool-prepared-statements: false
  30.       connection-properties: false
  31.   task:
  32.     execution:
  33.       thread-pool:
  34.         core-size: 10
  35.         max-size: 20
  36.         queue-capacity: 100
  37. mybatis-plus:
  38.   mapper-locations: classpath:/mapper/**/*Mapper.xml
  39.   global-config:
  40.     db-config:
  41.       #主键类型  0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID";
  42.       id-type: assign_uuid
  43.       # 默认数据库表下划线命名
  44.       table-underline: true
  45.   configuration:
  46.     # 返回类型为Map,显示null对应的字段
  47.     call-setters-on-nulls: true
  48.     map-underscore-to-camel-case: true #开启驼峰和下划线互转
  49.     # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
  50.     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  51. leixi:
  52.   saas:
  53.     data_source_key: data_source_key
  54.     load_source_form_db: true
复制代码
        以下是设置数据源的核心代码,其原理为:在项目启动时先通过LoadDataSourceRunner从数据库中查询相关的数据连接,存储在内存中,对Controller中的方法添加@DataSource注解,执行方法时,通过注解中的静态摆列切换对应的数据源,对指定的数据库举行操作。
  1. package com.leixi.hub.saasdb.config;
  2. import com.alibaba.druid.pool.DruidDataSource;
  3. import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
  4. import com.leixi.hub.saasdb.entity.SysDbInfo;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.beans.BeanUtils;
  7. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  8. import javax.sql.DataSource;
  9. import java.sql.DriverManager;
  10. import java.sql.SQLException;
  11. import java.util.List;
  12. import java.util.Map;
  13. import java.util.Objects;
  14. /**
  15. * 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
  16. *
  17. * @author 雷袭月启
  18. * @since 2024/12/5 19:39
  19. */
  20. @Slf4j
  21. public class DynamicDataSource extends AbstractRoutingDataSource {
  22.     // 数据源列表,多数据源情况下,具体使用哪一个数据源,由此获取
  23.     private final Map<Object, Object> targetDataSourceMap;
  24.     /**
  25.      * 构造方法,设置默认数据源和目标多数据源
  26.      *
  27.      * @param defaultDataSource 默认主数据源,只能有一个
  28.      * @param targetDataSources 从数据源,可以是多个
  29.      */
  30.     public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
  31.         super.setDefaultTargetDataSource(defaultDataSource);
  32.         super.setTargetDataSources(targetDataSources);
  33.         this.targetDataSourceMap = targetDataSources;
  34.     }
  35.     /**
  36.      * 动态数据源的切换(核心)
  37.      * 决定使用哪个数据源
  38.      *
  39.      * @return Object
  40.      */
  41.     @Override
  42.     protected Object determineCurrentLookupKey() {
  43.         return DynamicDataSourceContextHolder.getDataSource();
  44.     }
  45.     /**
  46.      * 添加数据源信息
  47.      *
  48.      * @param dataSources 数据源实体集合
  49.      */
  50.     public void createDataSource(List<SysDbInfo> dataSources) {
  51.         try {
  52.             if (CollectionUtils.isNotEmpty(dataSources)) {
  53.                 for (SysDbInfo ds : dataSources) {
  54.                     //校验数据库是否可以连接
  55.                     Class.forName(ds.getDriverClassName());
  56.                     DriverManager.getConnection(ds.getUrl(), ds.getUsername(), ds.getPassword());
  57.                     //定义数据源
  58.                     DruidDataSource dataSource = new DruidDataSource();
  59.                     BeanUtils.copyProperties(ds, dataSource);
  60.                     //申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用
  61.                     dataSource.setTestOnBorrow(true);
  62.                     //建议配置为true,不影响性能,并且保证安全性。
  63.                     //申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
  64.                     dataSource.setTestWhileIdle(true);
  65.                     //用来检测连接是否有效的sql,要求是一个查询语句。
  66.                     dataSource.setValidationQuery("select 1 ");
  67.                     dataSource.init();
  68.                     // 将数据源放入Map中,key为数据源名称,要和DataSourceType中的枚举项对应,包括大小写,并且保证唯一
  69.                     this.targetDataSourceMap.put(ds.getDbKey(), dataSource);
  70.                 }
  71.                 // 更新数据源配置列表,这里主要是从数据源
  72.                 super.setTargetDataSources(this.targetDataSourceMap);
  73.                 // 将TargetDataSources中的连接信息放入resolvedDataSources管理
  74.                 super.afterPropertiesSet();
  75.             }
  76.         } catch (ClassNotFoundException | SQLException e) {
  77.             log.error("---解析数据源出错---:{}", e.getMessage());
  78.         }
  79.     }
  80.     /**
  81.      * 校验数据源是否存在
  82.      *
  83.      * @param key 数据源保存的key
  84.      * @return 返回结果,true:存在,false:不存在
  85.      */
  86.     public boolean existsDataSource(String key) {
  87.         return Objects.nonNull(this.targetDataSourceMap) && Objects.nonNull(this.targetDataSourceMap.get(key));
  88.     }
  89. }
复制代码
  1. package com.leixi.hub.saasdb.config;
  2. import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
  3. import org.springframework.boot.context.properties.ConfigurationProperties;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.context.annotation.Primary;
  7. import javax.sql.DataSource;
  8. import java.util.HashMap;
  9. import java.util.Map;
  10. /**
  11. * 设置数据源
  12. *
  13. * @author 雷袭月启
  14. * @since 2024/12/5 19:39
  15. */
  16. @Configuration
  17. public class DataSourceConfig {
  18.     private static final String MASTER_SOURCE_KEY = "MASTER";
  19.     /**
  20.      * 配置主数据源,默认使用该数据源,并且主数据源只能配置一个
  21.      *
  22.      * @return DataSource
  23.      * @description 该数据源是在application配置文件master中所配置的
  24.      */
  25.     @Bean
  26.     @ConfigurationProperties("spring.datasource")
  27.     public DataSource masterDataSource() {
  28.         return DruidDataSourceBuilder.create().build();
  29.     }
  30.     /**
  31.      * 配置动态数据源的核心配置项
  32.      *
  33.      * @return DynamicDataSource
  34.      */
  35.     @Primary
  36.     @Bean(name = "dynamicDataSource")
  37.     public DynamicDataSource createDynamicDataSource() {
  38.         Map<Object, Object> dataSourceMap = new HashMap<>();
  39.         // 默认的数据源(主数据源)
  40.         DataSource defaultDataSource = masterDataSource();
  41.         // 配置主数据源,默认使用该数据源,并且主数据源只能配置一个
  42.         dataSourceMap.put(MASTER_SOURCE_KEY, defaultDataSource);
  43.         // 配置动态数据源,默认使用主数据源,如果有从数据源配,则使用从数据库中读取源,并加载到dataSourceMap中
  44.         return new DynamicDataSource(defaultDataSource, dataSourceMap);
  45.     }
  46. }
复制代码
  1. package com.leixi.hub.saasdb.config;
  2. /**
  3. * 动态数据源类型
  4. *
  5. * @author 雷袭月启
  6. * @since 2024/12/5 19:39
  7. */
  8. public enum DataSourceType {
  9.     // 注意:枚举项要和 DataSourceConfig 中的 createDynamicDataSource()方法dataSourceMap的key保持一致
  10.     /**
  11.      * 主库
  12.      */
  13.     MASTER,
  14.     /**
  15.      * 从库
  16.      */
  17.     UAT,
  18. }
复制代码
  1. package com.leixi.hub.saasdb.config;
  2. import lombok.extern.slf4j.Slf4j;
  3. /**
  4. * 创建一个类用于实现ThreadLocal,主要是通过get,set,remove方法来获取、设置、删除当前线程对应的数据源。
  5. *
  6. * @author 雷袭月启
  7. * @since 2024/12/5 19:39
  8. */
  9. @Slf4j
  10. public class DynamicDataSourceContextHolder {
  11.     //此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。
  12.     private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
  13.     /**
  14.      * 设置数据源
  15.      *
  16.      * @param dataSourceName 数据源名称
  17.      */
  18.     public static void setDataSource(String dataSourceName) {
  19.         log.info("切换数据源到:{}", dataSourceName);
  20.         DATASOURCE_HOLDER.set(dataSourceName);
  21.     }
  22.     /**
  23.      * 获取当前线程的数据源
  24.      *
  25.      * @return 数据源名称
  26.      */
  27.     public static String getDataSource() {
  28.         return DATASOURCE_HOLDER.get();
  29.     }
  30.     /**
  31.      * 删除当前数据源
  32.      */
  33.     public static void removeDataSource() {
  34.         log.info("删除当前数据源:{}", getDataSource());
  35.         DATASOURCE_HOLDER.remove();
  36.     }
  37. }
复制代码
  1. package com.leixi.hub.saasdb.config;
  2. import cn.hutool.core.util.StrUtil;
  3. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4. import com.leixi.hub.saasdb.dao.SysDbInfoMapper;
  5. import com.leixi.hub.saasdb.entity.SysDbInfo;
  6. import jakarta.annotation.Resource;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.springframework.beans.factory.annotation.Value;
  9. import org.springframework.boot.CommandLineRunner;
  10. import org.springframework.stereotype.Component;
  11. import org.springframework.util.CollectionUtils;
  12. import java.util.ArrayList;
  13. import java.util.List;
  14. /**
  15. *  CommandLineRunner 项目启动时执行
  16. *
  17. * @author 雷袭月启
  18. * @since 2024/12/5 19:39
  19. */
  20. @Slf4j
  21. @Component
  22. public class LoadDataSourceRunner implements CommandLineRunner {
  23.     /**
  24.      * 是否启用从库多数据源配置
  25.      */
  26.     @Value("${leixi.saas.load_source_form_db:false}")
  27.     private boolean enabled;
  28.     @Resource
  29.     private DynamicDataSource dynamicDataSource;
  30.     @Resource
  31.     private SysDbInfoMapper dbInfoMapper;
  32.     /**
  33.      * 项目启动时加载数据源
  34.      */
  35.     @Override
  36.     public void run(String... args) {
  37.         if (!enabled) return;
  38.         refreshDataSource();
  39.     }
  40.     /**
  41.      * 刷新数据源
  42.      */
  43.     public void refreshDataSource() {
  44.         List<SysDbInfo> dbInfos = dbInfoMapper.selectList(new LambdaQueryWrapper<SysDbInfo>().eq(SysDbInfo::getStatus, 0));
  45.         if (CollectionUtils.isEmpty(dbInfos)) return;
  46.         List<SysDbInfo> ds = new ArrayList<>();
  47.         log.info("====开始加载数据源====");
  48.         for (SysDbInfo info : dbInfos) {
  49.             if (StrUtil.isAllNotBlank(
  50.                     info.getUrl(), // 数据库连接地址
  51.                     info.getDriverClassName(), // 数据库驱动
  52.                     info.getUsername(), // 数据库用户名
  53.                     info.getPassword(), // 数据库密码
  54.                     info.getDbKey() // 数据源key
  55.             )) {
  56.                 ds.add(info);
  57.                 log.info("加载到数据源 ---> dbName:{}、dbKey:{}、remark:{}", info.getDbName(), info.getDbKey(), info.getRemark());
  58.             }
  59.         }
  60.         dynamicDataSource.createDataSource(ds);
  61.         log.info("====数据源加载完成====");
  62.     }
  63. }
复制代码
  1. package com.leixi.hub.saasdb.config;
  2. import java.lang.annotation.*;
  3. /**
  4. * 自定义多数据源切换注解
  5. * <p>
  6. * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
  7. * @author 雷袭月启
  8. * @since 2024/12/5 19:39
  9. */
  10. @Inherited
  11. @Documented
  12. @Retention(RetentionPolicy.RUNTIME)
  13. @Target({ElementType.METHOD, ElementType.TYPE})
  14. public @interface DataSource {
  15.     /**
  16.      * 切换数据源名称(默认是主数据源test01)
  17.      */
  18.     public DataSourceType value() default DataSourceType.MASTER;
  19. }
复制代码
  1. package com.leixi.hub.saasdb.config;
  2. import io.micrometer.common.util.StringUtils;
  3. import org.aspectj.lang.ProceedingJoinPoint;
  4. import org.aspectj.lang.annotation.Around;
  5. import org.aspectj.lang.annotation.Aspect;
  6. import org.aspectj.lang.annotation.Pointcut;
  7. import org.springframework.core.annotation.AnnotationUtils;
  8. import org.springframework.core.annotation.Order;
  9. import org.springframework.stereotype.Component;
  10. import org.aspectj.lang.reflect.MethodSignature;
  11. import java.util.Objects;
  12. /**
  13. * 多数据源切换
  14. * @author 雷袭月启
  15. * @since 2024/12/5 19:39
  16. */
  17. @Aspect
  18. @Order(1)
  19. @Component
  20. public class DataSourceAspect {
  21.     // 配置织入点,为DataSource 注解
  22.     @Pointcut("@annotation(com.leixi.hub.saasdb.config.DataSource)"
  23.             + "|| @within(com.leixi.hub.saasdb.config.DataSource)")
  24.     public void dsPointCut() {
  25.     }
  26.     /**
  27.      * * 环绕通知
  28.      *
  29.      * @param point 切入点
  30.      * @return Object
  31.      * @throws Throwable 异常
  32.      */
  33.     @Around("dsPointCut()")
  34.     public Object around(ProceedingJoinPoint point) throws Throwable {
  35.         DataSource dataSource = getDataSource(point);
  36.         if (Objects.nonNull(dataSource) && StringUtils.isNotEmpty(dataSource.value().name())) {
  37.             // 将用户自定义配置的数据源添加到线程局部变量中
  38.             DynamicDataSourceContextHolder.setDataSource(dataSource.value().name());
  39.         }
  40.         try {
  41.             return point.proceed();
  42.         } finally {
  43.             // 在执行完方法之后,销毁数据源
  44.             DynamicDataSourceContextHolder.removeDataSource();
  45.         }
  46.     }
  47.     /**
  48.      * 获取需要切换的数据源
  49.      * 注意:顺序为:方法>类,方法上加了注解后类上的将不会生效
  50.      * 注意:当类上配置后,方法上没有该注解,那么当前类中的所有方法都将使用类上配置的数据源
  51.      */
  52.     public DataSource getDataSource(ProceedingJoinPoint point) {
  53.         MethodSignature signature = (MethodSignature) point.getSignature();
  54.         // 从方法上获取注解
  55.         DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
  56.         // 方法上不存在时,再从类上匹配
  57.         return Objects.nonNull(dataSource) ? dataSource : AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
  58.     }
  59. }
复制代码
        接下来是测试的一些实体类,Controller方法:
  1. package com.leixi.hub.saasdb.entity;
  2. import com.baomidou.mybatisplus.annotation.TableField;
  3. import com.baomidou.mybatisplus.annotation.TableName;
  4. import lombok.Data;
  5. import lombok.EqualsAndHashCode;
  6. import lombok.NoArgsConstructor;
  7. import lombok.ToString;
  8. import lombok.experimental.Accessors;
  9. import java.io.Serial;
  10. import java.io.Serializable;
  11. /**
  12. *
  13. * @author 雷袭月启
  14. * @since 2024/12/5 19:39
  15. */
  16. @Data
  17. @ToString
  18. @EqualsAndHashCode
  19. @NoArgsConstructor
  20. @Accessors(chain = true)
  21. @TableName(value = "sys_db_info")
  22. public class SysDbInfo implements Serializable {
  23.     @Serial
  24.     @TableField(exist = false)
  25.     private static final long serialVersionUID = 8115921127536664152L;
  26.     /**
  27.      * 数据库地址
  28.      */
  29.     private String url;
  30.     /**
  31.      * 数据库用户名
  32.      */
  33.     private String username;
  34.     /**
  35.      * 密码
  36.      */
  37.     private String password;
  38.     /**
  39.      * 数据库驱动
  40.      */
  41.     private String driverClassName;
  42.     /**
  43.      * 数据库key,即保存Map中的key(保证唯一)
  44.      * 定义一个key用于作为DynamicDataSource中Map中的key。
  45.      * 这里的key需要和DataSourceType中的枚举项保持一致
  46.      */
  47.     private String dbKey;
  48.     /**
  49.      * 数据库名称
  50.      */
  51.     private String dbName;
  52.     /**
  53.      * 是否停用:0-正常,1-停用
  54.      */
  55.     private Integer status;
  56.     /**
  57.      * 备注
  58.      */
  59.     private String remark;
  60. }
复制代码
  1. package com.leixi.hub.saasdb.dao;
  2. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  3. import com.leixi.hub.saasdb.entity.SysDbInfo;
  4. import org.apache.ibatis.annotations.Mapper;
  5. @Mapper
  6. public interface SysDbInfoMapper extends BaseMapper<SysDbInfo> {}
复制代码
  1. package com.leixi.hub.saasdb.dao;
  2. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  3. import org.apache.ibatis.annotations.Mapper;
  4. import org.apache.ibatis.annotations.Param;
  5. import java.util.List;
  6. import java.util.Map;
  7. /**
  8. *
  9. * @author 雷袭月启
  10. * @since 2024/12/5 19:39
  11. */
  12. @Mapper
  13. public interface CommonMapper extends BaseMapper {
  14.     List<Map<String, Object>> getDataBySql(@Param("sql") String sql);
  15.     void updateDataBySql(@Param("sql") String sql);
  16. }
复制代码
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="com.leixi.hub.saasdb.dao.CommonMapper">
  4.     <select id="getDataBySql" resultType="java.util.Map">
  5.         ${sql}
  6.     </select>
  7.     <update id="updateDataBySql">
  8.         ${sql}
  9.     </update>
  10. </mapper>
复制代码
  1. package com.leixi.hub.saasdb.controller;
  2. import com.leixi.hub.saasdb.config.DataSource;
  3. import com.leixi.hub.saasdb.config.DataSourceType;
  4. import com.leixi.hub.saasdb.dao.CommonMapper;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.web.bind.annotation.GetMapping;
  7. import org.springframework.web.bind.annotation.RequestParam;
  8. import org.springframework.web.bind.annotation.RestController;
  9. /**
  10. *
  11. * @author 雷袭月启
  12. * @since 2024/12/5 19:39
  13. */
  14. @RestController
  15. public class DemoController {
  16.     @GetMapping("/demo")
  17.     public Object demo() {
  18.         return "Hello World";
  19.     }
  20.     @Autowired
  21.     private CommonMapper commonMapper;
  22.     @GetMapping("/getDataBySqlFromMaster")
  23.     @DataSource(DataSourceType.MASTER)
  24.     public Object getDataBySqlFromMaster(@RequestParam(value = "sql") String sql) {
  25.         return commonMapper.getDataBySql(sql);
  26.     }
  27.     @GetMapping("/getDataBySqlFromUat")
  28.     @DataSource(DataSourceType.UAT)
  29.     public Object getDataBySqlFromSlave(@RequestParam(value = "sql") String sql) {
  30.         return commonMapper.getDataBySql(sql);
  31.     }
  32.     @GetMapping("/getDataBySql")
  33.     public Object getDataBySql(@RequestParam(value = "sql") String sql) {
  34.         return commonMapper.getDataBySql(sql);
  35.     }
  36. }
复制代码
        3、启动项目,通过Postman测试,效果和预期同等:

        
      

       二、通过Filter实现

        上述的方法固然有用,但多少有些固化了,为何?一:只有添加了注解的类或方法才气动态切换数据源,需要对已有代码举行修改,那就多少会有漏改,少改的位置,二来,可选的数据源在摆列或代码中写死了,假设在数据库里新增了一个数据源,则步伐中必须要做相应的调解,可扩展性不高,综合思量后,我决定再用过滤器的方式试试。
        过滤器的原理其实和AOP相似,只是在Header中添加一个数据库的Key,在过滤器中根据这个Key来指定数据源,实现代码如下:
  1. package com.leixi.hub.saasdb.filter;
  2. import com.leixi.hub.saasdb.config.DynamicDataSourceContextHolder;
  3. import io.micrometer.common.util.StringUtils;
  4. import jakarta.servlet.Filter;
  5. import jakarta.servlet.FilterChain;
  6. import jakarta.servlet.ServletException;
  7. import jakarta.servlet.ServletRequest;
  8. import jakarta.servlet.ServletResponse;
  9. import jakarta.servlet.http.HttpServletRequest;
  10. import org.springframework.core.annotation.Order;
  11. import java.io.IOException;
  12. /**
  13. *
  14. * @author 雷袭月启
  15. * @since 2024/12/5 19:39
  16. */
  17. @Order(1)
  18. public class DataSourceChangeFilter implements Filter {
  19.     private String dataSourceKey;
  20.     public DataSourceChangeFilter(String dataSourceKey) {
  21.         this.dataSourceKey = dataSourceKey;
  22.     }
  23.     @Override
  24.     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
  25.         HttpServletRequest httpRequest = (HttpServletRequest) request;
  26.         String dataSource = httpRequest.getHeader(dataSourceKey);
  27.         if (StringUtils.isNotEmpty(dataSource)) {
  28.             DynamicDataSourceContextHolder.setDataSource(dataSource);
  29.             chain.doFilter(request, response);
  30.             destroy();
  31.         } else {
  32.             chain.doFilter(request, response);
  33.         }
  34.     }
  35.     @Override
  36.     public void destroy() {
  37.         DynamicDataSourceContextHolder.removeDataSource();
  38.     }
  39. }
复制代码
  1. package com.leixi.hub.saasdb.filter;
  2. import org.springframework.beans.factory.annotation.Value;
  3. import org.springframework.boot.web.servlet.FilterRegistrationBean;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. /**
  7. *
  8. * @author 雷袭月启
  9. * @since 2024/12/5 19:39
  10. */
  11. @Configuration
  12. public class FilterConfig {
  13.     @Value("${leixi.saas.data_source_key:data_source_key}")
  14.     private String dataSourceKey;
  15.     @Bean
  16.     public FilterRegistrationBean<DataSourceChangeFilter> licenseValidationFilterRegistration() {
  17.         FilterRegistrationBean<DataSourceChangeFilter> registration = new FilterRegistrationBean<>();
  18.         registration.setFilter(new DataSourceChangeFilter(dataSourceKey));
  19.         registration.addUrlPatterns("/*"); // 应用于所有URL /* 应用于登陆 /login
  20.         return registration;
  21.     }
  22. }
复制代码
        测试过程如下:

        而不传Header时,默认查询的是Master库:


      三、推广使用

        当前这个项目是已经实现了多数据源的动态切换,那么假如想让其他项目也支持,应该怎么办呢?咱可以把这个项目打成一个jar包,然后让其他项目引入依靠即可,改动如下:
        1、删除Application.java文件。
        2、在pom中用以下打包语法举行打包。
  1.     <!--可以打成供其他包依赖的包-->
  2.     <build>
  3.         <plugins>
  4.             <plugin>
  5.                 <groupId>org.apache.maven.plugins</groupId>
  6.                 <artifactId>maven-archetype-plugin</artifactId>
  7.                 <version>3.0.0</version>
  8.             </plugin>
  9.             <plugin>
  10.                 <groupId>org.apache.maven.plugins</groupId>
  11.                 <artifactId>maven-compiler-plugin</artifactId>
  12.                 <version>3.11.0</version>
  13.                 <configuration>
  14.                     <source>17</source>
  15.                     <target>17</target>
  16.                     <encoding>UTF-8</encoding>
  17.                 </configuration>
  18.             </plugin>
  19.         </plugins>
  20.         <resources>
  21.             <resource>
  22.                 <directory>src/main/resources/config</directory>
  23.                 <filtering>true</filtering>
  24.                 <excludes>
  25.                     <exclude>*</exclude>
  26.                 </excludes>
  27.             </resource>
  28.         </resources>
  29.     </build>
复制代码
        3、打包完成后可以在target中看到对应的jar文件,也可以在其他项目中引用该文件,如下:



      跋文与致谢

        以上就是我今天的全部门享了, Demo比较简单,手法也相对稚嫩,希望不会贻笑大方,也希望新手看到这个Demo能有所启发。这次实践也并非一蹴而就的,离不开大佬们的支持和点拨,雷袭在网上找了许多资料,以下这篇博客是最有代价的,可以说雷袭完全是照抄了他的成果,这里附上原文链接,拜谢大佬!
        SpringBoot3多数据源动态切换-陌路



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

李优秀

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表