当前位置: 首页 > news >正文

SpringBoot3动态切换数据源

     背景

        随着公司业务战略的发展,相关的软件服务也逐步的向多元化转变,之前是单纯的拿项目,赚人工钱,现在开始向产品化\服务化转变。最近雷袭又接到一项新的挑战:了解SAAS模型,考虑怎么将公司的产品转换成多租户架构。

        经过一番百度,雷袭对多租户架构总算有了一番了解,以下是整理的笔记。

        多租户架构是一种软件架构,用于实现多用户环境下,使用相同的系统或程序组件时,能保证用户之间数据的隔离性。简单说就是使用共用的数据中心,通过单一系统架构与服务提供多数客户端相同甚至可定制化的服务,并且保障客户的数据隔离。一个支持多租户架构的系统需要在设计上对它的数据和配置进行虚拟分区,从而使系统的每个租户或组织都能够使用一个单独的系统实例,每个租户都可以根据自己的需求对租用的系统实例进行个性化配置。


        多租户技术的实现重点在于不同租户间应用程序环境的隔离以及数据的隔离,使得不同租户间应用程序不会相互干扰。应用程序可通过进程隔离或者多种运维工具实现,数据存储上的隔离方案则是有三种:
        1、独立数据库:优点是独立性最高,缺点是数据库较多,购置和维护成本高。
        2、共享数据库,隔离数据架构:同一个数据库实例内多个用户/schema来对应多个租户,优点是单实例可以支持更多租户,缺点是数据恢复比较困难。
        3、共享数据库,共享数据结构:物理分表,表分区,或者在表中通过字段区分,优点是成本最低,实现难度低,缺点是数据隔离程度低。

        第三种其实雷袭已经试过了,之前的博客里就提到了表分区,分表的实现方式,这里不多缀述,今天雷袭想试试前面两种,因此不得不解决的一个问题:如何实现同一个项目中,数据源的动态切换?

     代码实践

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

       一、通过切面实现

        1、准备工作,创建数据库模式,添加测试数据:

--先创建三个用户,设置密码
SAAS_MASTER   leixi123
SAAS_DEV   leixi123
SAAS_UAT   leixi123--再用sysdba给用户授权
grant dba to SAAS_MASTER;
grant resource to SAAS_MASTER;
grant dba to SAAS_DEV;
grant resource to SAAS_DEV;
grant dba to SAAS_UAT;
grant resource to SAAS_UAT;CREATE TABLE SAAS_MASTER."sys_db_info"
("id" VARCHAR2(32) NOT NULL,"url" VARCHAR2(255) NOT NULL,"username" VARCHAR2(255) NOT NULL,"password" VARCHAR2(255) NOT NULL,"driver_class_name" VARCHAR2(255) NOT NULL,"db_name" VARCHAR2(255) NOT NULL,"db_key" VARCHAR2(255) NOT NULL,"status" INT DEFAULT '0' NOT NULL,"remark" VARCHAR2(255) DEFAULT NULL,PRIMARY KEY("id")) ;COMMENT ON TABLE SAAS_MASTER."sys_db_info" IS '数据库配置信息表';
COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."db_key" IS '数据库key,即保存Map中的key(保证唯一,并且和DataSourceType中的枚举项保持一致,包括大小写)';
COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."db_name" IS '数据库名称';
COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."driver_class_name" IS '数据库驱动';
COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."id" IS '主键ID';
COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."password" IS '密码';
COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."remark" IS '备注说明';
COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."status" IS '是否停用';
COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."url" IS '数据库URL';
COMMENT ON COLUMN SAAS_MASTER."sys_db_info"."username" IS '用户名';--添加数据源信息
insert into SAAS_MASTER."sys_db_info" ("id","url","username","password","driver_class_name","db_name","db_key","status","remark")
values ('1', 'jdbc:dm://127.0.0.1:5236/SAAS_DEV', 'SAAS_DEV', 'leixi123', 'dm.jdbc.driver.DmDriver', 'DEV', 'DEV', 0, '连接DEV数据库');
insert into SAAS_MASTER."sys_db_info" ("id","url","username","password","driver_class_name","db_name","db_key","status","remark")
values ('2', 'jdbc:dm://127.0.0.1:5236/SAAS_UAT', 'SAAS_UAT', 'leixi123', 'dm.jdbc.driver.DmDriver', 'UAT', 'UAT', 0, '连接UAT数据库');--添加测试数据库
CREATE TABLE SAAS_MASTER.leixi_test (id VARCHAR2(32) NOT NULL,name VARCHAR2(255) NOT NULL,PRIMARY KEY (id)
) ;CREATE TABLE SAAS_DEV.leixi_test (id VARCHAR2(32) NOT NULL,name VARCHAR2(255) NOT NULL,PRIMARY KEY (id)
) ;CREATE TABLE SAAS_UAT.leixi_test (id VARCHAR2(32) NOT NULL,name VARCHAR2(255) NOT NULL,PRIMARY KEY (id)
) ;insert into SAAS_MASTER.leixi_test(id, name) values('', '这里是leixi_test 的MASTER库数据');
insert into SAAS_DEV.leixi_test(id, name) values('1', '这里是leixi_test 的DEV库数据');
insert into SAAS_UAT.leixi_test(id, name) values('1', '这里是leixi_test 的UAT数据');

        2、创建一个springboot项目,项目环境为JDK17,以下是相关配置和代码:

        pom.xml

