当前位置: 首页 > news >正文

JAVA后端开发——多租户

数据隔离是多租户系统中的核心概念,确保一个租户(在这个系统中可能是一个公司或一个独立的客户)的数据对其他租户是不可见的。在 RuoYi 框架(您当前项目所使用的基础框架)中,这通常是通过在数据表中增加一个租户标识(如 tenant_id),并在所有的数据库查询中自动加入这个租户ID作为过滤条件来实现的。

使用MyBatis-Plus实现了多租户功能

1、MybatisPlusConfig.java:配置类,通过TenantLineInnerInterceptor开启租户功能。该拦截器是关键,能自动向SQL查询添加WHERE条件。

public class MybatisPlusConfig {@Autowiredprivate TenantProperties tenantProperties;@Autowiredprivate MyTenantLineHandler myTenantLineHandler;@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();if (tenantProperties.isEnable()) { // 根据配置文件决定是否启用多租户插件TenantLineInnerInterceptor tenantLineInnerInterceptor = new TenantLineInnerInterceptor();tenantLineInnerInterceptor.setTenantLineHandler(myTenantLineHandler); // 设置我们自定义的处理器interceptor.addInnerInterceptor(tenantLineInnerInterceptor);System.out.println("INFO: TenantLineInnerInterceptor has been added to MybatisPlusInterceptor."); // 用于启动时确认}// 分页插件interceptor.addInnerInterceptor(paginationInnerInterceptor());// 乐观锁插件interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());// 阻断插件interceptor.addInnerInterceptor(blockAttackInnerInterceptor());return interceptor;}/*** 分页插件,自动识别数据库类型*/public PaginationInnerInterceptor paginationInnerInterceptor() {PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();// 设置数据库类型为mysqlpaginationInnerInterceptor.setDbType(DbType.MYSQL);// 设置最大单页限制数量,默认 500 条,-1 不受限制paginationInnerInterceptor.setMaxLimit(-1L);return paginationInnerInterceptor;}/*** 乐观锁插件*/public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() {return new OptimisticLockerInnerInterceptor();}/*** 如果是对全表的删除或更新操作,就会终止该操作*/public BlockAttackInnerInterceptor blockAttackInnerInterceptor() {return new BlockAttackInnerInterceptor();}
}

2、MyTenantLineHandler.java:自定义的TenantLineHandler实现,负责提供租户ID、列名以及指定需要过滤的表。它通过TenantUtils.getCurrentTenantId()获取租户ID,并从TenantProperties获取列名,同时判断是否需要对特定表应用过滤器。

package com.ruoyi.framework.config;import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.ruoyi.common.utils.TenantUtils;
import com.ruoyi.framework.config.properties.TenantProperties;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.util.Set;/*** 这个类实现了 MyBatis Plus 的 TenantLineHandler 接口,负责两件事:* 告诉拦截器当前操作的租户ID是什么。* 告诉拦截器当前操作的表是否应该被忽略。*/
@Component
public class MyTenantLineHandler implements TenantLineHandler {private static final Logger log = LoggerFactory.getLogger(MyTenantLineHandler.class);@Autowiredprivate TenantProperties tenantProperties;/*** 【核心修改】* 只有当用户选择了租户时,才返回真实的租户ID。* 否则,我们返回一个特殊的、无效的ID(如0L),并配合 shouldFilter 方法来确保拦截器不工作。*/@Overridepublic Expression getTenantId() {Long currentTenantId = TenantUtils.getCurrentTenantId();if (currentTenantId == null) {// 当没有租户ID时,返回一个不可能匹配任何记录的ID// 真实的过滤逻辑由 shouldFilter 控制return new LongValue(0L); }return new LongValue(currentTenantId);}/*** 获取租户ID的数据库字段名*/@Overridepublic String getTenantIdColumn() {return tenantProperties.getColumn(); // 从配置文件获取}/*** 这是一个总开关。只有当用户登录并明确选择了某个租户后,才允许拦截器进行SQL处理。* 否则,拦截器应忽略所有表。* @param tableName 表名* @return 是否忽略。true = 忽略,false = 不忽略(进行处理)*/@Overridepublic boolean ignoreTable(String tableName) {// 如果用户还未选择租户,则忽略所有表Long currentTenantId = TenantUtils.getCurrentTenantId();if (currentTenantId == null) {log.debug("No tenant selected, ignoring all tables.");return true;}// 如果用户已选择租户,则根据配置文件中的 ignore-tables 列表来判断Set<String> ignoreTables = tenantProperties.getIgnoreTables();boolean shouldIgnore = ignoreTables != null && ignoreTables.stream().anyMatch(item -> item.equalsIgnoreCase(tableName.trim()));if (shouldIgnore) {log.debug("Table [{}] is in ignore-tables list, skipping.", tableName);} else {log.debug("Table [{}] is not in ignore-tables list, applying tenant filter.", tableName);}return shouldIgnore;}
}

3、TenantProperties.java:从配置文件加载多租户配置,包括是否启用、租户列名(tenant_id)和忽略的表。

package com.ruoyi.framework.config.properties;import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;@Component
@ConfigurationProperties(prefix = "tenant")
public class TenantProperties {/*** 是否开启多租户功能*/private boolean enable = true; // 默认开启,可以在YML中覆盖/*** 多租户 ID 的数据库字段名*/private String column = "tenant_id"; // 默认列名为 tenant_id/*** 需要忽略多租户处理的表名集合 (在配置文件中用逗号分隔)*/private Set<String> ignoreTables = new HashSet<>();// --- Getters and Setters ---public boolean isEnable() {return enable;}public void setEnable(boolean enable) {this.enable = enable;}public String getColumn() {return column;}public void setColumn(String column) {this.column = column;}public Set<String> getIgnoreTables() {return ignoreTables;}// 这个setter允许YML中用逗号分隔的字符串配置 ignoreTablespublic void setIgnoreTables(String ignoreTablesStr) {if (ignoreTablesStr != null && !ignoreTablesStr.isEmpty()) {this.ignoreTables = Set.of(ignoreTablesStr.toLowerCase().split(",")).stream().map(String::trim).collect(Collectors.toSet());} else {this.ignoreTables = new HashSet<>();}}
}

整个拦截流程 

请求进入SysProjectController后,最终调用projectMapper.selectProjectList(...)。MyBatis-Plus的TenantLineInnerInterceptor会拦截该调用,通过MyTenantLineHandler获取当前租户ID和列名,然后检查sys_project表是否需要过滤。如果需要,拦截器会自动在SQL查询的WHERE子句中添加AND tenant_id = ?,从而确保只返回当前租户的数据。 这个“拦截”行为不是在业务代码(如 Controller 或 Service)中能直接看到的,它是通过 Spring Boot 的自动配置 和 MyBatis 的插件机制 在底层实现的。具体如下:

