开发者

mybatis-plus的多租户不同版本实现的两种方式

目录
  • MyBATis plus 3.4.0后的拓展插件
    • InnerInterceptor
  • Mybatis plus 3.4.0 之前版本的自定义实现

    Mybatis plus 3.4.0后的拓展插件

    在mybatis- plus 3.4.0 版本之后可以,官方提供了MybatisPlusInterceptor 拓展插件

    该插件是核心插件,目前代理了 Executor#query 和 Executor#update 和 StatementHandler#prepare 方法

    InnerInterceptor

    我们提供的插件都将基于此接口来实现功能

    目前已有的功能:

    • 自动分页: PaginationInnerInterceptor
    • 多租户: TenantLineInnerInterceptor
    • 动态表名: DynamicTableNameInnerInterceptor
    • 乐观锁: OptimisticLockerInnerInterceptor
    • sql 性能规范: IllegalSQLInnerInterceptor
    • 防止全表更新与删除: blockAttackInnerInterceptor

    注意:

    使用多个功能需要注意顺序关系,建议使用如下顺序

    • 多租户,动态表名
    • 分页,乐观锁
    • sql 性能规范,防止全表更新与删除

    总结: 对 sql 进行单次改造的优先放入,不对 sql 进行改造的最后放入

    如果是mybatis plus 3.4.0 之后的版本可以直接使用多租户插件

    官方示例:

    package com.baomidou.mybatisplus.samples.tenant.config;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
    import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
    
    import net.sf.jsqlparser.expression.Expression;
    import net.sf.jsqlparser.expression.LongValue;
    
    /**
     * @author miemie
     * @since 2018-08-10
     */
    @Configuration
    @MapperScan("com.baomidou.mybatisplus.samples.tenant.mapper")
    public class MybatisPlusConfig {
    
        /**
         * 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
         */
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
                @Override
                public Expression getTenantId() {
                    // 这里可以写自己系统的获取租户id的方法   比如下面自定义方法
                    // return new LongValue(CurrentUserUtils.getTenantId());
                    return new LongValue(1);
                }
                
                /**
                 * 获取租户字段名
                 * <p>
                 * 默认字段名叫: tenant_id
                 *
                 * @return 租户字段名
                 */
                 @Override
                default String getTenantIdColumn() {
                    // 如果该字段你不是固定的,请使用 SqlInjectionUtils.check 检查安全性
                    return "tenant_id";
                }
            
                 /**
                  * 根据表名判断是否忽略拼接多租户条件
                  * <p>
                  * 默认都要进行解析并拼接多租户条件
                  *
                  * @param tableName 表名
                  * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
                  */
                @Override
                public boolean ignoreTable(String tableName) {
                   String[] arr = new String[]{
                           "susCode",
                           "suName",
                           "suSex",
                           "suAge"
                   };
                   return ArrayUtil.contains(arr, tableName);
                }
            }));
            // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
            // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
    //        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
            return interceptor;
        }
    
    //    @Bean
    //    public ConfigurationCustomizer configurationCustomizer() {
    //        return configuration -> configuration.setUseDeprecatedExecutor(false);
    //    }
    }

    Mybatis plus 3.4.0 之前版本的自定义实现

    因为本次开发的模块需要与系统的老版本兼容,使用的版本为3.1.1则出现了不能够使用官方自带的增强插件,所以需要自己通过实现拦截器来达到类似效果。

    写的过程可以借鉴分页插件PaginationInterceptor

    mybatis-plus的多租户不同版本实现的两种方式

    mybatis-plus的多租户不同版本实现的两种方式

    自定义Interceptor 继承AbstractSqlPAserHandler(SQL 解析处理器) 实现Inteceptor(拦截器)

    拦截时机与PaginationInterceptor 一样,在StatementHandler的prepare 进行处理 照搬即可

    mybatis-plus的多租户不同版本实现的两种方式

    这个 Interceptor 接口定义了三个方法:

    • intercept(Invocation invocation):这是拦截器的核心方法,它允许拦截器在执行目标方法之前或之后添加自定义逻辑。当拦截器被激活时,intercept 方法会被调用。拦截器可以通过 Invocation 对象访问目标方法的参数、目标对象等信息,并且可以通过调用 invocation.proceed() 来继续执行目标方法,或者在此之前/之后添加自定义逻辑。
    • plugin(Object target):这个方法用于包装目标对象,返回一个代理对象。拦截器通过调用此方法来生成一个目标对象的代理,代理对象中包含了拦截器的逻辑。这样,在调用目标对象的方法时,拦截器的逻辑就会被触发。
    • setProperties(Properties properties):这个方法用于设置拦截器的属性。拦截器可以通过这个方法接收外部传入的配置参数,以便在运行时动态调整其行为。

    这三个方法结合起来,允许开发者在 MyBatis 中实现自定义的拦截器逻辑,例如添加日志记录、权限控制、性能监控等功能。

    这里plugin()和setProperties()照搬即可

    主要对intercept 进行处理

    package com.panpass.rebate.service.config;
    
    import cn.hutool.core.collection.CollectionUtil;
    import cn.hutool.core.util.ReflectUtil;
    import com.alibaba.fastjson.JSON;
    import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
    import com.baomidou.mybatisplus.extension.handlers.AbstractSqlParserHandler;
    import com.panpass.rebate.service.utils.CurrentRebateUserUtil;
    import lombok.extern.slf4j.Slf4j;
    import net.sf.jsqlparser.JSQLParserException;
    import net.sf.jsqlparser.expression.LongValue;
    import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
    import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
    import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
    import net.sf.jsqlparser.expression.operators.relational.ItemsList;
    import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
    import net.sf.jsqlparser.parser.CCJSqlParserUtil;
    import net.sf.jsqlparser.statement.Statement;
    import net.sf.jsqlparser.statement.select.*;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.ibatis.cache.CacheKey;
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.executor.statement.BaseStatementHandler;
    import org.apache.ibatis.executor.statement.PreparedStatementHandler;
    import org.apache.ibatis.executor.statement.RoutingStatementHandler;
    import org.apache.ibatis.executor.statement.StatementHandler;
    import org.apache.ibatis.mapping.BoundSql;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.plugin.*;
    import org.apache.ibatis.reflection.MetaObject;
    import org.apache.ibatis.reflection.SystemMetaObject;
    import org.apache.ibatis.session.ResultHandler;
    import org.apache.ibatis.session.RowBounds;
    import org.junit.jupiter.api.Order;
    import org.springframework.core.Ordered;
    import org.springframework.stereotype.Component;
    
    import Java.lang.reflect.Field;
    import java.sql.Connection;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Properties;
    
    import net.sf.jsqlparser.expression.Expression;
    import net.sf.jsqlparser.schema.Column;
    import net.sf.jsqlparser.schema.Table;
    import net.sf.jsqlparser.statement.delete.Delete;
    import net.sf.jsqlparser.statement.insert.Insert;
    import net.sf.jsqlparser.statement.update.Update;
    import org.apache.ibatis.plugin.Interceptor;
    import org.apache.ibatis.plugin.Intercepts;
    import org.apache.ibatis.plugin.Invocation;
    import org.apache.ibatis.plugin.Signature;
    
    
    
    /**
     * @Description:
     * @Author: potato
     * @Date: 2024/4/8  9:49
     */
    
    
    @Slf4j
    @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
    //@Component
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class DataScopeInterceptor  extends AbstractSqlParserHandler implements Interceptor {
    
        /**
         * 是否对XML中SQL 进行多租户增强
         *  false   不进行SQL增强
         *  true    进行SQL增强
         *  配置 可转移到nacos 中
         */
        private boolean openIngore = true;
    
        /**
         * 可以指定某些特定的XML中的SQL的方法进行手写,不通过该拦截器增强
         */
        private List<String> ingoreList;
    
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            Object target = invocation.getTarget();
            //确保只有拦截的目标对象是 StatementHandler 类型时才执行特定逻辑
            boolean flag = true;
            if (target instanceof StatementHandler) {
                StatementHandler statementHandler = (StatementHandler) target;
                StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(statementHandler, "delegate");
    
                // true 则需要SQL多租户增强
                try {
                    flag = isIngoreXML(invocation);
                } catch (Exception e) {
                    log.info("StatementHandler 解析出现问题,请注意查看"+e.getMessage());
    //                e.printStackTrace();
                }
                if (flag) {
                    // 获取 BoundSql 对象,包含原始 SQL 语句
                    BoundSql boundSql = statementHandler.getBoundSql();
                    String originalSql = boundSql.getSql();
                    String newSql = setEnvToStatement(originalSql);
                    // 使用MetaObject对象将新的SQL语句设置到BoundSql对象中
                    MetaObject metaObject = SystemMetaObject.forObject(boundSql);
                    metaObject.setValue("sql", newSql);
                }
    
            }
            // 执行SQL
            return invocation.proceed();
        }
    
        /**
         *
         *  根据xml名称判断是否需要进行xml 中SQL解析   可以拓展
         *
         * @param invocation
         * @return  fasle = 不进行SQL增强   true 进行SQL增强
         */
        public boolean isIngoreXML(Invocation invocation){
            // 不开启增强直接返回false
            if(!openIngore){
                return  false;
            }
            // 存在多层代理,SystemMetaObject获取不确定,  借鉴PaginationInterceptor 中获取Target的方法来获取
            StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
            MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
            // SQL 解析
    //        this.sqlParser(metaObject);
            MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
    
    //         ingoreList  为空,没有设置需要忽略增强的方法名   模拟存在配置了忽略的方法
    //         ingoreList =new ArrayList<>();
    //         ingoreList.add("RebateFlowBulkMapper.flowList");
            if(CollectionUtil.isEmpty(ingoreList)){
                return true;
            }
            // Resource 示例 :   file [D:\bjzx\project\rebate\rebate-dao\target\classes\mapper\plan\PlanConfigMapper.xml]   xml路径
            // id       示例 :   com.panpass.rebate.plan.persistent.mapper.PlanConfigMapper.getPageList
            // 确保Resource 来自xml,并且配置的存在忽略的方法名例如: PlanConfigMapper.getPageList,返回false  不进行增强处理;
            for (String menthodName : ingoreList) {
                if (mappedStatement.getResource().contains("xml") && mappedStatement.getId().contains(menthodName)) {
                    return false;
                }
            }
    
            return true;
        }
    
        public static void printFields(Object obj) {
            Class clazz = obj.getClass();
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                String fieldName = field.getName();
                Object fieldValue = null;
                try {
                    fieldValue = field.get(obj);
                } catch (IllegalAccessExceptiophpn e) {
                    e.printStackTrace();
                }
                System.out.println(fieldName + ": " + fieldValue);
            }
        }
    
        private String setEnvToStatement(String originalSql) {
            net.sf.jsqlparser.statement.Statement statement;
            try {
                statement = CCJSqlParserUtil.parse(originalSql);
            } catch (JSQLParserException e) {
                throw new RuntimeException("EnvironmentVariableInterceptor::SQL语句解析异常:"+originalSql);
            }
    
            if (statement instanceof Select) {
                //TODO需要递归处理
                Select select = (Select) statement;
                PlainSelect selectBody = (PlainSelect) select.getSelectBody();
                // 递归处理子查询
                processSubQuery(selectBody);
    
                return select.toString();
            }
    //        else if (statement instanceof Insert) {
    //            Insert insert = (Insert) statement;
    //            setEnvToInsert(insert);
    //
    //            return insert.toString();
    //        } else if (statement instanceof Update) {
    //            Update update = (Update) statement;
    //            Expression newWhereExpression = setEnvToWhereExpression(update.getWhere(),null);
    //            update.setWhere(newWhereExpression);
    //
    //            return update.toString();
    //        } else if (statement instanceof Delete) {
    //            Delete delete = (Delete) statement;
    //            Expression newWhereExpression = setEnvToWhereExpression(delete.getWhere(),null);
    //          PbeqfnLGM  delete.setWhere(newWhereExpression);
    //
    //            return delete.toString();
    //        }
            return originalSql;
        }
    
        private  void processSubQuery(PlainSelect selectBody ) {
    
                if(selectBody == null){
                    return;
                }
            if (selectBody.getFromItem() instanceof Table) {
                Expression newWhereExpression;
                if (selectBody.getJoins() == null || selectBody.getJoins().isEmpty()) {
                    newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), null);
                } else {
                    newWhereExpression = multipleTableJoinWhereExpression(selectBody);
                }
                selectBody.setWhere(newWhereExpression);
            } else {
                // 处理子查询
                SelectBody subSelectBody = ((SubSelect) selectBody.getFromItem()).getSelectBody();
                processSubQuery((PlainSelect) subSelectBody);
    //            selectBody.setFromItem((FromItem) processSubQuery((PlainSelect) subSelectBody));
            }
    //        return selectBody;
        }
    
        //非递查询
        private  String setEnvToStatement2(String originalSql) {
            net.sf.jsqlparser.statement.Statement statement;
            try {
                statement = CCJSqlParserUtil.parse(originalSql);
            } catch (JSQLParserException e) {
                throw new RuntimeException("EnvironmentVariableInterceptor::SQL语句解析异常:"+originalSql);
            }
            if (statement instanceof Select) {
                Select select = (Select) statement;
                PlainSelect selectBody = (PlainSelect) select.getSelectBody();
                if (selectBody.getFromItem() instanceof Table) {
                    Expression newWhereExpression;
                    if (selectBody.getJoins() == null || selectBody.getJoins().isEmpty()) {
                        newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), null);
                    } else {
                        // 如果是多表关联查询,在关联查询中新增每个表的环境变量条件
                        newpythonWhereExpression = multipleTableJoinWhereExpression(selectBody);
                    }
                    // 将新的where设置到Select中
                    selectBody.setWhere(newWhereExpression);
                } else if (selectBody.getFromItem() instanceof SubSelect) {
                    // 如果是子查询,在子查询中新增环境变量条件
                    // 当前方法只能处理单层子查询,如果有多层级的子查询的场景需要通过递归设置环境变量
                    SubSelect subSelect = (SubSelect) selectBody.getFromItem();
                    PlainSelect subSelectBody = (PlainSelect) subSelect.getSelectBody();
                    Expression newWhereExpression = setEnvToWhereExpression(subSelectBody.getWhere(), null);
                    subSelectBody.setWhere(newWhereExpression);
                }
    
                // 获得修改后的语句
                return select.toString();
            } else if (statement instanceof Insert) {
                Insert insert = (Insert) statement;
                setEnvToInsert(insert);
    
                return insert.toString();
            } else if (statement instanceof Update) {
                Update update = (Update) statement;
                Expression newWhereExpression = setEnvToWhereExpression(update.getWhere(),null);
                // 将新的where设置到Update中
                update.setWhere(newWhereExpression);
    
                return update.toString();
            } else if (statement instanceof Delete) {
                Delete delete = (Delete) statement;
                Expression newWhereExpression = setEnvToWhereExpression(delete.getWhere(),null);
                // 将新的where设置到delete中
                delete.setWhere(newWhereExpression);
    
                return delete.toString();
            }
            return originalSql;
        }
    
        /**
         * 将需要隔离的字段加入到SQL的Where语法树中
         * @param whereExpression SQL的Where语法树
         * @param alias 表别名
         * @return 新的SQL Where语法树
         */
        private Expression setEnvToWhereExpression(Expression whereExpression, String alias) {
            // 添加SQL语法树的一个where分支,并添加环境变量条件
    
            EqualsTo envEquals = new EqualsTo();
            envEquals.setLeftExpression(new Column(StringUtils.isNotBlank(alias) ? String.format("%s.tenant_id", alias) : "tenant_id"));
            envEquals.setRightExpression(new LongValue(CurrentRebateUserUtil.getTenantId() == null ? 1 :CurrentRebateUserUtil.getTenantId()));
            if (whereExpression == null){
                return envEquals;
            } else {
                AndExpression andExpression = new AndExpression(whereExpression,envEquals);
                // 将新的where条件加入到原where条件的右分支树
                andExpression.setRightExpression(envEquals);
                andExpression.setLeftExpression(whereExpression);
                return andExpression;
            }
        }
    
        /**
         * 多表关联查询时,给关联的所有表加入环境隔离条件
         * @param selectBody select语法树
         * @return 新的SQL Where语法树
         */
        private Expression multipleTableJoinWhereExpression(PlainSelect selectBody){
            Table mainTable = (Table) selectBody.getFromItem();
            String mainTableAlias = mainTable.getAlias().getName();
            // 将 t1.tenant_id = tenant_id 的条件添加到where中
            Expression newWhereExpression = setEnvToWhereExpression(selectBody.getWhere(), mainTableAlias);
            List<Join> joins = selectBody.getJoins();
            for (Join join : joins) {
                FromItem joinRightItem = join.getRightItem();
                if (joinRightItem instanceof Table) {
                    Table joinTable = (Table) joinRightItem;
                    String joinTableAlias = joinTable.getAlias().getName();
                    // 将每一个join的 tx.env = ENV 的条件添加到where中
                    newWhereExpression = setEnvToWhereExpression(newWhereExpression, joinTableAlias);
                }
            }
            return newWhereExpression;
        }
    
        /**
         * 新增数据时,插入tenant_id字段javascript
         * @param insert Insert 语法树
         */
        private void setEnvToInsertandroid(Insert insert) {
            // 添加tenant_id列
            List<Column> columns = insert.getColumns();
            for (Column column : columns) {
                //若存在,不进行处理
                if (column.getColumnName().equals("tenant_id")) {
                    return;
                }
            }
            columns.add(new Column("tenant_id"));
            // values中添加环境变量值
    
            // 获取插入值列表
            ItemsList itemsList = insert.getItemsList();
            if(itemsList instanceof MultiExpressionList ){
                List<ExpressionList> exprList = ((MultiExpressionList) itemsList).getExprList();
                for (ExpressionList expressionList : exprList) {
                    expressionList.getExpressions().add(new LongValue(CurrentRebateUserUtil.getTenantId() == null ? 1 :CurrentRebateUserUtil.getTenantId()));
                }
            } else if (itemsList instanceof SubSelect) {
                // 处理子查询
                log.info("子查询插入语句业务");
            }
    
        }
    
        //测试逻辑使用的
        public static void main(String[] args) throws JSQLParserException {
    
            String slectSql = "select * from (select id from (select 1 from table3 where id =2))";
    //        String s = setEnvToStatement(slectSql);
    
            String sql = "INSERT INTO my_table (column1, column2) VALUES (value1, value2), (value3, value4)";
    
            try {
                // 解析 INSERT 语句
                Statement statement = CCJSqlParserUtil.parse(sql);
    
                // 判断是否是 INSERT 语句
                if (statement instanceof Insert) {
                    Insert insert = (Insert) statement;
                    List<Column> columns = insert.getColumns();
    
                    columns.add(new Column("tenant_id"));
                    // 获取插入值列表
                    ItemsList itemsList = insert.getItemsList();
                    if(itemsList instanceof MultiExpressionList ){
                        itemsList =(MultiExpressionList)itemsList;
                        List<ExpressionList> exprList = ((MultiExpressionList) itemsList).getExprList();
                        for (ExpressionList expressionList : exprList) {
                            expressionList.getExpressions().add(new LongValue(CurrentRebateUserUtil.getTenantId() == null ? 1 :CurrentRebateUserUtil.getTenantId()));
                        }
                    } else if (itemsList instanceof SubSelect) {
                        // 处理子查询
                        log.info("子查询插入语句使用");
                    }
                }
    
            } catch (JSQLParserException e) {
                e.printStackTrace();
            }
        }
    
    
    
    
        /**
         * 生成拦截对象的代理
         *
         * @param target 目标对象
         * @return 代理对象
         */
        @Override
        public Object plugin(Object target) {
            if (target instanceof StatementHandler) {
                return Plugin.wrap(target, this);
            }
            return target;
        }
    
        /**
         * mybatis配置的属性
         *
         * @param properties mybatis配置的属性
         */
        @Override
        public void setProperties(Properties properties) {
    
        }
    
    }

    自对其逻辑进行修改适配自己系统项目,实现了递归支持多级子查询,对部分xml,方法级别 的SQL进行忽略增强。

    注册拦截器:

    package com.panpass.rebate.service.config;
    
    
    import org.apache.ibatis.plugin.Interceptor;
    import org.apache.ibatis.session.SqlSessionFactory;
    
    import org.springframework.beans.factory.annotation.Autowired;
    
    import org.springframework.context.annotation.Configuration;
    
    import javax.annotation.PostConstruct;
    import java.util.List;
    
    /**
     * @Description:
     * @Author: potato
     * @Date: 2024/4/3  13:49
     */
    
    @Configuration
    public class MyBatisConfig {
    
    
    
    
    
        @Autowired
        private List<SqlSessionFactory> sqlSessionFactoryList;
    
        // 只执行一次
        @PostConstruct
        public void addDefaultTimeInterceptor() {
            /**
             * Mybatis拦截器可以使用@Component注解也可以在这里进行配置
             * 在这里配置可以控制拦截器的执行顺序,所以注意去掉@Component注解
             */
            for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
                org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
                List<Interceptor> interceptors = configuration.getInterceptors();
                // 最后添加的会更早执行
                
                configuration.addInterceptor(new PaginationInterceptor());
                configuration.addInterceptor(new DataScopeInterceptor());
            }
        }
    
    }

    拦截器不用@Component 注解,否则会被自动配置扫描进拦截,导致存在多个拦截器,

    查看项目拦截器顺序可以在InterceptorChain对象查看

    mybatis-plus的多租户不同版本实现的两种方式

    或者org.apache.ibatis.session.Configuration 对象中的 interceptorChain 对象

    到此这篇关于mybatis-plus的多租户不同版本实现的两种方式的文章就介绍到这了,更多相关mybatis-plus多租户不同版本内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