Mybatis源代碼分析之parsing包

parsing,從字面上理解就是編譯解析的意思,那麼這個包中的內容就應該和mybatis配置文件的編譯解析有關係。本文首先會按照引用層次來分別介紹這個包中各個類的做用,然後再用實際的例子解釋它們是如何組合到一塊兒去解決了什麼樣的問題。node

1、類和接口介紹

1.TokenHandler
public interface TokenHandler {
  String handleToken(String content);
}

這個接口中只有一個函數,就是對字符串進行處理。mysql

2.GenericTokenParser

從這個類的名字看到,這個類是對經常使用Token進行parser的類,咱們首先了解這個類的屬性和構造函數:spring

private final String openToken;//開始標識
  private final String closeToken;//結束標識
  private final TokenHandler handler;//token處理器

  //利用帶參數的構造函數初始化各項屬性
  public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
  }

在瞭解完這個類的屬性及構造函數後,咱們來看下這個的主要的也是惟一的函數到底作了那些事情:sql

public String parse(String text) {
    StringBuilder builder = new StringBuilder();
    if (text != null && text.length() > 0) {//若是傳入的字符串有值
      //將字符串轉爲字符數組
      char[] src = text.toCharArray();
      int offset = 0;
      //判斷openToken在text中的位置,注意indexOf函數的返回值-1表示不存在,0表示在在開頭的位置
      int start = text.indexOf(openToken, offset);
      while (start > -1) {
        if (start > 0 && src[start - 1] == '\\') {
          //若是text中在openToken前存在轉義符就將轉義符去掉。若是openToken前存在轉義符,start的值必然大於0,最小也爲1
          //由於此時openToken是不須要進行處理的,因此也不須要處理endToken。接着查找下一個openToken
          builder.append(src, offset, start - 1).append(openToken);
          offset = start + openToken.length();//重設offset
        } else {
          int end = text.indexOf(closeToken, start);
          if (end == -1) {//若是不存在openToken,則直接將offset位置後的字符添加到builder中
            builder.append(src, offset, src.length - offset);
            offset = src.length;//重設offset
          } else {
            builder.append(src, offset, start - offset);//添加openToken前offset後位置的字符到bulider中
            offset = start + openToken.length();//重設offset
            String content = new String(src, offset, end - offset);//獲取openToken和endToken位置間的字符串
            builder.append(handler.handleToken(content));//調用handler進行處理
            offset = end + closeToken.length();//重設offset
          }
        }
        start = text.indexOf(openToken, offset);//開始下一個循環
      }
      //只有當text中不存在openToken且text.length大於0時纔會執行下面的語句
      if (offset < src.length) {
        builder.append(src, offset, src.length - offset);
      }
    }
    return builder.toString();
  }

簡單的說,這個函數的做用就是將openToken和endToken間的字符串取出來用handler處理下,而後再拼接到一塊。咱們接下來看一個具體的handler,瞭解下它對傳入的字符串作了怎樣的處理。express

3.PropertyParser

PropertyParser這個類中包含一個內部私有的靜態類VariableTokenHandler。VariableTokenHandler實現了TokenHandler接口,包含了一個Properties類型的屬性,在初始化這個類時需指定該屬性的值。VariableTokenHandler類對handleToken函數的具體實現以下:apache

public String handleToken(String content) {
       //若是variables不爲空且存在key爲content的property,就從variables中返回具體的值,不然在content兩端添加上${和}
      if (variables != null && variables.containsKey(content)) {
        return variables.getProperty(content);
      }
      return "${" + content + "}";
    }

在瞭解完PropertyParser的內部類VariableTokenHandler後,咱們在來了解下PropertyParser類的parser靜態方法:數組

public static String parse(String string, Properties variables) {
    //先初始化一個handler
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    //在初始化GenericTokenParser對象,設置openToken爲${,endToken爲}
    //有沒有對${}比較熟悉,這個符號就是mybatis配置文件中的佔位符,例如定義datasource時用到的 <property name="driverClassName" value="${driver}" />
    //同時也能夠解釋在VariableTokenHandler中的handleToken時,若是content在properties中不存在時,返回的內容要加上${}了。
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }
4.XPathParser

XPathParser類parsing包中的核心類之一,既然這個類是XPath的Parser,就須要對xpath的語法有所瞭解,若是對此不熟悉的讀者最好能先了解xpath的語法(http://www.w3school.com.cn/xpath/index.asp)。mybatis

打開這個類的outline會發現這個類包含的函數真的是「蔚爲壯觀」,雖然數量衆多,基本上能夠分爲兩類:初始化(構造函數)、evalXXX。app

4.1初始化(構造函數)

XPathParser類的構造函數數量衆多,是因爲這個類的屬性比較多,這些構造函數內部都會調用到以下函數:commonConstructor和createDocument。接下來咱們來看看這兩個函數具體作了那些事情:dom

private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
    //初始化這個類的基本屬性
    this.validation = validation;
    this.entityResolver = entityResolver;
    this.variables = variables;
    //利用XPathFactory建立一個新的xpath對象
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();
  }
