# scaffold 项目之多租户字段隔离
# 多租户是什么?
多租户,简单来说是指一个业务,区分 多个组织单位
,每个组织单位之间的数据是 相互隔离
的。
例如说,有一个系统,可以支持不同公司使用,这里的 一个公司就是一个租户
。
每个用户必然是属于某一个租户的。因此,用户也只能看到自己租户下的内容,其他的租户内容是看不到的。
# 数据的隔离方案
多租户隔的数据隔离方案,有以下几种方式:
- DATASOURCE 模式: 独立数据库模式
- SCHEMA 模式:共享数据库,独立 SCHEMA 模式
- COLUMN 模式:共享数据库,共享 SCHEMA,共享数据表模式
# DATASOURCE 模式
每个租户拥有一个独立的数据库实例,隔离级别更高,安全性最好,成本也最大。
优点:数据隔离级别最高,安全性最好,可以为不同租户提供个性化服务。
缺点:成本较高,因为需要为每个租户独立安装和维护数据库实例。
适用场景:适用于对数据隔离和安全性要求极高的场景,如金融和医疗行业。
# SCHEMA 模式
所有租户共享同一个数据库实例,但每个租户有一个独立的 Schema,即一个租户一张表。
优点:提供了一定程度的逻辑数据隔离,资源利用率较高。
缺点:数据库管理复杂,数据恢复相对困难。
适用场景:适用于需要一定程度数据隔离,但希望减少硬件成本的场景。
# COLUMN 模式
所有租户共享同一个数据库实例、Schema 和数据表,通过一个租户 ID 字段来区分不同租户的数据。
优点:维护和购置成本最低,支持的租户数量最多。
缺点:隔离级别最低,安全性最低,数据备份和恢复复杂。
适用场景:适用于对成本非常敏感,且租户数量较多的场景。
# 方案的选用
一般情况下,可以考虑选用
COLUMN
模式,成本的低,易开发,以最少的服务器为最多的租户提供服务如果租户规模比较大,对安全问题敏感,可以考虑
DATASOURCE
模式,同时也就意味着成本的增大不推荐
SCHEMA
模式,缺点很明显,优点不明显,对复杂的 sql 查询很难支持
# 实现
通过封装成组件作为专门的处理多租户问题,实现透明化多租户功能,针对 WEB, Security, DB, Redis, AOP, Job, MQ, Async
等多个层面的封装。
# 租户上下文
用于获取当前用户所属租户
package com.tz.scaffold.framework.tenant.core.context; | |
import com.tz.scaffold.framework.common.enums.DocumentEnum; | |
import com.alibaba.ttl.TransmittableThreadLocal; | |
/** | |
* <p> Project: scaffold - TenantContextHolder </p> | |
* | |
* 多租户上下文 Holder | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
public class TenantContextHolder { | |
/** | |
* 当前租户编号 | |
*/ | |
private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>(); | |
/** | |
* 是否忽略租户 | |
*/ | |
private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>(); | |
/** | |
* 获得租户编号。 | |
* | |
* @return 租户编号 | |
*/ | |
public static Long getTenantId() { | |
return TENANT_ID.get(); | |
} | |
/** | |
* 获得租户编号。如果不存在,则抛出 NullPointerException 异常 | |
* | |
* @return 租户编号 | |
*/ | |
public static Long getRequiredTenantId() { | |
Long tenantId = getTenantId(); | |
if (tenantId == null) { | |
throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:" | |
+ DocumentEnum.TENANT.getUrl()); | |
} | |
return tenantId; | |
} | |
public static void setTenantId(Long tenantId) { | |
TENANT_ID.set(tenantId); | |
} | |
public static void setIgnore(Boolean ignore) { | |
IGNORE.set(ignore); | |
} | |
/** | |
* 当前是否忽略租户 | |
* | |
* @return 是否忽略 | |
*/ | |
public static boolean isIgnore() { | |
return Boolean.TRUE.equals(IGNORE.get()); | |
} | |
public static void clear() { | |
TENANT_ID.remove(); | |
IGNORE.remove(); | |
} | |
} |
# WEB
主要是在处理请求的时候,设置当前用户的租户号到上下文中
默认情况下每个请求的
Head
都要带上租户号tenant_id
, 如果不带会报错, 如果一定需要不带的请求可以在配置排除多租户的情况
ignore-urls: - /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号
package com.tz.scaffold.framework.tenant.core.web; | |
import com.tz.scaffold.framework.tenant.core.context.TenantContextHolder; | |
import com.tz.scaffold.framework.web.core.util.WebFrameworkUtils; | |
import org.springframework.web.filter.OncePerRequestFilter; | |
import javax.servlet.FilterChain; | |
import javax.servlet.ServletException; | |
import javax.servlet.http.HttpServletRequest; | |
import javax.servlet.http.HttpServletResponse; | |
import java.io.IOException; | |
/** | |
* <p> Project: scaffold - TenantContextWebFilter </p> | |
* | |
* 多租户 Context Web 过滤器 | |
* <p> | |
* 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
public class TenantContextWebFilter extends OncePerRequestFilter { | |
@Override | |
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) | |
throws ServletException, IOException { | |
// 设置 | |
Long tenantId = WebFrameworkUtils.getTenantId(request); | |
if (tenantId != null) { | |
TenantContextHolder.setTenantId(tenantId); | |
} | |
try { | |
chain.doFilter(request, response); | |
} finally { | |
// 清理 | |
TenantContextHolder.clear(); | |
} | |
} | |
} |
# Security
主要是对多租户下权限的校验,防止越权,或者租户不合法
package com.tz.scaffold.framework.tenant.core.security; | |
/** | |
* <p> Project: scaffold - TenantSecurityWebFilter </p> | |
* | |
* 多租户 Security Web 过滤器 | |
* <p> | |
* <li> | |
* 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。 | |
* <li> | |
* 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。 | |
* <li> | |
* 3. 校验租户是合法,例如说被禁用、到期 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Slf4j | |
public class TenantSecurityWebFilter extends ApiRequestFilter { | |
private final TenantProperties tenantProperties; | |
private final AntPathMatcher pathMatcher; | |
private final GlobalExceptionHandler globalExceptionHandler; | |
private final TenantFrameworkService tenantFrameworkService; | |
public TenantSecurityWebFilter(TenantProperties tenantProperties, | |
WebProperties webProperties, | |
GlobalExceptionHandler globalExceptionHandler, | |
TenantFrameworkService tenantFrameworkService) { | |
super(webProperties); | |
this.tenantProperties = tenantProperties; | |
this.pathMatcher = new AntPathMatcher(); | |
this.globalExceptionHandler = globalExceptionHandler; | |
this.tenantFrameworkService = tenantFrameworkService; | |
} | |
@Override | |
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) | |
throws ServletException, IOException { | |
Long tenantId = TenantContextHolder.getTenantId(); | |
// 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。 | |
LoginUser user = SecurityFrameworkUtils.getLoginUser(); | |
if (user != null) { | |
// 如果获取不到租户编号,则尝试使用登陆用户的租户编号 | |
if (tenantId == null) { | |
tenantId = user.getTenantId(); | |
TenantContextHolder.setTenantId(tenantId); | |
// 如果传递了租户编号,则进行比对租户编号,避免越权问题 | |
} else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) { | |
log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]", | |
user.getTenantId(), user.getId(), user.getUserType(), | |
TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod()); | |
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(), | |
"您无权访问该租户的数据")); | |
return; | |
} | |
} | |
// 如果非允许忽略租户的 URL,则校验租户是否合法 | |
if (!isIgnoreUrl(request)) { | |
// 2. 如果请求未带租户的编号,不允许访问。 | |
if (tenantId == null) { | |
log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod()); | |
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), | |
"请求的租户标识未传递,请进行排查")); | |
return; | |
} | |
// 3. 校验租户是合法,例如说被禁用、到期 | |
try { | |
tenantFrameworkService.validTenant(tenantId); | |
} catch (Throwable ex) { | |
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex); | |
ServletUtils.writeJSON(response, result); | |
return; | |
} | |
} else { | |
// 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错 | |
if (tenantId == null) { | |
TenantContextHolder.setIgnore(true); | |
} | |
} | |
// 继续过滤 | |
chain.doFilter(request, response); | |
} | |
private boolean isIgnoreUrl(HttpServletRequest request) { | |
// 快速匹配,保证性能 | |
if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) { | |
return true; | |
} | |
// 逐个 Ant 路径匹配 | |
for (String url : tenantProperties.getIgnoreUrls()) { | |
if (pathMatcher.match(url, request.getRequestURI())) { | |
return true; | |
} | |
} | |
return false; | |
} | |
} |
# Redis
在 key 值后面拼接租户 id 来区分每个租户自己的缓存内容 该类继承自:RedisCacheManager
package com.tz.scaffold.framework.tenant.core.redis; | |
import com.tz.scaffold.framework.redis.core.TimeoutRedisCacheManager; | |
import com.tz.scaffold.framework.tenant.core.context.TenantContextHolder; | |
import lombok.extern.slf4j.Slf4j; | |
import org.springframework.cache.Cache; | |
import org.springframework.data.redis.cache.RedisCacheConfiguration; | |
import org.springframework.data.redis.cache.RedisCacheManager; | |
import org.springframework.data.redis.cache.RedisCacheWriter; | |
/** | |
* <p> Project: scaffold - TenantRedisCacheManager </p> | |
* | |
* 多租户的 {@link RedisCacheManager} 实现类 | |
* <p> | |
* 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Slf4j | |
public class TenantRedisCacheManager extends TimeoutRedisCacheManager { | |
public TenantRedisCacheManager(RedisCacheWriter cacheWriter, | |
RedisCacheConfiguration defaultCacheConfiguration) { | |
super(cacheWriter, defaultCacheConfiguration); | |
} | |
@Override | |
public Cache getCache(String name) { | |
// 如果开启多租户,则 name 拼接租户后缀 | |
if (!TenantContextHolder.isIgnore() | |
&& TenantContextHolder.getTenantId() != null) { | |
name = name + ":" + TenantContextHolder.getTenantId(); | |
} | |
// 继续基于父方法 | |
return super.getCache(name); | |
} | |
} |
# AOP
用于忽略不需要区分租户的情况
通过在方法上添加
TenantIgnore
过滤
package com.tz.scaffold.framework.tenant.core.aop; | |
import java.lang.annotation.*; | |
/** | |
* <p> Project: scaffold - TenantIgnore </p> | |
* | |
* 忽略租户,标记指定方法不进行租户的自动过滤 | |
* <p> | |
* 注意,只有 DB 的场景会过滤,其它场景暂时不过滤: | |
* <li> | |
* 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的 | |
* <li> | |
* 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Target({ElementType.METHOD}) | |
@Retention(RetentionPolicy.RUNTIME) | |
@Inherited | |
public @interface TenantIgnore { | |
} |
/** | |
* <p> Project: scaffold - TenantIgnoreAspect </p> | |
* | |
* 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。 | |
* <p> | |
* 例如说,一个定时任务,读取所有数据,进行处理。 | |
* <p> | |
* 又例如说,读取所有数据,进行缓存。 | |
* <p> | |
* 整体逻辑的实现,和 {@link TenantUtils#executeIgnore (Runnable)} 需要保持一致 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Aspect | |
@Slf4j | |
public class TenantIgnoreAspect { | |
@Around("@annotation(tenantIgnore)") | |
public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable { | |
Boolean oldIgnore = TenantContextHolder.isIgnore(); | |
try { | |
TenantContextHolder.setIgnore(true); | |
// 执行逻辑 | |
return joinPoint.proceed(); | |
} finally { | |
TenantContextHolder.setIgnore(oldIgnore); | |
} | |
} | |
} |
# MQ
针对不同的租户,发送对应租户消息内容
主要是
addBeforePublishPostProcessors
方法 允许在发送前执行指定的处理器, 在发送前添加租户标识
package com.tz.scaffold.framework.tenant.core.mq.rabbitmq; | |
import org.springframework.amqp.rabbit.core.RabbitTemplate; | |
import org.springframework.beans.BeansException; | |
import org.springframework.beans.factory.config.BeanPostProcessor; | |
/** | |
* <p> Project: scaffold - TenantRabbitMQInitializer </p> | |
* | |
* 多租户的 RabbitMQ 初始化器 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
public class TenantRabbitMQInitializer implements BeanPostProcessor { | |
@Override | |
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { | |
if (bean instanceof RabbitTemplate) { | |
RabbitTemplate rabbitTemplate = (RabbitTemplate) bean; | |
rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor()); | |
} | |
return bean; | |
} | |
} |
package com.tz.scaffold.framework.tenant.core.mq.rabbitmq; | |
import com.tz.scaffold.framework.tenant.core.context.TenantContextHolder; | |
import org.apache.kafka.clients.producer.ProducerInterceptor; | |
import org.springframework.amqp.AmqpException; | |
import org.springframework.amqp.core.Message; | |
import org.springframework.amqp.core.MessagePostProcessor; | |
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; | |
import static com.tz.scaffold.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; | |
/** | |
* <p> Project: scaffold - TenantRabbitMQMessagePostProcessor </p> | |
* | |
* RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类 | |
* <p> | |
* <li> | |
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 | |
* <li> | |
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor { | |
@Override | |
public Message postProcessMessage(Message message) throws AmqpException { | |
Long tenantId = TenantContextHolder.getTenantId(); | |
if (tenantId != null) { | |
message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId); | |
} | |
return message; | |
} | |
} |
# JOB
任务执行时,会按照租户逐个执行 Job 的逻辑
package com.tz.scaffold.framework.tenant.core.job; | |
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
/** | |
* <p> Project: scaffold - TenantJob </p> | |
* | |
* 多租户 Job 注解 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Target({ElementType.METHOD}) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface TenantJob { | |
} |
package com.tz.scaffold.framework.tenant.core.job; | |
import cn.hutool.core.collection.CollUtil; | |
import cn.hutool.core.exceptions.ExceptionUtil; | |
import com.tz.scaffold.framework.common.util.json.JsonUtils; | |
import com.tz.scaffold.framework.tenant.core.service.TenantFrameworkService; | |
import com.tz.scaffold.framework.tenant.core.util.TenantUtils; | |
import lombok.RequiredArgsConstructor; | |
import lombok.extern.slf4j.Slf4j; | |
import org.aspectj.lang.ProceedingJoinPoint; | |
import org.aspectj.lang.annotation.Around; | |
import org.aspectj.lang.annotation.Aspect; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.concurrent.ConcurrentHashMap; | |
/** | |
* <p> Project: scaffold - TenantJobAspect </p> | |
* | |
* 多租户 JobHandler AOP | |
* <p> | |
* 任务执行时,会按照租户逐个执行 Job 的逻辑 | |
* <p> | |
* <p> | |
* 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Aspect | |
@RequiredArgsConstructor | |
@Slf4j | |
public class TenantJobAspect { | |
private final TenantFrameworkService tenantFrameworkService; | |
@Around("@annotation(tenantJob)") | |
public String around(ProceedingJoinPoint joinPoint, TenantJob tenantJob) { | |
// 获得租户列表 | |
List<Long> tenantIds = tenantFrameworkService.getTenantIds(); | |
if (CollUtil.isEmpty(tenantIds)) { | |
return null; | |
} | |
// 逐个租户,执行 Job | |
Map<Long, String> results = new ConcurrentHashMap<>(); | |
tenantIds.parallelStream().forEach(tenantId -> { | |
// TODO 先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况 | |
TenantUtils.execute(tenantId, () -> { | |
try { | |
joinPoint.proceed(); | |
} catch (Throwable e) { | |
results.put(tenantId, ExceptionUtil.getRootCauseMessage(e)); | |
} | |
}); | |
}); | |
return JsonUtils.toJsonString(results); | |
} | |
} |
# DB
最核心的部分,数据库层,用于处理不同租户操作自己对应的租户数据
通过 mybatis plus 自带的多租户实现, 开启后只需要在对应的表添加多租户字段
tenant_id
(默认的), 指定其他字段可通过配置更改。如果需要排除多租户通过配置
TenantProperties
添加排除的表如果需要获取对应的租户号功能则继承
TenantBaseDO
类
package com.tz.scaffold.framework.tenant.core.db; | |
import com.tz.scaffold.framework.mybatis.core.dataobject.BaseDO; | |
import lombok.Data; | |
import lombok.EqualsAndHashCode; | |
/** | |
* <p> Project: scaffold - TenantBaseDO </p> | |
* | |
* 拓展多租户的 BaseDO 基类 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Data | |
@EqualsAndHashCode(callSuper = true) | |
public abstract class TenantBaseDO extends BaseDO { | |
/** | |
* 多租户编号 | |
*/ | |
private Long tenantId; | |
} |
package com.tz.scaffold.framework.tenant.core.db; | |
import cn.hutool.core.collection.CollUtil; | |
import com.tz.scaffold.framework.tenant.config.TenantProperties; | |
import com.tz.scaffold.framework.tenant.core.context.TenantContextHolder; | |
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; | |
import net.sf.jsqlparser.expression.Expression; | |
import net.sf.jsqlparser.expression.LongValue; | |
import java.util.HashSet; | |
import java.util.Set; | |
/** | |
* <p> Project: scaffold - TenantDatabaseInterceptor </p> | |
* | |
* 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
public class TenantDatabaseInterceptor implements TenantLineHandler { | |
private final Set<String> ignoreTables = new HashSet<>(); | |
public TenantDatabaseInterceptor(TenantProperties properties) { | |
// 不同 DB 下,大小写的习惯不同,所以需要都添加进去 | |
properties.getIgnoreTables().forEach(table -> { | |
ignoreTables.add(table.toLowerCase()); | |
ignoreTables.add(table.toUpperCase()); | |
}); | |
// 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错 | |
ignoreTables.add("DUAL"); | |
} | |
@Override | |
public Expression getTenantId() { | |
return new LongValue(TenantContextHolder.getRequiredTenantId()); | |
} | |
@Override | |
public boolean ignoreTable(String tableName) { | |
// 情况一,全局忽略多租户 | |
return TenantContextHolder.isIgnore() | |
// 情况二,忽略多租户的表 | |
|| CollUtil.contains(ignoreTables, tableName); | |
} | |
} |
yml 配置
tenant: # 多租户相关配置项 | |
enable: true | |
ignore-urls: | |
- /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号 | |
ignore-tables: | |
- system_tenant | |
- system_tenant_package | |
- system_dict_data |
注意事项:mybatis plus 自带的多租户方案,如果在 mybatisXML 手写 sql 是不会生效的
需要我们手动拼接条件: and tenant_id = $
# 封装组件
自动配置类:
package com.tz.scaffold.framework.tenant.config; | |
import com.tz.scaffold.framework.common.enums.WebFilterOrderEnum; | |
import com.tz.scaffold.framework.mybatis.core.util.MyBatisUtils; | |
import com.tz.scaffold.framework.redis.config.ScaffoldCacheProperties; | |
import com.tz.scaffold.framework.tenant.core.aop.TenantIgnoreAspect; | |
import com.tz.scaffold.framework.tenant.core.db.TenantDatabaseInterceptor; | |
import com.tz.scaffold.framework.tenant.core.job.TenantJobAspect; | |
import com.tz.scaffold.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer; | |
import com.tz.scaffold.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor; | |
import com.tz.scaffold.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer; | |
import com.tz.scaffold.framework.tenant.core.redis.TenantRedisCacheManager; | |
import com.tz.scaffold.framework.tenant.core.security.TenantSecurityWebFilter; | |
import com.tz.scaffold.framework.tenant.core.service.TenantFrameworkService; | |
import com.tz.scaffold.framework.tenant.core.service.TenantFrameworkServiceImpl; | |
import com.tz.scaffold.framework.tenant.core.web.TenantContextWebFilter; | |
import com.tz.scaffold.framework.web.config.WebProperties; | |
import com.tz.scaffold.framework.web.core.handler.GlobalExceptionHandler; | |
import com.tz.scaffold.module.system.api.tenant.TenantApi; | |
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; | |
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; | |
import org.springframework.boot.autoconfigure.AutoConfiguration; | |
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; | |
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | |
import org.springframework.boot.context.properties.EnableConfigurationProperties; | |
import org.springframework.boot.web.servlet.FilterRegistrationBean; | |
import org.springframework.context.annotation.Bean; | |
import org.springframework.context.annotation.Primary; | |
import org.springframework.data.redis.cache.BatchStrategies; | |
import org.springframework.data.redis.cache.RedisCacheConfiguration; | |
import org.springframework.data.redis.cache.RedisCacheManager; | |
import org.springframework.data.redis.cache.RedisCacheWriter; | |
import org.springframework.data.redis.connection.RedisConnectionFactory; | |
import org.springframework.data.redis.core.RedisTemplate; | |
import java.util.Objects; | |
/** | |
* <p> Project: scaffold - ScaffoldTenantAutoConfiguration </p> | |
* | |
* 多租户配置类 | |
* <p> | |
* ConditionalOnProperty: 允许使用 scaffold.tenant.enable=false 禁用多租户 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@AutoConfiguration | |
@ConditionalOnProperty(prefix = "scaffold.tenant", value = "enable", matchIfMissing = true) | |
@EnableConfigurationProperties(TenantProperties.class) | |
public class ScaffoldTenantAutoConfiguration { | |
@Bean | |
public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) { | |
return new TenantFrameworkServiceImpl(tenantApi); | |
} | |
// ========== AOP ========== | |
@Bean | |
public TenantIgnoreAspect tenantIgnoreAspect() { | |
return new TenantIgnoreAspect(); | |
} | |
// ========== DB ========== | |
@Bean | |
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties, | |
MybatisPlusInterceptor interceptor) { | |
TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties)); | |
// 添加到 interceptor 中 | |
// 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 | |
MyBatisUtils.addInterceptor(interceptor, inner, 0); | |
return inner; | |
} | |
// ========== WEB ========== | |
@Bean | |
public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() { | |
FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>(); | |
registrationBean.setFilter(new TenantContextWebFilter()); | |
registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); | |
return registrationBean; | |
} | |
// ========== Security ========== | |
@Bean | |
public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties, | |
WebProperties webProperties, | |
GlobalExceptionHandler globalExceptionHandler, | |
TenantFrameworkService tenantFrameworkService) { | |
FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>(); | |
registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties, | |
globalExceptionHandler, tenantFrameworkService)); | |
registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); | |
return registrationBean; | |
} | |
// ========== MQ ========== | |
@Bean | |
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { | |
return new TenantRedisMessageInterceptor(); | |
} | |
@Bean | |
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") | |
public TenantRabbitMQInitializer tenantRabbitMQInitializer() { | |
return new TenantRabbitMQInitializer(); | |
} | |
@Bean | |
@ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate") | |
public TenantRocketMQInitializer tenantRocketMQInitializer() { | |
return new TenantRocketMQInitializer(); | |
} | |
// ========== Job ========== | |
@Bean | |
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) { | |
return new TenantJobAspect(tenantFrameworkService); | |
} | |
// ========== Redis ========== | |
@Bean | |
@Primary // 引入租户时,tenantRedisCacheManager 为主 Bean | |
public RedisCacheManager tenantRedisCacheManager(RedisTemplate<String, Object> redisTemplate, | |
RedisCacheConfiguration redisCacheConfiguration, | |
ScaffoldCacheProperties scaffoldCacheProperties) { | |
// 创建 RedisCacheWriter 对象 | |
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); | |
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, | |
BatchStrategies.scan(scaffoldCacheProperties.getRedisScanBatchSize())); | |
// 创建 TenantRedisCacheManager 对象 | |
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration); | |
} | |
} |
在 resource 添加对应的文件:
org.springframework.boot.autoconfigure.AutoConfiguration.imports,文件内容如下:
com.tz.scaffold.framework.tenant.config.ScaffoldTenantAutoConfiguration