精盡MyBatis源碼分析 - MyBatis初始化(三)之 SQL 初始化(上)

該系列文檔是本人在學習 Mybatis 的源碼過程當中總結下來的,可能對讀者不太友好,請結合個人源碼註釋(Mybatis源碼分析 GitHub 地址Mybatis-Spring 源碼分析 GitHub 地址Spring-Boot-Starter 源碼分析 GitHub 地址)進行閱讀html

MyBatis 版本:3.5.2java

MyBatis-Spring 版本:2.0.3node

MyBatis-Spring-Boot-Starter 版本:2.1.4git

MyBatis的初始化

在MyBatis初始化過程當中,大體會有如下幾個步驟:github

  1. 建立Configuration全局配置對象,會往TypeAliasRegistry別名註冊中心添加Mybatis須要用到的相關類,並設置默認的語言驅動類爲XMLLanguageDriverspring

  2. 加載mybatis-config.xml配置文件、Mapper接口中的註解信息和XML映射文件,解析後的配置信息會造成相應的對象並保存到Configuration全局配置對象中sql

  3. 構建DefaultSqlSessionFactory對象,經過它能夠建立DefaultSqlSession對象,MyBatis中SqlSession的默認實現類數據庫

由於整個初始化過程涉及到的代碼比較多,因此拆分紅了四個模塊依次對MyBatis的初始化進行分析:express

因爲在MyBatis的初始化過程當中去解析Mapper接口與XML映射文件涉及到的篇幅比較多,XML映射文件的解析過程也比較複雜,因此才分紅了後面三個模塊,逐步分析,這樣便於理解apache

初始化(三)之SQL初始化(上)

在前面的MyBatis初始化相關文檔中已經大體講完了MyBatis初始化的整個流程,其中遺漏了一部分,就是在解析<select /> <insert /> <update /> <delete />節點的過程當中,是如何解析SQL語句,如何實現動態SQL語句,最終會生成一個org.apache.ibatis.mapping.SqlSource對象的,對於這煩瑣且易出錯的過程,咱們來看看MyBatis如何實現的?

咱們回顧org.apache.ibatis.builder.xml.XMLStatementBuilderparseStatementNode()解析 Statement 節點時,經過下面的方法建立對應的SqlSource對象

// 建立對應的 SqlSource 對象,保存了該節點下 SQL 相關信息
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

langDriver是從Configuration全局配置對象中獲取的默認實現類,對應的也就是XMLLanguageDriver,在Configuration初始化的時候設置的

public Configuration() {
    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
	languageRegistry.register(RawLanguageDriver.class);
}

主要包路徑:org.apache.ibatis.scripting、org.apache.ibatis.builder、org.apache.ibatis.mapping

主要涉及到的類:

  • org.apache.ibatis.scripting.xmltags.XMLLanguageDriver:語言驅動接口的默認實現,建立ParameterHandler參數處理器對象和SqlSource資源對象

  • org.apache.ibatis.scripting.xmltags.XMLScriptBuilder:繼承 BaseBuilder 抽象類,負責將SQL腳本(XML或者註解中定義的SQL語句)解析成SqlSource(DynamicSqlSource或者RawSqlSource)資源對象

  • org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.NodeHandler:定義在XMLScriptBuilder內部的一個接口,用於處理MyBatis自定義標籤(<if /> <foreach />等),生成對應的SqlNode對象,不一樣的實現類處理不一樣的標籤

  • org.apache.ibatis.scripting.xmltags.DynamicContext:解析動態SQL語句時的上下文,用於解析SQL時,記錄動態SQL處理後的SQL語句,內部提供ContextMap對象保存上下文的參數

  • org.apache.ibatis.scripting.xmltags.SqlNode:SQL Node接口,每一個XML Node會解析成對應的SQL Node對象,經過上下文能夠對動態SQL進行邏輯處理,生成須要的結果

  • org.apache.ibatis.scripting.xmltags.OgnlCache:用於處理Ognl表達式

語言驅動接口的實現類以下圖所示:

LanguageDriver

LanguageDriver

org.apache.ibatis.scripting.LanguageDriver:語言驅動接口,代碼以下:

public interface LanguageDriver {

	/**
	 * Creates a {@link ParameterHandler} that passes the actual parameters to the the JDBC statement.
	 * 建立 ParameterHandler 對象
	 *
	 * @param mappedStatement The mapped statement that is being executed
	 * @param parameterObject The input parameter object (can be null)
	 * @param boundSql        The resulting SQL once the dynamic language has been executed.
	 * @return 參數處理器
	 * @author Frank D. Martinez [mnesarco]
	 * @see DefaultParameterHandler
	 */
	ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);

	/**
	 * Creates an {@link SqlSource} that will hold the statement read from a mapper xml file. 
	 * It is called during startup, when the mapped statement is read from a class or an xml file.
	 * 建立 SqlSource 對象,從 Mapper XML 配置的 Statement 標籤中,即 <select /> 等。
	 *
	 * @param configuration The MyBatis configuration
	 * @param script        XNode parsed from a XML file
	 * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
	 * @return SQL 資源
	 */
	SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);

	/**
	 * Creates an {@link SqlSource} that will hold the statement read from an annotation. 
	 * It is called during startup, when the mapped statement is read from a class or an xml file.
	 * 建立 SqlSource 對象,從方法註解配置,即 @Select 等。
	 *
	 * @param configuration The MyBatis configuration
	 * @param script        The content of the annotation
	 * @param parameterType input parameter type got from a mapper method or specified in the parameterType xml attribute. Can be null.
	 * @return SQL 資源
	 */
	SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

定義了三個方法:

  1. createParameterHandler:獲取 ParameterHandler 參數處理器對象

  2. createSqlSource:建立 SqlSource 對象,解析 Mapper XML 配置的 Statement 標籤中,即 <select /> <update /> <delete /> <insert />

  3. createSqlSource:建立 SqlSource 對象,從方法註解配置,即 @Select 等

XMLLanguageDriver

org.apache.ibatis.scripting.xmltags.XMLLanguageDriver:語言驅動接口的默認實現,代碼以下:

public class XMLLanguageDriver implements LanguageDriver {

  @Override
  public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    // 建立 DefaultParameterHandler 對象
    return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
  }

  /**
   * 用於解析 XML 映射文件中的 SQL
   *
   * @param configuration The MyBatis configuration
   * @param script        XNode parsed from a XML file
   * @param parameterType input parameter type got from a mapper method or
   *                      specified in the parameterType xml attribute. Can be
   *                      null.
   * @return SQL 資源
   */
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    // 建立 XMLScriptBuilder 對象,執行解析
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

  /**
   * 用於解析註解中的 SQL
   *
   * @param configuration The MyBatis configuration
   * @param script        The content of the annotation
   * @param parameterType input parameter type got from a mapper method or
   *                      specified in the parameterType xml attribute. Can be
   *                      null.
   * @return SQL 資源
   */
  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // issue #3
    // <1> 若是是 <script> 開頭,表示是在註解中使用的動態 SQL
    if (script.startsWith("<script>")) {
      // <1.1> 建立 XPathParser 對象,解析出 <script /> 節點
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // issue #127
      // <2.1> 變量替換
      script = PropertyParser.parse(script, configuration.getVariables());
      // <2.2> 建立 TextSqlNode 對象
      TextSqlNode textSqlNode = new TextSqlNode(script);
      if (textSqlNode.isDynamic()) { // <2.3.1> 若是是動態 SQL ,則建立 DynamicSqlSource 對象
        return new DynamicSqlSource(configuration, textSqlNode);
      } else { // <2.3.2> 若是非動態 SQL ,則建立 RawSqlSource 對象
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }
}

