Spring系列——MybatisPlus

1. 前言

之前是Mybatis XML配套的方式去寫,而MybaitsPlus是Mybatis的加強版,拋去了XML文件內容。後者雖然減小了不少繁瑣的SQL內容編寫,可是一樣的,對於複雜的SQL場景,相似流似的SQL生成仍是沒有XML寫法直觀。java

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、DB二、H二、HSQL、SQLite、Postgre、SQLServer 等多種數據庫
  • 內置性能分析插件:可輸出 Sql 語句以及其執行時間,建議開發測試時啓用該功能,能快速揪出慢查詢
  • 內置全局攔截插件:提供全表 delete 、 update 操做智能分析阻斷,也可自定義攔截規則,預防誤操做

3. Mybatis層次結構

Spring系列——MybatisPlus

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 所容許攔截的方法以下:spring

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

5.1 原理sql

(1)配置加載插件數據庫

com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration:加載任何實現org.apache.ibatis.plugin.Interceptor的自定義插件類。apache

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#openSessionFromDataSourcesession

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#newExecutormybatis

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#pluginAllapp

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」;
Spring系列——MybatisPlus

Q:除了配置的方式,QueryWrap能夠經過打斷點的方式查看組裝的SQL嗎?

A:不能

Q:數據庫時間戳接收用Date仍是LocalDateTime接收好?

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

Q:Mybatis-plus selectOne函數如何限制返回一條數據?

11. 結尾

本文到這裏就結束了,感謝看到最後的朋友,都看到最後了,點個贊再走啊,若有不對之處還請多多指正。

相關文章
相關標籤/搜索