<?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><packaging>jar</packaging><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.2</version> <!-- lookup parent from repository --></parent><groupId>com.leixi.hub.saasdb</groupId><artifactId>leixi-saas-db</artifactId><version>1.0-SNAPSHOT</version><name>leixi-saas-db</name><description>用于动态切换数据源</description><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><hutool.version>5.8.15</hutool.version><mysql.version>8.0.28</mysql.version><druid.version>1.2.16</druid.version><mybatis-plus.version>3.5.3.1</mybatis-plus.version><lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version></properties><dependencies><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><!--编译测试环境,不打包在lib--><scope>provided</scope></dependency><!-- 允许使用Lombok的Java Bean类中使用MapStruct注解 (Lombok 1.18.20+) --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok-mapstruct-binding</artifactId><version>${lombok-mapstruct-binding.version}</version><scope>provided</scope></dependency><!--    hutool工具包    --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>${hutool.version}</version></dependency><!-- web支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- aop --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- DM驱动 --><dependency><groupId>com.dameng</groupId><artifactId>DmJdbcDriver18</artifactId><version>8.1.1.193</version></dependency><!--    阿里druid工具包    --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>${druid.version}</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.40</version></dependency><!-- mybatis-plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.5</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-archetype-plugin</artifactId><version>3.0.0</version></plugin></plugins></build></project>

        application.yml

server:port: 19200servlet:context-path: /leixispring:jackson:## 默认序列化时间格式date-format: yyyy-MM-dd HH:mm:ss## 默认序列化时区time-zone: GMT+8datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: dm.jdbc.driver.DmDriverurl: jdbc:dm://127.0.0.1:5236/SAAS_MASTERusername: SAAS_MASTERpassword: leixi123druid:slave: falseinitial-size: 15min-idle: 15max-active: 200max-wait: 60000time-between-eviction-runs-millis: 60000min-evictable-idle-time-millis: 300000validation-query: ""test-while-idle: truetest-on-borrow: falsetest-on-return: falsepool-prepared-statements: falseconnection-properties: falsetask:execution:thread-pool:core-size: 10max-size: 20queue-capacity: 100
mybatis-plus:mapper-locations: classpath:/mapper/**/*Mapper.xmlglobal-config:db-config:#主键类型  0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID";id-type: assign_uuid# 默认数据库表下划线命名table-underline: trueconfiguration:# 返回类型为Map,显示null对应的字段call-setters-on-nulls: truemap-underscore-to-camel-case: true #开启驼峰和下划线互转# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
leixi:saas:data_source_key: data_source_keyload_source_form_db: true

        以下是设置数据源的核心代码,其原理为:在项目启动时先通过LoadDataSourceRunner从数据库中查询相关的数据连接,存储在内存中,对Controller中的方法添加@DataSource注解,执行方法时,通过注解中的静态枚举切换对应的数据源,对指定的数据库进行操作。

