Solon 基于 Mybatis-flex多租户实现

以前用过ruoyi的框架,基于mybatis-plus实现多租户,后来接触Mybatis-flex。多租户思路按照plus做走了很多弯路,写下来记录下。其实mybatis-flex实现多租户很简单,一切围绕
@Column(tenantId=ture)和TenantFaction来做工作就行了。

先说下目的
1.要可以对指定url实现多忽略多租户,因为程序是注册邀请制,所以登录时用户是不需要多租户验证的,所以对一些列登录url如auth/**要可以实现自动忽略
2.可以随时使用注解@TenantIgnore忽略方法多租户,其实mybatis-flex也提供了忽略方法,可是还是喜欢注解,而且自己实现更灵活
3.可以在执行特定方法时,以某个租户的身份去执行(如定时任务)

基于以上3点,需要如下几个文件来实现
1.上下文类,存储当前租户及可用性

/**
 * 租户上下文
 */
public class TenantContext {
    private static final ThreadLocal<Long> CURRENT_TENANT = new ThreadLocal<>();
    private static final ThreadLocal<Boolean> IGNORE_TENANT = ThreadLocal.withInitial(()->false);


    /**
     * 设置租户ID
     * @param tenantId
     */
    public static void setCurrentTenant(Long tenantId) {
        CURRENT_TENANT.set(tenantId);
    }

    /**
     * 获取租户ID
     * @return
     */
    public static Long getCurrentTenant() {
        return CURRENT_TENANT.get();
    }

    public static void clear() {
        CURRENT_TENANT.remove();
        IGNORE_TENANT.remove();
    }

    /**
     * 设置是否忽略租户验证
     */
    public static void setIgnore(boolean ignore) {
        IGNORE_TENANT.set(ignore);
    }

    /**
     * 是否忽略租户
     * @return
     */
    public static boolean isIgnore(){
       return IGNORE_TENANT.get();
    }
}

2.请求过滤TenantFilter,主要过滤url请求,设置租户上下文,具体业务场景,根据自己情况调整
 

/**
 * 租户请求过滤
 * 主要用于初始化 租户相关信息、租户角色认证、权限过滤等
 */
@Component(index = 1)
@Slf4j
public class TenantFilter implements Filter {

    @Inject("${tenant.path-ignore}")
    private List<String> ignorePathList;

    @Inject("${tenant.enable}")
    private Boolean tenantEnable;
    @Inject
    private SysUserApi sysUserApi;

    @Inject
    private SysTenantApi sysTenantApi;

    private final AntPathMatcher pathMatcher = new AntPathMatcher();


    @Override
    public void doFilter(Context ctx, FilterChain chain) throws Throwable {
        // 0. 放行OPTIONS预检请求
        if ("OPTIONS".equalsIgnoreCase(ctx.method())) {
            chain.doFilter(ctx);
            return;
        }


        SaBaseLoginUser userInfo = null;
        // 检查是否匹配忽略路径
        boolean shouldIgnore = ignorePathList.stream()
                .anyMatch(pattern -> pathMatcher.match(pattern,  ctx.path()));
        if(!tenantEnable || shouldIgnore){
            TenantContext.setIgnore(true);
            chain.doFilter(ctx);
            return ;
        }
        //1.得到header中的租户ID
        Long tenantId = ConvertUtils.toLong(ctx.header(TENANT_ID_HEADER),0L);

        if(tenantId.compareTo(0L)<=0){
            log.info("[TenantFilter] -> 此次请求未携带租户id ip:{},method:{},uri:{}", ctx.realIp(), ctx.method(), ctx.uri());
            ctx.render(CommonResult.error("请携带租户ID:Tenant-Id"));
            return;
        }
        TenantContext.setCurrentTenant(tenantId);

        userInfo = StpLoginUserUtil.getLoginUser();

        if (userInfo == null) {
            ctx.render(CommonResult.error("未登录"));
            return;
        }

        if (!userInfo.getTenantId().equals(tenantId)) {
            log.info("[TenantFilter] -> 无该租户访问权限user_id:{},tenant_id:{} ip:{},method:{},uri:{}", userInfo.getId(), tenantId, ctx.realIp(), ctx.method(), ctx.uri());
            ctx.render(CommonResult.error("无权访问此租户信息"));
            return;
        }

        if(tenantId.equals(TenantConstants.TENANT_SYSTEM_ID))
        {
            chain.doFilter(ctx); //直接放行
            return;
        }

        //验证租户状态
        SysTenantDTO tenant = sysTenantApi.getTenant(tenantId);
        if(tenant==null){
            ctx.render(CommonResult.error("不存在的租户"));
            return;
        }

        if(tenant.getExpireTime().getTime()>= DateUtils.getTimestampMillis()){
            ctx.render(CommonResult.error("您的账户所属单位已过期"));
            return;
        }
        chain.doFilter(ctx);

    }



}

3.租户Aop,使用solon的拦截器Interceptor 实现,结合注解TenantIgnore可实现对指定方法及其调用的其他子方法忽略租户

@Component
public class TenantAspect implements Interceptor {
    @Inject("${tenant.enable}")
    private boolean tenantEnable;

