美文网首页干货分享
Spring系列——MybatisPlus

Spring系列——MybatisPlus

作者: 小白菜aaa | 来源:发表于2020-09-30 19:32 被阅读0次

    1. 前言

    以前是Mybatis XML配套的方式去写,而MybaitsPlus是Mybatis的增强版,抛去了XML文件内容。后者虽然减少了很多繁琐的SQL内容编写,但是同样的,对于复杂的SQL场景,类似流似的SQL生成还是没有XML写法直观。

    2. 特性

    • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
    • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
    • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
    • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
    • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
    • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
    • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
    • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
    • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
    • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
    • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
    • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

    3. Mybatis层次结构

    4. Maven依赖

      <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.4.0</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.2.23</version>
            </dependency>
    

    5. 插件机制

    插件是方便自定义额外功能,比如分页、模糊查询处理特殊字符。我们在编写插件时,除了需要让插件类实现 Interceptor 接口外,还需要通过注解标注 该插件的拦截点。所谓拦截点指的是插件所能拦截的方法,MyBatis 所允许拦截的方法如下:

    • Executor: update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
    • ParameterHandler: getParameterObject, setParameters
    • ResultSetHandler: handleResultSets, handleOutputParameters
    • StatementHandler: prepare, parameterize, batch, update, query

    5.1 原理

    (1)配置加载插件

    com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration:加载任何实现org.apache.ibatis.plugin.Interceptor的自定义插件类。

    
    public MybatisPlusAutoConfiguration(MybatisPlusProperties properties,
                                            ObjectProvider<Interceptor[]> interceptorsProvider,
                                            ObjectProvider<TypeHandler[]> typeHandlersProvider,
                                            ObjectProvider<LanguageDriver[]> languageDriversProvider,
                                            ResourceLoader resourceLoader,
                                            ObjectProvider<DatabaseIdProvider> databaseIdProvider,
                                            ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider,
                                            ObjectProvider<List<MybatisPlusPropertiesCustomizer>> mybatisPlusPropertiesCustomizerProvider,
                                            ApplicationContext applicationContext) {
            this.properties = properties;
            this.interceptors = interceptorsProvider.getIfAvailable();
            this.typeHandlers = typeHandlersProvider.getIfAvailable();
            this.languageDrivers = languageDriversProvider.getIfAvailable();
            this.resourceLoader = resourceLoader;
            this.databaseIdProvider = databaseIdProvider.getIfAvailable();
            this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
            this.mybatisPlusPropertiesCustomizers = mybatisPlusPropertiesCustomizerProvider.getIfAvailable();
            this.applicationContext = applicationContext;
        }
    
    
    @Bean
    @ConditionalOnMissingBean
     public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
      // ...
      if (!ObjectUtils.isEmpty(this.interceptors)) {
         factory.setPlugins(this.interceptors);
      }
      // ...
    }
    

    (2)Executor

    org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource

    
    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
          // ...
          // 创建Executor
          final Executor executor = configuration.newExecutor(tx, execType);
          return new DefaultSqlSession(configuration, executor, autoCommit);
        } 
        catch (Exception e) {...} 
        finally {...}
      }
    

    com.baomidou.mybatisplus.core.MybatisConfiguration#newExecutor

    
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
            if (useDeprecatedExecutor) {
                executorType = executorType == null ? defaultExecutorType : executorType;
                executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
                Executor executor;
                if (ExecutorType.BATCH == executorType) {
                    executor = new MybatisBatchExecutor(this, transaction);
                } else if (ExecutorType.REUSE == executorType) {
                    executor = new MybatisReuseExecutor(this, transaction);
                } else {
                    executor = new MybatisSimpleExecutor(this, transaction);
                }
                if (cacheEnabled) {
                    executor = new MybatisCachingExecutor(executor);
                }
                // 植入插件
                executor = (Executor) interceptorChain.pluginAll(executor);
                return executor;
            }
            return super.newExecutor(transaction, executorType);
        }
    

    org.apache.ibatis.plugin.InterceptorChain#pluginAll

    public Object pluginAll(Object target) {
        // 遍历拦截器
        for (Interceptor interceptor : interceptors) {
          target = interceptor.plugin(target);
        }
        return target;
      }
    

    案例插件

    
    public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
    

    org.apache.ibatis.plugin.Plugin#wrap

    
    public static Object wrap(Object target, Interceptor interceptor) {
        // 获取@Intercepts注解的信息,每一个@Signature注解圈定一个方法
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        // 获取目标类实现的接口
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
          // JDK动态代理,调用invoke方法
          return Proxy.newProxyInstance(
              type.getClassLoader(),
              interfaces,
              new Plugin(target, interceptor, signatureMap));
        }
        return target;
      }
    

    org.apache.ibatis.plugin.Plugin#invoke

    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          Set<Method> methods = signatureMap.get(method.getDeclaringClass());
          if (methods != null && methods.contains(method)) {
            // 如何方法声明的类包含@Signature声明的方法,则调用拦截器
            return interceptor.intercept(new Invocation(target, method, args));
          }
          return method.invoke(target, args);
        } catch (Exception e) {
          throw ExceptionUtil.unwrapThrowable(e);
        }
      }
    

    (3)StatementHandler

    com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor#doQuery

    
    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
            Statement stmt = null;
            try {
                Configuration configuration = ms.getConfiguration();
                // 创建StatementHandler
                StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
                stmt = prepareStatement(handler, ms.getStatementLog(), false);
                return stmt == null ? Collections.emptyList() : handler.query(stmt, resultHandler);
            } finally {
                closeStatement(stmt);
            }
        }
    

    org.apache.ibatis.session.Configuration#newStatementHandler

    
    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
        // 植入插件
        statementHandler = (StatementHandler) 
    interceptorChain.pluginAll(statementHandler);
        return statementHandler;
      }
    

    (4)ParameterHandler

    org.apache.ibatis.executor.statement.BaseStatementHandler#BaseStatementHandler

    
    protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        // ...
        // 创建ParameterHandler
        this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
        // 创建ResultSetHandler
        this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
      }
    

    org.apache.ibatis.session.Configuration#newParameterHandler

    
    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        // 植入插件
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
      }
    

    (5)ResultHandler

    org.apache.ibatis.session.Configuration#newResultSetHandler

    
    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
          ResultHandler resultHandler, BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        // 植入插件
        resultSetHandler = (ResultSetHandler) 
    interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
      }
    

    4.2 实战

    (1)分页插件

       @Bean
        public PaginationInnerInterceptor paginationInnerInterceptor() {
            PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
            paginationInnerInterceptor.setDbType(DbType.MYSQL);
            return paginationInnerInterceptor;
        }
     
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor(ObjectProvider<List<InnerInterceptor>> innerInterceptorProviders) {
            MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
            List<InnerInterceptor> interceptors = innerInterceptorProviders.getIfAvailable();
            if (!CollectionUtils.isEmpty(interceptors)) {
                for (InnerInterceptor innerInterceptor : interceptors) {
                    mybatisPlusInterceptor.addInnerInterceptor(innerInterceptor);
                }
            }
            return mybatisPlusInterceptor;
        }
    

    (2)模糊查询处理特殊字符

             @Bean
        public FuzzyQueryInterceptor fuzzyQueryInterceptor() {
            return new FuzzyQueryInterceptor();
        }
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import org.apache.ibatis.cache.CacheKey;
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.mapping.BoundSql;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.plugin.*;
    import org.apache.ibatis.session.ResultHandler;
    import org.apache.ibatis.session.RowBounds;
    import priv.whh.std.boot.mybatis.plus.util.EscapeUtil;
     
    import java.lang.reflect.InvocationTargetException;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Properties;
    import java.util.Set;
     
    /**
     * 模糊查询拦截器方法,处理模糊查询中包含特殊字符(_、%、\)
     *
     */
    @Intercepts({
            @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 FuzzyQueryInterceptor implements Interceptor {
        private static final String LIKE = " like ";
        private static final String QUESTION_MARK = "?";
        private static final String LIKE_WITH_QUESTION_MARK = " like ?";
        private static final String UNDERLINE = "_";
        private static final String PERCENT = "%";
        private static final String EW = "ew";
        private static final String EW_PARAM_NAME_VALUE_PAIRS = "ew.paramNameValuePairs.";
        private static final String DOUBLE_SLASH = "\\";
        private static final String DOUBLE_SLASH_WITH_SPOT = "\\.";
        private static final String DOUBLE_SLASH_WITH_QUESTION_MARK = "\\?";
     
        @Override
        public Object intercept(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
            // 拦截sql
            Object[] args = invocation.getArgs();
            MappedStatement statement = (MappedStatement) args[0];
            Object parameterObject = args[1];
            BoundSql boundSql = statement.getBoundSql(parameterObject);
            String sql = boundSql.getSql();
            // 处理特殊字符
            Object param = modifyLikeSql(sql, parameterObject, boundSql);
            args[1] = param;
            // 返回
            return invocation.proceed();
        }
     
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
     
        @Override
        public void setProperties(Properties properties) {
     
        }
     
        @SuppressWarnings("unchecked")
        public static Object modifyLikeSql(String sql, Object parameterObject, BoundSql boundSql) {
            boolean paramJudge = parameterObject instanceof HashMap || parameterObject instanceof String;
            if (!paramJudge) {
                return parameterObject;
            }
            if (!sql.toLowerCase().contains(LIKE) || !sql.toLowerCase().contains(QUESTION_MARK)) {
                return parameterObject;
            }
            // 获取关键字的个数(去重)
            String[] strList = sql.split(DOUBLE_SLASH_WITH_QUESTION_MARK);
            Set<String> keyNames = new HashSet<>();
            for (int i = 0; i < strList.length; i++) {
                if (strList[i].toLowerCase().contains(LIKE)) {
                    String keyName = boundSql.getParameterMappings().get(i).getProperty();
                    keyNames.add(keyName);
                }
            }
            // 对关键字进行特殊字符“清洗”,如果有特殊字符的,在特殊字符前添加转义字符(\)
            for (String keyName : keyNames) {
                HashMap<String, Object> parameter;
                if (parameterObject instanceof HashMap) {
                    parameter = (HashMap<String, Object>) parameterObject;
                } else {
                    parameter = new HashMap<>(2);
                    parameter.put(keyName, parameterObject);
                }
                if (keyName.contains(EW_PARAM_NAME_VALUE_PAIRS) && sql.toLowerCase().contains(LIKE_WITH_QUESTION_MARK)) {
                    // 第一种情况:在业务层进行条件构造产生的模糊查询关键字
                    QueryWrapper<Object> wrapper = (QueryWrapper<Object>) parameter.get(EW);
                    parameter = (HashMap<String, Object>) wrapper.getParamNameValuePairs();
     
                    String[] keyList = keyName.split(DOUBLE_SLASH_WITH_SPOT);
                    // ew.paramNameValuePairs.MPGENVAL1,截取字符串之后,获取第三个,即为参数名
                    Object a = parameter.get(keyList[2]);
                    boolean judge = a instanceof String && (a.toString().contains(UNDERLINE)
                            || a.toString().contains(DOUBLE_SLASH) || a.toString().contains(PERCENT));
                    if (judge) {
                        parameter.put(keyList[2],
                                PERCENT + EscapeUtil.escapeChar(a.toString().substring(1, a.toString().length() - 1)) + PERCENT);
                    }
                } else if (!keyName.contains(EW_PARAM_NAME_VALUE_PAIRS) && sql.toLowerCase().contains(LIKE_WITH_QUESTION_MARK)) {
                    // 第二种情况:未使用条件构造器,但是在service层进行了查询关键字与模糊查询符`%`手动拼接
                    Object a = parameter.get(keyName);
                    boolean judge = a instanceof String && (a.toString().contains(UNDERLINE)
                            || a.toString().contains(DOUBLE_SLASH) || a.toString().contains(PERCENT));
                    if (judge) {
                        parameter.put(keyName,
                                PERCENT + EscapeUtil.escapeChar(a.toString().substring(1, a.toString().length() - 1)) + PERCENT);
                    }
                } else {
                    // 第三种情况:在Mapper类的注解SQL或者XML中进行了模糊查询的拼接
                    Object a = parameter.get(keyName);
                    boolean judge = a instanceof String && (a.toString().contains(UNDERLINE)
                            || a.toString().contains(DOUBLE_SLASH) || a.toString().contains(PERCENT));
                    if (judge) {
                        parameter.put(keyName, EscapeUtil.escapeChar(a.toString()));
                        if (parameterObject instanceof String) {
                            parameterObject = EscapeUtil.escapeChar(a.toString());
                        }
                    }
                }
            }
            return parameterObject;
        }
    }
    

    6. 缓存机制

    7. 会话机制

    9. 配置

    10. FAQ

    Q:Mybatis如何做到防SQL注入?

    A: Mybatis 使用 #{?} 时,会进行sql的预编译(select * from t_user where a = ?);使用${?}时,不会进行sql的预编译(select * from t_user where a = 1),这样的话,会有sql注入的风险。经测试,sql注入无法注入两条sql语句,会报错。只能注入类似(select * from t_user where a = 1 or 1 = 1), 注入的变量为 “‘1’ or 1 = 1”;


    Q:除了配置的方式,QueryWrap可以通过打断点的方式查看组装的SQL吗?

    A:不能

    Q:数据库时间戳接收用Date还是LocalDateTime接收好?

    Q:在继承BaseMapper<User>文件中, MybatisPlus是否支持任意自定义对象返回,如返回Person?

    Q:Mybatis-plus selectOne函数如何限制返回一条数据?

    11. 结尾

    本文到这里就结束了,感谢看到最后的朋友,都看到最后了,点个赞再走啊,如有不对之处还请多多指正。

    相关文章

      网友评论

        本文标题:Spring系列——MybatisPlus

        本文链接:https://www.haomeiwen.com/subject/fnfpuktx.html