mybatis-plus源碼分析之sql注入器

微信公衆號「後端進階」,專一後端技術分享:Java、Golang、WEB框架、分佈式中間件、服務治理等等。
老司機傾囊相授,帶你一路進階,來不及解釋了快上車!

mybatis-plus是徹底基於mybatis開發的一個加強工具,它的設計理念是在mybatis的基礎上只作加強不作改變,爲簡化開發、提升效率而生,它在mybatis的基礎上增長了不少實用性的功能,好比增長了樂觀鎖插件、字段自動填充功能、分頁插件、條件構造器、sql注入器等等,這些在開發過程當中都是很是實用的功能,mybatis-plus可謂是站在巨人的肩膀上進行了一系列的創新,我我的極力推薦。下面我會詳細地從源碼的角度分析mybatis-plus(下文簡寫成mp)是如何實現sql自動注入的原理。java

溫故知新

咱們回顧一下mybatis的Mapper的註冊與綁定過程,我以前也寫過一篇「Mybatis源碼分析之Mapper註冊與綁定」,在這篇文章中,我詳細地講解了Mapper綁定的最終目的是將xml或者註解上的sql信息與其對應Mapper類註冊到MappedStatement中,既然mybatis-plus的設計理念是在mybatis的基礎上只作加強不作改變,那麼sql注入器必然也是在將咱們預先定義好的sql和預先定義好的Mapper註冊到MappedStatement中。程序員

如今我將Mapper的註冊與綁定過程用時序圖再梳理一遍:spring

解析一下這幾個類的做用:sql

  • SqlSessionFactoryBean:繼承了FactoryBean和InitializingBean,符合spring loc容器bean的基本規範,可在獲取該bean時調用getObject()方法到SqlSessionFactory。
  • XMLMapperBuilder:xml文件解析器,解析Mapper對應的xml文件信息,並將xml文件信息註冊到Configuration中。
  • XMLStatementBuilder:xml節點解析器,用於構建select/insert/update/delete節點信息。
  • MapperBuilderAssistant:Mapper構建助手,將Mapper節點信息封裝成statement添加到MappedStatement中。
  • MapperRegistry:Mapper註冊與綁定類,將Mapper的類信息與MapperProxyFactory綁定。
  • MapperAnnotationBuilder:Mapper註解解析構建器,這也是爲何mybatis能夠直接在Mapper方法添加註解信息就能夠不用在xml寫sql信息的緣由,這個構建器專門用於解析Mapper方法註解信息,並將這些信息封裝成statement添加到MappedStatement中。

從時序圖可知,Configuration配置類存儲了全部Mapper註冊與綁定的信息,而後建立SqlSessionFactory時再將Configuration注入進去,最後通過SqlSessionFactory建立出來的SqlSession會話,就能夠根據Configuration信息進行數據庫交互,而MapperProxyFactory會爲每一個Mapper建立一個MapperProxy代理類,MapperProxy包含了Mapper操做SqlSession全部的細節,所以咱們就能夠直接使用Mapper的方法就能夠跟SqlSession進行交互。數據庫

饒了一圈,發現我如今還沒講sql注入器的源碼分析,你不用慌,你得體現出老司機的成熟穩定,以前我也跟你說了sql注入器的原理了,只剩下源碼分析,這時候咱們應該在源碼分析以前作足前戲,前戲作足就剩下撕、拉、扯、剝開源碼的外衣了,來不及解釋了快上車!後端

源碼分析

從Mapper的註冊與綁定過程的時序圖看,要想將sql注入器無縫連接地添加到mybatis裏面,那就得從Mapper註冊步驟添加,果真,mp很雞賊地繼承了MapperRegistry這個類而後重寫了addMapper方法:緩存

com.baomidou.mybatisplus.MybatisMapperRegistry#addMapper:微信