    /**
     * 设置忽略多租户
     * @param methodAnno
     * @param classAnno
     * @return
     */
    private boolean ignoreTenant(TenantIgnore methodAnno, TenantIgnore classAnno) {
        if(!tenantEnable){
            return true; //未开启
        }
        if(TenantContext.isIgnore()){
            return true;
        }

        TenantContext.setIgnore(methodAnno !=null || classAnno!=null);
        return TenantContext.isIgnore(); // 有忽略注解,则跳过
    }


    @Override
    public Object doIntercept(Invocation inv) throws Throwable {
        //1.读取忽略注解
        TenantIgnore methodAnnotation = inv.getMethodAnnotation(TenantIgnore.class);
        TenantIgnore classAnnotation  = inv.getTargetAnnotation(TenantIgnore.class);

        //2.忽略租户过滤
        ignoreTenant(methodAnnotation,classAnnotation);


        return inv.invoke();
    }
}

4.多租户忽略注解@TenantIgnore,使用@Inherited可以实现注解向下传递给调用的其他方法

@Around(TenantAspect.class)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface TenantIgnore {
}

 5.TenantFactory实现,getTenantIds中返回的是一个数字,这里如果上下文忽略租户,就直接返回一个空数组,这样mybatis-flex就不会过滤租户了

public class YnTenantFactory implements TenantFactory {
    @Inject("${tenant.table-ignore}")
    private List<String> ignoreTableList;
    @Override
    public Object[] getTenantIds() {

        if(TenantContext.isIgnore() || TenantContext.getCurrentTenant()==null){
            return new Object[0];
        }

        return new Object[]{TenantContext.getCurrentTenant()};
    }
}

6.注册TenantFactory

@Configuration
public class TenConfigure {

    @Bean
    public TenantFactory tenantFactory(){
        TenantFactory tenantFactory = new YnTenantFactory();
        return tenantFactory;
    }

}

7.执行工具,这个是参考的yudao框架实现的
 

public class TenantUtils {
    /**
     * 使用指定租户执行方法
     *
     * 制定后将不再忽略租户,执行完成后恢复原状态
     *
     * @param tenantId 租户编号
     * @param runnable 要执行的方法
     */
    public static void execute(Long tenantId, Runnable runnable) {
        Long oldTenantId = TenantContext.getCurrentTenant();
        Boolean oldIgnore = TenantContext.isIgnore();
        try {
            TenantContext.setCurrentTenant(tenantId);
            TenantContext.setIgnore(false);
            // 执行逻辑
            runnable.run();
        } finally {
            TenantContext.setCurrentTenant(oldTenantId);
            TenantContext.setIgnore(oldIgnore);
        }
    }

    /**
     * 使用指定租户,执行对应的逻辑
     *
     * 制定后将不再忽略租户,执行完成后恢复原状态
     *
     * @param tenantId 租户编号
     * @param callable 要执行的方法
     */
    public static <V> V execute(Long tenantId, Callable<V> callable) {
        Long oldTenantId = TenantContext.getCurrentTenant();
        Boolean oldIgnore = TenantContext.isIgnore();
        try {
            TenantContext.setCurrentTenant(tenantId);
            TenantContext.setIgnore(false);
            // 执行逻辑
            return callable.call();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            TenantContext.setCurrentTenant(oldTenantId);
            TenantContext.setIgnore(oldIgnore);
        }
    }

    /**
     * 忽略租户执行方法,一般用于多租户后台数据定时任务执行。
     * 如数据分拣、聚合等
     *
     * @param runnable 逻辑
     */
    public static void executeIgnore(Runnable runnable) {
        Boolean oldIgnore = TenantContext.isIgnore();
        try {
            TenantContext.setIgnore(true);
            // 执行逻辑
            runnable.run();
        } finally {
            TenantContext.setIgnore(oldIgnore);
        }
    }

    /**
     * 将多租户编号,添加到 header 中
     *
     * @param headers HTTP 请求 headers
     * @param tenantId 租户编号
     */
    public static void addTenantHeader(Map<String, String> headers, Long tenantId) {
        if (tenantId != null) {
            headers.put(TenantConstants.TENANT_ID_HEADER, tenantId.toString());
        }
    }
}

写法比较简单,记录下,下边简单介绍下使用方法
1.mybatis-flex中,只要给字段增加@Column(tenantId=true)就可以启用多租户的过滤条件,所以只需要在需要进行租户过滤的entity进行设置就行了
 

    @ApiModelProperty(value = "租户号", position = 29)
    @Column(tenantId = true)
    private Long tenantId;

2.忽略多租户,因为@TenantIgnore使用了@Inherited,可以向下传导,所以我们只需要在最顶级方法中设置即可

    /*可以是一个api*/   
 @TenantIgnore
    @Mapping("/iot/device/page")
    public CommonResult<PageResult<IotDevicePageResult>> page(IotDevicePageParam iotDevicePageParam) {
        return CommonResult.data(iotDeviceService.page(iotDevicePageParam));
    }

 /*u也可以是一个方法api*/   
@TenantIgnore
    public Long add(@Validated IotDeviceAddParam iotDeviceAddParam) {}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值