从零实现"默认给 SQL 查询语句加上租户条件 "的功能,本质上是利用 MyBatis Plus 的插件机制配合 ThreadLocal 上下文来实现的。
我们需要构建一条完整的 "数据 -> 规则 -> 执行" 的链路。以下是标准化的 5 步实现指南:
第一步:准备"背包" (定义上下文容器)
你需要一个地方在当前线程中存储"当前租户是谁"。
代码核心: 利用 ThreadLocal。
java
public class TenantContextHolder {
// 存放当前租户ID
private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
public static void setTenantId(Long tenantId) {
TENANT_ID.set(tenantId);
}
public static Long getTenantId() {
return TENANT_ID.get();
}
public static void clear() {
TENANT_ID.remove();
}
}
第二步:制定"规则" (实现 Handler 接口)
你需要告诉 MyBatis Plus 具体的过滤逻辑:租户ID是多少?列名叫什么?哪些表不需要加?
代码核心: 实现 TenantLineHandler 接口。
java
@Component
public class MyTenantLineHandler implements TenantLineHandler {
// 1. 告诉 MP,当前租户ID是多少 (从背包里拿)
@Override
public Expression getTenantId() {
Long tenantId = TenantContextHolder.getTenantId();
// 如果没拿到ID(比如没登录),返回 NullValue 可能会导致报错或查不到数据
// 通常这里会做判空或者返回默认值
return new LongValue(tenantId);
}
// 2. 告诉 MP,数据库里租户列的名字叫什么
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
// 3. 告诉 MP,哪些表需要忽略 (白名单)
@Override
public boolean ignoreTable(String tableName) {
// A. 全局白名单:系统表(字典、菜单)不需要隔离
if ("sys_dict".equals(tableName) || "sys_menu".equals(tableName)) {
return true;
}
// B. 动态白名单:配合 @TenantIgnore 注解使用
if (TenantContextHolder.isIgnore()) {
return true;
}
// 默认:必须加过滤条件
return false;
}
}
第三步:组装"引擎" (配置 MyBatis 拦截器)
有了规则(Handler),你需要把它交给执行者(Interceptor),并把执行者放入 MyBatis 的插件链中。
代码核心: 配置 MybatisPlusInterceptor Bean。
java
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(MyTenantLineHandler tenantLineHandler) {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 核心动作:创建租户拦截器,并注入上面的规则 Handler
// ⚠️注意:建议放在分页插件之前
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler));
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
第四步:数据"注入" (配置 Web 过滤器)
引擎装好了,但还得有人把燃料(租户ID)塞进第一步的"背包"里。通常是在请求刚进来时处理。
代码核心: 实现 Filter 或 HandlerInterceptor。
java
@Component
public class TenantContextFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
try {
// 1. 从请求头 Header 获取 tenant-id
HttpServletRequest req = (HttpServletRequest) request;
String tenantIdStr = req.getHeader("tenant-id");
if (tenantIdStr != null) {
// 2. 塞进 ThreadLocal 背包
TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr));
}
// 3. 放行,执行后续业务逻辑 (Service -> Mapper -> SQL拦截器)
chain.doFilter(request, response);
} finally {
// 4. 【重要】请求结束,清空背包,防止线程污染
TenantContextHolder.clear();
}
}
}
🌟 总结:这一套下来发生了什么?
- 请求进来 :Filter 从 Header 拿到
tenant_id=1,存入TenantContextHolder。 - 业务查询 :你写了
userMapper.selectList(null)。 - 拦截改写 :
TenantLineInnerInterceptor拦截 SQL,调用MyTenantLineHandler。 - 读取规则 :Handler 从
TenantContextHolder拿到1。 - SQL 变身 :SQL 被自动拼接为
SELECT * FROM user WHERE tenant_id = 1。 - 请求结束 :Filter 清空
TenantContextHolder。
这就是实现"全自动多租户隔离"的完整标准流程。