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

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

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

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类型的表达式,它的操作符为“=”。
    在这里插入图片描述
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
MybatisPlus是Mybatis的增强工具,它简化了Mybatis的开发流程,提供了许多实用的功能,例如自动生成代码、分页查询、乐观锁、多租户等。使用MybatisPlus可以大大提高开发效率,减少重复代码的编写。 MybatisPlus的优点: 1.简化了Mybatis的开发流程,提供了许多实用的功能。 2.提供了代码生成器,可以自动生成实体类、Mapper接口、XML文件等。 3.提供了通用的Mapper接口,可以直接继承使用,无需编写SQL语句。 4.提供了分页插件,可以方便地进行分页查询。 5.提供了乐观锁插件,可以避免并发更新时的数据冲突。 6.提供了多租户插件,可以方便地实现多租户系统。 使用MybatisPlus的步骤: 1.添加MybatisPlus的依赖。 2.配置数据源和MybatisPlus的插件。 3.编写实体类和Mapper接口。 4.使用MybatisPlus提供的方法进行数据库操作。 示例代码: ```java // 添加MybatisPlus的依赖 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.1</version> </dependency> // 配置数据源和MybatisPlus的插件 @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DruidDataSource(); } @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean(); sqlSessionFactory.setDataSource(dataSource()); sqlSessionFactory.setPlugins(new Interceptor[]{new OptimisticLockerInterceptor()}); return sqlSessionFactory.getObject(); } } // 编写实体类和Mapper接口 @Data public class User { private Long id; private String name; private Integer age; } public interface UserMapper extends BaseMapper<User> { } // 使用MybatisPlus提供的方法进行数据库操作 @Service public class UserService { @Autowired private UserMapper userMapper; public User getUserById(Long id) { return userMapper.selectById(id); } public List<User> getUserList() { return userMapper.selectList(null); } public int addUser(User user) { return userMapper.insert(user); } public int updateUser(User user) { return userMapper.updateById(user); } public int deleteUser(Long id) { return userMapper.deleteById(id); } } ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值