實現了LanguageDriver接口:

  1. 建立 DefaultParameterHandler 默認參數處理器並返回

  2. 解析 XML 映射文件中的 SQL,經過建立 XMLScriptBuilder 對象,調用其 parseScriptNode() 方法解析

  3. 解析註解定義的 SQL

    1. 若是是 <script> 開頭,表示是在註解中使用的動態 SQL,將其轉換成 XNode 而後調用上述方法,不瞭解的能夠看看MyBatis三種動態SQL配置方式
    2. 先將註解中定義的 SQL 中包含的變量進行轉換,而後建立對應的 SqlSource 對象

RawLanguageDriver

org.apache.ibatis.scripting.defaults.RawLanguageDriver:繼承了XMLLanguageDriver,在的基礎上增長了是否爲靜態SQL語句的校驗,也就是判斷建立的 SqlSource 是否爲 RawSqlSource 靜態 SQL 資源

XMLScriptBuilder

org.apache.ibatis.scripting.xmltags.XMLScriptBuilder:繼承 BaseBuilder 抽象類,負責將 SQL 腳本(XML或者註解中定義的 SQL )解析成 SqlSource 對象

構造方法

public class XMLScriptBuilder extends BaseBuilder {
    /**
	 * 當前 SQL 的 XNode 對象
	 */
	private final XNode context;
	/**
	 * 是否爲動態 SQL
	 */
	private boolean isDynamic;
	/**
	 * SQL 的 Java 入參類型
	 */
	private final Class<?> parameterType;
	/**
	 * NodeNodeHandler 的映射
	 */
	private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

	public XMLScriptBuilder(Configuration configuration, XNode context) {
		this(configuration, context, null);
	}

	public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
		super(configuration);
		this.context = context;
		this.parameterType = parameterType;
		initNodeHandlerMap();
	}

	private void initNodeHandlerMap() {
		nodeHandlerMap.put("trim", new TrimHandler());
		nodeHandlerMap.put("where", new WhereHandler());
		nodeHandlerMap.put("set", new SetHandler());
		nodeHandlerMap.put("foreach", new ForEachHandler());
		nodeHandlerMap.put("if", new IfHandler());
		nodeHandlerMap.put("choose", new ChooseHandler());
		nodeHandlerMap.put("when", new IfHandler());
		nodeHandlerMap.put("otherwise", new OtherwiseHandler());
		nodeHandlerMap.put("bind", new BindHandler());
	}
}

在構造函數中會初始化 NodeHandler 處理器,分別用於處理不一樣的MyBatis自定義的XML標籤,例如<if /> <where /> <foreach />等標籤

parseScriptNode方法

parseScriptNode()方法將 SQL 腳本(XML或者註解中定義的 SQL )解析成 SqlSource 對象,代碼以下:

public SqlSource parseScriptNode() {
    // 解析 XML 或者註解中定義的 SQL
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
        // 動態語句,使用了 ${} 也算
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}
  1. 經過調用parseDynamicTags(XNode node)方法,將解析 SQL 成 MixedSqlNode 對象,主要是將一整個 SQL 解析成一系列的 SqlNode 對象
  2. 若是是動態SQL語句,使用了MyBatis自定義的XML標籤(<if />等)或者使用了${},則封裝成DynamicSqlSource對象
  3. 不然就是靜態SQL語句,封裝成RawSqlSource對象

parseDynamicTags方法

parseDynamicTags()將 SQL 腳本(XML或者註解中定義的 SQL )解析成MixedSqlNode對象,代碼以下:

protected MixedSqlNode parseDynamicTags(XNode node) {
    // <1> 建立 SqlNode 數組
    List<SqlNode> contents = new ArrayList<>();
    /*
     * <2> 遍歷 SQL 節點中全部子節點
     * 這裏會對該節點內的全部內容進行處理而後返回 NodeList 對象
     * 1. 文本內容會被解析成 '<#text></#text>' 節點,就算一個換行符也會解析成這個
     * 2. <![CDATA[ content ]]> 會被解析成 '<#cdata-section>content</#cdata-section>' 節點
     * 3. 其餘動態<if /> <where />
     */
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
        // 當前子節點
        XNode child = node.newXNode(children.item(i));
        // <2.1> 若是類型是 Node.CDATA_SECTION_NODE 或者 Node.TEXT_NODE 時
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE // <![CDATA[ ]]>節點
                || child.getNode().getNodeType() == Node.TEXT_NODE) { // 純文本
            // <2.1.1> 得到內容
            String data = child.getStringBody("");
            // <2.1.2> 建立 TextSqlNode 對象
            TextSqlNode textSqlNode = new TextSqlNode(data);
            if (textSqlNode.isDynamic()) { // <2.1.2.1> 若是是動態的 TextSqlNode 對象,也就是使用了 '${}'
                // 添加到 contents 中
                contents.add(textSqlNode);
                // 標記爲動態 SQL
                isDynamic = true;
            } else { // <2.1.2.2> 若是是非動態的 TextSqlNode 對象,沒有使用 '${}'
                // <2.1.2> 建立 StaticTextSqlNode 添加到 contents 中
                contents.add(new StaticTextSqlNode(data));
            }
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 <2.2> 若是類型是 Node.ELEMENT_NODE
            // <2.2.1> 根據子節點的標籤,得到對應的 NodeHandler 對象
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) { // 得到不到,說明是未知的標籤,拋出 BuilderException 異常
                throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            // <2.2.2> 執行 NodeHandler 處理
            handler.handleNode(child, contents);
            // <2.2.3> 標記爲動態 SQL
            isDynamic = true;
        }
    }
    // <3> 建立 MixedSqlNode 對象
    return new MixedSqlNode(contents);
}

<1> 建立 SqlNode 數組 contents,用於保存解析 SQL 後的一些列 SqlNode 對象

<2> 獲取定義的 SQL 節點中全部子節點,返回一個 NodeList 對象,這個對象中包含了該 SQL 節點內的全部信息,而後逐個遍歷子節點

1. 其中文本內容會被解析成`<#text></#text>`節點,就算一個換行符也會解析成這個
   	2. `<![CDATA[ ]]>` 會被解析成 `<#cdata-section></#cdata-section>` 節點
         	3. 還有其餘MyBatis自定義的標籤`<if /> <where />`等等

<2.1> 若是子節點是<#text />或者<#cdata-section />類型

<2.1.1> 獲取子節點的文本內容

<2.1.2> 建立 TextSqlNode 對象

<2.1.2.1> 調用 TextSqlNode 的 isDynamic() 方法,點擊去該進去看看就知道了,若是文本中使用了${},則標記爲動態 SQL 語句,將其添加至 contents 數組中

<2.1.2.2> 不然就是靜態文本內容,建立對應的 StaticTextSqlNode 對象,將其添加至 contents 數組中

<2.2> 若是類型是 Node.ELEMENT_NODE 時,也就是 MyBatis 的自定義標籤

<2.2.1> 根據子節點的標籤名稱,得到對應的 NodeHandler 對象

<2.2.2> 執行NodeHandlerhandleNode方法處理該節點,建立不通類型的 SqlNode 並添加到 contents 數組中,如何處理的在下面講述

<2.2.3> 標記爲動態 SQL 語句

<3> 最後將建立 contents 封裝成 MixedSqlNode 對象

NodeHandler

XMLScriptBuilder的內部接口,用於處理MyBatis自定義標籤,接口實現類以下圖所示:

