This commit is contained in:
2025-11-28 16:23:32 +08:00
commit a9e0e16c29
826 changed files with 89805 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
<?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>
<dependencies>
<!--Blade-->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-tenant</artifactId>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<scope>provided</scope>
</dependency>
<!--Dynamic-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</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>

View File

@@ -0,0 +1,213 @@
/**
* 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.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationAdvisor;
import com.baomidou.dynamic.datasource.creator.DataSourceCreator;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.creator.druid.DruidDataSourceCreator;
import com.baomidou.dynamic.datasource.event.DataSourceInitEvent;
import com.baomidou.dynamic.datasource.event.EncDataSourceInitEvent;
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.provider.DynamicDataSourceProvider;
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.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.Primary;
import org.springframework.context.annotation.Role;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.util.List;
import static org.springblade.core.tenant.constant.TenantBaseConstant.TENANT_DYNAMIC_DATASOURCE_PROP;
import static org.springblade.core.tenant.constant.TenantBaseConstant.TENANT_DYNAMIC_GLOBAL_PROP;
/**
* 多租户数据源配置类
*
* @author Chill
*/
@RequiredArgsConstructor
@EnableConfigurationProperties({DataSourceProperties.class, DynamicDataSourceProperties.class})
@AutoConfiguration(before = {DruidDataSourceAutoConfigure.class, DynamicDataSourceAutoConfiguration.class})
@Import(value = {DynamicDataSourceCreatorAutoConfiguration.class})
@ConditionalOnProperty(value = TENANT_DYNAMIC_DATASOURCE_PROP, havingValue = "true")
public class TenantDataSourceConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSourceInitEvent dataSourceInitEvent() {
return new EncDataSourceInitEvent();
}
@Bean
@ConditionalOnMissingBean
public DefaultDataSourceCreator dataSourceCreator(List<DataSourceCreator> dataSourceCreators, DataSourceInitEvent dataSourceInitEvent, DynamicDataSourceProperties properties) {
DefaultDataSourceCreator creator = new DefaultDataSourceCreator();
creator.setCreators(dataSourceCreators);
creator.setDataSourceInitEvent(dataSourceInitEvent);
creator.setPublicKey(properties.getPublicKey());
creator.setLazy(properties.getLazy());
creator.setP6spy(properties.getP6spy());
creator.setSeata(properties.getSeata());
creator.setSeataMode(properties.getSeataMode());
return creator;
}
@Bean
@Primary
public DynamicDataSourceProvider dynamicDataSourceProvider(DefaultDataSourceCreator dataSourceCreator, DataSourceProperties dataSourceProperties, DynamicDataSourceProperties dynamicDataSourceProperties) {
String driverClassName = dataSourceProperties.getDriverClassName();
String url = dataSourceProperties.getUrl();
String username = dataSourceProperties.getUsername();
String password = dataSourceProperties.getPassword();
DataSourceProperty master = dynamicDataSourceProperties.getDatasource().get(dynamicDataSourceProperties.getPrimary());
if (master != null) {
driverClassName = master.getDriverClassName();
url = master.getUrl();
username = master.getUsername();
password = master.getPassword();
}
return new TenantDataSourceJdbcProvider(dataSourceCreator, dynamicDataSourceProperties, driverClassName, url, username, password);
}
@Bean
@Primary
public DataSource dataSource(List<DynamicDataSourceProvider> providers, DynamicDataSourceProperties dynamicDataSourceProperties) {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(providers);
dataSource.setPrimary(dynamicDataSourceProperties.getPrimary());
dataSource.setStrict(dynamicDataSourceProperties.getStrict());
dataSource.setStrategy(dynamicDataSourceProperties.getStrategy());
dataSource.setP6spy(dynamicDataSourceProperties.getP6spy());
dataSource.setSeata(dynamicDataSourceProperties.getSeata());
return dataSource;
}
@Bean
@ConditionalOnMissingBean
public TenantDataSourceAnnotationInterceptor tenantDataSourceAnnotationInterceptor(DsProcessor dsProcessor, DynamicDataSourceProperties dynamicDataSourceProperties) {
return new TenantDataSourceAnnotationInterceptor(dynamicDataSourceProperties.getAop().getAllowedPublicOnly(), dsProcessor);
}
@Bean
@ConditionalOnMissingBean
@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor(TenantDataSourceAnnotationInterceptor tenantDataSourceAnnotationInterceptor, DynamicDataSourceProperties dynamicDataSourceProperties) {
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(tenantDataSourceAnnotationInterceptor, DS.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;
}
@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;
}
@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);
}
}
}

