在 SaaS(Software as a Service)系统中,多租户(Multi-Tenant) 是常见架构模式。MyBatis-Plus 提供了强大的多租户支持,通过 TenantLineInnerInterceptor
配合自定义 TenantLineHandler
接口实现,可自动在 SQL 中注入租户条件(如 tenant_id = ?
),从而实现数据隔离。
一、核心概念
1. 什么是 TenantLineHandler
?
TenantLineHandler
是 MyBatis-Plus 多租户模块的核心接口,用于定义:- 哪些表需要进行租户隔离。
- 租户字段名(如
tenant_id
)。 - 当前租户 ID 的获取方式。
- 它不直接执行拦截,而是由
TenantLineInnerInterceptor
调用,决定是否在 SQL 的WHERE
条件中自动添加tenant_id = ?
。
2. 核心方法
public interface TenantLineHandler {
/**
* 获取当前租户 ID 值表达式,不参数化(即直接返回值或表达式)
*/
Expression getTenantId();
/**
* 数据表名是否需要拼接多租户字段(如 tenant_id)
*/
default boolean ignoreTable(String tableName) {
return false;
}
}
⚠️ 注意:
getTenantId()
返回的是Expression
类型(来自 JSQLParser),不是简单的String
或Long
。
二、操作步骤(非常详细)
步骤 1:引入 MyBatis-Plus 多租户依赖
确保使用的是 MyBatis-Plus 3.4.0 及以上版本(推荐 3.5.5):
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
多租户功能内置在主包中,无需额外引入。
步骤 2:创建租户处理器实现类
创建类实现 TenantLineHandler
接口。
package com.example.handler;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.springframework.stereotype.Component;
@Component
public class MyTenantLineHandler implements TenantLineHandler {
/**
* 定义租户字段名
*/
private static final String TENANT_ID_COLUMN = "tenant_id";
/**
* 模拟从 ThreadLocal 或 SecurityContext 获取当前租户 ID
* 实际项目中可从 JWT、Session、请求头等获取
*/
@Override
public Expression getTenantId() {
Long tenantId = TenantContextHolder.getTenantId(); // 自定义上下文工具类
return new LongValue(tenantId); // 返回 JSQLParser 的 LongValue 表达式
}
/**
* 指定哪些表不进行多租户处理
* @param tableName 表名
* @return true 表示忽略(不加 tenant_id 条件),false 表示需要处理
*/
@Override
public boolean ignoreTable(String tableName) {
// 系统表、用户表、角色表等不隔离
return "sys_user".equalsIgnoreCase(tableName)
|| "sys_role".equalsIgnoreCase(tableName)
|| "sys_dept".equalsIgnoreCase(tableName)
|| "tenant_info".equalsIgnoreCase(tableName); // 租户信息表本身不隔离
}
}
步骤 3:创建租户上下文工具类(推荐)
用于在请求过程中存储和获取当前租户 ID。
package com.example.util;
public class TenantContextHolder {
private static final ThreadLocal<Long> CONTEXT = new ThreadLocal<>();
public static void setTenantId(Long tenantId) {
CONTEXT.set(tenantId);
}
public static Long getTenantId() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
步骤 4:配置多租户拦截器
创建 MyBatis-Plus 配置类,注册 TenantLineInnerInterceptor
。
package com.example.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.example.handler.MyTenantLineHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(TenantLineHandler tenantLineHandler) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加多租户拦截器
TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor(tenantLineHandler);
interceptor.addInnerInterceptor(tenantInterceptor);
return interceptor;
}
// 如果不想用 @Component,也可在此处注入
// @Bean
// public TenantLineHandler tenantLineHandler() {
// return new MyTenantLineHandler();
// }
}
步骤 5:在请求入口设置租户 ID(关键!)
通常在 拦截器或过滤器 中解析请求头或 Token 获取 tenant_id
并设置到上下文中。
@Component
public class TenantIdInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tenantIdStr = request.getHeader("X-Tenant-Id");
if (tenantIdStr != null && !tenantIdStr.isEmpty()) {
try {
Long tenantId = Long.valueOf(tenantIdStr);
TenantContextHolder.setTenantId(tenantId);
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid tenant ID: " + tenantIdStr);
}
} else {
throw new RuntimeException("Missing tenant ID in request header");
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 清理 ThreadLocal,防止内存泄漏
TenantContextHolder.clear();
}
}
注册拦截器:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TenantIdInterceptor());
}
}
步骤 6:验证效果
假设有一个 order
表,执行查询:
@Mapper
public interface OrderMapper extends BaseMapper<Order> {}
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public List<Order> getAllOrders() {
return orderMapper.selectList(null); // 无条件查询
}
}
实际执行 SQL 将自动变为:
SELECT * FROM order WHERE tenant_id = 1001;
✅ 成功实现自动租户过滤。
三、常见错误与解决方案
错误现象 | 原因 | 解决方案 |
---|---|---|
SQL 未添加 tenant_id 条件 |
ignoreTable 返回 true 或表名匹配错误 |
检查表名大小写、拼写 |
报错 Tenant ID is null |
getTenantId() 返回 null |
确保 TenantContextHolder 已设置值 |
启动报错找不到 TenantLineHandler |
未注入 Spring 容器 | 使用 @Component 或在配置类中 @Bean 注册 |
多线程环境下租户 ID 错乱 | ThreadLocal 未清理 |
在 afterCompletion 中调用 clear() |
Expression 类型错误 |
返回了 String 而非 Expression |
使用 new LongValue(123L) 或 new StringValue("xxx") |
四、注意事项
getTenantId()
必须返回Expression
子类
常用实现:new LongValue(123L)
new StringValue("tenant_001")
new DoubleValue(1.5)
new NullValue()
避免在
ignoreTable
中忽略错误的表
如租户业务表被忽略,将导致数据泄露。ThreadLocal 内存泄漏风险
务必在请求结束时调用clear()
。不支持
INSERT
自动填充tenant_id
多租户插件只处理SELECT
、UPDATE
、DELETE
的WHERE
条件。
INSERT
的tenant_id
需通过@TableField(fill = FieldFill.INSERT)
+MetaObjectHandler
实现。
五、使用技巧
技巧 1:支持字符串型 tenant_id
@Override
public Expression getTenantId() {
String tenantCode = TenantContextHolder.getTenantCode();
return new StringValue(tenantCode);
}
技巧 2:结合 JWT 获取租户信息
在过滤器中解析 JWT Token,提取 tenant_id
并设置到上下文。
技巧 3:动态忽略表(基于注解)
可自定义注解 @IgnoreTenant
,在 ignoreTable
中通过反射判断 Mapper 方法是否标记该注解。
六、最佳实践
实践 | 说明 |
---|---|
✅ 使用 ThreadLocal 传递租户 ID |
简单高效,适合 Web 场景 |
✅ 请求结束清理上下文 | 防止内存泄漏和脏数据 |
✅ 为系统表添加 ignoreTable |
避免无限递归或逻辑错误 |
✅ INSERT 使用 MetaObjectHandler 填充 |
保证 tenant_id 字段必填 |
✅ 生产环境开启日志 | 记录 SQL,便于调试 |
✅ 支持多种租户识别方式 | 如域名、Header、Token 等 |
七、性能优化
- 性能影响极小:拦截器仅在 SQL 解析阶段添加条件,无额外查询。
- 缓存租户信息:若从数据库获取租户 ID,建议缓存(如 Redis)。
- 避免复杂逻辑:
getTenantId()
和ignoreTable
中避免远程调用或复杂计算。 - 使用连接池:合理配置数据库连接池,减少连接开销。
八、总结
项目 | 内容 |
---|---|
核心接口 | TenantLineHandler |
拦截器 | TenantLineInnerInterceptor |
关键方法 | getTenantId() 、ignoreTable() |
租户传递 | 推荐 ThreadLocal |
自动填充 INSERT | 需配合 MetaObjectHandler |
适用场景 | SaaS 系统、数据隔离需求 |
✅ 一句话总结:
通过实现 TenantLineHandler
并注册 TenantLineInnerInterceptor
,MyBatis-Plus 可自动为 SQL 添加租户条件,是实现多租户数据隔离的优雅方案。
📌 立即实践建议:
- 创建
TenantLineHandler
实现类。 - 配置拦截器。
- 在过滤器中设置租户 ID。
- 测试 SQL 是否自动添加
tenant_id
条件。
⚠️ 切记:多租户安全依赖正确配置,务必测试验证!