public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        if (hasMapper(type)) {
            // TODO 若是以前注入 直接返回
            return;
            // throw new BindingException("Type " + type +
            // " is already known to the MybatisPlusMapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // It's important that the type is added before the parser is run
            // otherwise the binding may automatically be attempted by the
            // mapper parser. If the type is already known, it won't try.
            // TODO 自定義無 XML 注入
            MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

方法中將MapperAnnotationBuilder替換成了自家的MybatisMapperAnnotationBuilder,在這裏特別說明一下,mp爲了避免更改mybatis原有的邏輯,會用繼承或者直接粗暴地將其複製過來,而後在原有的類名上加上前綴「Mybatis」。mybatis

com.baomidou.mybatisplus.MybatisMapperAnnotationBuilder#parse:架構

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
        loadXmlResource();
        configuration.addLoadedResource(resource);
        assistant.setCurrentNamespace(type.getName());
        parseCache();
        parseCacheRef();
        Method[] methods = type.getMethods();
        // TODO 注入 CURD 動態 SQL (應該在註解以前注入)
        if (BaseMapper.class.isAssignableFrom(type)) {
            GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
        }
        for (Method method : methods) {
            try {
                // issue #237
                if (!method.isBridge()) {
                    parseStatement(method);
                }
            } catch (IncompleteElementException e) {
                configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
        }
    }
    parsePendingMethods();
}

sql注入器就是從這個方法裏面添加上去的,首先判斷Mapper是不是BaseMapper的超類或者超接口,BaseMapper是mp的基礎Mapper,裏面定義了不少默認的基礎方法,意味着咱們一旦使用上mp,經過sql注入器,不少基礎的數據庫操做均可以直接繼承BaseMapper實現了,開發效率爆棚有木有!

com.baomidou.mybatisplus.toolkit.GlobalConfigUtils#getSqlInjector:

public static ISqlInjector getSqlInjector(Configuration configuration) {
  // fix #140
  GlobalConfiguration globalConfiguration = getGlobalConfig(configuration);
  ISqlInjector sqlInjector = globalConfiguration.getSqlInjector();
  if (sqlInjector == null) {
    sqlInjector = new AutoSqlInjector();
    globalConfiguration.setSqlInjector(sqlInjector);
  }
  return sqlInjector;
}

GlobalConfiguration是mp的全局緩存類,用於存放mp自帶的一些功能,很明顯,sql注入器就存放在GlobalConfiguration中。

這個方法是先從全局緩存類中獲取自定義的sql注入器,若是在GlobalConfiguration中沒有找到自定義sql注入器,就會設置一個mp默認的sql注入器AutoSqlInjector。

sql注入器接口:

// SQL 自動注入器接口
public interface ISqlInjector {
  
  // 根據mapperClass注入SQL
  void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);

  // 檢查SQL是否注入(已經注入過再也不注入)
  void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass);

  // 注入SqlRunner相關
  void injectSqlRunner(Configuration configuration);

}

全部自定義的sql注入器都須要實現ISqlInjector接口,mp已經爲咱們默認實現了一些基礎的注入器:

  • com.baomidou.mybatisplus.mapper.AutoSqlInjector
  • com.baomidou.mybatisplus.mapper.LogicSqlInjector

其中AutoSqlInjector提供了最基本的sql注入,以及一些通用的sql注入與拼裝的邏輯,LogicSqlInjector在AutoSqlInjector的基礎上覆寫了刪除邏輯,由於咱們的數據庫的數據刪除實質上是軟刪除,並非真正的刪除。

com.baomidou.mybatisplus.mapper.AutoSqlInjector#inspectInject:

public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
    String className = mapperClass.toString();
    Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
    if (!mapperRegistryCache.contains(className)) {
        inject(builderAssistant, mapperClass);
        mapperRegistryCache.add(className);
    }
}

該方法是sql注入器的入口,在入口處添加了注入事後再也不注入的判斷功能。

// 注入單點 crudSql
@Override
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
  this.configuration = builderAssistant.getConfiguration();
  this.builderAssistant = builderAssistant;
  this.languageDriver = configuration.getDefaultScriptingLanguageInstance();

  // 駝峯設置 PLUS 配置 > 原始配置
  GlobalConfiguration globalCache = this.getGlobalConfig();
  if (!globalCache.isDbColumnUnderline()) {
    globalCache.setDbColumnUnderline(configuration.isMapUnderscoreToCamelCase());
  }
  Class<?> modelClass = extractModelClass(mapperClass);
  if (null != modelClass) {
    // 初始化 SQL 解析
    if (globalCache.isSqlParserCache()) {
      PluginUtils.initSqlParserInfoCache(mapperClass);
    }
    TableInfo table = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
    injectSql(builderAssistant, mapperClass, modelClass, table);
  }
}

