mybatis源碼解讀(四):XMLStatementBuilder詳解

功能

XMLMapperBuilder 中會將 mapper 映射文件中除 CRUD 外的標籤解析驗證,輪到CRUD標籤的時候,是交給專門的類去做處理的,也就是XMLStatementBuilder。XMLStatementBuilder的解析工作:
第一步:解析一些較基本的屬性,比如 id、databaseId(多數據庫配置)、useCache(是否啓動二級緩存)、flushCache(只要語句調用就刷新緩存)等。
第二步;替換sql中的include標籤,將複用其它地方的sql整合處理成爲一個完整的sql
第三步:處理主鍵生成策略,mybatis 默認使用的是 NoKeyGenerator,如果設置了 useGeneratedKeys 會使用 Jdbc3KeyGenerator
第四步:mybatis到了這一步,會將sql處理成爲sqlSource存儲起來並且替換類似#{}這樣的特殊字符,SqlSource是mybatis中比較重要的一個部分,所以這一步sql處理會比較複雜。

UML

在這裏插入圖片描述

代碼解析

parseStatementNode() 是 XMLStatementBuilder 解析的入口,首先,他會去找CURD標籤上的唯一id,然後就是databaseId,databaseId是數據庫廠商標識,MyBatis 會加載所有不帶 databaseId 或匹配當前 databaseId 的語句,如果帶和不帶的語句都有,則不帶的會被忽略,一般選擇databaseId的時候是需要配置多數據庫廠商的時候。在解析完databaseId 之後就是解析flushCache、useCache等標籤,默認情況下,如果是select語句,則是開啓緩存的

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    // 如果是select語句,則默認是 useCache 啓動緩存
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    ...
  }

在CURD標籤中,有時候爲了方便是會配置一些 include標籤的,複用其他sql,所以XMLStatementBuilder 在接下來的工作就是解析 include標籤,解析 include標籤的功能是由 XMLIncludeTransformer 這個類完成的,構造方法是將當前的configuration、builderAssistant傳入進去,之所以傳入這兩個參數是爲了在接下來的解析動作中可以加載其它的sqlSeqment。

// 解析 include標籤
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

在XMLIncludeTransformer 的 applyIncludes()方法中,會去解析include標籤,這一塊的邏輯比較複雜,不過條例還算清晰,在這之前需要介紹一下mybatis的幾個概念,我們在CRUD標籤中所寫的sql最終都會解析成爲一個個sqlnode,比如 select * from 這種純文本的sql會對應爲一個靜態文本sql,foreach這種動態sql會對應爲一個foreachnode,詳細細節在解析動態sql中會介紹。
首先進入到applyIncludes()方法中,是幾個 條件case, 我們的CURD標籤,根節點是一個element標籤,仔細看這段邏輯,空標籤會走第二個條件case,也就是 source.getNodeType() == Node.ELEMENT_NODE 這個條件,在這塊代碼塊中會獲取到這個節點下的所有的孩子節點,進行遞歸處理,其實這裏就可以拿到 include標籤了。進入遞歸,如果是include標籤,則會走第一個條件節點,會去configuration中找到這個include包括的sql加載進來,並且進行遞歸去處理,處理完成後則會將處理完成的sql拼接到當前node後並且移除當前include,至於如果是靜態文本sql的話,會做一個非常重要的處理,就是替換 $ {}這種格式的佔位符,也就是預先替換。

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    // 如果包括include標籤則會走這裏
    if (source.getNodeName().equals("include")) {
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
      applyIncludes(toInclude, toIncludeContext, true);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      source.getParentNode().replaceChild(toInclude, source);
      // 會將include包含的sql文本拼接到這裏並且將自己給移除掉
      while (toInclude.hasChildNodes()) {
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      toInclude.getParentNode().removeChild(toInclude);
    // 如果是element則是走一塊邏輯,其實也就是 select insert等標籤
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      if (included && !variablesContext.isEmpty()) {
        // replace variables in attribute values
        NamedNodeMap attributes = source.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
          Node attr = attributes.item(i);
          attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
        }
      }
      //
      NodeList children = source.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        applyIncludes(children.item(i), variablesContext, included);
      }
    // 如果是 select * from 這種的sql走得這裏,TEXT_NODE
    } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
        && !variablesContext.isEmpty()) {
      // replace variables in text node
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

替換 $ {}這種佔位符是交給PropertyParser.parse()去完成,mybatis對於$ {}的處理是提前處理的,它會從環境變量中去取相對應key的值拿過來在這裏替換掉,可以看他的處理邏輯,通過GenericTokenParser去匹配 $ {} ,如果匹配到的話就通過handler去環境變量中拿,variables就是configuration中配置的properties,至於parser.parse(string)的內容是一個替換算法,感興趣的同學可以自己去看看。
其實從這段邏輯也可以看出,$ {} 是並不安全的,因爲他會完全將符合key的值替換到sql中,會有sql注入的風險。

public static String parse(String string, Properties variables) {
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }

經過incude標籤的處理,sqlnode已經是粗略加工過的sql了,接下來就是解析一些標籤,諸如selectKey 、根據selectKey去選擇主鍵生成策略,通過LanguageDriver去加工處理前邊初步加工過的sql爲sqlsource,然後就是fetchSize、timeout、parameterMap、resultType、resultMap、resultSetType、keyProperty、keyColumn、resultSets標籤。

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
	// 通過 langDriver 去解析sqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    // mybatus默認是PrepareStatement處理
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

在這一步中,其實有個很重要的東西,就是 SqlSource 的解析,mybatis會將 CRUD標籤中的sql解析成爲 sqlSource存放起來(主要是解析動態sql),解析sql是由 LanguageDriver 去處理的,LanguageDriver 是個接口專門負責動態sql的解析接口,有幾個實現類,一般mybatis解析是由XMLLanguageDriver去做的,當然你如果有需求也可以自己去配置driver

 String lang = context.getStringAttribute("lang");
 LanguageDriver langDriver = getLanguageDriver(lang);
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

LanguageDriver 的實現類
在這裏插入圖片描述
在createSqlSource()中可以看到,解析動態sql是由 XMLScriptBuilder 去完成的

public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

下一章介紹XMLScriptBuilder 的解析工作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章