系统要调整为S A S S版实现多 租 户功能,首先想到的两个解决方案就是:
1、通过表字段隔离租户数据信息
2、通过分库来隔离租户数据(这种方案还是比较安全的)
方案最终确定为第二种实现。接下来就是要实现动 态 切 换 数 据 源,以满足不同租户访问自己的数据源了
用来存储不同用户的key值对应的数据源信息
CREATE TABLE `tenant_datasource` (
`id` int NOT NULL AUTO_INCREMENT,
`tenant_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '租户key',
`tenant_type` int DEFAULT '0' COMMENT '数据库类型',
`url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '数据库连接URL',
`username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '数据库连接用户名',
`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '数据库连接密码',
`active` bit(1) DEFAULT b'1' COMMENT '数据是否有效',
`created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_uid` (`tenant_type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
我之前发不过去一个开源的自动生成代码工具
https://gitee.com/yuxuntoo/yuxuntoo_generator
自行下载,生成代码,可选择生成的mybatis或者是mybatisPlus都可以。
定一个新的数据源继承一个抽象类AbstractRoutingDataSource。
其中,必须实现一个方法protected Object determineCurrentLookupKey(),
必须实现其方法动态数据源类集成了Spring提供的AbstractRoutingDataSource类,AbstractRoutingDataSource中获取数据源的方法就是 determineTargetDataSource,而此方法又通过 determineCurrentLookupKey 方法获取查询数据源的key,通过key在resolvedDataSources这个map中获取对应的数据源,resolvedDataSources的值是由afterPropertiesSet()这个方法从TargetDataSources获取的
package com.example.tenant.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.example.tenant.constant.GlobalConstant;
import com.example.tenant.entity.Datasource;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.util.Map;
/**
* 自定义一个数据源继承AbstractRoutingDataSource
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
/**
* 用于保存租户key和数据源的映射关系,目标数据源map的拷贝
*/
public Map
初始化类继承ApplicationRunner,将需要切换的数据源全部通过DynamicDataSource,拿到我们的缓存中
springBoot项目启动时,若想在启动之后直接执行某一段代码,就可以用 ApplicationRunner这个接口,并实现接口里面run(ApplicationArguments args)方法,方法中写上自己的想要的代码逻辑。
package com.example.tenant.init;
import com.example.tenant.config.DataSourceContextHolder;
import com.example.tenant.config.DynamicDataSource;
import com.example.tenant.entity.Datasource;
import com.example.tenant.mapper.DatasourceMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* 初始化runner
* @version 1.0
* @user: Camel
* @date: 2023/6/13 10:09
* @description: springBoot项目启动时,若想在启动之后直接执行某一段代码,就可以用 ApplicationRunner这个接口,
* 并实现接口里面的run(ApplicationArguments args)方法,方法中写上自己的想要的代码逻辑。
*/
@Component
@Order(value = 1)
public class InitializationRunner implements ApplicationRunner {
private Logger logger = LoggerFactory.getLogger(InitializationRunner.class);
@Resource
private DatasourceMapper tenantDatasourceMapper;
@Autowired
private DynamicDataSource dynamicDataSource;
@Override
public void run(ApplicationArguments args) {
// 租户端不进行服务调用
logger.info("服务启动,初始化数据源,以供切换数据源使用");
//切换默认数据源 即tenant库的数据源,用于查询tenant表中的所有tenant数据库配置
DataSourceContextHolder.setDBKey("default");
//设置所有数据源信息
logger.info("获取当前数据源:" + DataSourceContextHolder.getDBKey());
List tenantInfoList = tenantDatasourceMapper.selectList(null);
for (Datasource info : tenantInfoList) {
dynamicDataSource.addDataSource(info);
}
logger.info("初始化租户动态数据源已完成,共加载 【{}】 个", dynamicDataSource.copyTargetDataSources.size());
}
}
从中获取到yml文件的默认数据源,也就是我们保存租户及对应数据源的数据库表。并配置事务控制等bean
package com.example.tenant.config;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.example.tenant.constant.GlobalConstant;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* Mybatis配置类,并且支持事务
* @version 1.0
* @user: Camel
* @date: 2023/6/13 10:09
* @description:
*/
@Configuration
@MapperScan({"com.xxx.xxx.mapper"})
public class MybatisConfig {
private Logger logger = LoggerFactory.getLogger(MybatisConfig.class);
/**
* 配置事务
* @param dynamicDataSource
* @return
*/
@Bean
@Qualifier("transactionManager")
public PlatformTransactionManager txManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
/**
* 配置文件yml中的默认数据源
* @return
*/
@Bean(name = "defaultDataSource")
@ConfigurationProperties(prefix="spring.datasource")
public DataSource getDefaultDataSource() {
return DruidDataSourceBuilder.create().build();
}
/**
* 将动态数据源对象放入spring中管理
* @return
*/
@Bean
public DynamicDataSource dynamicDataSource() {
Map targetDataSources = new HashMap<>();
logger.info("将druid数据源放入默认动态数据源对象中");
targetDataSources.put(GlobalConstant.TENANT_CONFIG_KEY, getDefaultDataSource());
return new DynamicDataSource(getDefaultDataSource(), targetDataSources);
}
/**
* 数据库连接会话工厂
* @param dynamicDataSource 自定义动态数据源
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/**/*.xml"));
return bean.getObject();
}
@Bean
public GlobalConfig getGlobalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
//已删除
dbConfig.setLogicDeleteValue("0");
//未删除
dbConfig.setLogicNotDeleteValue("1");
dbConfig.setLogicDeleteField("active");
globalConfig.setDbConfig(dbConfig);
return globalConfig;
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
# 拦截器实现动态切换
通过拦截器,获取用户信息来完成动态数据源的切换工作
package com.example.tenant.config;
import com.example.tenant.interceptor.DynamicDatasourceInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 数据源拦截器
* @version 1.0
* @user: Camel
* @date: 2023/6/13 10:07
* @description:
*/
@Configuration
public class DynamicDatasourceInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 不需要拦截的接口路径
String[] excludePath = {};
// 如果拦截全部可以设置为 /**,example:"/sys/role/**"
String[] path = {};
DynamicDatasourceInterceptor dynamicDatasourceInterceptor = new DynamicDatasourceInterceptor();
registry.addInterceptor(dynamicDatasourceInterceptor).addPathPatterns(path).excludePathPatterns(excludePath);
}
}
好了,欢迎大家点击下方卡片,关注《coder练习生》