一 前言
- Mybatis在初始化過程中處理mybatis-config.xml核心配置文件時,使用的是DOM解析方式,並結合使用XPath解析XML配置文件
- Mybatis對XML解析功能進行了封裝,由解析器模塊提供支持,其中核心類爲XPathParser,它封裝了XPath、Document以及EntityResolver等
二 基礎知識
- XML解析常見的三種方式:
- DOM(Document Object Model)解析方式
- SAX(Simple API for XML)解析方式
- StAX(Streaming API for XML)解析方式
- DOM:
- 基於樹形結構的解析方式,將整個XML文檔讀入到內存並構建一個DOM樹,基於這顆樹形結構對各個節點Node進行操作
- 優點:
- 易於編程,可以在根據需求在各個節點之間進行導航
- 缺點:
- 將整個XML加載到內存中並構建樹形結構,對於比較大的XML文檔,消耗很大
- SAX:
- SAX基於事件模型進行解析,它每次只是加載一部分文檔到內存,即可開始解析。
- 當SAX解析器解析到某類型節點時,會觸發註冊在該節點上的回調函數,開發人員可以註冊相應的回調函數進行操作,一般情況下繼承SAX提供的DefaultHandler基類,重寫相應的事件處理器並進行註冊即可
- 優點:
- 不會將整個XML文檔加載到內存,資源消耗較少
- 當程序處理過程滿足條件時可以即刻停止,不必解析剩餘的XML文檔
- 缺點:
- 基於事件驅動進行解析,不會將整個XML文檔加載到內存,需要開發人員自己維護業務邏輯涉及的各個節點
- StAX:
- 它可以很好的支持DOM和SAX解析方式,和SAX解析方式類似,但是不同之處在於StAX採用的是"拉模式",即應用程序通過調用解析器推進解析的過程
- StAX提供了兩套API:
- 基於指針的API,一種底層的API,優勢是效率高,缺點是抽象度低不易使用
- 基於迭代器的API,允許應用程序將XML文檔作爲一系列的事件對象來處理,缺點是效率低,但是易於使用
- XPath簡介
- XPath是一種爲查詢XML文檔而設計的語言,可以和DOM解析方式配合使用,實現對XML文檔的解析。它使用路徑表達式來選取XML文檔中指定的節點或者節點集合
- XPath表達式的查找結果類型有五種,在XPathConstants類中提供:
- nodeset、boolean、number、string和Node
三 核心接口解析
- 相關類:XPathParser、XNode、PropertyParser、GenericTokenParser
- XNode對象由XPathParser創建,並且每個XNode都持有其對應XPathParser的引用
- 相關接口及其實現類:EntityResolver及其實現類XMLMapperEntityResolver,TokenHandler及其實現類VariableTokenHandler
- XPathParser類
- 各個字段的含義和功能
-
//Document對象 private final Document document; //是否開啓驗證 private boolean validation; //用於加載本地dtd文件 private EntityResolver entityResolver; //核心配置文件中<properties>標籤中定義的鍵值對 private Properties variables; //XPath對象,用於查找節點 private XPath xpath;
- createDocument()方法用於根據文件流創建文檔對象,在構造器中會被調用
-
private Document createDocument(InputSource inputSource) { // important: this must only be called AFTER common constructor try { 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() { @Override public void error(SAXParseException exception) throws SAXException { throw exception; } @Override public void fatalError(SAXParseException exception) throws SAXException { throw exception; } @Override public void warning(SAXParseException exception) throws SAXException { } }); return builder.parse(inputSource); } catch (Exception e) { throw new BuilderException("Error creating document instance. Cause: " + e, e); } }
- 提供了一系列的eval*()方法用於解析boolean、short、long等類型信息:
- 底層通過調用XPath.evaluate()方法查找指定路徑的節點或屬性,並進行相應的類型轉換
- 注意evalString()方法調用了PropertyParser.parser()方法,該類在後面會介紹
- commonConstructor()方法,在構造器中會調用該方法設置相應屬性:
-
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) { this.validation = validation; this.entityResolver = entityResolver; this.variables = variables; XPathFactory factory = XPathFactory.newInstance(); this.xpath = factory.newXPath(); }
- EntityResolver接口及其子類XMLMapperEntityResolver
- 注:EntityResolver接口由JDK的org.xml.sax包提供,mybatis只是實現了該接口
- EntityResolver接口實現類對象的作用:
- 加載本地的DTD文件,避免互聯網加載DTD文件。比如在解析mybatis的核心配置文件mybatis-config.xml時,默認互聯網記載http://mybatis.org/dtd/mybatis-3-config.dtd這個DTD文檔,但是當網絡比較慢時會導致驗證過程緩慢
- 代碼:
-
//指定了mybatis配置文件以mapper文件對應dtd的systemId private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd"; private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd"; private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd"; private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd"; //指定了dtd文件的具體地址 private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd"; private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd"; //解析DTD文件並返回InputSource,它包含DTD文件流 @Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException { try { if (systemId != null) { String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH); if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) { return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId); } else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) { return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId); } } return null; } catch (Exception e) { throw new SAXException(e.toString()); } } private InputSource getInputSource(String path, String publicId, String systemId) { InputSource source = null; if (path != null) { try { InputStream in = Resources.getResourceAsStream(path); source = new InputSource(in); source.setPublicId(publicId); source.setSystemId(systemId); } catch (IOException e) { // ignore, null is ok } } return source; }
- PropertyParser類
- 核心方法parser(),其中通過GenericTokenParser類找到字符串的佔位符,然後通過VariableTokenHandler處理佔位符中的內容。有點類似於策略模式
-
public static String parse(String string, Properties variables) { VariableTokenHandler handler = new VariableTokenHandler(variables); GenericTokenParser parser = new GenericTokenParser("${", "}", handler); return parser.parse(string); }
- 內部類VariableTokenHandler實現了接口TokenHandler,裏面會判斷配置文件中有沒有配置開啓默認值,若開啓了,在獲取屬性爲空時返回默認值,否者直接返回
-
private static class VariableTokenHandler implements TokenHandler { //屬性集合 private final Properties variables; //是否開啓默認值 private final boolean enableDefaultValue; //默認值分隔符 private final String defaultValueSeparator; private VariableTokenHandler(Properties variables) { this.variables = variables; this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE)); this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR); } private String getPropertyValue(String key, String defaultValue) { return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue); } @Override public String handleToken(String content) { if (variables != null) { String key = content; //判斷是否開啓了默認值 if (enableDefaultValue) { final int separatorIndex = content.indexOf(defaultValueSeparator); String defaultValue = null; if (separatorIndex >= 0) { key = content.substring(0, separatorIndex); defaultValue = content.substring(separatorIndex + defaultValueSeparator.length()); } if (defaultValue != null) { return variables.getProperty(key, defaultValue); } } //未開啓默認值的情況 if (variables.containsKey(key)) { return variables.getProperty(key); } } return "${" + content + "}"; } }
- GenericTokenParser類
- 用於解析佔位符,包含三種屬性:開始佔位符、結束佔位符、具體的處理算法
-
private final String openToken; private final String closeToken; private final TokenHandler handler;
- 核心方法parse(),用於處理字符串,將佔位符通過TokenHandler替換爲實際值並返回
-
public String parse(String text) { if (text == null || text.isEmpty()) { return ""; } // search open token int start = text.indexOf(openToken, 0); if (start == -1) { return text; } char[] src = text.toCharArray(); int offset = 0; final StringBuilder builder = new StringBuilder(); StringBuilder expression = null; while (start > -1) { if (start > 0 && src[start - 1] == '\\') { // this open token is escaped. remove the backslash and continue. builder.append(src, offset, start - offset - 1).append(openToken); offset = start + openToken.length(); } else { // found open token. let's search close token. if (expression == null) { expression = new StringBuilder(); } else { expression.setLength(0); } builder.append(src, offset, start - offset); offset = start + openToken.length(); int end = text.indexOf(closeToken, offset); while (end > -1) { if (end > offset && src[end - 1] == '\\') { // this close token is escaped. remove the backslash and continue. expression.append(src, offset, end - offset - 1).append(closeToken); offset = end + closeToken.length(); end = text.indexOf(closeToken, offset); } else { expression.append(src, offset, end - offset); offset = end + closeToken.length(); break; } } if (end == -1) { // close token was not found. builder.append(src, start, src.length - start); offset = src.length; } else { builder.append(handler.handleToken(expression.toString())); offset = end + closeToken.length(); } } start = text.indexOf(openToken, offset); } if (offset < src.length) { builder.append(src, offset, src.length - offset); } return builder.toString(); }
- XNode類
- 對org.w3c.dom.Node進行了封裝,屬性包含如下
-
//封裝的Node對象 private final Node node; //Node節點名稱 private final String name; //節點內容 private final String body; //節點屬性集合 private final Properties attributes; //配置文件中配置的鍵值對 private final Properties variables; //解析該節點的解析器對象 private final XPathParser xpathParser;
- 提供了很多get*()方法,信息來源於上述屬性