package com.leixi.hub.saasdb.config;import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.leixi.hub.saasdb.entity.SysDbInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import javax.sql.DataSource;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Objects;/*** 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中** @author 雷袭月启* @since 2024/12/5 19:39*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {// 数据源列表,多数据源情况下,具体使用哪一个数据源,由此获取private final Map<Object, Object> targetDataSourceMap;/*** 构造方法,设置默认数据源和目标多数据源** @param defaultDataSource 默认主数据源,只能有一个* @param targetDataSources 从数据源,可以是多个*/public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {super.setDefaultTargetDataSource(defaultDataSource);super.setTargetDataSources(targetDataSources);this.targetDataSourceMap = targetDataSources;}/*** 动态数据源的切换(核心)* 决定使用哪个数据源** @return Object*/@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.getDataSource();}/*** 添加数据源信息** @param dataSources 数据源实体集合*/public void createDataSource(List<SysDbInfo> dataSources) {try {if (CollectionUtils.isNotEmpty(dataSources)) {for (SysDbInfo ds : dataSources) {//校验数据库是否可以连接Class.forName(ds.getDriverClassName());DriverManager.getConnection(ds.getUrl(), ds.getUsername(), ds.getPassword());//定义数据源DruidDataSource dataSource = new DruidDataSource();BeanUtils.copyProperties(ds, dataSource);//申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用dataSource.setTestOnBorrow(true);//建议配置为true,不影响性能,并且保证安全性。//申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。dataSource.setTestWhileIdle(true);//用来检测连接是否有效的sql,要求是一个查询语句。dataSource.setValidationQuery("select 1 ");dataSource.init();// 将数据源放入Map中,key为数据源名称,要和DataSourceType中的枚举项对应,包括大小写,并且保证唯一this.targetDataSourceMap.put(ds.getDbKey(), dataSource);}// 更新数据源配置列表,这里主要是从数据源super.setTargetDataSources(this.targetDataSourceMap);// 将TargetDataSources中的连接信息放入resolvedDataSources管理super.afterPropertiesSet();}} catch (ClassNotFoundException | SQLException e) {log.error("---解析数据源出错---:{}", e.getMessage());}}/*** 校验数据源是否存在** @param key 数据源保存的key* @return 返回结果,true:存在,false:不存在*/public boolean existsDataSource(String key) {return Objects.nonNull(this.targetDataSourceMap) && Objects.nonNull(this.targetDataSourceMap.get(key));}
}
package com.leixi.hub.saasdb.config;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;/*** 设置数据源** @author 雷袭月启* @since 2024/12/5 19:39*/
@Configuration
public class DataSourceConfig {private static final String MASTER_SOURCE_KEY = "MASTER";/*** 配置主数据源,默认使用该数据源,并且主数据源只能配置一个** @return DataSource* @description 该数据源是在application配置文件master中所配置的*/@Bean@ConfigurationProperties("spring.datasource")public DataSource masterDataSource() {return DruidDataSourceBuilder.create().build();}/*** 配置动态数据源的核心配置项** @return DynamicDataSource*/@Primary@Bean(name = "dynamicDataSource")public DynamicDataSource createDynamicDataSource() {Map<Object, Object> dataSourceMap = new HashMap<>();// 默认的数据源(主数据源)DataSource defaultDataSource = masterDataSource();// 配置主数据源,默认使用该数据源,并且主数据源只能配置一个dataSourceMap.put(MASTER_SOURCE_KEY, defaultDataSource);// 配置动态数据源,默认使用主数据源,如果有从数据源配,则使用从数据库中读取源,并加载到dataSourceMap中return new DynamicDataSource(defaultDataSource, dataSourceMap);}
}
package com.leixi.hub.saasdb.config;
/*** 动态数据源类型** @author 雷袭月启* @since 2024/12/5 19:39*/
public enum DataSourceType {// 注意:枚举项要和 DataSourceConfig 中的 createDynamicDataSource()方法dataSourceMap的key保持一致/*** 主库*/MASTER,/*** 从库*/UAT,
}
package com.leixi.hub.saasdb.config;import lombok.extern.slf4j.Slf4j;/*** 创建一个类用于实现ThreadLocal,主要是通过get,set,remove方法来获取、设置、删除当前线程对应的数据源。** @author 雷袭月启* @since 2024/12/5 19:39*/
@Slf4j
public class DynamicDataSourceContextHolder {//此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();/*** 设置数据源** @param dataSourceName 数据源名称*/public static void setDataSource(String dataSourceName) {log.info("切换数据源到:{}", dataSourceName);DATASOURCE_HOLDER.set(dataSourceName);}/*** 获取当前线程的数据源** @return 数据源名称*/public static String getDataSource() {return DATASOURCE_HOLDER.get();}/*** 删除当前数据源*/public static void removeDataSource() {log.info("删除当前数据源:{}", getDataSource());DATASOURCE_HOLDER.remove();}
}
package com.leixi.hub.saasdb.config;import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.leixi.hub.saasdb.dao.SysDbInfoMapper;
import com.leixi.hub.saasdb.entity.SysDbInfo;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import java.util.ArrayList;
import java.util.List;/***  CommandLineRunner 项目启动时执行** @author 雷袭月启* @since 2024/12/5 19:39*/
@Slf4j
@Component
public class LoadDataSourceRunner implements CommandLineRunner {/*** 是否启用从库多数据源配置*/@Value("${leixi.saas.load_source_form_db:false}")private boolean enabled;@Resourceprivate DynamicDataSource dynamicDataSource;@Resourceprivate SysDbInfoMapper dbInfoMapper;/*** 项目启动时加载数据源*/@Overridepublic void run(String... args) {if (!enabled) return;refreshDataSource();}/*** 刷新数据源*/public void refreshDataSource() {List<SysDbInfo> dbInfos = dbInfoMapper.selectList(new LambdaQueryWrapper<SysDbInfo>().eq(SysDbInfo::getStatus, 0));if (CollectionUtils.isEmpty(dbInfos)) return;List<SysDbInfo> ds = new ArrayList<>();log.info("====开始加载数据源====");for (SysDbInfo info : dbInfos) {if (StrUtil.isAllNotBlank(info.getUrl(), // 数据库连接地址info.getDriverClassName(), // 数据库驱动info.getUsername(), // 数据库用户名info.getPassword(), // 数据库密码info.getDbKey() // 数据源key)) {ds.add(info);log.info("加载到数据源 ---> dbName:{}、dbKey:{}、remark:{}", info.getDbName(), info.getDbKey(), info.getRemark());}}dynamicDataSource.createDataSource(ds);log.info("====数据源加载完成====");}
}
package com.leixi.hub.saasdb.config;import java.lang.annotation.*;/*** 自定义多数据源切换注解* <p>* 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准* @author 雷袭月启* @since 2024/12/5 19:39*/@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DataSource {/*** 切换数据源名称(默认是主数据源test01)*/public DataSourceType value() default DataSourceType.MASTER;
}
package com.leixi.hub.saasdb.config;import io.micrometer.common.util.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.aspectj.lang.reflect.MethodSignature;
import java.util.Objects;/*** 多数据源切换* @author 雷袭月启* @since 2024/12/5 19:39*/
@Aspect
@Order(1)
@Component
public class DataSourceAspect {// 配置织入点,为DataSource 注解@Pointcut("@annotation(com.leixi.hub.saasdb.config.DataSource)"+ "|| @within(com.leixi.hub.saasdb.config.DataSource)")public void dsPointCut() {}/*** * 环绕通知** @param point 切入点* @return Object* @throws Throwable 异常*/@Around("dsPointCut()")public Object around(ProceedingJoinPoint point) throws Throwable {DataSource dataSource = getDataSource(point);if (Objects.nonNull(dataSource) && StringUtils.isNotEmpty(dataSource.value().name())) {// 将用户自定义配置的数据源添加到线程局部变量中DynamicDataSourceContextHolder.setDataSource(dataSource.value().name());}try {return point.proceed();} finally {// 在执行完方法之后,销毁数据源DynamicDataSourceContextHolder.removeDataSource();}}/*** 获取需要切换的数据源* 注意:顺序为:方法>类,方法上加了注解后类上的将不会生效* 注意:当类上配置后,方法上没有该注解,那么当前类中的所有方法都将使用类上配置的数据源*/public DataSource getDataSource(ProceedingJoinPoint point) {MethodSignature signature = (MethodSignature) point.getSignature();// 从方法上获取注解DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);// 方法上不存在时,再从类上匹配return Objects.nonNull(dataSource) ? dataSource : AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);}
}

        接下来是测试的一些实体类,Controller方法:

package com.leixi.hub.saasdb.entity;import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;import java.io.Serial;
import java.io.Serializable;/**** @author 雷袭月启* @since 2024/12/5 19:39*/
@Data
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@Accessors(chain = true)
@TableName(value = "sys_db_info")
public class SysDbInfo implements Serializable {@Serial@TableField(exist = false)private static final long serialVersionUID = 8115921127536664152L;/*** 数据库地址*/private String url;/*** 数据库用户名*/private String username;/*** 密码*/private String password;/*** 数据库驱动*/private String driverClassName;/*** 数据库key,即保存Map中的key(保证唯一)* 定义一个key用于作为DynamicDataSource中Map中的key。* 这里的key需要和DataSourceType中的枚举项保持一致*/private String dbKey;/*** 数据库名称*/private String dbName;/*** 是否停用:0-正常,1-停用*/private Integer status;/*** 备注*/private String remark;
}
package com.leixi.hub.saasdb.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.leixi.hub.saasdb.entity.SysDbInfo;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface SysDbInfoMapper extends BaseMapper<SysDbInfo> {}
package com.leixi.hub.saasdb.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;
import java.util.Map;/**** @author 雷袭月启* @since 2024/12/5 19:39*/
@Mapper
public interface CommonMapper extends BaseMapper {List<Map<String, Object>> getDataBySql(@Param("sql") String sql);void updateDataBySql(@Param("sql") String 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.leixi.hub.saasdb.dao.CommonMapper"><select id="getDataBySql" resultType="java.util.Map">${sql}</select><update id="updateDataBySql">${sql}</update>
</mapper>
package com.leixi.hub.saasdb.controller;import com.leixi.hub.saasdb.config.DataSource;
import com.leixi.hub.saasdb.config.DataSourceType;
import com.leixi.hub.saasdb.dao.CommonMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;/**** @author 雷袭月启* @since 2024/12/5 19:39*/
@RestController
public class DemoController {@GetMapping("/demo")public Object demo() {return "Hello World";}@Autowiredprivate CommonMapper commonMapper;@GetMapping("/getDataBySqlFromMaster")@DataSource(DataSourceType.MASTER)public Object getDataBySqlFromMaster(@RequestParam(value = "sql") String sql) {return commonMapper.getDataBySql(sql);}@GetMapping("/getDataBySqlFromUat")@DataSource(DataSourceType.UAT)public Object getDataBySqlFromSlave(@RequestParam(value = "sql") String sql) {return commonMapper.getDataBySql(sql);}@GetMapping("/getDataBySql")public Object getDataBySql(@RequestParam(value = "sql") String sql) {return commonMapper.getDataBySql(sql);}}

        3、启动项目,通过Postman测试,结果和预期一致:

        

      

       二、通过Filter实现

        上述的方法虽然有效,但多少有些固化了,为何?一:只有添加了注解的类或方法才能动态切换数据源,需要对已有代码进行修改,那就多少会有漏改,少改的位置,二来,可选的数据源在枚举或代码中写死了,假设在数据库里新增了一个数据源,则程序中必须要做相应的调整,可扩展性不高,综合考虑后,我决定再用过滤器的方式试试。

        过滤器的原理其实和AOP相似,只是在Header中添加一个数据库的Key,在过滤器中根据这个Key来指定数据源,实现代码如下:

package com.leixi.hub.saasdb.filter;import com.leixi.hub.saasdb.config.DynamicDataSourceContextHolder;
import io.micrometer.common.util.StringUtils;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.annotation.Order;import java.io.IOException;/**** @author 雷袭月启* @since 2024/12/5 19:39*/
@Order(1)
public class DataSourceChangeFilter implements Filter {private String dataSourceKey;public DataSourceChangeFilter(String dataSourceKey) {this.dataSourceKey = dataSourceKey;}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest httpRequest = (HttpServletRequest) request;String dataSource = httpRequest.getHeader(dataSourceKey);if (StringUtils.isNotEmpty(dataSource)) {DynamicDataSourceContextHolder.setDataSource(dataSource);chain.doFilter(request, response);destroy();} else {chain.doFilter(request, response);}}@Overridepublic void destroy() {DynamicDataSourceContextHolder.removeDataSource();}}
package com.leixi.hub.saasdb.filter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/**** @author 雷袭月启* @since 2024/12/5 19:39*/
@Configuration
public class FilterConfig {@Value("${leixi.saas.data_source_key:data_source_key}")private String dataSourceKey;@Beanpublic FilterRegistrationBean<DataSourceChangeFilter> licenseValidationFilterRegistration() {FilterRegistrationBean<DataSourceChangeFilter> registration = new FilterRegistrationBean<>();registration.setFilter(new DataSourceChangeFilter(dataSourceKey));registration.addUrlPatterns("/*"); // 应用于所有URL /* 应用于登陆 /loginreturn registration;}
}

        测试过程如下:

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

      三、推广使用

        当前这个项目是已经实现了多数据源的动态切换,那么如果想让其他项目也支持,应该怎么办呢?咱可以把这个项目打成一个jar包,然后让其他项目引入依赖即可,改动如下:

        1、删除Application.java文件。

        2、在pom中用以下打包语法进行打包。

    <!--可以打成供其他包依赖的包--><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-archetype-plugin</artifactId><version>3.0.0</version></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.11.0</version><configuration><source>17</source><target>17</target><encoding>UTF-8</encoding></configuration></plugin></plugins><resources><resource><directory>src/main/resources/config</directory><filtering>true</filtering><excludes><exclude>*</exclude></excludes></resource></resources></build>

        3、打包完成后可以在target中看到对应的jar文件,也可以在其他项目中引用该文件,如下:

      后记与致谢

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

        SpringBoot3多数据源动态切换-陌路

相关文章:

SpringBoot3动态切换数据源

背景 随着公司业务战略的发展&#xff0c;相关的软件服务也逐步的向多元化转变&#xff0c;之前是单纯的拿项目&#xff0c;赚人工钱&#xff0c;现在开始向产品化\服务化转变。最近雷袭又接到一项新的挑战&#xff1a;了解SAAS模型&#xff0c;考虑怎么将公司的产品转换成多租…...

OSPF - 特殊区域

OSPF路由器需要同时维护域内路由、域间路由、外部路由信息数据库。当网络规模不断扩大时&#xff0c;LSDB规模也不断增长。如果某区域不需要为其他区域提供流量中转服务&#xff0c;那么该区域内的路由器就没有必要维护本区域外的链路状态数据库。  OSPF通过划分区域可以减少网…...

Linux 系统下磁盘相关指令:df、du、fdisk、lsblk

文章目录 I df、du、fdisk、lsblk指令df命令用于显示文件系统的磁盘空间使用情况du命令用于估算目录或文件的磁盘空间使用情况fdisk命令用于对磁盘进行分区操作lsblk指令查看设备信息II 应用du估算目录或文件的磁盘空间使用情况lsblk查看服务器上查看硬盘个数III 知识扩展磁盘阵…...

基于单片机的肺功能MVV简单测算

肺功能MVV一般是指肺部每分钟的最大通气量。 MVV本身是最大值的英文缩写&#xff0c;在临床上&#xff0c;肺功能MVV表示肺部每分钟最大通气量&#xff0c;用以衡量气道的通畅度&#xff0c;以及肺部和胸廓的弹性、呼吸肌的力量。 肺部每分钟的最大通气量的参考值男性与女性之…...

如何用Python编程实现自动整理XML发票文件

传统手工整理发票耗时费力且易出错&#xff0c;而 XML 格式发票因其结构化、标准化的特点&#xff0c;为实现发票的自动化整理与保存提供了可能。本文将详细探讨用python来编程实现对 XML 格式的发票进行自动整理。 一、XML 格式发票的特点 结构化数据&#xff1a;XML 格式发票…...

腾讯云AI代码助手编程挑战赛-百事一点通

作品简介 百事通问答是一款功能强大的智能问答工具。它依托海量知识储备&#xff0c;无论你是想了解生活窍门、学习难点&#xff0c;还是工作中的专业疑惑&#xff0c;只需输入问题&#xff0c;就能瞬间获得精准解答&#xff0c;以简洁易懂的方式呈现&#xff0c;随时随地为你…...

Spring学习笔记1

目录 1 什么是spring2 spring的优势3 IOC的概念和作用3.1 无参数构造函数的实例化方式3.2 使用工厂中的普通方法实例化对象 4 Bean4.1 Bean相关概念4.2 Bean对象的作用范围 5 DI5.1 构造函数注入5.2 set方法注入5.3 复杂类型数据注入5.4 基于注解的IOC5.4.1 包扫描5.4.2 Compon…...

LeetCode 2185. Counting Words With a Given Prefix

&#x1f517; https://leetcode.com/problems/counting-words-with-a-given-prefix 题目 给一个字符串数组&#xff0c;返回其中前缀为 pref 的个数 思路 模拟 代码 class Solution { public:int prefixCount(vector<string>& words, string pref) {int count…...

图漾相机基础操作

1.客户端概述 1.1 简介 PercipioViewer是图漾基于Percipio Camport SDK开发的一款看图软件&#xff0c;可实时预览相机输出的深度图、彩色图、IR红外图和点云图,并保存对应数据&#xff0c;还支持查看设备基础信息&#xff0c;在线修改gain、曝光等各种调节相机成像的参数功能…...

前端开发中页面优化的方法

前端页面优化是指通过改进网页的加载速度、提高用户体验和SEO优化等手段来优化页面性能的过程。以下是一些常见的前端页面优化方法&#xff1a; 压缩和合并文件&#xff1a;通过压缩CSS和JavaScript文件&#xff0c;并将多个文件合并成一个文件&#xff0c;减少网络传输和HTTP请…...

Qt QDockWidget详解以及例程

Qt QDockWidget详解以及例程 引言一、基本用法二、深入了解2.1 窗口功能相关2.2 停靠区域限制2.3 在主窗体布局 引言 QDockWidget类提供了一个可以停靠在QMainWindow内的小窗口 (理论上可以在QMainWindow中任意排列)&#xff0c;也可以作为QMainWindow上的顶级窗口浮动 (类似一…...

机器学习之贝叶斯分类器和混淆矩阵可视化

贝叶斯分类器 目录 贝叶斯分类器1 贝叶斯分类器1.1 概念1.2算法理解1.3 算法导入1.4 函数 2 混淆矩阵可视化2.1 概念2.2 理解2.3 函数导入2.4 函数及参数2.5 绘制函数 3 实际预测3.1 数据及理解3.2 代码测试 1 贝叶斯分类器 1.1 概念 贝叶斯分类器是基于贝叶斯定理构建的分类…...

关于大数据的基础知识(一)——定义特征结构要素

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【14后&#x1f60a;///计算机爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于大数据的基础知识&#xff08;一&a…...

2025 GitCode 开发者冬日嘉年华:AI 与开源的深度交融之旅

在科技的浪潮中&#xff0c;AI 技术与开源探索的火花不断碰撞&#xff0c;催生出无限可能。2025 年 1 月 4 日&#xff0c;由 GitCode 联合 CSDN COC 城市开发者社区精心打造的开年首场开发者活动&#xff1a;冬日嘉年华在北京中关村 • 鼎好 DH3-A 座 22 层盛大举行&#xff0…...

【MyBatis-Plus 进阶功能】开发中常用场景剖析

MyBatis-Plus&#xff08;MP&#xff09;除了封装常见的 CRUD 操作&#xff0c;还提供了一些高级功能&#xff0c;进一步简化复杂场景下的开发工作。本文将逐一讲解 逻辑删除、自动填充、多表关联查询的原理与使用方式&#xff0c;让你快速掌握这些技巧&#xff01; 一、逻辑删…...

【C++/控制台】2048小游戏

源代码&#xff1a; #include <iostream> #include <windows.h> #include <stdio.h> #include <math.h> #include <stdlib.h> #include <conio.h> #include <time.h>// #define KEY_DOWN(VK_NONAME) ((GetAsyncKeyState(VK_NONAME)…...

Linux 中 top 命令的使用与实例解读

目录 Linux 中 top 命令的使用与实例解读一、top 命令参数二、输出字段含义&#xff08;一&#xff09;系统信息&#xff08;二&#xff09;任务信息&#xff08;三&#xff09;CPU 信息&#xff08;四&#xff09;内存信息 三、实例解读系统信息任务信息CPU信息内存信息进程列…...

C++ STL 中的 `unordered_map` 和 `unordered_set` 总结

1. unordered_map unordered_map 是一个基于哈希表实现的容器&#xff0c;存储键值对&#xff08;key-value&#xff09;&#xff0c;每个键必须唯一&#xff0c;可以快速插入、删除、查找。 基本特性 存储结构&#xff1a;键值对 (key-value)。键唯一性&#xff1a;每个键在…...

机器学习基础-概率图模型

&#xff08;一阶&#xff09;马尔科夫模型的基本概念 状态、状态转换概率、初始概率 状态转移矩阵的基本概念 隐马尔可夫模型&#xff08;HMM&#xff09;的基本概念 条件随机场&#xff08;CRF&#xff09;的基本概念 实际应用中的马尔科夫性 自然语言处理&#xff1a; 在词性…...

【MySQL】九、表的内外连接

文章目录 前言Ⅰ. 内连接案例&#xff1a;显示SMITH的名字和部门名称 Ⅱ. 外连接1、左外连接案例&#xff1a;查询所有学生的成绩&#xff0c;如果这个学生没有成绩&#xff0c;也要将学生的个人信息显示出来 2、右外连接案例&#xff1a;对stu表和exam表联合查询&#xff0c;把…...

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...

论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)

HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...

java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别

UnsatisfiedLinkError 在对接硬件设备中&#xff0c;我们会遇到使用 java 调用 dll文件 的情况&#xff0c;此时大概率出现UnsatisfiedLinkError链接错误&#xff0c;原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用&#xff0c;结果 dll 未实现 JNI 协…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享

文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的&#xff0c;根据Excel列的需求预估的工时直接打骨折&#xff0c;不要问我为什么&#xff0c;主要…...

Python爬虫(一):爬虫伪装

一、网站防爬机制概述 在当今互联网环境中&#xff0c;具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类&#xff1a; 身份验证机制&#xff1a;直接将未经授权的爬虫阻挡在外反爬技术体系&#xff1a;通过各种技术手段增加爬虫获取数据的难度…...

宇树科技,改名了!

提到国内具身智能和机器人领域的代表企业&#xff0c;那宇树科技&#xff08;Unitree&#xff09;必须名列其榜。 最近&#xff0c;宇树科技的一项新变动消息在业界引发了不少关注和讨论&#xff0c;即&#xff1a; 宇树向其合作伙伴发布了一封公司名称变更函称&#xff0c;因…...

【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案

目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后&#xff0c;迭代器会失效&#xff0c;因为顺序迭代器在内存中是连续存储的&#xff0c;元素删除后&#xff0c;后续元素会前移。 但一些场景中&#xff0c;我们又需要在执行删除操作…...

【Linux系统】Linux环境变量:系统配置的隐形指挥官

。# Linux系列 文章目录 前言一、环境变量的概念二、常见的环境变量三、环境变量特点及其相关指令3.1 环境变量的全局性3.2、环境变量的生命周期 四、环境变量的组织方式五、C语言对环境变量的操作5.1 设置环境变量&#xff1a;setenv5.2 删除环境变量:unsetenv5.3 遍历所有环境…...

深度剖析 DeepSeek 开源模型部署与应用:策略、权衡与未来走向

在人工智能技术呈指数级发展的当下&#xff0c;大模型已然成为推动各行业变革的核心驱动力。DeepSeek 开源模型以其卓越的性能和灵活的开源特性&#xff0c;吸引了众多企业与开发者的目光。如何高效且合理地部署与运用 DeepSeek 模型&#xff0c;成为释放其巨大潜力的关键所在&…...

Unity中的transform.up

2025年6月8日&#xff0c;周日下午 在Unity中&#xff0c;transform.up是Transform组件的一个属性&#xff0c;表示游戏对象在世界空间中的“上”方向&#xff08;Y轴正方向&#xff09;&#xff0c;且会随对象旋转动态变化。以下是关键点解析&#xff1a; 基本定义 transfor…...