黔南布依族苗族自治州网站建设_网站建设公司_JavaScript_seo优化
2025/12/26 13:14:14 网站建设 项目流程

在企业级应用中,多租户架构(Multi-Tenancy)是一个非常常见的需求。比如 SaaS 平台中,每个客户(租户)的数据需要隔离存储,通常通过在每张业务表中增加一个tenant_id字段来实现。

MyBatis-Plus 提供了强大的多租户插件(TenantLineInnerInterceptor),可以自动在 SQL 中注入租户条件。但在某些特殊场景下,我们可能希望临时忽略租户过滤,比如:

  • 管理员查看所有租户数据
  • 数据迁移、批量处理任务
  • 异步任务中绕过多租户限制

这时候问题来了:如何做到“一次设置,全程忽略租户”?即使在异步线程、嵌套方法调用中也能生效?

今天我们就来手把手教你实现这个高级功能!


一、需求场景说明

假设你正在开发一个 SaaS 后台管理系统,普通用户只能看到自己租户的数据,但超级管理员可以查看全部租户的数据。

✅ 正确做法:在请求入口处判断是否为超级管理员,如果是,则在整个请求链路(包括异步任务、Service 嵌套调用)中都忽略租户过滤。

❌ 反例做法:每次调用 Mapper 时手动传参或写特殊 SQL,代码冗余且容易出错。


二、技术选型与原理

  • Spring Boot 3.x
  • MyBatis-Plus 3.5+
  • ThreadLocal + InheritableThreadLocal:用于在线程上下文中传递“忽略租户”标志
  • 自定义 MyBatis-Plus 租户处理器:动态决定是否应用租户过滤

💡 核心思想:
使用InheritableThreadLocal存储“是否忽略租户”的开关。
因为普通ThreadLocal在异步线程(如@Async)中无法继承父线程的值,而InheritableThreadLocal可以。


三、代码实现

1. 自定义忽略租户上下文工具类

// TenantIgnoreContext.java public class TenantIgnoreContext { // 使用 InheritableThreadLocal 支持子线程继承 private static final InheritableThreadLocal<Boolean> IGNORE_TENANT = new InheritableThreadLocal<>(); public static void setIgnore(boolean ignore) { IGNORE_TENANT.set(ignore); } public static boolean isIgnore() { Boolean ignore = IGNORE_TENANT.get(); return ignore != null && ignore; } public static void clear() { IGNORE_TENANT.remove(); } }

⚠️ 注意:必须在请求结束时调用clear(),否则可能造成内存泄漏或污染下一个请求!


2. 自定义租户处理器(关键!)

// CustomTenantHandler.java import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; public class CustomTenantHandler implements TenantLineHandler { @Override public Expression getTenantId() { // 正常返回租户ID,比如从登录信息中获取 // 这里简化为固定值,实际项目应从 SecurityContext 或 ThreadLocal 获取 return new LongValue(1L); } @Override public String getTenantIdColumn() { return "tenant_id"; } @Override public boolean ignoreTable(String tableName) { // 某些表不需要租户隔离,比如字典表、系统配置表 return false; } // 重写此方法!决定是否应用租户过滤 @Override public boolean ignoreInsert(List<Column> columns, String tenantIdColumn) { return TenantIgnoreContext.isIgnore(); } // 关键:全局控制是否忽略租户 @Override public boolean ignoreTableWhenInsertOrUpdateOrDelete(String tableName) { return TenantIgnoreContext.isIgnore(); } // MyBatis-Plus 3.5+ 推荐重写此方法 @Override public boolean ignoreTableForSelect(String tableName) { return TenantIgnoreContext.isIgnore(); } }

🔥 重点:所有ignoreXXX方法都返回TenantIgnoreContext.isIgnore(),实现统一控制。


3. 配置 MyBatis-Plus 多租户插件

// MybatisPlusConfig.java @Configuration @MapperScan("com.example.demo.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 多租户插件 TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor(); tenantInterceptor.setTenantLineHandler(new CustomTenantHandler()); interceptor.addInnerInterceptor(tenantInterceptor); // 分页插件等其他拦截器... interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

4. 在 Controller 中使用(支持同步/异步)

@RestController public class UserController { @Autowired private UserService userService; @Autowired private AsyncTask asyncTask; @GetMapping("/users") public List<User> getAllUsers(@RequestParam(required = false) Boolean admin) { try { // 如果是管理员,开启忽略租户模式 if (Boolean.TRUE.equals(admin)) { TenantIgnoreContext.setIgnore(true); } // 同步调用 List<User> users = userService.listAll(); // 异步调用(子线程会继承 ignore 标志!) asyncTask.logUserCount(); return users; } finally { // 必须清理!防止 ThreadLocal 泄漏 TenantIgnoreContext.clear(); } } }
// UserService.java @Service public class UserService { @Autowired private UserMapper userMapper; public List<User> listAll() { // 即使嵌套调用,也能正确识别是否忽略租户 return userMapper.selectList(null); } }
// AsyncTask.java @Component public class AsyncTask { @Autowired private UserMapper userMapper; @Async public void logUserCount() { // 异步线程中依然能拿到 ignore 标志! long count = userMapper.selectCount(null); System.out.println("Total users (ignoring tenant): " + count); } }

✅ 测试结果:

  • 访问/users?admin=true→ 查询所有租户数据
  • 访问/users→ 仅查询当前租户数据
  • 异步任务中也生效!

四、反例 & 注意事项

❌ 反例1:只在 Service 层硬编码忽略

// 错误!每次都要改,无法复用 public List<User> getAllUsersForAdmin() { // 手动写 SQL 或关闭插件?不可维护! }

❌ 反例2:忘记清理 ThreadLocal

// 危险!可能导致下一个请求错误地继承了 ignore 标志 TenantIgnoreContext.setIgnore(true); // ... 忘记 clear()

⚠️ 注意事项:

  1. 必须使用InheritableThreadLocal,否则@Async、线程池中的任务无法继承上下文。
  2. 务必在 finally 块中调用clear(),建议封装成 AOP 切面自动清理。
  3. 不要在过滤器/拦截器之外的地方随意设置,避免逻辑混乱。
  4. 测试时要覆盖同步、异步、异常路径,确保上下文正确传播和清理。

五、进阶建议:用 AOP 自动管理上下文

你可以写一个注解@IgnoreTenant,配合 AOP 自动设置和清理:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface IgnoreTenant {}
@Aspect @Component public class TenantIgnoreAspect { @Around("@annotation(IgnoreTenant)") public Object ignoreTenant(ProceedingJoinPoint joinPoint) throws Throwable { try { TenantIgnoreContext.setIgnore(true); return joinPoint.proceed(); } finally { TenantIgnoreContext.clear(); } } }

然后在方法上直接使用:

@IgnoreTenant @GetMapping("/admin/users") public List<User> adminGetAllUsers() { return userService.listAll(); // 自动忽略租户 }

总结

通过InheritableThreadLocal + 自定义 TenantHandler,我们实现了:

✅ 一次设置,全程生效
✅ 支持同步、异步、嵌套调用
✅ 代码清晰,无侵入性
✅ 安全可控,避免内存泄漏

这才是多租户系统中“临时绕过租户隔离”的优雅解法!


视频看了几百小时还迷糊?关注我,几分钟让你秒懂!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询