View File

@@ -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.secure.utils.AuthUtil;
/**
* 租户动态数据源解析器
*
* @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 AuthUtil.getTenantId();
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,59 @@
/**
* 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.aop.DynamicDataSourceAnnotationInterceptor;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import lombok.Setter;
import org.aopalliance.intercept.MethodInvocation;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tenant.exception.TenantDataSourceException;
/**
* 租户数据源切换拦截器
*
* @author Chill
*/
public class TenantDataSourceAnnotationInterceptor extends DynamicDataSourceAnnotationInterceptor {
@Setter
private TenantDataSourceHolder holder;
public TenantDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
super(allowedPublicOnly, dsProcessor);
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
holder.handleDataSource(AuthUtil.getTenantId());
return super.invoke(invocation);
} catch (Exception exception) {
throw new TenantDataSourceException(exception.getMessage());
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,63 @@
/**
* 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 org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tenant.exception.TenantDataSourceException;
import org.springblade.core.tool.utils.StringUtil;
/**
* 租户数据源全局拦截器
*
* @author Chill
*/
public class TenantDataSourceGlobalInterceptor implements MethodInterceptor {
@Setter
private TenantDataSourceHolder holder;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String tenantId = AuthUtil.getTenantId();
try {
if (StringUtil.isNotBlank(tenantId)) {
holder.handleDataSource(tenantId);
DynamicDataSourceContextHolder.push(tenantId);
}
return invocation.proceed();
} catch (Exception exception) {
throw new TenantDataSourceException(exception.getMessage());
} finally {
if (StringUtil.isNotBlank(tenantId)) {
DynamicDataSourceContextHolder.poll();
}
}
}
}

View File

@@ -0,0 +1,146 @@
/**
* 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.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.TenantBaseConstant.*;
/**
* 租户数据源核心处理类
*
* @author Chill
*/
@AllArgsConstructor
public class TenantDataSourceHolder {
private final DataSource dataSource;
private final DataSourceCreator dataSourceCreator;
private final JdbcTemplate jdbcTemplate;
/**
* 数据源缓存处理
*
* @param tenantId 租户ID
*/
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 String[]{tenantId}, new BeanPropertyRowMapper<>(TenantDataSource.class));
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 String[]{tenantId}, new BeanPropertyRowMapper<>(TenantDataSource.class));
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;
}
}

View File

