MybatisPlus源码解析4:租户拦截器

文章解析了MybatisPlus中的租户拦截器工作原理,包括拦截器如何在查询时动态添加租户信息,以及解析和构建SQL表达式的过程。重点介绍了`TenantLineInnerInterceptor`的`beforeQuery`方法和`buildTableExpression`方法的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文主要是对租户拦截器进行解析,不同租户之间的数据隔离的,一个租户的数据查询、更新、插入、删除操作都不会对他的租户的数据产生任何影响

1.项目结构

源码地址:https://github.com/lmhdsad/mybatis-plus-source-study/tree/main/mybatis-plus-plugin-tenant

项目结构:
在这里插入图片描述

2. 源码分析 MybatisPlusInterceptor

@SuppressWarnings({"rawtypes"})
@Intercepts(
    {
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
        @Signature(type = StatementHandler.class, method = "getBoundSql", args = {}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    }
)
public class MybatisPlusInterceptor implements Interceptor {

    @Setter
    private List<InnerInterceptor> interceptors = new ArrayList<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();
        if (target instanceof Executor) {
            final Executor executor = (Executor) target;
            Object parameter = args[1];
            boolean isUpdate = args.length == 2;
            MappedStatement ms = (MappedStatement) args[0];
            if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
                RowBounds rowBounds = (RowBounds) args[2];
                ResultHandler resultHandler = (ResultHandler) args[3];
                BoundSql boundSql;
                if (args.length == 4) {
                    boundSql = ms.getBoundSql(parameter);
                } else {
                    // 几乎不可能走进这里面,除非使用Executor的代理对象调用query[args[6]]
                    boundSql = (BoundSql) args[5];
                }
                for (InnerInterceptor query : interceptors) {
                    if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
                        return Collections.emptyList();
                    }
                    query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                }
                CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
                return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else if (isUpdate) {
                for (InnerInterceptor update : interceptors) {
                    if (!update.willDoUpdate(executor, ms, parameter)) {
                        return -1;
                    }
                    update.beforeUpdate(executor, ms, parameter);
                }
            }
        } else {
            // StatementHandler
            final StatementHandler sh = (StatementHandler) target;
            // 目前只有StatementHandler.getBoundSql方法args才为null
            if (null == args) {
                for (InnerInterceptor innerInterceptor : interceptors) {
                    innerInterceptor.beforeGetBoundSql(sh);
                }
            } else {
                Connection connections = (Connection) args[0];
                Integer transactionTimeout = (Integer) args[1];
                for (InnerInterceptor innerInterceptor : interceptors) {
                    innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
                }
            }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor || target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    public void addInnerInterceptor(InnerInterceptor innerInterceptor) {
        this.interceptors.add(innerInterceptor);
    }

    public List<InnerInterceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }

    /**
     * 使用内部规则,拿分页插件举个栗子:
     * <p>
     * - key: "@page" ,value: "com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor"
     * - key: "page:limit" ,value: "100"
     * <p>
     * 解读1: key 以 "@" 开头定义了这是一个需要组装的 `InnerInterceptor`, 以 "page" 结尾表示别名
     * value 是 `InnerInterceptor` 的具体的 class 全名
     * 解读2: key 以上面定义的 "别名 + ':'" 开头指这个 `value` 是定义的该 `InnerInterceptor` 属性需要设置的值
     * <p>
     * 如果这个 `InnerInterceptor` 不需要配置属性也要加别名
     */
    @Override
    public void setProperties(Properties properties) {
        PropertyMapper pm = PropertyMapper.newInstance(properties);
        Map<String, Properties> group = pm.group(StringPool.AT);
        group.forEach((k, v) -> {
            InnerInterceptor innerInterceptor = ClassUtils.newInstance(k);
            innerInterceptor.setProperties(v);
            addInnerInterceptor(innerInterceptor);
        });
    }
}
  1. 可以看到MybatisPlusInterceptor 实现了Mybatis的拦截器Interceptor,对StatementHandler和Executor进行了代理。
  2. MybatisPlusInterceptor 中定义了MP自己实现的内部拦截器interceptors,这些拦截器可供用户自己配置(拿来就用),或者重写少量关键的业务逻辑。
    在这里插入图片描述
    见名知意,其中的TenantLineInterceptor就是我们的主题
  3. 由于拦截的方法过多且复杂,intercept方法主要是判断了拦截的是哪个方法(判断方法签名逻辑:判断拦截对象的类型Executor or MappedStatement、判断是读还是写逻辑、判断参数的个数),然后调用相应的InnerInterceptor对其进行增强。
  4. 总的来说MybatisPlusInterceptor只是拦截器的总入口,具体实现的逻辑在InnerInterceptor里面。

3. 租户拦截器-TenantLineInnerInterceptor

