Spring series-MybatisPlus

1 Introduction

In the past, it was written in the way of Mybatis XML supporting, and MybaitsPlus is an enhanced version of Mybatis, leaving out the content of the XML file. Although the latter reduces a lot of tedious SQL content writing, but also, for complex SQL scenarios, the flow-like SQL generation is still not intuitive to write XML.

2. Features

  • No intrusion : only make enhancements without making changes, the introduction of it will not affect the existing project, it is as smooth as silk
  • Low loss : basic CURD will be automatically injected at startup, performance is basically no loss, direct object-oriented operation
  • Powerful CRUD operations : built-in general Mapper, general service, only a small amount of configuration can realize most of the CRUD operations of a single table, and a more powerful condition builder to meet various usage requirements
  • Support Lambda form call : through Lambda expressions, it is convenient to write various query conditions, no need to worry about writing wrong fields
  • Supports automatic generation of primary keys : supports up to 4 primary key strategies (contains a distributed unique ID generator-Sequence), which can be freely configured to perfectly solve primary key problems
  • Support ActiveRecord mode : Support ActiveRecord form call, entity classes only need to inherit Model class to perform powerful CRUD operations
  • Support custom global general operations : support global general method injection (Write once, use anywhere)
  • Built-in code generator : Use code or Maven plug-in to quickly generate Mapper, Model, Service, Controller layer code, support template engine, and more custom configurations for you to use
  • Built-in paging plug-in : Based on MyBatis physical paging, developers do not need to care about specific operations. After configuring the plug-in, writing paging is equivalent to ordinary List query
  • Paging plug-in supports multiple databases : supports MySQL, MariaDB, Oracle, DB2, H2, HSQL, SQLite, Postgre, SQLServer and other databases
  • Built-in performance analysis plug-in : Sql statement and its execution time can be output. It is recommended to enable this function during development and testing to quickly detect slow queries
  • Built-in global interception plug-in : Provides full table delete and update operation intelligent analysis and blocking, and can also customize interception rules to prevent misoperation

3. Mybatis hierarchy

Spring series-MybatisPlus

4. Maven dependencies

  <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. Plug-in mechanism

The plug-in is convenient to customize additional functions, such as paging, fuzzy query processing special characters. When we write a plug-in, in addition to the plug-in class implementing the Interceptor interface, we also need to mark the plug-in's interception point through annotations. The so-called interception point refers to the method that the plug-in can intercept. The methods that MyBatis allows to intercept are as follows:

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

5.1 Principle

(1) Configuration loading plugin

com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration: Load any custom plug-in class that implements 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;
  }

 Case plugin 


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 Actual combat

(1) Paging plugin

   @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) Fuzzy query processing special characters

         @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. Cache Mechanism

7. Session mechanism

9. Configuration

10. FAQ

Q: How does Mybatis prevent SQL injection?

A: When Mybatis uses #{?}, sql will be pre-compiled (select from t_user where a = ?); when ${?} is used, sql will not be pre-compiled (select from t_user where a = 1), In this case, there is a risk of SQL injection. After testing, SQL injection cannot inject two SQL statements, and an error will be reported. Can only inject something like (select * from t_user where a = 1 or 1 = 1), the injected variable is "'1' or 1 = 1";
Spring series-MybatisPlus

Q: In addition to the configuration method, can QueryWrap view the assembled SQL through breakpoints?

A: No

Q: Is it better to use Date or LocalDateTime to receive database timestamp?

Q: In the inherited BaseMapper<User> file, does MybatisPlus support any custom object return, such as Person?

Q: How does the Mybatis-plus selectOne function limit the return of one piece of data?

11. End

This is the end of this article. Thank you for seeing the last friends. They are all seen at the end. Just give me a thumbs up before leaving. If there is anything wrong, please correct me.

Guess you like

Origin blog.51cto.com/14969174/2542687