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动态切换数据源
背景 随着公司业务战略的发展,相关的软件服务也逐步的向多元化转变,之前是单纯的拿项目,赚人工钱,现在开始向产品化\服务化转变。最近雷袭又接到一项新的挑战:了解SAAS模型,考虑怎么将公司的产品转换成多租…...
Java虚拟机面试题:内存管理(上)
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编…...
WPF通过反射机制动态加载控件
Activator.CreateInstance 是 .NET 提供的一个静态方法,它属于 System 命名空间。此方法通过反射机制根据提供的类型信息。 写一个小demo演示一下 要求:在用户反馈界面点击建议或者评分按钮 弹出相应界面 编写MainWindow.xmal 主窗体 <Window x:C…...
前端学习-操作元素属性(二十三)
前言 假期快乐,大家加油 操作元素属性 操作元素常用属性 还可以通过 JS 设置/修改标签元素属性,比如通过 src更换 图片最常见的属性 比如:href、title、src等语法:对象.属性 值 const pic document.querySelector(img);pic.src ./images/b0.jpgp…...
Javascript 编写的一个红、黄、绿灯交替变亮
为了创建一个简单但功能完整的交通灯程序,我们将使用 HTML、CSS 和 JavaScript 来实现红、黄、绿三种颜色按照规定的顺序循环显示。这个例子将确保灯光按照红 -> 绿 -> 黄的顺序循环,并且可以调整每个灯光的持续时间以模拟真实的交通灯行为。 效果…...
基于64QAM的载波同步和定时同步性能仿真,包括Costas环和gardner环
目录 1.算法仿真效果 2.算法涉及理论知识概要 3.MATLAB核心程序 4.完整算法代码文件获得 1.算法仿真效果 matlab2022a仿真结果如下(完整代码运行后无水印): 仿真操作步骤可参考程序配套的操作视频。 2.算法涉及理论知识概要 载波同步是…...
小于n的最大数 - 贪心算法 - C++
字节经典面试题 给定一个整数n,并从1~9中给定若干个可以使用的数字,根据上述两个条件,得到每一位都为给定可使用数字的、最大的小于整数n的数,例如,给定可以使用的数字为 {2,3,8} 三个数:给定 n3589&#x…...
leetcode(hot100)4
解题思路:双指针思想 利用两个for循环,第一个for循环把所有非0的全部移到前面,第二个for循环将指针放在非0的末尾全部加上0。 还有一种解法就是利用while循环双指针条件,当不为0就两个指针一起移动 ,为0就只移动右指针…...
【Pandas】pandas Series xs
Pandas2.2 Series Indexing, iteration 方法描述Series.get()用于根据键(索引标签)从 Series 中获取值Series.at用于快速访问标量值(单个元素)的访问器Series.iat用于快速访问标量值(单个元素)的访问器Se…...
【linux内核分析-存储】EXT4源码分析之“文件删除”原理【七万字超长合并版】(源码+关键细节分析)
EXT4源码分析之“文件删除”原理【七万字超长合并版】(源码关键细节分析),详细的跟踪了ext4文件删除的核心调用链,分析关键函数的细节,解答了开篇中提出的三个核心疑问。 文章目录 提示前言全文重点索引1.源码解析1.1 …...
一个在ios当中采用ObjectC和opencv来显示图片的实例
前言 在ios中采用ObjectC编程利用opencv来显示一张图片,并简单绘图。听上去似乎不难,但是实际操作下来,却不是非常的容易的。本文较为详细的描述了这个过程,供后续参考。 一、创建ios工程 1.1、选择ios工程类型 1.2、选择接口模…...
使用Python实现基于强化学习的游戏AI:打造智能化游戏体验
友友们好! 我的新专栏《Python进阶》正式启动啦!这是一个专为那些渴望提升Python技能的朋友们量身打造的专栏,无论你是已经有一定基础的开发者,还是希望深入挖掘Python潜力的爱好者,这里都将是你不可错过的宝藏。 在这个专栏中,你将会找到: ● 深入解析:每一篇文章都将…...
STM32G0B1 can Error_Handler 解决方法
问题现象 MCU上电,发送0x13帧数据固定进入 Error_Handler 硬件介绍 MCU :STM32G0B1 can:NSI1042 tx 接TX RX 接RX 折腾了一下午,无解,问题依旧; 对比测试 STM32G431 手头有块G431 官方评估版CAN 模块; 同样的…...
洛谷 P2511 [HAOI2008] 木棍分割
第一问很简单,第二问 d p dp dp。 (真是哪都能混个 d p dp dp) 参考题解 #include <bits/stdc.h>using namespace std;int read() {int x 0, f 1; char c getchar();while (c < 0 || c > 9) {if (c -) f -1; c getcha…...
二极管钳位电路分享
二极管钳位(I/O的过压/浪涌保护等) 如果我们的电路环境接收外部输入信号容易受到噪声影响,那我们必须采取过压和浪涌保护措施,其中一个方式就是二极管钳位保护。 像上图,从INPUT输入的电压被钳位在-Vf与VCCVf之间&…...
guestfish/libguestfs镜像管理工具简介
文章目录 简介guestfishlibguestfs项目 例子原理代码libguestfs架构参考 简介 guestfish Guestfish 是libguestfs项目中的一个工具软件,提供修改虚机镜像内部配置的功能。它不需要把虚机镜像挂接到本地,而是为你提供一个shell接口,你可以查…...
AutoSar架构学习笔记
1.AUTOSAR(Automotive Open System Architecture,汽车开放系统架构)是一个针对汽车行业的软件架构标准,旨在提升汽车电子系统的模块化、可扩展性、可重用性和互操作性。AUTOSAR的目标是为汽车电子控制单元(ECU…...
Scade pragma: separate_io
概述 在 Scade 语言中,支持对用户自定义算子使用 separate_io pragma 进行修饰。其形式如: function #pragma kcg separate_io #end N(x: int8) returns (y,z: int8) let y x;z x; tel在上例中,算子N 就被 pragma #pragma kcg separate_i…...
三天速成微服务
微服务技术栈 总结 微服务技术对比 技术栈 SpringCloud SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud Springboot和SpringCould兼容性 代码目录结构如下 用于远程调用Bean 代码 package cn.itcast.order.config;//import …...
【MySQL】九、表的内外连接
文章目录 前言Ⅰ. 内连接案例:显示SMITH的名字和部门名称 Ⅱ. 外连接1、左外连接案例:查询所有学生的成绩,如果这个学生没有成绩,也要将学生的个人信息显示出来 2、右外连接案例:对stu表和exam表联合查询,把…...
GitLab 创建项目、删除项目
1、创建项目 点击左上角图标,回到首页 点击 Create a project 点击 Create blank project 输入项目名称,点击Create Project 创建成功 2、删除项目 进入项目列表 点击对应项目,进入项目 进入Settings页面 拖到页面底部,展开Adva…...
python学opencv|读取图像(二十六)使用cv2.putText()绘制文字进阶-在图像上写文字
【1】引言 前序已经学会了在画布上绘制文字的大部分技巧,相关文章链接为: python学opencv|读取图像(二十三)使用cv2.putText()绘制文字-CSDN博客 python学opencv|读取图像(二十四)使用cv2.putText()绘制…...
Apache HTTPD 多后缀解析漏洞
目录 漏洞简介 漏洞环境 漏洞复现 漏洞防御 漏洞简介 Apache HTTPD 支持一个文件拥有多个后缀,并为不同后缀执行不同的指令。比如,如下配置文件: AddType text/html .html AddLanguage zh-CN .cn 以上就是Apache多后缀的特性。如果运维…...
(二)当人工智能是一个函数,函数形式怎么选择?ChatGPT的函数又是什么?
在上一篇文章中,我们通过二次函数的例子,讲解了如何训练人工智能。今天,让我们进一步探讨:面对不同的实际问题,应该如何选择合适的函数形式? 一、广告推荐系统中的函数选择 1. 业务目标 想象一下&#x…...
JavaScript学习-入门篇
JavaScript的运行环境 开发环境就是开发JavaScript代码所需的环境,一般建议新手刚刚开始使用一些记事本工具(如sublime、editPlus、VScode),锻炼代码的手感。等学习到一定阶段,就可以使用集成开发工具IDE࿰…...
今日头条ip属地根据什么显示?不准确怎么办
在今日头条这样的社交媒体平台上,用户的IP属地信息对于维护网络环境的健康与秩序至关重要。然而,不少用户发现自己的IP属地显示与实际位置不符,这引发了广泛的关注和讨论。本文将深入探讨今日头条IP属地的显示依据,并提供解决IP属…...
python之移动端测试---appium
Appium Appium介绍环境准备新版本appium的用法介绍元素定位函数被封装,统一使用By.xxx(定位方式):通过文本定位的写法 一个简单的请求示例APP操作api基础apk安装卸载发送,拉取文件uiautomatorviewer工具使用获取页面元素及属性模拟事件操作模…...
【网络安全实验室】基础关实战详情
须知少时凌云志,曾许人间第一流 1.key在哪里 url:http://rdyx0/base1_4a4d993ed7bd7d467b27af52d2aaa800/index.php 查看网页源代码的方式有4种,分别是:1、鼠标右击会看到”查看源代码“,这个网页的源代码就出现在你眼前了&…...
在DJI无人机上运行VINS-FUISON(PSDK 转 ROS)
安装ceres出现以下报错,将2版本的ceres换成1版本的ceres CMake did not find one.Could not find a package configuration file provided by "absl" with any ofthe following names:abslConfig.cmakeabsl-config.cmakeAdd the installation prefix of …...
MarkDown怎么转pdf;Mark Text怎么使用;
MarkDown怎么转pdf 目录 MarkDown怎么转pdf先用CSDN进行编辑,能双向看版式;标题最后直接导出pdfMark Text怎么使用一、界面介绍二、基本操作三、视图模式四、其他功能先用CSDN进行编辑,能双向看版式; 标题最后直接导出pdf Mark Text怎么使用 Mark Text是一款简洁的开源Mar…...
代码实战:基于InvSR对视频进行超分辨率重建
Diffusion Models专栏文章汇总:入门与实战 前言:上一篇博客《使用Diffusion Models进行图像超分辩重建》中讲解了InvSR的原理,博主实测的效果是非常不错的,和PASD基本持平。这篇博客就讲解如何利用InvSR对视频进行超分辨率重建。 目录 环境准备 代码讲解 环境准备...
解决HBuilderX报错:未安装内置终端插件,是否下载?或使用外部命令行打开。
版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl 错误描述 在HBuilderX中执行npm run build总是提醒下载插件;图示如下: 但是,下载总是失败。运行项目时候依然弹出上述提醒。 解决方案 …...
邻接表深度优先遍历和广度优先遍历计算方法
DFS和BFS 一、 深度优先遍历次序(DFS)二、 深度优先遍历生成树三、 广度优先遍历次序(BFS)四、 广度优先遍历生成树示例说明1. DFS遍历2. BFS遍历 在图的遍历中,常用的两种算法是深度优先遍历(DFSÿ…...
计算机网络-数据链路层(CSMA/CD协议,CSMA/CA协议)
2.2 ppp协议 点对点协议ppp是目前使用最广泛的点对点数据链路层协议。 2.3 媒体接入控制基本概念 共享信道要着重考虑的一个问题就是如何协调多个发送和接收站点对一个共享传输媒体的占用,即媒体接入控制MAC。 2.3.1 静态划分信道 频分复用 时分复用 波分复用 码分复…...
网络安全的学习与实践经验(附资料合集)
学习资源 在线学习平台: Hack This Site:提供从初学者到高级难度的挑战任务,适合练习各种网络安全技术。XCTF_OJ:由XCTF组委会开发的免费在线网络安全网站,提供丰富的培训材料和资源。SecurityTube:提供丰…...
Navicat 17 for Mac 数据库管理软件
Mac分享吧 文章目录 效果一、准备工作二、开始安装1. 双击运行软件,将其从左侧拖入右侧文件夹中,等待安装完毕。2. 应用程序/启动台显示Navicat图标,表示安装成功。 二、运行测试运行后提示:“Navicat Premium.pp”已损坏&#x…...
Json与jsoncpp
目录 一、关于Json 1.数据类型 2.语法规则 二、写Json相关API 1.Json::Value类 2.append方法 3.toStyledString方法 三、读Json相关API 1.Json::Reader类 2.parse方法 3.类型判断方法 4.类型转换方法 5.getMemberNames方法 四、完整代码 一、关于Json Json是一种轻…...
Webpack、Vite区别知多少?
前端的项目打包,我们常用的构建工具有Webpack和Vite,那么Webpack和Vite是两种不同的前端构建工具,那么你们又是否了解它们的区别呢?我们在做项目时要如何选择呢? 一、工具定义 1、Webpack:是一个强大的静态模块打包工…...
以太网ICMP协议(ping指令)——FPGA学习笔记25
--素材来源原子哥 一、IP协议 1、IP简介 IP是Internet Protocol(网际互连协议)的缩写。IP 协议是 TCP/IP 协议簇中的核心协议,它为上层协议提供无状态、无连接、不可靠的服务。IP 协议规定了数据传输时的基本单元和格式 。 IP协议是 OSI 参考模型中网络层…...
ESP32自动下载电路分享
下面是一个ESP32系列或者ESP8266等电路的一个自动下载电路 在ESP32等模块需要烧写程序的时候,需要通过将EN引脚更改为低电平并将IO0引脚设置为低电平来切换到烧写模式。 有时候也会采用先将IO接到一个按键上,按住按键拉低IO0的同时重新上电的方式进入烧写…...
数据结构(ing)
学习内容 指针 指针的定义: 指针是一种变量,它的值为另一个变量的地址,即内存地址。 指针在内存中也是要占据位置的。 指针类型: 指针的值用来存储内存地址,指针的类型表示该地址所指向的数据类型并告诉编译器如何解…...
STM32-笔记32-ESP8266作为服务端
esp8266作为服务器的时候,这时候网络助手以客户端的模式连接到esp8266,其中IP地址写的是esp8266作为服务器时的IP地址,可以使用ATCIFSR查询esp8266的ip地址,端口号默认写333。 当esp8266作为服务器的时候,需要完成哪些…...
[Day 12]904.水果成篮
今天给带来的题目是滑动窗口的另一种题目,之前我们讲了滑动窗口题目中长度最小的子数组,今天这个题目实际上是求长度最长的子数组 题目描述:力扣链接 904.水果成篮 你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整…...
检查字符是否相同
给你一个字符串 s ,如果 s 是一个 好 字符串,请你返回 true ,否则请返回 false 。 如果 s 中出现过的 所有 字符的出现次数 相同 ,那么我们称字符串 s 是 好 字符串。 输入:s "abacbc" 输出:t…...
专家混合(MoE)大语言模型:免费的嵌入模型新宠
专家混合(MoE)大语言模型:免费的嵌入模型新宠 今天,我们深入探讨一种备受瞩目的架构——专家混合(Mixture-of-Experts,MoE)大语言模型,它在嵌入模型领域展现出了独特的魅力。 一、M…...
CSS3 框大小
CSS3 框大小 CSS3 是网页设计和开发中不可或缺的一部分,它为开发者提供了更多样化、更灵活的样式和布局选择。在 CSS3 中,框大小(Box Sizing)是一个重要的概念,它决定了元素内容的宽度和高度以及元素整体的大小。本文将详细介绍 CSS3 框大小的概念、用法以及最佳实践。 …...
Vue动态控制disabled属性
参考:https://blog.csdn.net/guhanfengdu/article/details/126082781 在Vue中disabled:的值是受布尔值影响的,false为关闭禁用,true为开启禁用效果。 结果就是true会让按钮禁用 相反false会让按钮重新可以使用 那如果想要通过id属性值来判断是否禁用…...
Python入门教程 —— 列表
1.列表的基本使用 列表的介绍 前面学习的字符串可以用来存储一串信息,那么想一想,怎样存储咱们班所有同学的名字呢? 定义100个变量,每个变量存放一个学生的姓名可行吗?有更好的办法吗? 列表 列表的格式 定义列的格式:[元素1, 元素2, 元素3, ..., 元素n] 变量tmp的类型…...
CSS2笔记
一、CSS基础 1.CSS简介 2.CSS的编写位置 2.1 行内样式 2.2 内部样式 2.3 外部样式 3.样式表的优先级 4.CSS语法规范 5.CSS代码风格 二、CSS选择器 1.CSS基本选择器 通配选择器元素选择器类选择器id选择器 1.1 通配选择器 1.2 元素选择器 1.3 类选择器 1.4 ID选择器 1.5 基…...
对一个双向链表,从尾部遍历找到第一个值为x的点,将node p插入这个点之前,如果找不到,则插在末尾。使用C语言实现
以下是一个用C语言实现的双向链表(Doubly Linked List)插入操作的代码。该代码从尾部遍历找到第一个值为x的节点,并在其前插入新节点p,或者在未找到时将其插入链表末尾。 #include <stdio.h> #include <stdlib.h>// 定…...