Mybatis-Plus-多租户体验

前沿

项目中有可能需要多租户功能,了解到Mybatis-Plus集成了多租户功能,因此尝试集成到项目中使用。

原理

Mybatis-Plus是通过配置多租户拦截实现多租户功能。

实现

创建租户内容


/**
 * 租户内容
 *
 * @author zhenghui
 * @date 2019-11-21
 */
public class TenantContext {
    private static Logger logger = LoggerFactory.getLogger(TenantContext.class);

    private static ThreadLocal currentTenant = new ThreadLocal<>();

    /**
     * 设置当前租户id
     *
     * @param tenantId
     */
    public static void setCurrentTenant(Long tenantId) {
        logger.debug("Setting tenant to " + tenantId);
        currentTenant.set(tenantId);
    }

    /**
     * 获取当前租户id
     */
    public static Long getCurrentTenant() {
        return currentTenant.get();
    }

    /**
     * 清除租户信息
     */
    public static void clear() {
        currentTenant.set(null);
    }
}
拦截租户信息
/**
 * 拦截租户信息
 *
 * @author zhenghui
 * @date 2019-11-21
 */
@Component
public class TenantInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        String tenantIdString = request.getParameter("tenantId");
        Long tenantId = 0L;
        if (!StringUtils.isBlank(tenantIdString)) {
            tenantId = Long.valueOf(tenantIdString);
        }
        TenantContext.setCurrentTenant(tenantId);
        return true;
    }

    @Override
    public void postHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
        TenantContext.clear();
    }
}

添加租户拦截器,因项目中使用了swagger,这里把swagger放行,否则会影响swagger的使用。

/**
 * 添加租户拦截器
 *
 * @author zhenghui
 * @date 2019-11-21
 */
@Configuration
public class TenantMvcConfig implements WebMvcConfigurer {

    @Autowired
    TenantInterceptor tenantInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tenantInterceptor)
                .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
    }
}

添加多租户预处理代码


/**
 * 租户处理器
 *
 * @Author zhenghui
 * @Date 2019-08-09 23:34
 */
@Slf4j
@Component
public class PreTenantHandler implements TenantHandler, CommandLineRunner {

    /**
     * 多租户标识
     */
    private static final String SYSTEM_TENANT_ID = "tenant_id";

    /**
     * 需要过滤的表
     */
    private static final HashSet IGNORE_TENANT_TABLES = new HashSet<>();

    @Override
    public void run(String... args) throws Exception {
        // 添加需要过滤多租户的表
        // IGNORE_TENANT_TABLES.add("");
    }

    @Override
    public Expression getTenantId(boolean where) {
        // 从当前系统上下文中取出当前请求的服务商ID,通过解析器注入到SQL中。
        Long tenantId = TenantContext.getCurrentTenant();
        log.debug("当前租户为{}", tenantId);
        if (tenantId == null) {
            return new LongValue(0L);
        }
        return new LongValue(tenantId);
    }


    /**
     * 租户字段名
     *
     * @return
     */
    @Override
    public String getTenantIdColumn() {
        return SYSTEM_TENANT_ID;
    }

    /**
     * 根据表名判断是否进行过滤
     * 忽略掉一些表:如租户表(sys_tenant)本身不需要执行这样的处理
     *
     * @param tableName
     * @return
     */
    @Override
    public boolean doTableFilter(String tableName) {
        return IGNORE_TENANT_TABLES.contains(tableName);
    }


}

添加多租户sql过滤


/**
 * 多租户过滤的方法
 *
 * @author zhenghui
 * @date 2019-11-22
 */
@Order(50)
@Slf4j
@Component
public class PreTenantSqlParserFilter implements ISqlParserFilter, CommandLineRunner {
    /**
     * 需要过滤的表
     */
    private static final HashSet IGNORE_TENANT_METHODS = new HashSet<>();

    @Override
    public void run(String... args) throws Exception {
        // 添加需要过滤多租户执行的方法

    }

    @Override
    public boolean doFilter(MetaObject metaObject) {
        MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
        //过滤指定mapper方法,不自动插入租户信息
        if (IGNORE_TENANT_METHODS.contains(ms.getId())) {
            return true;
        }
        return false;
    }

}

添加mybatis-plus配置


/**
 * MybatisPlus配置
 *
 * @author zhenghui
 * @date 2019-11-20
 */
@Configuration
public class MybatisPlusConfiguration {


    @Autowired
    private TenantHandler preTenantHandler;

    @Autowired
    private ISqlParserFilter preTenantSqlParserFilter;


    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();

        // SQL解析处理拦截:增加租户处理回调。
        List sqlParserList = new ArrayList<>();
        // 攻击 SQL 阻断解析器、加入解析链
        sqlParserList.add(new BlockAttackSqlParser());

        //Mybatis-Plus的插入多租户数据有问题,在set方法中赋值租户信息后,
        //底层没有判断又插入了一次,因此暂时屏蔽代码
        // 多租户拦截
//        TenantSqlParser tenantSqlParser = new TenantSqlParser();
//        tenantSqlParser.setTenantHandler(preTenantHandler);
//        sqlParserList.add(tenantSqlParser);
//        paginationInterceptor.setSqlParserFilter(preTenantSqlParserFilter);
        paginationInterceptor.setSqlParserList(sqlParserList);
        return paginationInterceptor;
    }

}

上面步骤已经可以实现多租户的功能,但是后面因为源码问题,已经将多租户功能注释。下面会分析问题。

问题

当前Mybatis-Plus实现多租户的方式还是有些问题,例如:

当前拦截的insert代码未考虑使用方主动设置租户id的内容,粗暴的直接新建租户列,导致insert重复列,插入数据失败。源码如下:


    /**
     * insert 语句处理
     */
    @Override
    public void processInsert(Insert insert) {
        if (tenantHandler.doTableFilter(insert.getTable().getName())) {
            // 过滤退出执行
            return;
        }
        insert.getColumns().add(new Column(tenantHandler.getTenantIdColumn()));
        if (insert.getSelect() != null) {
            processPlainSelect((PlainSelect) insert.getSelect().getSelectBody(), true);
        } else if (insert.getItemsList() != null) {
            // fixed github pull/295
            ItemsList itemsList = insert.getItemsList();
            if (itemsList instanceof MultiExpressionList) {
                ((MultiExpressionList) itemsList).getExprList().forEach(el -> el.getExpressions().add(tenantHandler.getTenantId(false)));
            } else {
                ((ExpressionList) insert.getItemsList()).getExpressions().add(tenantHandler.getTenantId(false));
            }
        } else {
            throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId");
        }
    }

之所有有主动赋值租户id,是因为很有可能在某些场景下,不是前端的数据无法拦截。有些是多线程场景,无法通过TenantContext赋值。
将此bug提交给官方后,被mybaits-plus开发人员直接关了,因此在项目中如果要使用mybaits-plus多租户的功能需要谨慎。

你可能感兴趣的:(Mybatis-Plus,SpringBoot)