做SaaS系统的小伙伴肯定对多租户不陌生,博主最近使用MybatisPlus的多租户插件时发现一些不方便的地方,因启用多租户时,租户之间是完全隔离的,现在需要一位管理员权限的用户在特定菜单功能下不能有租户隔离。常用的几种方法有:

  1. 在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注解都需要写两份,这两种方式都比较繁琐。

  1. 在MybatisPlusInterceptor中将不需要租户隔离的表排除掉

但是这样的话多租户就失去了意义,直接行不通。

  1. 第三种就是博主做的这种基于自定义注解实现的,比官方的 @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。以上就是通过自定义注解忽略多租户隔离的实现方式,如果有小伙伴有更好的方式欢迎评论区提供建议。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