  1.  注册拦截器 (应用启动时):在 ruoyi-framework 模块中看到的 MybatisPlusConfig.java 文件是这一切的起点。这是一个配置类。interceptor.addInnerInterceptor(tenantLineInnerInterceptor)是“连接点”!这行代码明确地将配置好的 TenantLineInnerInterceptor 注册 到了 MyBatis-Plus 的拦截器链中。

    当 Spring Boot 应用启动时,它会扫描到这个配置,执行 mybatisPlusInterceptor() 方法,创建一个包含了租户拦截器的 MybatisPlusInterceptor Bean。

  2. MyBatis 插件工作原理:

    MyBatis 自身设计了一套插件(Interceptor)机制,它允许在 SQL 执行过程的某些关键节点进行干预。这些节点包括:Executor: SQL 执行器、StatementHandler: SQL 语法构建处理器、ParameterHandler: 参数处理器、ResultSetHandler: 结果集处理器。TenantLineInnerInterceptor 正是利用了这套机制,它主要拦截的是 StatementHandler。

  3. SQL 执行时的拦截过程 (请求处理时) :前端应用发送一个请求;经过Controller -> Service -> Mapper;MyBatis 执行;进入拦截点: 在 MyBatis 将 XML 中的 SQL (select ... from sys_project ...) 转换成最终可以在数据库执行的 PreparedStatement 之前,它会触发 StatementHandler 的处理流程;TenantLineInnerInterceptor 生效: 因为我们在启动时已经注册了 TenantLineInnerInterceptor,此时它就会被激活。拦截器拿到原始的 SQL 语句。它调用我们提供的 MyTenantLineHandler,获取到当前的 tenantId (例如 123) 和租户列名 (tenant_id)。它会智能地分析原始 SQL,找到 FROM sys_project 这部分,并自动在 WHERE 条件中(或者新建一个 WHERE 条件)补充上租户过滤。

总结

拦截的动作发生在 MyBatis 执行 SQL 的生命周期内部,而不是在业务代码层面。MybatisPlusConfig.java 中的配置,就像是给 MyBatis 的执行引擎装上了一个“插件”或“mod”。一旦装上,它就会对所有经过的 SQL “自动审查和加工”,无需在每次调用 Mapper 时手动干预。

这种设计的好处是 透明和无侵入:业务开发人员只需要关注业务逻辑,而不需要关心多租户的过滤细节,框架会自动保证数据安全,大大减少了编码工作量和出错的可能性。

相关文章:

  • 力扣 1456. 定长子串中元音的最大数目 的多解
  • 【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变板
  • 一般视频剪辑的硬盘配置是什么
  • 什么时候用GraphRAG?RAG VS GraphRAG综合分析
  • 【西门子杯工业嵌入式-7-OLED】
  • 第二章 感知机
  • 火山 RTC 引擎10 ----远端视频 转网易视频格式
  • Houdini POP入门学习07 - 分组
  • 算法岗面试经验分享-大模型篇
  • 学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
  • 软件开发工程师如何在项目开发中了解学习 ISO 13485
  • WPS2024 软件下载及安装教程!
  • Java多线程实现之Thread类深度解析
  • AI电销机器人智能的发展趋势是什么?
  • 无需改造业务系统,数据导出行为也能“可控、可审、可溯”
  • 验证回文串
  • 根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
  • Docker拉取MySQL后数据库连接失败的解决方案
  • 解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
  • 如何在Android Studio中导出apk文件
  • 中国电力建设股份有限公司网站/识图
  • 延安做网站的公司电话/seo工资待遇怎么样
  • 网站建设中图片多少钱/seo推广人员
  • 深圳南山区住房和建设局网站/百度网站的域名地址
  • 方维网络的品牌网站建设/b站推广网站入口2023的推广形式
  • 做网站推广邢台/营销咨询公司排名