NodeHandler

代碼以下:

private interface NodeHandler {
    /**
     * 處理 Node
     *
     * @param nodeToHandle   要處理的 XNode 節點
     * @param targetContents 目標的 SqlNode 數組。實際上,被處理的 XNode 節點會建立成對應的 SqlNode 對象,添加到 targetContents 中
     */
    void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}

這些 NodeHandler 實現類都定義在 XMLScriptBuilder 內部,用於處理不一樣標籤,咱們逐個來看

BindHandler

實現了NodeHandler接口,<bind />標籤的處理器,代碼以下:

/**
 * <bind />元素容許你在 OGNL 表達式(SQL語句)之外建立一個變量,並將其綁定到當前的上下文
 */
private class BindHandler implements NodeHandler {
    public BindHandler() {
        // Prevent Synthetic Access
    }
    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析 name、value 屬性
        final String name = nodeToHandle.getStringAttribute("name");
        final String expression = nodeToHandle.getStringAttribute("value");
        // 建立 VarDeclSqlNode 對象
        final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
        targetContents.add(node);
    }
}
  1. 獲取<bind />標籤的name和value屬性

  2. 根據這些屬性建立一個 VarDeclSqlNode 對象

  3. 添加到targetContents集合中

例如這樣配置:

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

TrimHandler

實現了NodeHandler接口,<trim />標籤的處理器,代碼以下:

private class TrimHandler implements NodeHandler {
    public TrimHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // <1> 解析內部的 SQL 節點,成 MixedSqlNode 對象
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // <2> 得到 prefix、prefixOverrides、"suffix"、suffixOverrides 屬性
        String prefix = nodeToHandle.getStringAttribute("prefix");
        String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
        String suffix = nodeToHandle.getStringAttribute("suffix");
        String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
        // <3> 建立 TrimSqlNode 對象
        TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
        targetContents.add(trim);
    }
}
  1. 繼續調用parseDynamicTags方法解析<if />標籤內部的子標籤節點,嵌套解析,生成MixedSqlNode對象

  2. 得到 prefixprefixOverridessuffixsuffixOverrides 屬性

  3. 根據上面獲取到的屬性建立TrimSqlNode對象

  4. 添加到targetContents集合中

WhereHandler

實現了NodeHandler接口,<where />標籤的處理器,代碼以下:

private class WhereHandler implements NodeHandler {
    public WhereHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析內部的 SQL 節點,成 MixedSqlNode 對象
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // 建立 WhereSqlNode 對象
        WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
        targetContents.add(where);
    }
}
  1. 繼續調用parseDynamicTags方法解析<where />標籤內部的子標籤節點,嵌套解析,生成MixedSqlNode對象
  2. 建立WhereSqlNode對象,該對象繼承了TrimSqlNode,自定義前綴(WHERE)和須要刪除的前綴(AND、OR等)
  3. 添加到targetContents集合中

SetHandler

實現了NodeHandler接口,<set />標籤的處理器,代碼以下:

private class SetHandler implements NodeHandler {
    public SetHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析內部的 SQL 節點,成 MixedSqlNode 對象
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
        targetContents.add(set);
    }
}
  1. 繼續調用parseDynamicTags方法解析<set />標籤內部的子標籤節點,嵌套解析,生成MixedSqlNode對象
  2. 建立SetSqlNode對象,該對象繼承了TrimSqlNode,自定義前綴(SET)和須要刪除的前綴和後綴(,)
  3. 添加到targetContents集合中

ForEachHandler

實現了NodeHandler接口,<foreach />標籤的處理器,代碼以下:

private class ForEachHandler implements NodeHandler {
    public ForEachHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析內部的 SQL 節點,成 MixedSqlNode 對象
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // 得到 collection、item、index、open、close、separator 屬性
        String collection = nodeToHandle.getStringAttribute("collection");
        String item = nodeToHandle.getStringAttribute("item");
        String index = nodeToHandle.getStringAttribute("index");
        String open = nodeToHandle.getStringAttribute("open");
        String close = nodeToHandle.getStringAttribute("close");
        String separator = nodeToHandle.getStringAttribute("separator");
        // 建立 ForEachSqlNode 對象
        ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
        targetContents.add(forEachSqlNode);
    }
}
  1. 繼續調用parseDynamicTags方法解析<foreach />標籤內部的子標籤節點,嵌套解析,生成MixedSqlNode對象
  2. 得到 collection、item、index、open、close、separator 屬性
  3. 根據這些屬性建立ForEachSqlNode對象
  4. 添加到targetContents集合中

IfHandler

實現了NodeHandler接口,<if />標籤的處理器,代碼以下:

private class IfHandler implements NodeHandler {
    public IfHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析內部的 SQL 節點,成 MixedSqlNode 對象
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        // 得到 test 屬性
        String test = nodeToHandle.getStringAttribute("test");
        // 建立 IfSqlNode 對象
        IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
        targetContents.add(ifSqlNode);
    }
}
  1. 繼續調用parseDynamicTags方法解析<if />標籤內部的子標籤節點,嵌套解析,生成MixedSqlNode對象
  2. 得到 test 屬性
  3. 根據這個屬性建立IfSqlNode對象
  4. 添加到targetContents集合中

OtherwiseHandler

實現了NodeHandler接口,<otherwise />標籤的處理器,代碼以下:

private class OtherwiseHandler implements NodeHandler {
    public OtherwiseHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        // 解析內部的 SQL 節點,成 MixedSqlNode 對象
        MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
        targetContents.add(mixedSqlNode);
    }
}
  1. 繼續調用parseDynamicTags方法解析<otherwise />標籤內部的子標籤節點,嵌套解析,生成MixedSqlNode對象
  2. 添加到targetContents集合中,須要結合ChooseHandler使用

ChooseHandler

實現了NodeHandler接口,<choose />標籤的處理器,代碼以下:

private class ChooseHandler implements NodeHandler {
    public ChooseHandler() {
        // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
        List<SqlNode> whenSqlNodes = new ArrayList<>();
        List<SqlNode> otherwiseSqlNodes = new ArrayList<>();
        // 解析 `<when />` 和 `<otherwise />` 的節點們
        handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
        // 得到 `<otherwise />` 的節點,存在多個會拋出異常
        SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
        // 建立 ChooseSqlNode 對象
        ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
        targetContents.add(chooseSqlNode);
    }

    private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes,
            List<SqlNode> defaultSqlNodes) {
        List<XNode> children = chooseSqlNode.getChildren();
        for (XNode child : children) {
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler instanceof IfHandler) { // 處理 `<when />` 標籤的狀況
                handler.handleNode(child, ifSqlNodes);
            } else if (handler instanceof OtherwiseHandler) { // 處理 `<otherwise />` 標籤的狀況
                handler.handleNode(child, defaultSqlNodes);
            }
        }
    }

    private SqlNode getDefaultSqlNode(List<SqlNode> defaultSqlNodes) {
        SqlNode defaultSqlNode = null;
        if (defaultSqlNodes.size() == 1) {
            defaultSqlNode = defaultSqlNodes.get(0);
        } else if (defaultSqlNodes.size() > 1) {
            throw new BuilderException("Too many default (otherwise) elements in choose statement.");
        }
        return defaultSqlNode;
    }
}
  1. 先逐步處理<choose />標籤的<when /><otherwise />子標籤們,經過組合 IfHandler 和 OtherwiseHandler 兩個處理器,實現對子節點們的解析

  2. 若是存在<otherwise />子標籤,則拋出異常

  3. 根據這些屬性建立ChooseSqlNode對象

  4. 添加到targetContents集合中

