parsing,從字面上理解就是編譯解析的意思,那么這個包中的內容就應該和mybatis配置文件的編譯解析有關系。本文首先會按照引用層次來分別介紹這個包中各個類的作用,而后再用實際的例子解釋它們是如何組合到一起去解決了什么樣的問題。
一、類和接口介紹
1.TokenHandler
public interface TokenHandler { String handleToken(String content); }
這個接口中只有一個函數,就是對字符串進行處理。
2.GenericTokenParser
從這個類的名字看到,這個類是對常用Token進行parser的類,我們首先了解這個類的屬性和構造函數:
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; }
在了解完這個類的屬性及構造函數后,我們來看下這個的主要的也是唯一的函數到底做了那些事情:
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,了解下它對傳入的字符串做了怎樣的處理。
3.PropertyParser
PropertyParser這個類中包含一個內部私有的靜態類VariableTokenHandler。VariableTokenHandler實現了TokenHandler接口,包含了一個Properties類型的屬性,在初始化這個類時需指定該屬性的值。VariableTokenHandler類對handleToken函數的具體實現如下:
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)。
打開這個類的outline會發現這個類包含的函數真的是“蔚為壯觀”,雖然數量眾多,基本上可以分為兩類:初始化(構造函數)、evalXXX。
4.1初始化(構造函數)
XPathParser類的構造函數數量眾多,是由於這個類的屬性比較多,這些構造函數內部都會調用到如下函數:commonConstructor和createDocument。接下來我們來看看這兩個函數具體做了那些事情:
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; }
二、使用示例
我們寫一個示例看看這個包中的類是如何運行的。從上面的類和接口的介紹中可以發現,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中的替換過程進行下對比,看看兩者在實現上有何不同。