# scaffold 项目之多租户字段隔离

# 多租户是什么?

多租户,简单来说是指一个业务,区分 多个组织单位 ,每个组织单位之间的数据是 相互隔离 的。

例如说,有一个系统,可以支持不同公司使用,这里的 一个公司就是一个租户

每个用户必然是属于某一个租户的。因此,用户也只能看到自己租户下的内容,其他的租户内容是看不到的。

# 数据的隔离方案

多租户隔的数据隔离方案,有以下几种方式:

  1. DATASOURCE 模式: 独立数据库模式
  2. SCHEMA 模式:共享数据库,独立 SCHEMA 模式
  3. 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