做SaaS系统的小伙伴肯定对多租户不陌生,博主最近使用MybatisPlus的多租户插件时发现一些不方便的地方,因启用多租户时,租户之间是完全隔离的,现在需要一位管理员权限的用户在特定菜单功能下不能有租户隔离。常用的几种方法有:
- 在mapper类上或者特定的mapper方法上加上
@InterceptorIgnore(tenantLine = "true")
注解
@InterceptorIgnore(tenantLine = "true")
public interface XXXMapper extends BaseMapper<XXX> {
List<XXX> selectList();
}
public interface XXXMapper extends BaseMapper<XXX> {
@InterceptorIgnore(tenantLine = "true")
List<XXX> selectList();
}
这种方式的缺点是如果在特定类上加注解就需要写两个mapper类,在方法上加的话需要创建两个Mapper接口实现BaseMapper,同时xml或者crud注解都需要写两份,这两种方式都比较繁琐。
- 在MybatisPlusInterceptor中将不需要租户隔离的表排除掉
但是这样的话多租户就失去了意义,直接行不通。
- 第三种就是博主做的这种基于自定义注解实现的,比官方的
@InterceptorIgnore
注解更加灵活,以下是所有代码
首先定义MybatisPlus的配置类创建一个拦截器MybatisPlusInterceptor
@Configuration
@Slf4j
public class MybatisPlusSaasConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
String tenantId = UserUtils.getLoginTenantId();
if(StringUtils.isAnyEmpty(tenantId)){
//默认租户id
tenantId = "0";
}
return new LongValue(tenantId);
}
@Override
public String getTenantIdColumn(){
return "tenant_id";
}
// 返回 true 表示不走租户逻辑
@Override
public boolean ignoreTable(String tableName) {
if (Objects.nonNull(MybatisTenantContext.get())){
log.info("是否做租户隔离:{}",MybatisTenantContext.get());
return MybatisTenantContext.get();
}
//默认租户隔离
return false;
}
}));
return interceptor;
}
}
定义一个ThreadLocal本地线程变量 MybatisTenantContext用于维护是否开启租户隔离变量
public class MybatisTenantContext {
private static final ThreadLocal<Boolean> TENANT_CONTEXT_THREAD_LOCAL = new ThreadLocal<>();
public static Boolean get() {
return TENANT_CONTEXT_THREAD_LOCAL.get();
}
public static void set(boolean isIgnore){
TENANT_CONTEXT_THREAD_LOCAL.set(isIgnore);
}
public static void clear(){
TENANT_CONTEXT_THREAD_LOCAL.remove();
}
}
自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface IgnoreTenant {
/**
* true为不做租户隔离 false为做租户隔离
* @return
*/
boolean isIgnore() default true;
}
注解切面类
ps:如果方法或者类上有其他注解用到租户隔离的,如:日志注解,字典翻译注解在point.proceed()后执行逻辑。需要注意切面类的执行顺序,一定要保证TenantIgnoreAspect 先执行,不然其它注解还是会有租户隔离的情况。可以在TenantIgnoreAspect 切面类加上@Order(Integer.MIN_VALUE)注解 保证执行顺序
@Aspect
@Slf4j
@Component
public class TenantIgnoreAspect {
/**
* 切入点
*/
@Pointcut("@within(com.xxx.IgnoreTenant) ||@annotation(com.xxx.IgnoreTenant)")
public void pointcut() {}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
try {
Class<?> targetClass = point.getTarget().getClass();
IgnoreTenant classIgnoreTenant = targetClass.getAnnotation(IgnoreTenant.class);
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
IgnoreTenant methodIgnoreTenant = method.getAnnotation(IgnoreTenant.class);
//判断类上是否有注解
boolean isClassAnnotated = AnnotationUtils.isAnnotationDeclaredLocally(IgnoreTenant.class, targetClass);
//判断方法上是否有注解
boolean isMethodAnnotated = Objects.nonNull(methodIgnoreTenant);
//如果类上有
if (isClassAnnotated) {
MybatisTenantContext.set(classIgnoreTenant.isIgnore());
}
//如果方法上有 以方法上的为主
if (isMethodAnnotated) {
MybatisTenantContext.set(methodIgnoreTenant.isIgnore());
}
Object result = point.proceed();
return result;
}finally {
MybatisTenantContext.clear();
}
}
以上就是所有代码
使用示例
@Service
@IgnoreTenant
public class DemoService {
@IgnoreTenant
public List<String> demoList(){
return this.list();
};
}
ps:如果一个方法中有多个查询,但是只有特定查询需要忽略租户隔离,可以使用下面的方式
@Service
public class DemoService {
public List<String> demoList(String name){
try {
MybatisTenantContext.set(true);
this.listByName(name);
return this.list();
}finally {
MybatisTenantContext.clear();
}
}
}
以上代码是手动维护本地线程变量 MybatisTenantContext,不可以使用注解,使用完一定要记得clear。以上就是通过自定义注解忽略多租户隔离的实现方式,如果有小伙伴有更好的方式欢迎评论区提供建议。