原理:在执行Sql之前,通过CCJSqlParserUtil对Sql的语义进行了解析,然后拼接租户的信息,最终形成新的Sql,然后通过Mybatis执行Sql。需要注意的是,租户拦截器的整个执行流程是在业务Sql执行之前,只是对这个原始Sql进行了拼接而已

就拿查询来讲,在MybatisPlusInterceptor#intercept方法里面调用了query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)

3.1. 入口 - TenantLineInnerInterceptor#beforeQuery

@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) {
        return;
    }
    PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
    mpBs.sql(parserSingle(mpBs.sql(), null));
}
  1. 通过缓存判断是否需要对Sql进行拼接
  2. 对boundSql进行封装,以便更简单的通过反射对Sql和参数进行修改
  3. 调用parserSingle方法对boundSql.sql进行解析,最后修改到boundSql.sql

3.2. 解析流程 - JsqlParserSupport#parserSingle & #processParser

public String parserSingle(String sql, Object obj) {
    if (logger.isDebugEnabled()) {
        logger.debug("original SQL: " + sql);
    }
    try {
        Statement statement = CCJSqlParserUtil.parse(sql);
        return processParser(statement, 0, sql, obj);
    } catch (JSQLParserException e) {
        throw ExceptionUtils.mpe("Failed to process, Error SQL: %s", e.getCause(), sql);
    }
}

/**
  * 执行 SQL 解析
  *
  * @param statement JsqlParser Statement
  * @return sql
  */
 protected String processParser(Statement statement, int index, String sql, Object obj) {
     if (logger.isDebugEnabled()) {
         logger.debug("SQL to parse, SQL: " + sql);
     }
     if (statement instanceof Insert) {
         this.processInsert((Insert) statement, index, sql, obj);
     } else if (statement instanceof Select) {
         this.processSelect((Select) statement, index, sql, obj);
     } else if (statement instanceof Update) {
         this.processUpdate((Update) statement, index, sql, obj);
     } else if (statement instanceof Delete) {
         this.processDelete((Delete) statement, index, sql, obj);
     }
     sql = statement.toString();
     if (logger.isDebugEnabled()) {
         logger.debug("parse the finished SQL: " + sql);
     }
     return sql;
 }
  1. 打印了原始的Sql
  2. 通过CCJSqlParserUtil对sql进行了解析,并封装成Statement对象
  3. 打印了解析后完成的Sql
    在这里插入图片描述
  4. Insert、Select、Update、Delete分别对Statement进行处理

3.3. 构建表达式- BaseMultiTableInnerInterceptor#processPlainSelect & builderExpression

/**
 * 处理 PlainSelect
 */
protected void processPlainSelect(final PlainSelect plainSelect, final String whereSegment) {
    //#3087 github
    List<SelectItem> selectItems = plainSelect.getSelectItems();
    if (CollectionUtils.isNotEmpty(selectItems)) {
        selectItems.forEach(selectItem -> processSelectItem(selectItem, whereSegment));
    }

    // 处理 where 中的子查询
    Expression where = plainSelect.getWhere();
    processWhereSubSelect(where, whereSegment);

    // 处理 fromItem
    FromItem fromItem = plainSelect.getFromItem();
    List<Table> list = processFromItem(fromItem, whereSegment);
    List<Table> mainTables = new ArrayList<>(list);

    // 处理 join
    List<Join> joins = plainSelect.getJoins();
    if (CollectionUtils.isNotEmpty(joins)) {
        mainTables = processJoins(mainTables, joins, whereSegment);
    }

    // 当有 mainTable 时,进行 where 条件追加
    if (CollectionUtils.isNotEmpty(mainTables)) {
        plainSelect.setWhere(builderExpression(where, mainTables, whereSegment));
    }
}

 /**
 * 处理条件
 */
protected Expression builderExpression(Expression currentExpression, List<Table> tables, final String whereSegment) {
    // 没有表需要处理直接返回
    if (CollectionUtils.isEmpty(tables)) {
        return currentExpression;
    }
    // 构造每张表的条件
    List<Expression> expressions = tables.stream()
        .map(item -> buildTableExpression(item, currentExpression, whereSegment))
        .filter(Objects::nonNull)
        .collect(Collectors.toList());

    // 没有表需要处理直接返回
    if (CollectionUtils.isEmpty(expressions)) {
        return currentExpression;
    }

    // 注入的表达式
    Expression injectExpression = expressions.get(0);
    // 如果有多表,则用 and 连接
    if (expressions.size() > 1) {
        for (int i = 1; i < expressions.size(); i++) {
            injectExpression = new AndExpression(injectExpression, expressions.get(i));
        }
    }

    if (currentExpression == null) {
        return injectExpression;
    }
    if (currentExpression instanceof OrExpression) {
        return new AndExpression(new Parenthesis(currentExpression), injectExpression);
    } else {
        return new AndExpression(currentExpression, injectExpression);
    }
}

