add
This commit is contained in:
@ -1,19 +0,0 @@
|
||||
package com.qiaoba.common.base;
|
||||
|
||||
/**
|
||||
* 数据源接口
|
||||
*
|
||||
* @author ailanyin
|
||||
* @version 1.0
|
||||
* @since 2023/6/8 17:23
|
||||
*/
|
||||
public interface DatasourceService {
|
||||
|
||||
/**
|
||||
* 检查租户是否允许访问
|
||||
*
|
||||
* @param tenantId tenantId
|
||||
* @return true = 允许
|
||||
*/
|
||||
boolean checkTenantInfo(String tenantId);
|
||||
}
|
@ -29,6 +29,11 @@ public class BaseConstant {
|
||||
*/
|
||||
public static final String COLON_JOIN_STR = ":";
|
||||
|
||||
/**
|
||||
* 中划线拼接符号: '-'
|
||||
*/
|
||||
public static final String HYPHEN_JOIN_STR = "-";
|
||||
|
||||
/**
|
||||
* 树的key的命名
|
||||
*/
|
||||
|
@ -14,6 +14,11 @@ public class BaseContext {
|
||||
*/
|
||||
private static final ThreadLocal<String> DATABASE_TYPE_HOLDER = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* SCHEMA
|
||||
*/
|
||||
private static final ThreadLocal<Boolean> SCHEMA_HOLDER = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
@ -66,6 +71,24 @@ public class BaseContext {
|
||||
DATASOURCE_HOLDER.set(dataSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是 Schema模式
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
public static Boolean getSchema() {
|
||||
return SCHEMA_HOLDER.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否是 Schema模式
|
||||
*
|
||||
* @param isSchema 是/否
|
||||
*/
|
||||
public static void setSchema(boolean isSchema) {
|
||||
SCHEMA_HOLDER.set(isSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有的ThreadLocal
|
||||
*/
|
||||
@ -76,7 +99,8 @@ public class BaseContext {
|
||||
TENANT_ID_HOLDER.remove();
|
||||
// 清除上下文中数据库类型
|
||||
DATABASE_TYPE_HOLDER.remove();
|
||||
|
||||
// 清除上下文中 SCHEMA
|
||||
SCHEMA_HOLDER.remove();
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,12 +1,18 @@
|
||||
package com.qiaoba.common.database.config;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.alibaba.druid.pool.DruidDataSource;
|
||||
import com.qiaoba.common.base.constants.TenantConstant;
|
||||
import com.qiaoba.common.base.context.BaseContext;
|
||||
import com.qiaoba.common.base.enums.BaseEnum;
|
||||
import com.qiaoba.common.database.constants.DynamicDatasourceConstant;
|
||||
import com.qiaoba.common.database.entity.DynamicDataSource;
|
||||
import com.qiaoba.common.database.properties.DefaultDataSourceProperties;
|
||||
import com.qiaoba.common.database.properties.MasterInfo;
|
||||
import com.qiaoba.common.database.properties.PoolInfo;
|
||||
import com.qiaoba.common.database.properties.SlaveInfo;
|
||||
import com.qiaoba.common.database.service.DynamicDatasourceService;
|
||||
import com.qiaoba.common.database.utils.JdbcUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@ -15,10 +21,7 @@ import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import javax.annotation.Resource;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
@ -32,40 +35,88 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@Configuration
|
||||
public class DynamicDataSourceConfig {
|
||||
|
||||
/**
|
||||
* Bean 必须叫 dataSource
|
||||
*/
|
||||
@Resource
|
||||
private DynamicDataSourceContext dataSource;
|
||||
@Resource
|
||||
private DefaultDataSourceProperties defaultDataSourceProperties;
|
||||
@Resource
|
||||
private DynamicDatasourceService dynamicDatasourceService;
|
||||
|
||||
public static Map<Object, Object> DATA_SOURCE_MAP = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* 使用中的数据源
|
||||
*/
|
||||
public static Map<Object, Object> MASTER_SOURCE_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 备用数据源
|
||||
*/
|
||||
public static Map<String, List<SlaveInfo>> SLAVE_SOURCE_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 把DynamicDataSourceContext 纳入容器管理,其他地方使用DynamicDataSourceConfig 类可以直接从容器取对象,并调用freshDataSource方法
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 加载系统默认数据源
|
||||
initDefault();
|
||||
// 加载租户数据源
|
||||
initTenant();
|
||||
}
|
||||
|
||||
private void initDefault() {
|
||||
MasterInfo master = defaultDataSourceProperties.getMaster();
|
||||
List<SlaveInfo> slaves = defaultDataSourceProperties.getSlaves();
|
||||
addDataSourceToMap(DynamicDatasourceConstant.DEFAULT_MASTER_DATASOURCE_KEY, buildDataSource(master.getDriver(), master.getUrl(), master.getUsername(), master.getPassword(), master.getPool()));
|
||||
// 将主数据源使用
|
||||
addMasterMap(DynamicDatasourceConstant.MASTER_PREFIX + TenantConstant.DEFAULT_TENANT_ID, buildDataSource(master.getDriver(), master.getUrl(), master.getUsername(), master.getPassword(), master.getPool()));
|
||||
dataSource.freshDataSource(MASTER_SOURCE_MAP);
|
||||
// 将从数据源备用
|
||||
if (CollUtil.isNotEmpty(slaves)) {
|
||||
for (int i = 0; i < slaves.size(); i++) {
|
||||
SlaveInfo slave = slaves.get(i);
|
||||
if (slave.getIsUse()) {
|
||||
addDataSourceToMap(DynamicDatasourceConstant.DEFAULT_SLAVE_DATASOURCE_PREFIX + i, buildDataSource(slave.getDriver(), slave.getUrl(), slave.getUsername(), slave.getPassword(), slave.getPool()));
|
||||
if (!slaves.get(i).getIsUse()) {
|
||||
slaves.remove(i);
|
||||
}
|
||||
}
|
||||
if (CollUtil.isNotEmpty(slaves)) {
|
||||
SLAVE_SOURCE_MAP.put(DynamicDatasourceConstant.SLAVE_PREFIX + TenantConstant.DEFAULT_TENANT_ID, slaves);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initTenant() {
|
||||
setDefaultSetting();
|
||||
List<DynamicDataSource> dynamicDataSourceList = dynamicDatasourceService.loadAllTenantDatasource();
|
||||
for (DynamicDataSource dynamicDataSource : dynamicDataSourceList) {
|
||||
// 将主数据源加载
|
||||
if (BaseEnum.YES.getCode().equals(dynamicDataSource.getIsMaster())) {
|
||||
addMasterMap(DynamicDatasourceConstant.MASTER_PREFIX + dynamicDataSource.getTenantId(), buildDataSource(dynamicDataSource.getDriver(), dynamicDataSource.getUrl(), dynamicDataSource.getUsername(), dynamicDataSource.getPassword(), new PoolInfo(dynamicDataSource.getInitCount(), dynamicDataSource.getMinCount(), dynamicDataSource.getMaxCount())));
|
||||
dataSource.freshDataSource(MASTER_SOURCE_MAP);
|
||||
}
|
||||
// 将从数据源备用
|
||||
else {
|
||||
if (Objects.isNull(SLAVE_SOURCE_MAP.get(DynamicDatasourceConstant.MASTER_PREFIX + dynamicDataSource.getTenantId()))) {
|
||||
List<SlaveInfo> slaves = new ArrayList<>();
|
||||
slaves.add(datasourceToSlave(dynamicDataSource));
|
||||
SLAVE_SOURCE_MAP.put(DynamicDatasourceConstant.MASTER_PREFIX + dynamicDataSource.getTenantId(), slaves);
|
||||
} else {
|
||||
List<SlaveInfo> slaves = SLAVE_SOURCE_MAP.get(DynamicDatasourceConstant.MASTER_PREFIX + dynamicDataSource.getTenantId());
|
||||
slaves.add(datasourceToSlave(dynamicDataSource));
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSource.freshDataSource(DATA_SOURCE_MAP);
|
||||
BaseContext.clearAllHolder();
|
||||
}
|
||||
|
||||
public void addTenantDataSource(String tenantId, String driver, String url, String username, String password, PoolInfo poolInfo) {
|
||||
addDataSourceToMap(DynamicDatasourceConstant.MASTER_PREFIX + tenantId, buildDataSource(driver, url, username, password, poolInfo));
|
||||
dataSource.freshDataSource(DATA_SOURCE_MAP);
|
||||
addMasterMap(DynamicDatasourceConstant.MASTER_PREFIX + tenantId, buildDataSource(driver, url, username, password, poolInfo));
|
||||
dataSource.freshDataSource(MASTER_SOURCE_MAP);
|
||||
}
|
||||
|
||||
public void deleteTenantDataSource(String tenantId) {
|
||||
DATA_SOURCE_MAP.remove(DynamicDatasourceConstant.MASTER_PREFIX + tenantId);
|
||||
dataSource.freshDataSource(DATA_SOURCE_MAP);
|
||||
MASTER_SOURCE_MAP.remove(DynamicDatasourceConstant.MASTER_PREFIX + tenantId);
|
||||
dataSource.freshDataSource(MASTER_SOURCE_MAP);
|
||||
}
|
||||
|
||||
private static Object buildDataSource(String driver, String url, String username, String password, PoolInfo poolInfo) {
|
||||
@ -96,16 +147,27 @@ public class DynamicDataSourceConfig {
|
||||
*/
|
||||
@PreDestroy
|
||||
public void close() {
|
||||
Set<Map.Entry<Object, Object>> entries = DATA_SOURCE_MAP.entrySet();
|
||||
Set<Map.Entry<Object, Object>> entries = MASTER_SOURCE_MAP.entrySet();
|
||||
for (Map.Entry<Object, Object> entry : entries) {
|
||||
DruidDataSource dataSource = (DruidDataSource) entry.getValue();
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void addDataSourceToMap(String name, Object dataSource) {
|
||||
private void addMasterMap(String name, Object dataSource) {
|
||||
if (Objects.nonNull(dataSource)) {
|
||||
DATA_SOURCE_MAP.put(name, dataSource);
|
||||
MASTER_SOURCE_MAP.put(name, dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
private SlaveInfo datasourceToSlave(DynamicDataSource dynamicDataSource) {
|
||||
SlaveInfo slaveInfo = BeanUtil.copyProperties(dynamicDataSource, SlaveInfo.class);
|
||||
slaveInfo.setPool(new PoolInfo(dynamicDataSource.getInitCount(), dynamicDataSource.getMinCount(), dynamicDataSource.getMaxCount()));
|
||||
return slaveInfo;
|
||||
}
|
||||
|
||||
public void setDefaultSetting() {
|
||||
BaseContext.setDataSource(DynamicDatasourceConstant.MASTER_PREFIX + TenantConstant.DEFAULT_TENANT_ID);
|
||||
BaseContext.setTenantId(TenantConstant.DEFAULT_TENANT_ID);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.qiaoba.common.database.config;
|
||||
|
||||
import com.qiaoba.common.base.constants.TenantConstant;
|
||||
import com.qiaoba.common.base.context.BaseContext;
|
||||
import com.qiaoba.common.database.constants.DynamicDatasourceConstant;
|
||||
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
|
||||
@ -20,8 +21,8 @@ public class DynamicDataSourceContext extends AbstractRoutingDataSource {
|
||||
* 设置默认数据源、全部数据源,及刷新
|
||||
*/
|
||||
public void freshDataSource(Map<Object, Object> targetDataSources) {
|
||||
//默认数据源
|
||||
super.setDefaultTargetDataSource(targetDataSources.get(DynamicDatasourceConstant.DEFAULT_MASTER_DATASOURCE_KEY));
|
||||
//默认数据源 (determineCurrentLookupKey 如果找不到就使用默认数据源)
|
||||
super.setDefaultTargetDataSource(targetDataSources.get(DynamicDatasourceConstant.MASTER_PREFIX + TenantConstant.DEFAULT_TENANT_ID));
|
||||
//设置全部数据源
|
||||
super.setTargetDataSources(targetDataSources);
|
||||
//刷新(即把targetDataSources刷到resolvedDataSources中去,resolvedDataSources才是我们真正存放数据源的map)
|
||||
|
@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import com.qiaoba.common.base.context.BaseContext;
|
||||
import com.qiaoba.common.database.interceptors.SchemaInterceptor;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.StringValue;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@ -31,6 +32,7 @@ public class MybatisPlusConfig {
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new SchemaInterceptor());
|
||||
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
|
||||
@Override
|
||||
public Expression getTenantId() {
|
||||
|
@ -9,16 +9,6 @@ package com.qiaoba.common.database.constants;
|
||||
*/
|
||||
public class DynamicDatasourceConstant {
|
||||
|
||||
/**
|
||||
* 系统默认的主数据源名称
|
||||
*/
|
||||
public static final String DEFAULT_MASTER_DATASOURCE_KEY = "master_default";
|
||||
|
||||
/**
|
||||
* 系统默认的从数据源名称
|
||||
*/
|
||||
public static final String DEFAULT_SLAVE_DATASOURCE_PREFIX = "slave_default_";
|
||||
|
||||
/**
|
||||
* 主数据源拼接前缀 master_tenantId
|
||||
*/
|
||||
|
@ -23,6 +23,11 @@ public class DynamicDataSource {
|
||||
*/
|
||||
private String tenantId;
|
||||
|
||||
/**
|
||||
* 主库
|
||||
*/
|
||||
private String isMaster;
|
||||
|
||||
/**
|
||||
* 数据库-url
|
||||
*/
|
||||
@ -43,4 +48,19 @@ public class DynamicDataSource {
|
||||
*/
|
||||
private String driver;
|
||||
|
||||
/**
|
||||
* 连接池-初始化大小
|
||||
*/
|
||||
private Integer initCount;
|
||||
|
||||
/**
|
||||
* 连接池-最小空闲数
|
||||
*/
|
||||
private Integer minCount;
|
||||
|
||||
/**
|
||||
* 连接池-最大连接数
|
||||
*/
|
||||
private Integer maxCount;
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package com.qiaoba.common.database.factories;
|
||||
|
||||
import com.qiaoba.common.database.config.DynamicDataSourceConfig;
|
||||
import com.qiaoba.common.database.config.DynamicDataSourceContext;
|
||||
import com.qiaoba.common.database.filters.DynamicDataSourceFilter;
|
||||
import com.qiaoba.common.database.properties.DefaultDataSourceProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@ -27,11 +26,6 @@ public class DynamicDataSourceFactory {
|
||||
return new DynamicDataSourceConfig();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DynamicDataSourceFilter handleTenantFilter() {
|
||||
return new DynamicDataSourceFilter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultDataSourceProperties defaultDataSourceProperties() {
|
||||
return new DefaultDataSourceProperties();
|
||||
|
@ -1,80 +0,0 @@
|
||||
package com.qiaoba.common.database.filters;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.qiaoba.common.base.DatasourceService;
|
||||
import com.qiaoba.common.base.constants.TenantConstant;
|
||||
import com.qiaoba.common.base.context.BaseContext;
|
||||
import com.qiaoba.common.database.constants.DynamicDatasourceConstant;
|
||||
import com.qiaoba.common.web.utils.ResponseUtil;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 动态切换不同租户的数据源
|
||||
*
|
||||
* @author ailanyin
|
||||
* @version 1.0
|
||||
* @since 2023-04-25 22:48:43
|
||||
*/
|
||||
@Component
|
||||
@Order(-10000)
|
||||
public class DynamicDataSourceFilter extends OncePerRequestFilter {
|
||||
|
||||
@Resource
|
||||
private DatasourceService datasourceService;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
String tenantId = request.getHeader(TenantConstant.HEADER_KEY_TENANT);
|
||||
// 主库或没有tenantId
|
||||
if (StrUtil.isBlank(tenantId)
|
||||
|| TenantConstant.DEFAULT_TENANT_ID.equals(tenantId)
|
||||
|| request.getRequestURI().startsWith("/tenant")) {
|
||||
setDefaultSetting();
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkTenantInfo(tenantId)) {
|
||||
// 检查租户的信息,是否存在,是否过期,是否禁用
|
||||
ResponseUtil.response(response, "租户可能到期或被禁用");
|
||||
return;
|
||||
}
|
||||
before(tenantId);
|
||||
filterChain.doFilter(request, response);
|
||||
after();
|
||||
}
|
||||
|
||||
private void before(String tenantId) {
|
||||
//设置当前租户对应的数据源
|
||||
BaseContext.setDataSource(DynamicDatasourceConstant.MASTER_PREFIX + tenantId);
|
||||
//设置当前租户对应的租户ID
|
||||
BaseContext.setTenantId(tenantId);
|
||||
//设置当前租户对应的数据库类型
|
||||
BaseContext.setDatabaseType("MySQL");
|
||||
}
|
||||
|
||||
private void after() {
|
||||
BaseContext.clearAllHolder();
|
||||
}
|
||||
|
||||
private boolean checkTenantInfo(String tenantId) {
|
||||
return datasourceService.checkTenantInfo(tenantId);
|
||||
}
|
||||
|
||||
private void setDefaultSetting() {
|
||||
BaseContext.setDataSource(DynamicDatasourceConstant.DEFAULT_MASTER_DATASOURCE_KEY);
|
||||
//设置当前租户对应的租户ID
|
||||
BaseContext.setTenantId(TenantConstant.DEFAULT_TENANT_ID);
|
||||
//设置当前租户对应的数据库类型
|
||||
BaseContext.setDatabaseType("MySQL");
|
||||
}
|
||||
}
|
@ -1,24 +1,39 @@
|
||||
package com.qiaoba.common.database.interceptors;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.qiaoba.common.base.context.BaseContext;
|
||||
import org.apache.ibatis.executor.statement.StatementHandler;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
/**
|
||||
* SchemaInterceptor
|
||||
*
|
||||
* @author ailanyin
|
||||
* @version 1.0
|
||||
* @since 2023-04-25 22:48:43
|
||||
*/
|
||||
public class SchemaInterceptor implements InnerInterceptor {
|
||||
|
||||
@Value("${spring.application.name}")
|
||||
private String baseDatabase;
|
||||
|
||||
@Override
|
||||
public void beforePrepare(StatementHandler sh, Connection conn, Integer transactionTimeout) {
|
||||
String sql = "use `qiaoba-boot`;";
|
||||
try {
|
||||
conn.createStatement().execute(sql);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (Objects.nonNull(BaseContext.getSchema()) && BaseContext.getSchema()) {
|
||||
// use qiaoba-1;
|
||||
String sql = StrUtil.format("use `{}-{}`;", baseDatabase, BaseContext.getTenantId());
|
||||
try {
|
||||
conn.createStatement().execute(sql);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
InnerInterceptor.super.beforePrepare(sh, conn, transactionTimeout);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,22 @@
|
||||
package com.qiaoba.common.database.service;
|
||||
|
||||
import com.qiaoba.common.database.entity.DynamicDataSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 动态数据源接口
|
||||
*
|
||||
* @author ailanyin
|
||||
* @version 1.0
|
||||
* @since 2023/6/9 13:15
|
||||
*/
|
||||
public interface DynamicDatasourceService {
|
||||
|
||||
/**
|
||||
* 加载所有租户的数据源
|
||||
*
|
||||
* @return 数据源集合
|
||||
*/
|
||||
List<DynamicDataSource> loadAllTenantDatasource();
|
||||
}
|
Reference in New Issue
Block a user