DynamicContext

org.apache.ibatis.scripting.xmltags.DynamicContext:解析動態SQL語句時的上下文,用於解析SQL時,記錄動態SQL處理後的SQL語句,內部提供ContextMap對象保存上下文的參數

構造方法

public class DynamicContext {

  /**
   * 入參保存在 ContextMap 中的 Key
   *
   * {@link #bindings}
   */
  public static final String PARAMETER_OBJECT_KEY = "_parameter";
  /**
   * 數據庫編號保存在 ContextMap 中的 Key
   *
   * {@link #bindings}
   */
  public static final String DATABASE_ID_KEY = "_databaseId";

  static {
    // <1.2> 設置 OGNL 的屬性訪問器
    OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
  }

  /**
   * 上下文的參數集合,包含附加參數(經過`<bind />`標籤生成的,或者`<foreach />`標籤中的集合的元素等等)
   */
  private final ContextMap bindings;
  /**
   * 生成後的 SQL
   */
  private final StringJoiner sqlBuilder = new StringJoiner(" ");
  /**
   * 惟一編號。在 {@link org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.ForEachHandler} 使用
   */
  private int uniqueNumber = 0;

  public DynamicContext(Configuration configuration, Object parameterObject) {
    // <1> 初始化 bindings 參數
    if (parameterObject != null && !(parameterObject instanceof Map)) {
      // 構建入參的 MetaObject 對象
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      // 入參類型是否有對應的類型處理器
      boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
      bindings = new ContextMap(metaObject, existsTypeHandler);
    } else {
      bindings = new ContextMap(null, false);
    }
    // <2> 添加 bindings 的默認值
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }
}
類型 屬性 說明
ContextMap bindings 上下文的參數集合,包含附加參數(經過<bind />標籤生成的,或者<foreach />標籤解析參數保存的),以及幾個默認值
StringJoiner sqlBuilder 保存本次解析後的SQL,每次添加字符串以空格做爲分隔符
int uniqueNumber 惟一編號,在ForEachHandler處理節點時須要用到,生成惟一數組做爲集合中每一個元素的索引(做爲後綴)
  1. 初始化bindings參數,建立 ContextMap 對象
    1. 根據入參轉換成MetaObject對象
    2. 在靜態代碼塊中,設置OGNL的屬性訪問器,OgnlRuntime 是 OGNL 庫中的類,設置ContextMap對應的訪問器是ContextAccessor
  2. bindings中添加幾個默認值:_parameter > 入參對象,_databaseId -> 數據庫標識符

ContextMap

DynamicContext的內部靜態類,繼承HashMap,用於保存解析動態SQL語句時的上下文的參數集合,代碼以下:

static class ContextMap extends HashMap<String, Object> {
    private static final long serialVersionUID = 2977601501966151582L;
    /**
     * parameter 對應的 MetaObject 對象
     */
    private final MetaObject parameterMetaObject;
    /**
     * 是否有對應的類型處理器
     */
    private final boolean fallbackParameterObject;

    public ContextMap(MetaObject parameterMetaObject, boolean fallbackParameterObject) {
      this.parameterMetaObject = parameterMetaObject;
      this.fallbackParameterObject = fallbackParameterObject;
    }

    @Override
    public Object get(Object key) {
      String strKey = (String) key;
      if (super.containsKey(strKey)) {
        return super.get(strKey);
      }

      if (parameterMetaObject == null) {
        return null;
      }

      if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {
        return parameterMetaObject.getOriginalObject();
      } else {
        // issue #61 do not modify the context when reading
        return parameterMetaObject.getValue(strKey);
      }
    }
}

重寫了 HashMap 的 get(Object key) 方法,增長支持對 parameterMetaObject 屬性的訪問

ContextAccessor

DynamicContext的內部靜態類,實現 ognl.PropertyAccessor 接口,上下文訪問器,代碼以下:

static class ContextAccessor implements PropertyAccessor {
    @Override
    public Object getProperty(Map context, Object target, Object name) {
      Map map = (Map) target;

      // 優先從 ContextMap 中,得到屬性
      Object result = map.get(name);
      if (map.containsKey(name) || result != null) {
        return result;
      }

      // <x> 若是沒有,則從 PARAMETER_OBJECT_KEY 對應的 Map 中,得到屬性
      Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
      if (parameterObject instanceof Map) {
        return ((Map) parameterObject).get(name);
      }

      return null;
    }

    @Override
    public void setProperty(Map context, Object target, Object name, Object value) {
      Map<Object, Object> map = (Map<Object, Object>) target;
      map.put(name, value);
    }

    @Override
    public String getSourceAccessor(OgnlContext arg0, Object arg1, Object arg2) {
      return null;
    }

    @Override
    public String getSourceSetter(OgnlContext arg0, Object arg1, Object arg2) {
      return null;
    }
}

在DynamicContext的靜態代碼塊中,設置OGNL的屬性訪問器,設置了ContextMap.class的屬性訪問器爲ContextAccessor

這裏方法的入參中的target,就是 ContextMap 對象

  1. 在重寫的getProperty方法中,先從 ContextMap 裏面獲取屬性值(能夠回過去看下ContextMap的get方法)

  2. 沒有獲取到則獲取 PARAMETER_OBJECT_KEY 屬性的值,若是是 Map 類型,則從這裏面獲取屬性值

回看 DynamicContext 的構造方法,細品一下😄😄😄,先從Map中獲取屬性值,沒有獲取到則從parameterObject入參對象中獲取屬性值

SqlNode

org.apache.ibatis.scripting.xmltags.SqlNode:SQL Node接口,每一個XML Node會解析成對應的SQL Node對象,經過上下文能夠對動態SQL進行邏輯處理,生成須要的結果

實現類以下圖所示:

SqlNode

代碼以下:

public interface SqlNode {
	/**
	 * 應用當前 SQLNode 節點
	 *
	 * @param context 正在解析 SQL 語句的上下文
	 * @return 是否應用成功
	 */
	boolean apply(DynamicContext context);
}

由於在解析SQL語句的時候咱們須要根據入參來處理不一樣的SqlNode,經過其apply(DynamicContext context)方法應用SqlNode節點,將節點轉換成相應的SQL

咱們來看看它的實現類是如何處理相應的SQL Node的

VarDeclSqlNode

org.apache.ibatis.scripting.xmltags.VarDeclSqlNode:實現 SqlNode 接口,<bind /> 標籤對應的 SqlNode 實現類,代碼以下:

public class VarDeclSqlNode implements SqlNode {
    /**
     * 變量名稱
     */
	private final String name;
    /**
     * 表達式
     */
	private final String expression;

	public VarDeclSqlNode(String var, String exp) {
		name = var;
		expression = exp;
	}

	@Override
	public boolean apply(DynamicContext context) {
	    // 獲取該表達式轉換後結果
		final Object value = OgnlCache.getValue(expression, context.getBindings());
		// 將該結果與變量名稱設置到解析 SQL 語句的上下文中,這樣接下來的解析過程當中能夠獲取到 name 的值
		context.bind(name, value);
		return true;
	}
}
  1. 經過OGNL表達式expression從DynamicContext上下文的ContextMap中獲取轉換後的結果,OgnlCache在後面講到

  2. name與轉換後的結果綁定到DynamicContext上下文中,後續處理其餘節點能夠獲取到

TrimSqlNode

org.apache.ibatis.scripting.xmltags.TrimSqlNode:實現 SqlNode 接口,<trim/> 標籤對應的 SqlNode 實現類