@@ -0,0 +1,184 @@
/**
* 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.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.provider.AbstractJdbcDataSourceProvider;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import com.baomidou.dynamic.datasource.toolkit.DsStrUtils;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tenant.utils.ShardingUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.StringUtil;
import javax.sql.DataSource;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static org.springblade.core.tenant.constant.TenantBaseConstant.*;
/**
* 租户数据源初始加载
*
* @author Chill
*/
@Slf4j
public class TenantDataSourceJdbcProvider extends AbstractJdbcDataSourceProvider {
private final String driverClassName;
private final String url;
private final String username;
private final String password;
private final DefaultDataSourceCreator dataSourceCreator;
private final DynamicDataSourceProperties dynamicDataSourceProperties;
public TenantDataSourceJdbcProvider(DefaultDataSourceCreator dataSourceCreator, DynamicDataSourceProperties dynamicDataSourceProperties, String driverClassName, String url, String username, String password) {
super(dataSourceCreator, driverClassName, url, username, password);
this.dataSourceCreator = dataSourceCreator;
this.dynamicDataSourceProperties = dynamicDataSourceProperties;
this.driverClassName = driverClassName;
this.url = url;
this.username = username;
this.password = password;
}
@Override
protected Map<String, DataSourceProperty> executeStmt(Statement statement) throws SQLException {
// 构建数据源集合
Map<String, DataSourceProperty> map = new HashMap<>(16);
// 构建主数据源
DataSourceProperty masterProperty = new DataSourceProperty();
masterProperty.setDriverClassName(driverClassName);
masterProperty.setUrl(url);
masterProperty.setUsername(username);
masterProperty.setPassword(password);
masterProperty.setDruid(dynamicDataSourceProperties.getDruid());
map.put(dynamicDataSourceProperties.getPrimary(), masterProperty);
// 构建yml数据源
Map<String, DataSourceProperty> datasource = dynamicDataSourceProperties.getDatasource();
if (!datasource.isEmpty()) {
map.putAll(datasource);
}
// 构建动态数据源
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);
}
// Sharding分库分表配置
else if (category == SHARDING_CATEGORY && StringUtil.isNotBlank(shardingConfig)) {
DataSource dataSource = ShardingUtil.createDataSource(shardingConfig);
TenantDataSourceProperty shardingProperty = new TenantDataSourceProperty();
shardingProperty.setTenantId(tenantId);
shardingProperty.setDataSource(dataSource);
map.put(tenantId, shardingProperty);
}
}
return map;
}
@Override
public Map<String, DataSource> loadDataSources() {
Connection conn = null;
Statement stmt = null;
try {
// 由于 SPI 的支持,现在已无需显示加载驱动了
// 但在用户显示配置的情况下,进行主动加载
if (!DsStrUtils.isEmpty(driverClassName)) {
Class.forName(driverClassName);
log.info("成功加载数据库驱动程序");
}
conn = DriverManager.getConnection(url, username, password);
log.info("成功获取数据库连接");
stmt = conn.createStatement();
Map<String, DataSourceProperty> dataSourcePropertiesMap = executeStmt(stmt);
return createDataSourceMap(dataSourcePropertiesMap);
} catch (Exception e) {
log.error("获取数据库连接失败", e);
} finally {
closeResource(conn);
closeResource(stmt);
}
return null;
}
/**
* 关闭资源
*
* @param con 资源
*/
private static void closeResource(AutoCloseable con) {
if (con != null) {
try {
con.close();
} catch (SQLException ex) {
log.debug("关闭连接失败", ex);
} catch (Throwable ex) {
log.debug("关闭连接时遇到异常", ex);
}
}
}
@Override
protected Map<String, DataSource> createDataSourceMap(Map<String, DataSourceProperty> dataSourcePropertiesMap) {
Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
String dsName = item.getKey();
DataSourceProperty dataSourceProperty = item.getValue();
String poolName = dataSourceProperty.getPoolName();
if (StringUtil.isBlank(poolName)) {
poolName = dsName;
}
TenantDataSourceProperty tenantDataSourceProperty = BeanUtil.copyProperties(dataSourceProperty, TenantDataSourceProperty.class);
DataSource dataSource = Objects.requireNonNull(tenantDataSourceProperty).getDataSource();
if (dataSource == null) {
dataSourceProperty.setPoolName(poolName);
dataSourceMap.put(dsName, dataSourceCreator.createDataSource(dataSourceProperty));
} else {
dataSourceMap.put(dsName, dataSource);
}
}
return dataSourceMap;
}
}

View File

@@ -0,0 +1,55 @@
/**
* 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 lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.sql.DataSource;
/**
* 租户数据源配置类
*
* @author Chill
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class TenantDataSourceProperty extends DataSourceProperty {
/**
* 租户id
*/
private String tenantId;
/**
* 自定义数据源
*/
private DataSource dataSource;
}

View File

@@ -0,0 +1,57 @@
/**
* 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.processor;
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 AUTOCONFIGURE_EXCLUDE_KEY = "spring.autoconfigure.exclude";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
environment.getSystemProperties().put(DYNAMIC_DATASOURCE_KEY, "false");
environment.getSystemProperties().put(AUTOCONFIGURE_EXCLUDE_KEY, "com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure");
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}

View File

@@ -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));
}
}