/**
  * 构建租户条件表达式
  *
  * @param table        表对象
  * @param where        当前where条件
  * @param whereSegment 所属Mapper对象全路径(在原租户拦截器功能中,这个参数并不需要参与相关判断)
  * @return 租户条件表达式
  * @see BaseMultiTableInnerInterceptor#buildTableExpression(Table, Expression, String)
  */
 @Override
 public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {
     if (tenantLineHandler.ignoreTable(table.getName())) {
         return null;
     }
     return new EqualsTo(getAliasColumn(table), tenantLineHandler.getTenantId());
 }
  1. processPlainSelect方法对Where中的子查询、连表进行了解析
  2. 如果主表不为空,调用builderExpression方法构建表达式。Sql的封装最终保存在Expression里面,通过toString方法可以获取到最终的sql。而BinaryExpression表达式中有左表达式leftExpression(比如:tenant_id)、右表达式(t1)和中间的条件操作(=),而表达式的拼接位:左+操作符+右。所以,最终拼接成了 tenant_id = “t1”。
  3. buildTableExpression通过buildTableExpression方法,调用tenantLineHandler获取租户的字段名称(左表达式),租户的值(右表达式)构建了EqualsTo类型的表达式,它的操作符为“=”。
    在这里插入图片描述
### MyBatisPlus 中分页拦截器租户拦截器的功能集成 #### 功能概述 MyBatisPlus 提供了强大的插件机制,其中 `MybatisPlusInterceptor` 是核心组件之一。通过它,可以轻松实现诸如分页、多租户等功能[^1]。 为了同时支持分页和多租户功能,可以通过配置 `MybatisPlusInterceptor` 并注册多个子拦截器来完成需求。以下是具体实现方式: --- #### 配置分页拦截器租户拦截器Spring Boot 项目中,通常会在启动类或者配置类中初始化并注册这些拦截器。以下是一个完整的示例代码: ```java import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 注册分页拦截器 PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(); interceptor.addInnerInterceptor(paginationInterceptor); // 注册多租户拦截器 TenantLineInnerInterceptor tenantLineInterceptor = new TenantLineInnerInterceptor(new CustomTenantHandler()); tenantLineInterceptor.setIgnoreTable(Arrays.asList("sys_user", "sys_role")); // 可选:忽略某些表的租户过滤 interceptor.addInnerInterceptor(tenantLineInterceptor); return interceptor; } } ``` 上述代码中: - 使用 `PaginationInnerInterceptor` 来启用分页功能[^3]。 - 使用 `TenantLineInnerInterceptor` 来启用多租户功能,并传入自定义的租户处理器 `CustomTenantHandler`[^2]。 --- #### 自定义租户处理逻辑 多租户的核心在于动态注入 SQL 的 WHERE 子句中的租户条件。下面是一个简单的租户处理器实现: ```java import com.baomidou.mybatisplus.core.handlers.TenantLineHandler; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.StringValue; import java.util.Collections; public class CustomTenantHandler implements TenantLineHandler { private static final String TENANT_ID_COLUMN_NAME = "tenant_id"; @Override public Expression getTenantId(boolean where) { // 返回当前线程绑定的租户 ID (假设存储在 ThreadLocal 中) String currentTenantId = CurrentUserContext.getTenantId(); // 获取当前用户的租户ID return new StringValue(currentTenantId); } @Override public String getTenantIdColumn() { return TENANT_ID_COLUMN_NAME; // 数据库中用于区分租户的字段名 } @Override public boolean openTENANT_LINE(String tableName) { // 判断哪些表需要开启多租户模式 return !tableName.startsWith("sys_"); // sys_* 表不参与多租户过滤 } } ``` 此代码片段展示了如何通过 `getTenantId()` 方法获取当前租户 ID,并将其作为 SQL 查询的一部分自动附加到查询语句中。 --- #### 示例场景 假设有如下数据库表结构: | 表名 | 字段 | |------------|----------------| | order_info | id, name, price, tenant_id | 当执行以下分页查询时: ```sql SELECT * FROM order_info LIMIT 0, 10; ``` 实际生成的 SQL 将变为: ```sql SELECT * FROM order_info WHERE tenant_id = 'current_tenant' LIMIT 0, 10; ``` 这表明分页和多租户功能已成功集成。 --- #### 注意事项 1. **版本兼容性** 确保使用的 MyBatisPlus 版本不低于 3.4.0,因为低版本可能缺少部分功能或存在 Bug。 2. **性能优化** 对于高并发场景,建议缓存租户信息以减少频繁调用开销。 3. **异常处理** 如果未设置租户 ID 或者租户 ID 不合法,则可能导致查询失败。因此需提前校验输入参数。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值