構造方法
public class TrimSqlNode implements SqlNode {
	/**
	 * MixedSqlNode,包含該<if />節點內全部信息
	 */
	private final SqlNode contents;
	/**
	 * 前綴,行首添加
	 */
	private final String prefix;
	/**
	 * 後綴,行尾添加
	 */
	private final String suffix;
	/**
	 * 須要刪除的前綴,例如這樣定義:'AND|OR'
	 * 注意空格,這裏是不會去除的
	 */
	private final List<String> prefixesToOverride;
	/**
	 * 須要刪除的後綴,例如咱們這樣定義:',|AND'
	 * 注意空格,這裏是不會去除的
	 */
	private final List<String> suffixesToOverride;
	private final Configuration configuration;

	public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride,
			String suffix, String suffixesToOverride) {
		this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
	}

	protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride,
			String suffix, List<String> suffixesToOverride) {
		this.contents = contents;
		this.prefix = prefix;
		this.prefixesToOverride = prefixesToOverride;
		this.suffix = suffix;
		this.suffixesToOverride = suffixesToOverride;
		this.configuration = configuration;
	}
}

在構造方法中解析<trim />標籤的屬性,其中調用了parseOverrides方法將|做爲分隔符分隔該字符串並所有大寫,生成一個數組,相關屬性可查看上面的註釋

apply方法
@Override
public boolean apply(DynamicContext context) {
    // <1> 建立 FilteredDynamicContext 對象
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    // <2> 先解析 <trim /> 節點中的內容,將生成的 SQL 先存放在 FilteredDynamicContext 中
    boolean result = contents.apply(filteredDynamicContext);
    /*
     * <3> 執行 FilteredDynamicContext 的應用
     * 對上一步解析到的內容進行處理
     * 處理完成後再將處理後的 SQL 拼接到 DynamicContext 中
     */
    filteredDynamicContext.applyAll();
    return result;
}
  1. 經過裝飾器模式將context裝飾成FilteredDynamicContext對象

  2. 由於<trim />標籤中定義了內容或者其餘標籤,都會解析成相應的SqlNode,保存在contents中(MixedSqlNode)

    因此這裏須要先應用內部的SqlNode,轉換後的SQL會先保存在FilteredDynamicContext中

  3. FilteredDynamicContext中的SQL進行處理,也就是添加先後綴,去除先後綴的處理邏輯,而後將處理後的SQL拼接到context

FilteredDynamicContext

TrimSqlNode的私有內部類,繼承了DynamicContext類,對<trim />標籤邏輯的實現,代碼以下:

private class FilteredDynamicContext extends DynamicContext {
    /**
     * 裝飾的 DynamicContext 對象
     */
    private DynamicContext delegate;
    /**
     * 是否 prefix 已經被應用
     */
    private boolean prefixApplied;
    /**
     * 是否 suffix 已經被應用
     */
    private boolean suffixApplied;
    /**
     * StringBuilder 對象
     *
     * @see #appendSql(String)
     */
    private StringBuilder sqlBuffer;

    public FilteredDynamicContext(DynamicContext delegate) {
        super(configuration, null);
        this.delegate = delegate;
        this.prefixApplied = false;
        this.suffixApplied = false;
        this.sqlBuffer = new StringBuilder();
    }

    public void applyAll() {
        // <1> 去除先後多餘的空格,生成新的 sqlBuffer 對象
        sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
        // <2> 所有大寫
        String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
        // <3> 應用 TrimSqlNode 的 trim 邏輯
        if (trimmedUppercaseSql.length() > 0) {
            applyPrefix(sqlBuffer, trimmedUppercaseSql);
            applySuffix(sqlBuffer, trimmedUppercaseSql);
        }
        delegate.appendSql(sqlBuffer.toString());
    }

    @Override
    public Map<String, Object> getBindings() {
        return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
        delegate.bind(name, value);
    }

    @Override
    public int getUniqueNumber() {
        return delegate.getUniqueNumber();
    }

    @Override
    public void appendSql(String sql) {
        sqlBuffer.append(sql);
    }

    @Override
    public String getSql() {
        return delegate.getSql();
    }

    private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
        if (!prefixApplied) {
            prefixApplied = true;
            // prefixesToOverride 非空,先刪除
            if (prefixesToOverride != null) {
                for (String toRemove : prefixesToOverride) {
                    if (trimmedUppercaseSql.startsWith(toRemove)) {
                        sql.delete(0, toRemove.trim().length());
                        break;
                    }
                }
            }
            // prefix 非空,再添加
            if (prefix != null) {
                sql.insert(0, " ");
                sql.insert(0, prefix);
            }
        }
    }

    private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
        if (!suffixApplied) {
            suffixApplied = true;
            // suffixesToOverride 非空,先刪除
            if (suffixesToOverride != null) {
                for (String toRemove : suffixesToOverride) {
                    if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
                        int start = sql.length() - toRemove.trim().length();
                        int end = sql.length();
                        sql.delete(start, end);
                        break;
                    }
                }
            }
            // suffix 非空,再添加
            if (suffix != null) {
                sql.append(" ");
                sql.append(suffix);
            }
        }
    }
}

邏輯並不複雜,你們能夠看下

WhereSqlNode

org.apache.ibatis.scripting.xmltags.WhereSqlNode:繼承了TrimSqlNode類,<where /> 標籤對應的 SqlNode 實現類,代碼以下:

public class WhereSqlNode extends TrimSqlNode {

    /**
     * 也是經過 TrimSqlNode ,這裏定義須要刪除的前綴
     */
	private static List<String> prefixList = Arrays.asList("AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

	public WhereSqlNode(Configuration configuration, SqlNode contents) {
	  // 設置前綴和須要刪除的前綴
		super(configuration, contents, "WHERE", prefixList, null, null);
	}
}

基於TrimSqlNode類,定義了須要添加的前綴爲WHERE和須要刪除的前綴ANDOR

SetSqlNode

org.apache.ibatis.scripting.xmltags.SetSqlNode:繼承了TrimSqlNode類,<set /> 標籤對應的 SqlNode 實現類,代碼以下:

public class SetSqlNode extends TrimSqlNode {

  /**
   * 也是經過 TrimSqlNode ,這裏定義須要刪除的前綴
   */
  private static final List<String> COMMA = Collections.singletonList(",");

  public SetSqlNode(Configuration configuration,SqlNode contents) {
    // 設置前綴、須要刪除的前綴和後綴
    super(configuration, contents, "SET", COMMA, null, COMMA);
  }

}

基於TrimSqlNode類,定義了須要添加的前綴爲SET、須要刪除的前綴,和須要刪除的後綴,

ForeachNode

org.apache.ibatis.scripting.xmltags.ForeachNode:實現 SqlNode 接口,<foreach /> 標籤對應的 SqlNode 實現類

其中apply(DynamicContext context)方法的處理邏輯饒了我半天,你們能夠仔細看一下

構造方法
public class ForEachSqlNode implements SqlNode {
    /**
     * 集合中元素綁定到上下文中 key 的前綴
     */
	public static final String ITEM_PREFIX = "__frch_";
	/**
	 * 表達式計算器
	 */
	private final ExpressionEvaluator evaluator;
    /**
     * 須要遍歷的集合類型,支持:list set map array
     */
	private final String collectionExpression;
    /**
     * MixedSqlNode,包含該<where />節點內全部信息
     */
	private final SqlNode contents;
    /**
     * 開頭
     */
	private final String open;
    /**
     * 結尾
     */
	private final String close;
    /**
     * 每一個元素以什麼分隔
     */
	private final String separator;
	/**
	 * 集合中每一個元素的值
	 */
	private final String item;
	/**
	 * 集合中每一個元素的索引
	 */
	private final String index;
	private final Configuration configuration;

