This commit is contained in:
kk
2026-07-07 18:01:21 +08:00
commit b259f94d4b
1088 changed files with 121778 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springblade</groupId>
<artifactId>BladeX-Tool</artifactId>
<version>${revision}</version>
</parent>
<artifactId>blade-starter-tenant-dynamic</artifactId>
<name>${project.artifactId}</name>
<version>${project.parent.version}</version>
<packaging>jar</packaging>
<properties>
<module.name>org.springblade.blade.starter.tenant.dynamic</module.name>
</properties>
<dependencies>
<!--Blade-->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-tenant</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-db-dynamic</artifactId>
</dependency>
<!-- sharding-jdbc -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
<!-- Auto -->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-auto</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,163 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.config;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
import com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationAdvisor;
import com.baomidou.dynamic.datasource.creator.druid.DruidDataSourceCreator;
import com.baomidou.dynamic.datasource.processor.DsJakartaHeaderProcessor;
import com.baomidou.dynamic.datasource.processor.DsJakartaSessionProcessor;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import com.baomidou.dynamic.datasource.processor.DsSpelExpressionProcessor;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceCreatorAutoConfiguration;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springblade.core.db.dynamic.config.DynamicDataSourceConfiguration;
import org.springblade.core.db.dynamic.processor.DynamicDataSourceProcessor;
import org.springblade.core.tenant.annotation.TenantDS;
import org.springblade.core.tenant.dynamic.*;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Role;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import static org.springblade.core.tenant.constant.TenantDynamicConstant.TENANT_DYNAMIC_DATASOURCE_PROP;
import static org.springblade.core.tenant.constant.TenantDynamicConstant.TENANT_DYNAMIC_GLOBAL_PROP;
/**
* 多租户数据源配置类
*
* @author Chill
*/
@RequiredArgsConstructor
@EnableConfigurationProperties({DataSourceProperties.class, DynamicDataSourceProperties.class})
@AutoConfiguration(before = {DruidDataSourceAutoConfigure.class, DynamicDataSourceAutoConfiguration.class, DynamicDataSourceConfiguration.class})
@Import(value = {DynamicDataSourceCreatorAutoConfiguration.class})
@ConditionalOnProperty(value = TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "true")
public class TenantDataSourceConfiguration {
@Bean
@ConditionalOnMissingBean
public DsProcessor dsProcessor() {
DsProcessor headerProcessor = new DsJakartaHeaderProcessor();
DsProcessor sessionProcessor = new DsJakartaSessionProcessor();
DsTenantIdProcessor tenantIdProcessor = new DsTenantIdProcessor();
DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
headerProcessor.setNextProcessor(sessionProcessor);
sessionProcessor.setNextProcessor(tenantIdProcessor);
tenantIdProcessor.setNextProcessor(spelExpressionProcessor);
return headerProcessor;
}
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceProcessor dynamicTenantDataSourceProcessor() {
return new TenantDataSourceProcessor();
}
@Bean
@ConditionalOnMissingBean
public TenantDataSourceAnnotationInterceptor tenantDataSourceAnnotationInterceptor() {
return new TenantDataSourceAnnotationInterceptor();
}
@Bean
@ConditionalOnMissingBean
@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
public DynamicDataSourceAnnotationAdvisor dynamicTenantDatasourceAnnotationAdvisor(TenantDataSourceAnnotationInterceptor tenantDataSourceAnnotationInterceptor, DynamicDataSourceProperties dynamicDataSourceProperties) {
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(tenantDataSourceAnnotationInterceptor, TenantDS.class);
advisor.setOrder(dynamicDataSourceProperties.getAop().getOrder());
return advisor;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(value = TENANT_DYNAMIC_GLOBAL_PROP, havingValue = "true")
public TenantDataSourceGlobalInterceptor tenantDataSourceGlobalInterceptor() {
return new TenantDataSourceGlobalInterceptor();
}
@Bean
@ConditionalOnMissingBean
@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnProperty(value = TENANT_DYNAMIC_GLOBAL_PROP, havingValue = "true")
public TenantDataSourceGlobalAdvisor tenantDataSourceGlobalAdvisor(TenantDataSourceGlobalInterceptor tenantDataSourceGlobalInterceptor, DynamicDataSourceProperties dynamicDataSourceProperties) {
TenantDataSourceGlobalAdvisor advisor = new TenantDataSourceGlobalAdvisor(tenantDataSourceGlobalInterceptor);
advisor.setOrder(dynamicDataSourceProperties.getAop().getOrder() + 1);
return advisor;
}
@Order
@AutoConfiguration
@AllArgsConstructor
@ConditionalOnProperty(value = TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "true")
public static class TenantDataSourceAnnotationConfiguration implements SmartInitializingSingleton {
private final TenantDataSourceAnnotationInterceptor tenantDataSourceAnnotationInterceptor;
private final DataSource dataSource;
private final DruidDataSourceCreator dataSourceCreator;
private final JdbcTemplate jdbcTemplate;
@Override
public void afterSingletonsInstantiated() {
TenantDataSourceHolder tenantDataSourceHolder = new TenantDataSourceHolder(dataSource, dataSourceCreator, jdbcTemplate);
tenantDataSourceAnnotationInterceptor.setHolder(tenantDataSourceHolder);
}
}
@Order
@AutoConfiguration
@AllArgsConstructor
@ConditionalOnProperty(value = TENANT_DYNAMIC_GLOBAL_PROP, havingValue = "true")
public static class TenantDataSourceGlobalConfiguration implements SmartInitializingSingleton {
private final TenantDataSourceGlobalInterceptor tenantDataSourceGlobalInterceptor;
private final DataSource dataSource;
private final DruidDataSourceCreator dataSourceCreator;
private final JdbcTemplate jdbcTemplate;
@Override
public void afterSingletonsInstantiated() {
TenantDataSourceHolder tenantDataSourceHolder = new TenantDataSourceHolder(dataSource, dataSourceCreator, jdbcTemplate);
tenantDataSourceGlobalInterceptor.setHolder(tenantDataSourceHolder);
}
}
}
@@ -0,0 +1,95 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.constant;
/**
* 租户动态数据源常量.
*
* @author Chill
*/
public interface TenantDynamicConstant extends TenantBaseConstant{
/**
* 租户动态数据源键
*/
String TENANT_DYNAMIC_DATASOURCE_PROP = "blade.tenant.dynamic-datasource";
/**
* 租户全局动态数据源切面键
*/
String TENANT_DYNAMIC_GLOBAL_PROP = "blade.tenant.dynamic-global";
/**
* 租户是否存在数据源
*/
String TENANT_DATASOURCE_EXIST_STATEMENT = "select datasource_id from blade_tenant WHERE is_deleted = 0 AND tenant_id = ?";
/**
* 租户数据源基础SQL
*/
String TENANT_DATASOURCE_BASE_STATEMENT = "SELECT category, tenant_id as tenantId, driver_class as driverClass, url, username, password, sharding_config as shardingConfig from blade_tenant tenant LEFT JOIN blade_tenant_datasource datasource ON tenant.datasource_id = datasource.id ";
/**
* 租户单数据源SQL
*/
String TENANT_DATASOURCE_SINGLE_STATEMENT = TENANT_DATASOURCE_BASE_STATEMENT + "WHERE tenant.is_deleted = 0 AND tenant.tenant_id = ?";
/**
* 租户集动态数据源SQL
*/
String TENANT_DATASOURCE_GROUP_STATEMENT = TENANT_DATASOURCE_BASE_STATEMENT + "WHERE tenant.is_deleted = 0";
/**
* 租户未找到返回信息
*/
String TENANT_DATASOURCE_NOT_FOUND = "未找到租户信息,数据源加载失败!";
/**
* oracle驱动类
*/
String ORACLE_DRIVER_CLASS = "oracle.jdbc.OracleDriver";
/**
* oracle校验
*/
String ORACLE_VALIDATE_STATEMENT = "select 1 from dual";
/**
* 通用校验
*/
String COMMON_VALIDATE_STATEMENT = "select 1";
/**
* jdbc数据源分类
*/
int JDBC_CATEGORY = 1;
/**
* sharding数据源分类
*/
int SHARDING_CATEGORY = 2;
}
@@ -0,0 +1,51 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.dynamic;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import org.aopalliance.intercept.MethodInvocation;
import org.springblade.core.tenant.TenantUtil;
/**
* 租户动态数据源解析器
*
* @author Chill
*/
public class DsTenantIdProcessor extends DsProcessor {
public static final String TENANT_ID_KEY = "#token.tenantId";
@Override
public boolean matches(String key) {
return key.equals(TENANT_ID_KEY);
}
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
return TenantUtil.getTenantId();
}
}
@@ -0,0 +1,71 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.dynamic;
import lombok.Data;
/**
* 租户数据源
*
* @author Chill
*/
@Data
public class TenantDataSource {
/**
* 数据源类型
*/
private int category;
/**
* 租户ID
*/
private String tenantId;
/**
* 数据源ID
*/
private String datasourceId;
/**
* 驱动类
*/
private String driverClass;
/**
* 数据库链接
*/
private String url;
/**
* 数据库账号名
*/
private String username;
/**
* 数据库密码
*/
private String password;
/**
* 分库分表配置
*/
private String shardingConfig;
}
@@ -0,0 +1,67 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.dynamic;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springblade.core.tenant.TenantUtil;
import org.springblade.core.tenant.exception.TenantDataSourceException;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.lang.NonNull;
/**
* 租户数据源切换拦截器
*
* @author Chill
*/
@Slf4j
@Setter
public class TenantDataSourceAnnotationInterceptor implements MethodInterceptor {
private TenantDataSourceHolder holder;
@Override
public Object invoke(@NonNull MethodInvocation invocation) throws Throwable {
String tenantId = TenantUtil.getTenantId();
if (StringUtil.isBlank(tenantId)) {
throw new TenantDataSourceException("租户数据源切换失败,租户ID不能为空");
}
try {
holder.handleDataSource(tenantId);
DynamicDataSourceContextHolder.push(tenantId);
return invocation.proceed();
} catch (Exception exception) {
log.error("租户数据源切换异常", exception);
throw new TenantDataSourceException(exception.getMessage(), exception);
} finally {
DynamicDataSourceContextHolder.poll();
}
}
}
@@ -0,0 +1,84 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.dynamic;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Pointcut;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.lang.NonNull;
import static org.springblade.core.launch.constant.AppConstant.BASE_PACKAGES;
/**
* 租户数据源全局处理器
*
* @author Chill
*/
public class TenantDataSourceGlobalAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
private final Advice advice;
private final Pointcut pointcut;
public TenantDataSourceGlobalAdvisor(@NonNull TenantDataSourceGlobalInterceptor tenantDataSourceGlobalInterceptor) {
this.advice = tenantDataSourceGlobalInterceptor;
this.pointcut = buildPointcut();
}
@NonNull
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
@NonNull
@Override
public Advice getAdvice() {
return this.advice;
}
@Override
public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException {
if (this.advice instanceof BeanFactoryAware) {
((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
}
}
private Pointcut buildPointcut() {
AspectJExpressionPointcut cut = new AspectJExpressionPointcut();
cut.setExpression(
"(@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController)) && " +
"(!@annotation(" + BASE_PACKAGES + ".core.tenant.annotation.NonDS) && !@within(" + BASE_PACKAGES + ".core.tenant.annotation.NonDS))"
);
return new ComposablePointcut((Pointcut) cut);
}
}
@@ -0,0 +1,67 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.dynamic;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springblade.core.tenant.TenantUtil;
import org.springblade.core.tenant.exception.TenantDataSourceException;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.lang.NonNull;
/**
* 租户数据源全局拦截器
*
* @author Chill
*/
@Slf4j
@Setter
public class TenantDataSourceGlobalInterceptor implements MethodInterceptor {
private TenantDataSourceHolder holder;
@Override
public Object invoke(@NonNull MethodInvocation invocation) throws Throwable {
String tenantId = TenantUtil.getTenantId();
try {
if (StringUtil.isNotBlank(tenantId)) {
holder.handleDataSource(tenantId);
DynamicDataSourceContextHolder.push(tenantId);
}
return invocation.proceed();
} catch (Exception exception) {
log.error("租户全局数据源切换异常", exception);
throw new TenantDataSourceException(exception.getMessage(), exception);
} finally {
if (StringUtil.isNotBlank(tenantId)) {
DynamicDataSourceContextHolder.poll();
}
}
}
}
@@ -0,0 +1,148 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.dynamic;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceCreator;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.druid.DruidConfig;
import lombok.AllArgsConstructor;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.db.dynamic.holder.DynamicDataSourceHolder;
import org.springblade.core.tenant.utils.ShardingUtil;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.util.Set;
import static org.springblade.core.tenant.constant.TenantDynamicConstant.*;
/**
* 租户数据源核心处理类
*
* @author Chill
*/
@AllArgsConstructor
public class TenantDataSourceHolder implements DynamicDataSourceHolder {
private final DataSource dataSource;
private final DataSourceCreator dataSourceCreator;
private final JdbcTemplate jdbcTemplate;
/**
* 数据源缓存处理
*
* @param tenantId 租户ID
*/
@Override
public void handleDataSource(String tenantId) {
// 获取储存的数据源集合
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
Set<String> keys = ds.getDataSources().keySet();
// 配置不存在则动态添加数据源,以懒加载的模式解决分布式场景的配置同步
// 为了保证数据完整性,配置后生成数据源缓存,后台便无法修改更换数据源,若一定要修改请迁移数据后重启服务或自行修改底层逻辑
if (!keys.contains(tenantId)) {
TenantDataSource tenantDataSource = getDataSource(tenantId);
if (tenantDataSource != null) {
int category = tenantDataSource.getCategory();
if (category == JDBC_CATEGORY) {
// 创建数据源配置
DataSourceProperty dataSourceProperty = new DataSourceProperty();
// 拷贝数据源配置
BeanUtils.copyProperties(tenantDataSource, dataSourceProperty);
// 设置驱动类
dataSourceProperty.setDriverClassName(tenantDataSource.getDriverClass());
// 关闭懒加载
dataSourceProperty.setLazy(Boolean.FALSE);
// 设置SQL校验
DruidConfig druid = dataSourceProperty.getDruid();
if (StringUtil.equals(dataSourceProperty.getDriverClassName(), ORACLE_DRIVER_CLASS)) {
druid.setValidationQuery(ORACLE_VALIDATE_STATEMENT);
} else {
druid.setValidationQuery(COMMON_VALIDATE_STATEMENT);
}
// 创建Jdbc数据源
DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
// 添加最新数据源
ds.addDataSource(tenantId, dataSource);
} else if (category == SHARDING_CATEGORY) {
// 创建Sharding数据源
DataSource dataSource = ShardingUtil.createDataSource(tenantDataSource.getShardingConfig());
// 添加最新数据源
ds.addDataSource(tenantId, dataSource);
}
}
}
}
/**
* 判断租户是否有数据源配置
*
* @param tenantId 租户ID
*/
private Boolean existDataSource(String tenantId) {
// 将租户是否配置数据源进行缓存,若重新配置会将此缓存清空并在下次请求的时候懒加载
// 若租户没有配置数据源则会自动使用master数据源,此举是为了避免在没有数据库的时候频繁查询导致缓存击穿
Boolean exist = CacheUtil.get(TENANT_DATASOURCE_CACHE, TENANT_DATASOURCE_EXIST_KEY, tenantId, Boolean.class, Boolean.FALSE);
if (exist == null) {
TenantDataSource tenantDataSource = jdbcTemplate.queryForObject(TENANT_DATASOURCE_EXIST_STATEMENT, new BeanPropertyRowMapper<>(TenantDataSource.class), tenantId);
if (tenantDataSource != null && StringUtil.isNotBlank(tenantDataSource.getDatasourceId())) {
exist = Boolean.TRUE;
} else {
exist = Boolean.FALSE;
}
CacheUtil.put(TENANT_DATASOURCE_CACHE, TENANT_DATASOURCE_EXIST_KEY, tenantId, exist, Boolean.FALSE);
}
return exist;
}
/**
* 获取对应的数据源配置
*
* @param tenantId 租户ID
*/
private TenantDataSource getDataSource(String tenantId) {
// 不存在租户数据源则返回空,防止缓存击穿
if (!existDataSource(tenantId)) {
return null;
}
// 获取租户数据源信息
TenantDataSource tenantDataSource = CacheUtil.get(TENANT_DATASOURCE_CACHE, TENANT_DATASOURCE_KEY, tenantId, TenantDataSource.class, Boolean.FALSE);
if (tenantDataSource == null) {
tenantDataSource = jdbcTemplate.queryForObject(TENANT_DATASOURCE_SINGLE_STATEMENT, new BeanPropertyRowMapper<>(TenantDataSource.class), tenantId);
if (tenantDataSource != null && StringUtil.isNoneBlank(tenantDataSource.getTenantId(), tenantDataSource.getDriverClass(), tenantDataSource.getUrl(), tenantDataSource.getUsername(), tenantDataSource.getPassword())) {
CacheUtil.put(TENANT_DATASOURCE_CACHE, TENANT_DATASOURCE_KEY, tenantId, tenantDataSource, Boolean.FALSE);
} else {
tenantDataSource = null;
}
}
return tenantDataSource;
}
}
@@ -0,0 +1,93 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.dynamic;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.db.dynamic.creator.DynamicDataSourceProperty;
import org.springblade.core.db.dynamic.processor.DynamicDataSourceProcessor;
import org.springblade.core.tenant.utils.ShardingUtil;
import org.springblade.core.tool.utils.StringUtil;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
import static org.springblade.core.tenant.constant.TenantDynamicConstant.*;
/**
* 租户动态数据源扩展
* <p>
* 实现数据源扩展来支持租户级别的动态数据源
*
* @author BladeX
*/
@Slf4j
public class TenantDataSourceProcessor implements DynamicDataSourceProcessor {
@Override
public Map<String, DataSourceProperty> loadDataSourceProperties(Statement statement, DynamicDataSourceProperties dynamicDataSourceProperties) throws SQLException {
Map<String, DataSourceProperty> map = new HashMap<>(16);
// 构建动态数据源
ResultSet rs = statement.executeQuery(TENANT_DATASOURCE_GROUP_STATEMENT);
while (rs.next()) {
int category = rs.getInt("category");
String tenantId = rs.getString("tenantId");
String driver = rs.getString("driverClass");
String url = rs.getString("url");
String username = rs.getString("username");
String password = rs.getString("password");
String shardingConfig = rs.getString("shardingConfig");
// JDBC直连配置
if (category == JDBC_CATEGORY && StringUtil.isNoneBlank(tenantId, driver, url, username, password)) {
DataSourceProperty jdbcProperty = new DataSourceProperty();
jdbcProperty.setDriverClassName(driver);
jdbcProperty.setUrl(url);
jdbcProperty.setUsername(username);
jdbcProperty.setPassword(password);
jdbcProperty.setDruid(dynamicDataSourceProperties.getDruid());
map.put(tenantId, jdbcProperty);
log.info("Loaded JDBC datasource config for tenant {}", tenantId);
}
// Sharding分库分表配置
else if (category == SHARDING_CATEGORY && StringUtil.isNotBlank(shardingConfig)) {
DataSource dataSource = ShardingUtil.createDataSource(shardingConfig);
DynamicDataSourceProperty shardingProperty = new DynamicDataSourceProperty();
shardingProperty.setTenantId(tenantId);
shardingProperty.setDataSource(dataSource);
map.put(tenantId, shardingProperty);
log.info("Loaded Sharding datasource config for tenant {}", tenantId);
}
}
log.info("Tenant datasource processor loaded {} datasources", map.size());
return map;
}
}
@@ -0,0 +1,60 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.env;
import org.springblade.core.auto.annotation.AutoEnvPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* 初始化分库分表配置
*
* @author Chill
*/
@AutoEnvPostProcessor
public class TenantEnvPostProcessor implements EnvironmentPostProcessor, Ordered {
private static final String DYNAMIC_DATASOURCE_KEY = "spring.datasource.dynamic.enabled";
private static final String DYNAMIC_FILTER_KEY = "spring.datasource.dynamic.druid.proxy-filters[0]";
private static final String AUTOCONFIGURE_EXCLUDE_KEY = "spring.autoconfigure.exclude";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
environment.getSystemProperties().put(DYNAMIC_DATASOURCE_KEY, "false");
environment.getSystemProperties().put(DYNAMIC_FILTER_KEY, "sqlLogInterceptor");
environment.getSystemProperties().put(AUTOCONFIGURE_EXCLUDE_KEY, "com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure");
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
@@ -0,0 +1,47 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.tenant.utils;
import lombok.SneakyThrows;
import org.apache.shardingsphere.driver.api.yaml.YamlShardingSphereDataSourceFactory;
import javax.sql.DataSource;
import java.nio.charset.StandardCharsets;
/**
* ShardingUtil
*
* @author Chill
*/
public class ShardingUtil {
@SneakyThrows
public static DataSource createDataSource(String yamlConfig) {
return YamlShardingSphereDataSourceFactory.createDataSource(yamlConfig.getBytes(StandardCharsets.UTF_8));
}
}