private Document createDocument(InputSource inputSource) {
    // important: this must only be called AFTER common constructor
    // mybatis源代碼基本上沒有什麼註釋,可是上面這行註釋是源代碼中自帶的。
    // 那爲何必須在調用commonConstructor函數後才能調用這個函數呢?由於這個函數裏面用到了兩個屬性:validation和entityResolver
    // 若是在這兩個屬性沒有設置前就調用這個函數,就可能會致使這個類內部屬性衝突
    try {
      //建立document時用到了兩個類:DocumentBuilderFactory和DocumentBuilder。
      //爲何設置這兩個類的這些屬性,這些屬性有什麼做用。要徹底介紹清楚須要很多篇幅,在這裏就不作介紹了,
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setValidating(validation);
      factory.setNamespaceAware(false);
      factory.setIgnoringComments(true);
      factory.setIgnoringElementContentWhitespace(false);
      factory.setCoalescing(false);
      factory.setExpandEntityReferences(true);
      DocumentBuilder builder = factory.newDocumentBuilder();
      builder.setEntityResolver(entityResolver);
      builder.setErrorHandler(new ErrorHandler() {
        public void error(SAXParseException exception) throws SAXException {
          throw exception;
        }
        public void fatalError(SAXParseException exception) throws SAXException {
          throw exception;
        }
        public void warning(SAXParseException exception) throws SAXException {
        }
      });
      return builder.parse(inputSource);
    } catch (Exception e) {
      throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    }
  }
4.2 evalXXX

這個類中的evalXXX函數有兩種多態形式:一種是隻有一個expression參數;另外一種則有兩個函數,除了expression參數外還包含一個root參數。像咱們常常見到的那樣,帶一個參數的evalXXX函數會在設置一個默認值後調用帶有兩個參數的函數,咱們看一個具體的例子evalString:

public String evalString(String expression) {
    //設置類中的document屬性做爲root,
    return evalString(document, expression);
  }

  public String evalString(Object root, String expression) {
    String result = (String) evaluate(expression, root, XPathConstants.STRING);
    result = PropertyParser.parse(result, variables);
    return result;
  }

而在帶有兩個參數的evalString中調用了evaluate函數,這個函數纔是真正開始了對xpath表達式的解析:

private Object evaluate(String expression, Object root, QName returnType) {
    try {
      //調用xpath類進行相應的解析。
      //注意returnType參數,雖然evaluate返回的數據類型是Object的,可是若是指定了錯誤的returnType,那麼在進行類型轉換時將會報類型轉換異常
      return xpath.evaluate(expression, root, returnType);
    } catch (Exception e) {
      throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
    }
  }

其餘的evalXXX和evalString大同小異,主要的不一樣在類型轉換和returnType參數設置上。

5.XNode

接下來咱們來了解parsing包中的最後一個類XNode。該類是對org.w3c.dom.Node類的一個封裝,在Node類的基礎上添加了一些新功能。

5.1構造函數

咱們首先來看XNode類的構造函數:

public XNode(XPathParser xpathParser, Node node, Properties variables) {
    this.xpathParser = xpathParser;
    this.node = node;
    this.name = node.getNodeName();
    this.variables = variables;
    //獲取當前節點的全部屬性
    this.attributes = parseAttributes(node);
    //獲取當前節點的文本節點內容,固然獲取到的數據是已經通過TokenHandler處理過的
    this.body = parseBody(node);
  }

構造函數調用了兩個函數:parseAttributes和parseBody。parseAttributes函數相對簡單些,就是利用Node類的函數去獲取該節點的全部屬性名和值,只是在獲取屬性值後會調用PropertyParser.parse()去處理下,在次就不貼源代碼了。咱們重點看下parseBody函數:

private String parseBody(Node node) {
    String data = getBodyData(node);
    //若是該節點不是文本節點或者CDATA節點,取其子節點值
    if (data == null) {
      NodeList children = node.getChildNodes();
      //儘管這個for循環不是一個好的實現方式,由於 children.getLength()被執行了屢次,但在mybatis的源代碼常常出現
      for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        data = getBodyData(child);
        //只要一個節點爲文本節點或者CDATA節點,就結束循環。於是此時的body值只是node的第一個文本節點的內容
        if (data != null) break;
      }
    }
    return data;
  }

  private String getBodyData(Node child) {
    //若是這個節點是文本節點或者CDATA節點,就取節點的內容,而後用PropertyParser.parse()處理下
    if (child.getNodeType() == Node.CDATA_SECTION_NODE
        || child.getNodeType() == Node.TEXT_NODE) {
      String data = ((CharacterData) child).getData();
      data = PropertyParser.parse(data, variables);
      return data;
    }
    return null;
  }