	public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index,
			String item, String open, String close, String separator) {
		this.evaluator = new ExpressionEvaluator();
		this.collectionExpression = collectionExpression;
		this.contents = contents;
		this.open = open;
		this.close = close;
		this.separator = separator;
		this.index = index;
		this.item = item;
		this.configuration = configuration;
	}
}

對每一個屬性進行賦值,參考每一個屬性上面的註釋

apply方法
@Override
public boolean apply(DynamicContext context) {
    // 獲取入參
    Map<String, Object> bindings = context.getBindings();
    /*
     * <1> 得到遍歷的集合的 Iterable 對象,用於遍歷
     * 例如配置了 collection 爲如下類型
     * list:則從入參中獲取到 List 集合類型的屬性的值
     * array:則從入參中獲取到 Array 數組類型的屬性的值,會轉換成 ArrayList
     * map:則從入參中獲取到 Map 集合類型的屬性的值
     */
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
        // 集合中沒有元素則無需遍歷
        return true;
    }
    boolean first = true;
    // <2> 添加 open 到 SQL 中
    applyOpen(context);
    int i = 0;
    for (Object o : iterable) {
        // <3> 記錄原始的 context 對象,下面經過兩個裝飾器對他進行操做
        DynamicContext oldContext = context;
        // <4> 生成一個 context 裝飾器
        if (first || separator == null) {
            context = new PrefixedContext(context, "");
        } else {
            // 設置其須要添加的前綴爲分隔符
            context = new PrefixedContext(context, separator);
        }
        // <5> 生成一個惟一索引值
        int uniqueNumber = context.getUniqueNumber();
        // Issue #709
        // <6> 綁定到 context 中
        if (o instanceof Map.Entry) {
            @SuppressWarnings("unchecked")
            Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
            /*
             * 和下面同理,只不過索引是 Map 的 key
             */
            applyIndex(context, mapEntry.getKey(), uniqueNumber);
            applyItem(context, mapEntry.getValue(), uniqueNumber);
        } else {
            /*
             * 綁定當前集合中當前元素的索引到當前解析 SQL 語句的上下文中
             *
             * 1. 'index' -> i
             *
             * 2. __frch_'index'_uniqueNumber -> i
             */
            applyIndex(context, i, uniqueNumber);
            /*
             * 綁定集合中當前元素的值到當前解析 SQL 語句的上下文中
             *
             * 1. 'item' -> o
             *
             * 2. __frch_'item'_uniqueNumber -> o
             *
             */
            applyItem(context, o, uniqueNumber);
        }
        /*
         * 再裝飾一下 PrefixedContext -> FilteredDynamicContext
         *
         * 前者進行前綴的添加,第一個元素添加後設置爲已添加標記,後續不在添加
         * 後者將<foreach />標籤內的"#{item}"或者"#{index}"替換成上面咱們已經綁定的數據:"#{__frch_'item'_uniqueNumber}"或者"#{__frch_'index'_uniqueNumber}"
         *
         * <7> 進行轉換,將<foreach />標籤內部定義的內容進行轉換
         */
        contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
        if (first) { // <8> 判斷 prefix 是否已經插入
            first = !((PrefixedContext) context).isPrefixApplied();
        }
        // <9> 恢復原始的 context 對象,由於目前 context 是裝飾器
        context = oldContext;
        i++;
    }
    // <10> 添加 close 到 SQL 中
    applyClose(context);
    // <11> 移除 index 和 item 對應的綁定
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
}

private void applyIndex(DynamicContext context, Object o, int i) {
    if (index != null) {
        context.bind(index, o);
        context.bind(itemizeItem(index, i), o);
    }
}
private void applyItem(DynamicContext context, Object o, int i) {
    if (item != null) {
        context.bind(item, o);
        context.bind(itemizeItem(item, i), o);
    }
}
private void applyOpen(DynamicContext context) {
    if (open != null) {
        context.appendSql(open);
    }
}
private void applyClose(DynamicContext context) {
    if (close != null) {
        context.appendSql(close);
    }
}
private static String itemizeItem(String item, int i) {
    return ITEM_PREFIX + item + "_" + i;
}
  1. 得到須要遍歷的集合 Iterable 對象,調用ExpressionEvaluatorevaluateIterable(String expression, Object parameterObject)方法,根據表達式從參數中獲取集合對象

    1. 先經過OgnlCache根據Ognl表達式從上下文的ContextMap中獲取轉換後的結果,OgnlCache在後面會講到
    2. 若是是Array數組類型,則轉換成ArrayList集合後返回
    3. 若是是Map類型,則調用Map.Entry的集合
  2. 若是定義了open屬性,則先拼接到SQL中

  3. 開始遍歷集合 Iterable 對象,先記錄context原始對象爲oldContext,由於接下來須要對其進行兩次裝飾,而這裏會再次進入

  4. 建立一個PrefixedContext對象,裝飾context,主要是對集合中的每一個元素添加separator分隔符

  5. 生成一個惟一索引值,也就是DynamicContext的uniqueNumber++,這樣集合中每一個元素都有一個惟一索引了

  6. 將集合中的當前元素綁定到上下文中,會保存如下信息:

    applyIndex:若是配置了index屬性,則將當前元素的索引值綁定到上下文的ContextMap中,保存兩個數據:

    1. 'index' -> i,其中'index'就是咱們在<foreach />標籤中配置的index屬性,i就是當前元素在集合中的索引

    2. __frch_'index'_uniqueNumber -> i

    applyItem:若是配置了item屬性,則將當前元素綁定到上下文的ContextMap中,保存兩個數據:

    1. 'item' -> o,其中'item'就是咱們在<foreach />標籤中配置的item屬性,o就是當前元素對象
    2. __frch_'item'_uniqueNumber -> o
  7. 再將PrefixedContext對象裝飾成FilteredDynamicContext對象

    而後應用<foreach />標籤內部的SqlNode節點們

    主要是替換咱們在<foreach />標籤中定義的內容,替換成上面第6步綁定的數據的key值,這樣就能夠獲取到該key對應的value了

    例如:將<foreach />標籤內的#{item}或者#{index}替換成第6步已經綁定的數據的key值#{__frch_'item'_uniqueNumber}或者#{__frch_'index'_uniqueNumber},而後拼接到SQL中

  8. 判斷是否添加了open前綴,添加了則遍歷時不用再添加前綴

  9. 恢復原始的oldContext對象,由於目前context是裝飾器,而後繼續遍歷

  10. 若是定義了close屬性,則拼接到SQL中

  11. 從上下文的ContextMap中移除第6步綁定的第1條數據

6步中,若是是Map類型,i對應的就是key值,o對應的就是value值,爲何兩個方法都須要保存第1條數據?

由於<foreach />標籤中可能還有其餘的標籤,例如<if />標籤,它的判斷條件中可能須要用到當前元素或者索引值,而表達式中使用了'index'或者'item',那麼就須要從上下文中獲取到對應的值了

那麼接下來咱們來看看內部定義的兩個類:PrefixedContext和FilteredDynamicContext

PrefixedContext

ForeachNode的內部類,繼承了DynamicContext,用於應用<foreach />標籤時添加分隔符

重寫了appendSql方法,邏輯比較簡單,判斷是否須要添加分隔符,代碼以下:

