核心在于用注解标记租户上下文,结合AOP拦截与反射解析注解元数据(如tenantField、from等),为AbstractRoutingDataSource的determineCurrentLookupKey提供租户ID,实现动态数据源路由切换。

自定义注解实现多租户隔离,核心在于:用注解标记租户上下文,结合 AOP 拦截 + 反射动态识别租户标识,再通过 动态数据源路由 切换到对应租户的数据源。关键不是“反射”本身做切换,而是用反射读取注解元数据(如租户 ID、租户字段名、获取方式等),为数据源路由提供决策依据。
一、定义租户隔离注解
注解用于标记哪些方法/类需要按租户隔离,同时携带必要元信息:
- @TenantRoute:声明式开启租户路由
- 支持属性如 value()(租户 ID 字面值)、tenantField()(从参数对象中取哪个字段)、from()(从 Header / ThreadLocal / Spring Context 等哪获取)
- 必须声明为 @Retention(RetentionPolicy.RUNTIME),才能在运行时通过反射读取
二、AOP 拦截 + 反射解析注解
切面负责在目标方法执行前,提取租户标识并绑定到当前线程上下文(如 TenantContextHolder):
- 使用 JoinPoint.getSignature() 获取被调用方法
- 调用 method.getAnnotation(TenantRoute.class) 读取注解实例
- 根据 from() 属性决定如何取值: – 若为 "header",从 RequestContextHolder 取 X-Tenant-ID; – 若为 "param",用反射遍历 joinPoint.getArgs(),找到含 tenantField 的对象并 getDeclaredField(...).get(...) 获取值; – 若为 "threadlocal",直接从预设的 ThreadLocal 中取
三、动态数据源路由与切换
Spring 的 AbstractRoutingDataSource 是核心载体:
- 重写 determineCurrentLookupKey() 方法,返回当前租户标识(如 TenantContextHolder.getCurrentTenantId())
- 初始化时向 targetDataSources 注册多个数据源(如 ds_tenant_a、ds_tenant_b),key 即租户 ID
- AOP 设置完租户 ID 后,后续 MyBatis/JDBC 操作自动命中对应数据源 —— 这个切换是透明的,不依赖反射,靠 Spring 数据源路由机制完成
四、注意事项与避坑点
- 反射读取字段时,记得 setAccessible(true),尤其对 private 字段
- 租户 ID 必须全局唯一且稳定,避免用随机数或会变的业务字段(如用户名)作 key
- 务必在方法结束后清理 ThreadLocal(建议用 try-finally 或 AOP afterReturning + afterThrowing),防止线程复用导致租户污染
- 若用连接池(如 HikariCP),每个租户数据源应独立配置,避免连接混用

