注入以前先將Mapper類提取泛型模型,由於繼承BaseMapper須要將Mapper對應的model添加到泛型裏面,這時候咱們須要將其提取出來,提取出來後還須要將其初始化成一個TableInfo對象,TableInfo存儲了數據庫對應的model全部的信息,包括表主鍵ID類型、表名稱、表字段信息列表等等信息,這些信息經過反射獲取。

com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectSql:

protected void injectSql(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo table) {
  if (StringUtils.isNotEmpty(table.getKeyProperty())) {
    /** 刪除 */
    this.injectDeleteByIdSql(false, mapperClass, modelClass, table);
    /** 修改 */
    this.injectUpdateByIdSql(true, mapperClass, modelClass, table);
    /** 查詢 */
    this.injectSelectByIdSql(false, mapperClass, modelClass, table);
  } 
  /** 自定義方法 */
  this.inject(configuration, builderAssistant, mapperClass, modelClass, table);
}

全部須要注入的sql都是經過該方法進行調用,AutoSqlInjector還提供了一個inject方法,自定義sql注入器時,繼承AutoSqlInjector,實現該方法就好了。

com.baomidou.mybatisplus.mapper.AutoSqlInjector#injectDeleteByIdSql:

protected void injectSelectByIdSql(boolean batch, Class<?> mapperClass, Class<?> modelClass, TableInfo table) {
  SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
  SqlSource sqlSource;
  if (batch) {
    sqlMethod = SqlMethod.SELECT_BATCH_BY_IDS;
    StringBuilder ids = new StringBuilder();
    ids.append("\n<foreach item=\"item\" index=\"index\" collection=\"coll\" separator=\",\">");
    ids.append("#{item}");
    ids.append("\n</foreach>");
    sqlSource = languageDriver.createSqlSource(configuration, String.format(sqlMethod.getSql(),
                                                                            sqlSelectColumns(table, false), table.getTableName(), table.getKeyColumn(), ids.toString()), modelClass);
  } else {
    sqlSource = new RawSqlSource(configuration, String.format(sqlMethod.getSql(), sqlSelectColumns(table, false),
                                                              table.getTableName(), table.getKeyColumn(), table.getKeyProperty()), Object.class);
  }
  this.addSelectMappedStatement(mapperClass, sqlMethod.getMethod(), sqlSource, modelClass, table);
}

我隨機選擇一個刪除sql的注入,其它sql注入都是相似這麼寫,SqlMethod是一個枚舉類,裏面存儲了全部自動注入的sql與方法名,若是是批量操做,SqlMethod的定義的sql語句在添加批量操做的語句。再根據table和sql信息建立一個SqlSource對象。

com.baomidou.mybatisplus.mapper.AutoSqlInjector#addMappedStatement:

public MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource,
                                          SqlCommandType sqlCommandType, Class<?> parameterClass, String resultMap, Class<?> resultType,
                                          KeyGenerator keyGenerator, String keyProperty, String keyColumn) {
  // MappedStatement是否存在
  String statementName = mapperClass.getName() + "." + id;
  if (hasMappedStatement(statementName)) {
    System.err.println("{" + statementName
                       + "} Has been loaded by XML or SqlProvider, ignoring the injection of the SQL.");
    return null;
  }
  /** 緩存邏輯處理 */
  boolean isSelect = false;
  if (sqlCommandType == SqlCommandType.SELECT) {
    isSelect = true;
  }
  return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType, null, null, null,
                                             parameterClass, resultMap, resultType, null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn,
                                             configuration.getDatabaseId(), languageDriver, null);
}

sql注入器的最終操做,這裏會判斷MappedStatement是否存在,這個判斷是有緣由的,它會防止重複注入,若是你的Mapper方法已經在Mybatis的邏輯裏面註冊了,mp不會再次注入。最後調用MapperBuilderAssistant助手類的addMappedStatement方法執行註冊操做。

到這裏,一個sql自動注入器的源碼就分析完了,其實實現起來很簡單,由於它利用了Mybatis的機制,站在巨人的肩膀上進行創新。

我但願在大家從此的職業生涯裏,不要只作一個只會調用API的crud程序員,咱們要有一種刨根問底的精神。閱讀源碼很枯燥,但閱讀源碼不只會讓你知道API底層的實現原理,讓你知其然也知其因此然,還能夠開闊你的思惟,提高你的架構設計能力,經過閱讀源碼,能夠看到大佬們是如何設計一個框架的,爲何會這麼設計。

公衆號「後端進階」,專一後端技術分享!

相關文章
相關標籤/搜索