private class PrefixedContext extends DynamicContext {
    /**
     * 裝飾的 DynamicContext 對象
     */
    private final DynamicContext delegate;
    /**
     * 須要添加的前綴
     */
    private final String prefix;
    /**
     * 是否已經添加
     */
    private boolean prefixApplied;

    public PrefixedContext(DynamicContext delegate, String prefix) {
        super(configuration, null);
        this.delegate = delegate;
        this.prefix = prefix;
        this.prefixApplied = false;
    }

    public boolean isPrefixApplied() {
        return prefixApplied;
    }

    @Override
    public Map<String, Object> getBindings() {
        return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
        delegate.bind(name, value);
    }

    @Override
    public void appendSql(String sql) {
        if (!prefixApplied && sql != null && sql.trim().length() > 0) {
            delegate.appendSql(prefix);
            prefixApplied = true;
        }
        delegate.appendSql(sql);
    }

    @Override
    public String getSql() {
        return delegate.getSql();
    }

    @Override
    public int getUniqueNumber() {
        return delegate.getUniqueNumber();
    }
}
FilteredDynamicContext

ForeachNode的私有靜態內部類,繼承了DynamicContext,用於應用<foreach />標籤時替換內部的#{item}或者#{index},

重寫了appendSql方法,代碼以下:

private static class FilteredDynamicContext extends DynamicContext {
    /**
     * 裝飾的對象
     */
    private final DynamicContext delegate;
    /**
     * 集合中當前元素的索引
     */
    private final int index;
    /**
     * <foreach />定義的 index 屬性
     */
    private final String itemIndex;
    /**
     * <foreach />定義的 item 屬性
     */
    private final String item;

    public FilteredDynamicContext(Configuration configuration, DynamicContext delegate, String itemIndex, String item, int i) {
        super(configuration, null);
        this.delegate = delegate;
        this.index = i;
        this.itemIndex = itemIndex;
        this.item = item;
    }

    @Override
    public Map<String, Object> getBindings() {
        return delegate.getBindings();
    }

    @Override
    public void bind(String name, Object value) {
        delegate.bind(name, value);
    }

    @Override
    public String getSql() {
        return delegate.getSql();
    }

    @Override
    public void appendSql(String sql) {
        GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
            // 若是在`<foreach />`標籤下的內容爲經過item獲取元素,則替換成`__frch_'item'_uniqueNumber`
            String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
            /*
             * 若是在`<foreach />`標籤中定義了index屬性,而且標籤下的內容爲經過index獲取元素
             * 則替換成`__frch_'index'_uniqueNumber`
             */
            if (itemIndex != null && newContent.equals(content)) {
                newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
            }
            /*
             * 返回`#{__frch_'item'_uniqueNumber}`或者`#{__frch_'index'_uniqueNumber}`
             * 由於在前面已經將集合中的元素綁定在上下文的ContextMap中了,因此能夠經過上面兩個key獲取到對應元素的值
             * 例如綁定的數據:
             * 1. __frch_'item'_uniqueNumber = 對應的元素值
             * 2. __frch_'index'_uniqueNumber = 對應的元素值的索引
             */
            return "#{" + newContent + "}";
        });

        delegate.appendSql(parser.parse(sql));
    }

    @Override
    public int getUniqueNumber() {
        return delegate.getUniqueNumber();
    }
}
  1. 建立一個GenericTokenParser對象parser,用於處理 #{}

  2. 建立一個TokenHandler處理器,大體的處理邏輯:

    1. 若是在<foreach />標籤下的內容爲經過item獲取元素,則替換成__frch_'item'_uniqueNumber
    2. 若是在<foreach />標籤中定義了index屬性,而且標籤下的內容爲經過index獲取元素,則替換成__frch_'index'_uniqueNumber
    3. 返回#{__frch_'item'_uniqueNumber}或者#{__frch_'index'_uniqueNumber},由於在前面已經將集合中的元素綁定在上下文的ContextMap中了,因此能夠經過上面兩個key獲取到對應元素的值
  3. 調用parser進行解析,使用第2建立處理器進行處理,而後將轉換後的結果拼接到SQL中

IfSqlNode

org.apache.ibatis.scripting.xmltags.IfSqlNode:實現 SqlNode 接口,<if /> 標籤對應的 SqlNode 實現類,代碼以下:

public class IfSqlNode implements SqlNode {
    /**
     * 表達式計算器
     */
	private final ExpressionEvaluator evaluator;
    /**
     * 判斷條件的表達式
     */
	private final String test;
    /**
     * MixedSqlNode,包含該<if />節點內全部信息
     */
	private final SqlNode contents;

	public IfSqlNode(SqlNode contents, String test) {
		this.test = test;
		this.contents = contents;
		this.evaluator = new ExpressionEvaluator();
	}

	@Override
	public boolean apply(DynamicContext context) {
		// <1> 判斷是否符合條件
		if (evaluator.evaluateBoolean(test, context.getBindings())) {
			// <2> 解析該<if />節點中的內容
			contents.apply(context);
			return true;
		}
		// <3> 不符合
		return false;
	}
}
  1. 調用ExpressionEvaluatorevaluateBoolean(String expression, Object parameterObject)方法,根據表達式從參數中獲取結果

    1. 先經過OgnlCache根據Ognl表達式從上下文的ContextMap中獲取轉換後的結果,OgnlCache在後面會講到
    2. 若是是Boolean,則轉換成Boolean類型返回
    3. 若是是Number類型,則判斷是否不等於 0
    4. 其餘類則判斷是否不等於null
  2. 根據第1步的結果判斷是否應用<if />標籤內的SqlNode節點們

ChooseSqlNode

org.apache.ibatis.scripting.xmltags.ChooseSqlNode:實現 SqlNode 接口,<choose /> 標籤對應的 SqlNode 實現類,代碼以下:

public class ChooseSqlNode implements SqlNode {
  /**
   * <otherwise /> 標籤對應的 SqlNode 節點
   */
  private final SqlNode defaultSqlNode;
  /**
   * <when /> 標籤對應的 SqlNode 節點數組
   */
  private final List<SqlNode> ifSqlNodes;

  public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
    this.ifSqlNodes = ifSqlNodes;
    this.defaultSqlNode = defaultSqlNode;
  }

  @Override
  public boolean apply(DynamicContext context) {
    // <1> 先判斷 <when /> 標籤中,是否有符合條件的節點。
    // 若是有,則進行應用。而且只因應用一個 SqlNode 對象
    for (SqlNode sqlNode : ifSqlNodes) {
      if (sqlNode.apply(context)) {
        return true;
      }
    }
    // <2> 再判斷  <otherwise /> 標籤,是否存在
    // 若是存在,則進行應用
    if (defaultSqlNode != null) {
      defaultSqlNode.apply(context);
      return true;
    }
    // <3> 返回都失敗
    return false;
  }
}
  1. 先應用<choose />下的全部<when />標籤所對應的IfSqlNode,有一個應用成功則返回true
  2. 若是全部的<when />都不知足條件,則應用<otherwise />標籤下的內容所對應的SqlNode

StaticTextSqlNode

org.apache.ibatis.scripting.xmltags.StaticTextSqlNode:實現 SqlNode 接口,用於保存靜態文本,邏輯比較簡單,直接拼接文本,代碼以下:

public class StaticTextSqlNode implements SqlNode {
    /**
     * 靜態內容
     */
	private final String text;

	public StaticTextSqlNode(String text) {
		this.text = text;
	}

	@Override
	public boolean apply(DynamicContext context) {
	    // 直接往正在解析 SQL 語句的上下文的 SQL 中添加該內容
		context.appendSql(text);
		return true;
	}
}

