MyBatis印象閱讀之NodeHandler和SqlNode解析(上)

回想在上一章中我們欠下的技術債:

nodeHandler
sqlNode
sqlSource
ParameterMapping

下面我們將一個個解決,首先我們先選擇nodeHandler和sqlNode這個技術點,因為他們是連在一起的。

1. NodeHandler源碼分析

首先我們先要回顧下之前是在哪里欠下的債,不能糊里糊涂的。

之前在構建XMLScriptBuilder的過程中的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());
  }

這些標簽在我們使用MyBatis的過程中經(jīng)常會用到,不過這里我們還是結合官網(wǎng)幫我們做的歸類來進行分析。

第一批是 :

if
choose (when, otherwise)
trim (where, set)
foreach

我們先看NodeHandler接口的方法:

  private interface NodeHandler {
    void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
  }

有了這個底之后,我們接下來邊進行分析,首先我們從if標簽的handler入手:

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

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      String test = nodeToHandle.getStringAttribute("test");
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);
    }
  }

這里有個很冷門的知識,關于 // Prevent Synthetic Access的,具體可以推薦文檔:
java合成方法
這里不做展開。我們關注點放在MyBatis本身。

OK,我們繼續(xù),在上面的IfHandler中,我們又引入了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;
  }
}

至于SqlNode接口,我們也來回憶下:

public interface SqlNode {
  boolean apply(DynamicContext context);
}

總結:這里的MixedSqlNode顧名思義就是混合節(jié)點,對于存儲多個SqlNode,然后調(diào)用。

我們繼續(xù)向下看,介紹完了MixedSqlNode,我們來看它是怎么出現(xiàn)的:

  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

這里就是解析sql標簽,轉為沒一個個SqlNode,涉及到遞歸調(diào)用,硬看的話會比較復雜難理解,我換成一種人腦比較好理解的,用實例理解:

  <select id="selectOddPostsInKeysList" resultType="org.apache.ibatis.domain.blog.Post">
    SELECT *
    FROM POST P
    WHERE ID in
    <foreach item="item" index="index" collection="keys"
             open="(" separator="," close=")">
      <if test="index % 2 != 0">
        #{item}
      </if>
    </foreach>
    ORDER BY P.ID
  </select>

解析:在這里,因為是遞歸的,所以我們先會解析<if>,在解析<foreach>最后解析<select>,不要跟著代碼順序走,很容易讓你煩躁,失去耐心,其實理解其意便可。最主要還是理解解析的流程:

  • 如果是單純的SQL,內(nèi)部沒有其他任何標簽了,那么我們會進入到第一個方法
  • 如果內(nèi)部還有標簽,會把標簽解析的信息放在contents中,一層套一層,最終contents會返回我們所有的標簽解析信息。

解析完這些我們在回過頭來看:


      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      String test = nodeToHandle.getStringAttribute("test");
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);

這里大概的意思是解析if標簽下的內(nèi)容和if標簽中的test內(nèi)容,放入到IfSqlNode中,所以又引出了IfSqlNode類。我們來看下源碼:

public class IfSqlNode implements SqlNode {
  private final ExpressionEvaluator evaluator;
  private final String test;
  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) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }

}

其中 new ExpressionEvaluator中使用了Ognl來進行驗證,我不太會這部分內(nèi)容,這里也不做展開。只需知道這里是用來判斷的。更多的可以查看調(diào)試ExpressionEvaluatorTest:

  //ExpressionEvaluatorTest
  
  @Test
  void shouldCompareStringsReturnTrue() {
    boolean value = evaluator.evaluateBoolean("username == 'cbegin'", new Author(1, "cbegin", "******", "cbegin@apache.org", "N/A", Section.NEWS));
    assertTrue(value);
  }

這一部分關于IfSqlNode標簽解析的過程很可能會把你繞暈,這里我還是推薦跟讀調(diào)試源碼來進行閱讀。

下面是我們第二個標簽:choose (when, otherwise)標簽


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<>();
      handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
      SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
      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) {
          handler.handleNode(child, ifSqlNodes);
        } else if (handler instanceof OtherwiseHandler) {
          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;
    }
  }

}

這一段的代碼比較長,在handleWhenOtherwiseNodes主要是用來解析choose下的when和otherwise節(jié)點,但是我們看到在注冊hander的時候,我們的when節(jié)點其實注冊的是IfHandler,顧所以這里判斷了IfHandler。


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

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      targetContents.add(mixedSqlNode);
    }
  }

而getDefaultSqlNode方法是主要保證只有一個Otherwise標簽。

最后封裝進ChooseSqlNode:


public class ChooseSqlNode implements SqlNode {
  private final SqlNode defaultSqlNode;
  private final List<SqlNode> ifSqlNodes;

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

  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : ifSqlNodes) {
      if (sqlNode.apply(context)) {
        return true;
      }
    }
    if (defaultSqlNode != null) {
      defaultSqlNode.apply(context);
      return true;
    }
    return false;
  }
}

這里的邏輯也比較簡單,大家自行閱讀即可。

2.今日總結

今天我們分析了關于nodeHandler和sqlnode源碼的分析和解析過程,會很繞,但是如果把握住重心調(diào)試幾遍,應該還是沒有問題的~~~

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容