5.2 evalXXX

這個類中的evalXXX函數是經過XPathParser中的evalXXX來實現,以evalString爲例

public String evalString(String expression) {
    //傳入的object爲XNode類的node屬性
    return xpathParser.evalString(node, expression);
  }
5.3 getXXXBody

前面介紹了parseBody函數,經過這個函數設置了XNode類的body屬性,如今就要經過getXXXBody函數獲取body屬性並將其轉換爲對應的數據類型。咱們以getBooleanBody函數爲例:

public Boolean getBooleanBody() {
    //設置默認值爲null
    return getBooleanBody(null);
  }
  //兩個函數的不一樣在於這個函數具備一個默認值,而上面的沒有
  public Boolean getBooleanBody(Boolean def) {
    if (body == null) {
      return def;
    } else {
      return Boolean.valueOf(body);
    }
  }
5.4 getXXXAttribute

在介紹完getXXXBody後,咱們再來看看getXXXAttribute。XNode類中的attributes屬性是經過parseAttributes函數設置的,前面咱們也作過簡單的介紹。如今咱們來看看getXXXAttribute的運行機制,以getBooleanAttribute爲例,它的總體設計和getXXXBody很類似。

public Boolean getBooleanAttribute(String name) {
    return getBooleanAttribute(name, null);
  }

  public Boolean getBooleanAttribute(String name, Boolean def) {
    //從attributes獲取key,若是存在則進行類型轉換,不然就返回默認值
    String value = attributes.getProperty(name);
    if (value == null) {
      return def;
    } else {
      return Boolean.valueOf(value);
    }
  }
5.5 getChildren和getChildrenAsProperties

這是咱們最後要介紹的兩個函數。咱們先來看看getChildren,從字面上看這個函數的功能是獲取node的全部子節點,但其實是不是如此呢?咱們看看它的實現:

public List<XNode> getChildren() {
    List<XNode> children = new ArrayList<XNode>();
    //獲取全部子節點
    NodeList nodeList = node.getChildNodes();
    if (nodeList != null) {
      for (int i = 0, n = nodeList.getLength(); i < n; i++) {
        Node node = nodeList.item(i);
        //若是子節點類型是元素節點,就添加到list中
        if (node.getNodeType() == Node.ELEMENT_NODE) {
          children.add(new XNode(xpathParser, node, variables));
        }
      }
    }
    return children;
  }

從代碼中能夠看到該函數並非獲取node全部的節點,它只是獲取node的子元素節點。接下來咱們看getChildrenAsProperties函數:

public Properties getChildrenAsProperties() {
    Properties properties = new Properties();
    for (XNode child : getChildren()) {
      String name = child.getStringAttribute("name");
      String value = child.getStringAttribute("value");
      //只有當節點同時具備name和value屬性纔會添加到properties中
      if (name != null && value != null) {
        properties.setProperty(name, value);
      }
    }
    return properties;
  }

 

2、使用示例

咱們寫一個示例看看這個包中的類是如何運行的。從上面的類和接口的介紹中能夠發現,XPathParser類是比較上層的類(這裏的上層,不是說這個類是各個類的超類,而是說它依賴的類較多)。用XPathParser類解析一段數據源定義的配置文件片斷,首先設定properties文件的內容,文件名爲jdbc.properties:

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test
username=root
password=1q2w3e

代碼以下:

Properties properties = Resources.getResourceAsProperties("jdbc.properties");
        //定義數據源的xml片斷
        String xml ="<?xml version='1.0' encoding='utf-8'?>"+ 
                "<bean id='dataSource' class='org.apache.commons.dbcp.BasicDataSource' destroy-method='close' >    " +
                "    <property name='driverClassName' value='${driver}' />" +
                "    <property name='url' value='${url}' />    " +
                "    <property name='username' value='${username}' />    " +
                "    <property name='password' value='${password}' />    " +
                "</bean>";
        //初始化XPathParser
        XPathParser xPathParser = new XPathParser(xml,false,properties);
        //解析表達式,獲取XNode對象
        XNode xnode = xPathParser.evalNode("//bean");
        //下面調用對應的函數
        System.out.println(xnode);
        System.out.println(xnode.getValueBasedIdentifier());
        System.out.println(xnode.getStringAttribute("id"));
        System.out.println(xnode.getStringAttribute("class"));

這段代碼的執行結果以下:

<bean destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource" id="dataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="1q2w3e"/>
</bean>

bean[dataSource]
dataSource
org.apache.commons.dbcp.BasicDataSource

從代碼的執行結果能夠看到${}中的內容已經被properties文件中對應的值所替換。

這是mybatis中進行${}轉換的過程,下次再和spring中的替換過程進行下對比,看看二者在實現上有何不一樣。