TextSqlNode

org.apache.ibatis.scripting.xmltags.TextSqlNode:實現了 SqlNode 接口,用於處理${},注入對應的值,代碼以下:

public class TextSqlNode implements SqlNode {
  /**
   * 動態文本
   */
  private final String text;
  /**
   * 注入時的過濾器
   */
  private final Pattern injectionFilter;

  public TextSqlNode(String text) {
    this(text, null);
  }

  public TextSqlNode(String text, Pattern injectionFilter) {
    this.text = text;
    this.injectionFilter = injectionFilter;
  }

  public boolean isDynamic() {
    // <1> 建立 DynamicCheckerTokenParser 對象
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    // <2> 建立 GenericTokenParser 對象
    GenericTokenParser parser = createParser(checker);
    // <3> 執行解析,若是存在 '${ }',則 checker 會設置 isDynamic 爲true
    parser.parse(text);
    // <4> 判斷是否爲動態文本
    return checker.isDynamic();
  }

  @Override
  public boolean apply(DynamicContext context) {
    // <1> 建立 BindingTokenParser 對象
    // <2> 建立 GenericTokenParser 對象
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    // <3> 執行解析
    // <4> 將解析的結果,添加到 context 中
    context.appendSql(parser.parse(text));
    return true;
  }

  private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }

  private static class BindingTokenParser implements TokenHandler {

    private DynamicContext context;
    /**
     * 注入時的過濾器
     */
    private Pattern injectionFilter;

    public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
      this.context = context;
      this.injectionFilter = injectionFilter;
    }

    @Override
    public String handleToken(String content) {
      // 從上下文中獲取入參對象,在DynamicContext的構造方法中能夠看到爲何能夠獲取到
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        context.getBindings().put("value", parameter);
      }
      // 使用 OGNL 表達式,得到對應的值
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
      // 使用過濾器進行過濾
      checkInjection(srtValue);
      return srtValue;
    }

    private void checkInjection(String value) {
      if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
        throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
      }
    }
  }
}

在XML文件中編寫SQL語句時,若是使用到了${}做爲變量時,那麼會生成TextSqlNode對象,能夠回看XMLScriptBuilderparseDynamicTags()方法

在MyBatis處理SQL語句時就會將${}進行替換成對應的參數,存在SQL注入的安全性問題

#{}就不同了,MyBatis會將其替換成?佔位符,經過java.sql.PreparedStatement進行預編譯處理,不存在上面的問題

  1. 建立GenericTokenParser對象parser,用於處理${},設置的Token處理器爲BindingTokenParser
  2. 執行解析,咱們能夠看到BindingTokenParser的handleToken(String content)方法
    1. 從上下文中獲取入參對象,在DynamicContext的構造方法中能夠看到爲何能夠獲取到
    2. 在將入參對象綁定到上下文中,設置key爲"value",爲何這麼作呢??沒有仔細探究,可能跟OGNL相關,感興趣的小夥伴能夠探討一下😈
    3. 使用 OGNL 表達式,從上下文中得到${}中內容對應的值,若是爲null則設置爲空字符串
    4. 使用注入過濾器對注入的值過濾
  3. 將解析後的結果拼接到SQL中

MixedSqlNode

org.apache.ibatis.scripting.xmltags.MixedSqlNode:實現 SqlNode 接口,用於保存多個SqlNode對象

由於一個SQL語句會被解析成多個SqlNode,且內部還會嵌套多個,因此使用MixedSqlNode進行保存,代碼以下:

public class MixedSqlNode implements SqlNode {
  /**
   * 動態節點集合
   */
	private final List<SqlNode> contents;

	public MixedSqlNode(List<SqlNode> contents) {
		this.contents = contents;
	}

	@Override
	public boolean apply(DynamicContext context) {
	  // 逐個應用
		contents.forEach(node -> node.apply(context));
		return true;
	}
}

OgnlCache

org.apache.ibatis.scripting.xmltags.OgnlCache:用於處理Ognl表達式

在上面SqlNode的apply方法中,使用到的邏輯判斷時獲取表達式的結果則須要經過OgnlCache來進行解析

對OGNL不瞭解的小夥伴能夠看下這篇文章:Ognl表達式基本原理和使用方法

代碼以下:

public final class OgnlCache {

	/**
	 * OgnlMemberAccess 單例,用於修改某個對象的成員爲可訪問
	 */
	private static final OgnlMemberAccess MEMBER_ACCESS = new OgnlMemberAccess();
	/**
	 * OgnlClassResolver 單例,用於建立 Class 對象
	 */
	private static final OgnlClassResolver CLASS_RESOLVER = new OgnlClassResolver();
	/**
	 * 表達式的緩存的映射
	 *
	 * KEY:表達式 VALUE:表達式的緩存 @see #parseExpression(String)
	 */
	private static final Map<String, Object> expressionCache = new ConcurrentHashMap<>();

	private OgnlCache() {
		// Prevent Instantiation of Static Class
	}

	public static Object getValue(String expression, Object root) {
		try {
            /*
             * <1> 建立 OgnlContext 對象,設置 OGNL 的成員訪問器和類解析器,設置根元素爲 root 對象
             * 這裏是調用 OgnlContext 的s etRoot 方法直接設置根元素,能夠經過 'user.id' 獲取結果
             * 若是是經過 put 方法添加的對象,則取值時須要使用'#',例如 '#user.id'
             */
            Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null);
            /*
             * <2> expression 轉換成 Ognl 表達式
             * <3> 根據 Ognl 表達式獲取結果
             */
			return Ognl.getValue(parseExpression(expression), context, root);
		} catch (OgnlException e) {
			throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
		}
	}

  /**
   * 根據表達式構建一個 Ognl 表達式
   *
   * @param expression 表達式,例如<if test="user.id &gt; 0"> </if>,那這裏傳入的就是 "user.id &gt; 0"
   * @return Ognl 表達式
   * @throws OgnlException 異常
   */
  private static Object parseExpression(String expression) throws OgnlException {
    Object node = expressionCache.get(expression);
    if (node == null) {
      node = Ognl.parseExpression(expression);
      expressionCache.put(expression, node);
    }
    return node;
  }

}

getValue方法:根據Ognl表達式從Object中獲取結果

  1. 建立 OgnlContext 對象,設置 OGNL 的成員訪問器和類解析器,設置根元素爲 root 對象

  2. 將建立 expression 轉換成 Ognl 表達式,緩存起來

  3. 根據 Ognl 表達式從 root 對象中獲取結果

總結

本文講述的是XML映射文件中的<select /> <insert /> <update /> <delete />節點內的SQL語句如何被解析的

XMLLanguageDriver語言驅動類中,經過XMLScriptBuilder對該到節點的內容進行解析,建立相應的SqlSource資源對象

在其解析的過程會根據不一樣的NodeHandler節點處理器對MyBatis自定義的標籤(<if /> <foreach />等)進行處理,生成相應的SqlNode對象,最後將全部的SqlNode對象存放在MixedSqlNode

解析的過程當中會判斷是否爲動態的SQL語句,包含了MyBatis自定義的標籤或者使用了${}都是動態的SQL語句,動態的SQL語句建立DynamicSqlSource對象,不然建立RawSqlSource對象

那麼關於SqlSource是什麼其實這裏還不是特別瞭解,因爲其涉及到的篇幅並很多,因此另起一篇文檔《MyBatis初始化(四)之SQL初始化(下)》進行分析

參考文章:芋道源碼《精盡 MyBatis 源碼分析》

相關文章
相關標籤/搜索