一、先实现动态数据源上下文模式代码,保证在多租户模式下,能自动根据租户Id切换数据源
/**
* 动态数据源上下文
*
* @author 李嘉
* @version 1.0
* @Description 动态数据源上下文
* @date 2020/5/18 23:33
*/
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
/**
* 将 master 数据源的 key作为默认数据源的 key
*/
@Override
protected String initialValue() {
return "master";
}
};
/**
* 数据源的 key集合,用于切换时判断数据源是否存在
*/
public static List<Object> dataSourceKeys = new ArrayList<>();
/**
* 切换数据源
* @param key 数据源
*/
public static void setDataSourceKey(String key) {
if (!StringUtil.isEmpty(key)) {
contextHolder.set(key);
}
}
/**
* 获取数据源
* @return
*/
public static String getDataSourceKey() {
return contextHolder.get();
}
/**
* 重置数据源
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}
/**
* 判断是否包含数据源
* @param key 数据源
* @return
*/
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
/**
* 添加数据源Keys
* @param keys
* @return
*/
public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
return dataSourceKeys.addAll(keys);
}
}
二、实现动态数据源添加和设置,并继承自AbstractRoutingDataSource类,实现其determineTargetDataSource和determineCurrentLookupKey方法
/**
* 动态数据源
*
* @author 李嘉
* @version 1.0
* @Description 动态数据源
* @date 2020/5/18 23:26
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源
* 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可
* @return
*/
@Override
protected DataSource determineTargetDataSource() {
return super.determineTargetDataSource();
}
/**
* 如果希望所有数据源在启动配置时就加载好,这里通过设置数据源Key值来切换数据,定制这个方法
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
/**
* 设置默认数据源
* @param defaultDataSource
*/
public void setDefaultDataSource(Object defaultDataSource) {
super.setDefaultTargetDataSource(defaultDataSource);
}
public void setDataSources(Map<Object, Object> dataSources) {
super.setTargetDataSources(dataSources);
// TODO 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
}
}
三、实现动态数据源切面拦截,并根据租户Id实现数据源的动态切换
/**
* 动态数据源切面拦截
*
* @author 李嘉
* @version 1.0
* @Description 动态数据源切面拦截
* @date 2020/5/19 00:29
*/
@Slf4j
@Aspect
@Component
@Order(1) // 请注意:这里order一定要小于tx:annotation-driven的order,即先执行DynamicDataSourceAspectAdvice切面,再执行事务切面,才能获取到最终的数据源
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DynamicDataSourceAspect {
@Around("execution(* com.example.demo.controller.*.*(..)) || execution(* com.example.demo.*.*(..))")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
Object result = null;
try {
HttpServletRequest request = sra.getRequest();
HttpSession session = sra.getRequest().getSession(true);
String tenantId = (String)session.getAttribute("tenantId");
if (StringUtil.isEmpty(tenantId)) {
tenantId = request.getParameter("tenantId");
}
log.info("当前租户Id:{}", tenantId);
if (!StringUtil.isEmpty(tenantId)) {
DynamicDataSourceContextHolder.setDataSourceKey(tenantId);
result = jp.proceed();
} else {
result = CustomResult.Fail("查询失败,当前租户信息未取到,请联系技术专家!");
}
} catch (Exception ex) {
ex.printStackTrace();
result = CustomResult.Fail("系统异常,请联系技术专家!");
} finally {
DynamicDataSourceContextHolder.clearDataSourceKey();
}
return result;
}
}
四、实现动态数据源初始化,并将租户信息表中的数据库链接等查询出来一并初始化
/**
* 动态数据源初始化
*
* @author 李嘉
* @version 1.0
* @Description 动态数据源初始化
* @date 2020/5/19 00:08
*/
@Slf4j
@Configuration
public class DynamicDataSourceInit {
@Autowired
private ITenantInfoService tenantInfoService;
@Bean
public void initDataSource() {
log.info("======初始化动态数据源=====");
DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringContextUtils.getBean("dynamicDataSource");
HikariDataSource master = (HikariDataSource) SpringContextUtils.getBean("master");
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("master", master);
List<TenantInfo> tenantList = tenantInfoService.list();
for (TenantInfo tenantInfo : tenantList) {
log.info(tenantInfo.toString());
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName(tenantInfo.getDatasourceDriver());
dataSource.setJdbcUrl(tenantInfo.getDatasourceUrl());
dataSource.setUsername(tenantInfo.getDatasourceUsername());
dataSource.setPassword(tenantInfo.getDatasourcePassword());
dataSource.setDataSourceProperties(master.getDataSourceProperties());
dataSourceMap.put(tenantInfo.getTenantId(), dataSource);
}
// 设置数据源
dynamicDataSource.setDataSources(dataSourceMap);
/**
* 必须执行此操作,才会重新初始化AbstractRoutingDataSource 中的 resolvedDataSources,也只有这样,动态切换才会起效
*/
dynamicDataSource.afterPropertiesSet();
}
}
五、配置Mybatis
/**
* MyBatisPlus配置
*
* @author 李嘉
* @version 1.0
* @Description MyBatisPlus配置
* @date 2020/5/18 23:50
*/
@EnableTransactionManagement
@Configuration
@MapperScan({"com.example.demo.dao","com.example.demo.*.*.mapper"})
public class MybatisPlusConfig {
@Bean("master")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public DataSource master() {
return DataSourceBuilder.create().build();
}
@Bean("dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("master", master());
// 将 master 数据源作为默认指定的数据源
dynamicDataSource.setDefaultDataSource(master());
// 将 master 和 slave 数据源作为指定的数据源
dynamicDataSource.setDataSources(dataSourceMap);
return dynamicDataSource;
}
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
/**
* 重点,使分页插件生效
*/
Interceptor[] plugins = new Interceptor[1];
plugins[0] = paginationInterceptor();
sessionFactory.setPlugins(plugins);
//配置数据源,此处配置为关键配置,如果没有将 dynamicDataSource作为数据源则不能实现切换
sessionFactory.setDataSource(dynamicDataSource());
// 扫描Model
sessionFactory.setTypeAliasesPackage("com.example.demo.*.*.entity,com.example.demo.model");
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// 扫描映射文件
sessionFactory.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));
return sessionFactory;
}
@Bean
public PlatformTransactionManager transactionManager() {
// 配置事务管理, 使用事务时在方法头部添加@Transactional注解即可
return new DataSourceTransactionManager(dynamicDataSource());
}
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
List<ISqlParser> sqlParserList = new ArrayList<>();
// 攻击 SQL 阻断解析器、加入解析链
sqlParserList.add(new BlockAttackSqlParser());
paginationInterceptor.setSqlParserList(sqlParserList);
return paginationInterceptor;
}
}
六、租户表相关建表语句
CREATE TABLE `tenant_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`TENANT_ID` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '租户id',
`TENANT_NAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '租户名称',
`DATASOURCE_URL` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '数据源url',
`DATASOURCE_USERNAME` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '数据源用户名',
`DATASOURCE_PASSWORD` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '数据源密码',
`DATASOURCE_DRIVER` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '数据源驱动',
`SYSTEM_ACCOUNT` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '系统账号',
`SYSTEM_PASSWORD` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '账号密码',
`SYSTEM_PROJECT` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '系统PROJECT',
`STATUS` tinyint(1) DEFAULT NULL COMMENT '是否启用(1是0否)',
`CREATE_TIME` datetime DEFAULT NULL COMMENT '创建时间',
`UPDATE_TIME` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
实现源码地址https://github.com/achievejia/springboot_saas