mybatis-解析器模塊


本文,我們來分享 MyBatis 的解析器模塊,對應 parsing 包。如下圖所示:`parsing` 包

在 《精盡 MyBatis 源碼解析 —— 項目結構一覽》 中,簡單介紹了這個模塊如下:

解析器模塊,主要提供了兩個功能:

  • 一個功能,是對 XPath 進行封裝,為 MyBatis 初始化時解析 mybatis-config.xml 配置文件以及映射配置文件提供支持。
  • 另一個功能,是為處理動態 SQL 語句中的占位符提供支持。

下面,我們就來看看具體的源碼。因為 parsing 是基礎支持層,所以建議胖友在我們講解到的類和方法中,打折斷點一起來了解。

2. XPathParser

org.apache.ibatis.parsing.XPathParser ,基於 Java XPath 解析器,用於解析 MyBatis mybatis-config.xml 和 **Mapper.xml 等 XML 配置文件。屬性如下:

// XPathParser.java

/**
* XML Document 對象
*/
private final Document document;
/**
* 是否校驗
*/
private boolean validation;
/**
* XML 實體解析器
*/
private EntityResolver entityResolver;
/**
* 變量 Properties 對象
*/
private Properties variables;
/**
* Java XPath 對象
*/
private XPath xpath;
  • document 屬性,XML 被解析后,生成的 org.w3c.dom.Document 對象。
    • validation 屬性,是否校驗 XML 。一般情況下,值為 true 。
    • entityResolver 屬性,org.xml.sax.EntityResolver 對象,XML 實體解析器。默認情況下,對 XML 進行校驗時,會基於 XML 文檔開始位置指定的 DTD 文件或 XSD 文件。例如說,解析 mybatis-config.xml 配置文件時,會加載 http://mybatis.org/dtd/mybatis-3-config.dtd 這個 DTD 文件。但是,如果每個應用啟動都從網絡加載該 DTD 文件,勢必在弱網絡下體驗非常下,甚至說應用部署在無網絡的環境下,還會導致下載不下來,那么就會出現 XML 校驗失敗的情況。所以,在實際場景下,MyBatis 自定義了 EntityResolver 的實現,達到使用本地 DTD 文件,從而避免下載網絡 DTD 文件的效果。詳細解析,見 「3. XMLMapperEntityResolver」 。
  • xpath 屬性,javax.xml.xpath.XPath 對象,用於查詢 XML 中的節點和元素。如果對 XPath 的使用不了解的胖友,請先跳轉 《Java XPath 解析器 - 解析 XML 文檔》 中,進行簡單學習,灰常簡單。
  • variables 屬性,變量 Properties 對象,用來替換需要動態配置的屬性值。例如:

    <dataSource type="POOLED">
    <property name="driver" value="${driver}"/>
    <property name="url" value="${url}"/>
    <property name="username" value="${username}"/>
    <property name="password" value="${password}"/>
    </dataSource>
    • variables 的來源,即可以在常用的 Java Properties 文件中配置,也可以使用 MyBatis <property /> 標簽中配置。例如:

      <properties resource="org/mybatis/example/config.properties">
      <property name="username" value="dev_user"/>
      <property name="password" value="F2Fa3!33TYyg"/>
      </properties>
      • 這里配置的 username 和 password 屬性,就可以替換上面的 ${username} 和 ${password} 這兩個動態屬性。
      • 具體如何實現的,我們來看下面的 PropertyParser#parse(String string, Properties variables) 方法。

2.1 構造方法

XPathParser 的構造方法有 16 個之多,當然基本都非常相似,我們來挑選其中一個。代碼如下:

// XPathParser.java

/**
* 構造 XPathParser 對象
*
* @param xml XML 文件地址
* @param validation 是否校驗 XML
* @param variables 變量 Properties 對象
* @param entityResolver XML 實體解析器
*/
public XPathParser(String xml, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(new StringReader(xml)));
}
  • 調用 #commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) 方法,公用的構造方法邏輯。代碼如下:

    // XPathParser.java

    private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
    this.validation = validation;
    this.entityResolver = entityResolver;
    this.variables = variables;
    // 創建 XPathFactory 對象
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();
    }
  • 調用 #createDocument(InputSource inputSource) 方法,將 XML 文件解析成 Document 對象。代碼如下:

    // XPathParser.java

    /**
    * 創建 Document 對象
    *
    * @param inputSource XML 的 InputSource 對象
    * @return Document 對象
    */
    private Document createDocument(InputSource inputSource) {
    // important: this must only be called AFTER common constructor
    try {
    // 1> 創建 DocumentBuilderFactory 對象
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setValidating(validation); // 設置是否驗證 XML

    factory.setNamespaceAware(false);
    factory.setIgnoringComments(true);
    factory.setIgnoringElementContentWhitespace(false);
    factory.setCoalescing(false);
    factory.setExpandEntityReferences(true);

    // 2> 創建 DocumentBuilder 對象
    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 {
    }

    });
    // 3> 解析 XML 文件
    return builder.parse(inputSource);
    } catch (Exception e) {
    throw new BuilderException("Error creating document instance. Cause: " + e, e);
    }
    }
    • 就是簡單的 Java XML API 的使用,不了解的胖友,可以 Google 學習下。

2.2 eval 方法族

XPathParser 提供了一系列的 #eval* 方法,用於獲得 Boolean、Short、Integer、Long、Float、Double、String、Node 類型的元素或節點的“值”。當然,雖然方法很多,但是都是基於 #evaluate(String expression, Object root, QName returnType) 方法,代碼如下:

// XPathParser.java

/**
* 獲得指定元素或節點的值
*
* @param expression 表達式
* @param root 指定節點
* @param returnType 返回類型
* @return 值
*/
private Object evaluate(String expression, Object root, QName returnType) {
try {
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
throw new BuilderException("Error evaluating XPath. Cause: " + e, e);
}
}
  • 調用 xpath 的 evaluate(String expression, Object root, QName returnType) 方法,獲得指定元素或節點的值。

2.2.1 eval 元素

eval 元素的方法,用於獲得 Boolean、Short、Integer、Long、Float、Double、String 類型的元素的值。我們以 #evalString(Object root, String expression) 方法為例子,代碼如下:

// XPathParser.java

public String evalString(Object root, String expression) {
// <1> 獲得值
String result = (String) evaluate(expression, root, XPathConstants.STRING);
// <2> 基於 variables 替換動態值,如果 result 為動態值
result = PropertyParser.parse(result, variables);
return result;
}
  • <1> 處,調用 #evaluate(String expression, Object root, QName returnType) 方法,獲得值。其中,returnType 方法傳入的是 XPathConstants.STRING ,表示返回的值是 String 類型。
  • <2> 處,調用 PropertyParser#parse(String string, Properties variables) 方法,基於 variables 替換動態值,如果 result 為動態值。這就是 MyBatis 如何替換掉 XML 中的動態值實現的方式。關於 PropertyParser ,我們會在 「5. PropertyParser」 詳細解析。

2.2.2 eval 節點

eval 元素的方法,用於獲得 Node 類型的節點的值。代碼如下:

// XPathParser.java

public List<XNode> evalNodes(String expression) { // Node 數組
return evalNodes(document, expression);
}

public List<XNode> evalNodes(Object root, String expression) { // Node 數組
// <1> 獲得 Node 數組
NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
// <2> 封裝成 XNode 數組
List<XNode> xnodes = new ArrayList<>();
for (int i = 0; i < nodes.getLength(); i++) {
xnodes.add(new XNode(this, nodes.item(i), variables));
}
return xnodes;
}

public XNode evalNode(String expression) { // Node 對象
return evalNode(document, expression);
}

public XNode evalNode(Object root, String expression) { // Node 對象
// <1> 獲得 Node 對象
Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
if (node == null) {
return null;
}
// <2> 封裝成 XNode 對象
return new XNode(this, node, variables);
}
  • <1> 處,返回結果有 Node 對象和數組兩種情況,根據方法參數 expression 需要獲取的節點不同。
  • <2> 處, 最終結果會將 Node 封裝成 org.apache.ibatis.parsing.XNode 對象,主要為了動態值的替換。例如:

    // XNode.java

    public String evalString(String expression) {
    return xpathParser.evalString(node, expression);
    }
    • 其它方法,就不詳細解析。感興趣的胖友,可以自己翻翻。

3. XMLMapperEntityResolver

org.apache.ibatis.builder.xml.XMLMapperEntityResolver ,實現 EntityResolver 接口,MyBatis 自定義 EntityResolver 實現類,用於加載本地的 mybatis-3-config.dtd 和 mybatis-3-mapper.dtd 這兩個 DTD 文件。代碼如下:

// XMLMapperEntityResolver.java

public class XMLMapperEntityResolver implements EntityResolver {

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";

/**
* 本地 mybatis-config.dtd 文件
*/
private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
/**
* 本地 mybatis-mapper.dtd 文件
*/
private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";

/**
* Converts a public DTD into a local one
*
* @param publicId The public id that is what comes after "PUBLIC"
* @param systemId The system id that is what comes after the public id.
* @return The InputSource for the DTD
*
* @throws org.xml.sax.SAXException If anything goes wrong
*/
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
try {
if (systemId != null) {
String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
// 本地 mybatis-config.dtd 文件
if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
// 本地 mybatis-mapper.dtd 文件
} 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 {
// 創建 InputSource 對象
InputStream in = Resources.getResourceAsStream(path);
source = new InputSource(in);
// 設置 publicId、systemId 屬性
source.setPublicId(publicId);
source.setSystemId(systemId);
} catch (IOException e) {
// ignore, null is ok
}
}
return source;
}

}
  • 代碼比較簡單,胖友自己瞅瞅哈。

4. GenericTokenParser

org.apache.ibatis.parsing.GenericTokenParser ,通用的 Token 解析器。代碼如下:

// GenericTokenParser.java

public class GenericTokenParser {

/**
* 開始的 Token 字符串
*/
private final String openToken;
/**
* 結束的 Token 字符串
*/
private final String closeToken;
private final TokenHandler handler;

public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;
this.handler = handler;
}

public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
// 尋找開始的 openToken 的位置
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; // 匹配到 openToken 和 closeToken 之間的表達式
// 循環匹配
while (start > -1) {
// 轉義字符
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue.
// 因為 openToken 前面一個位置是 \ 轉義字符,所以忽略 \
// 添加 [offset, start - offset - 1] 和 openToken 的內容,添加到 builder 中
builder.append(src, offset, start - offset - 1).append(openToken);
// 修改 offset
offset = start + openToken.length();
// 非轉義字符
} else {
// found open token. let's search close token.
// 創建/重置 expression 對象
if (expression == null) {
expression = new StringBuilder();
} else {
expression.setLength(0);
}
// 添加 offset 和 openToken 之間的內容,添加到 builder 中
builder.append(src, offset, start - offset);
// 修改 offset
offset = start + openToken.length();
// 尋找結束的 closeToken 的位置
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.
// 因為 endToken 前面一個位置是 \ 轉義字符,所以忽略 \
// 添加 [offset, end - offset - 1] 和 endToken 的內容,添加到 builder 中
expression.append(src, offset, end - offset - 1).append(closeToken);
// 修改 offset
offset = end + closeToken.length();
// 繼續,尋找結束的 closeToken 的位置
end = text.indexOf(closeToken, offset);
// 非轉義
} else {
// 添加 [offset, end - offset] 的內容,添加到 builder 中
expression.append(src, offset, end - offset);
break;
}
}
// 拼接內容
if (end == -1) {
// close token was not found.
// closeToken 未找到,直接拼接
builder.append(src, start, src.length - start);
// 修改 offset
offset = src.length;
} else {
// <x> closeToken 找到,將 expression 提交給 handler 處理 ,並將處理結果添加到 builder 中
builder.append(handler.handleToken(expression.toString()));
// 修改 offset
offset = end + closeToken.length();
}
}
// 繼續,尋找開始的 openToken 的位置
start = text.indexOf(openToken, offset);
}
// 拼接剩余的部分
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}

}
  • 代碼看起來好冗長,但是淡定,就一個 #parse(String text) 方法,循環( 因為可能不只一個 ),解析以 openToken 開始,以 closeToken 結束的 Token ,並提交給 handler 進行處理,即 <x> 處。
  • 所以所以所以,胖友可以耐心看下這段邏輯,也可以忽略,大體理解就好。
  • 關於 handler 這個 TokenHandler ,詳細見 「5. TokenHandler」 。當然,這也是為什么 GenericTokenParser 叫做通用的原因,而 TokenHandler 處理特定的邏輯。

5. PropertyParser

org.apache.ibatis.parsing.PropertyParser ,動態屬性解析器。代碼如下:

// PropertyParser.java

public class PropertyParser {

// ... 省略部分無關的

private PropertyParser() { // <1>
// Prevent Instantiation
}

public static String parse(String string, Properties variables) { // <2>
// <2.1> 創建 VariableTokenHandler 對象
VariableTokenHandler handler = new VariableTokenHandler(variables);
// <2.2> 創建 GenericTokenParser 對象
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
// <2.3> 執行解析
return parser.parse(string);
}

}
  • <1> ,構造方法,修飾符為 private ,禁止構造 PropertyParser 對象,因為它是一個靜態方法的工具類。
  • <2> ,基於 variables 變量,替換 string 字符串中的動態屬性,並返回結果。
    • <2.1> ,創建 VariableTokenHandler 對象。
    • <2.2> ,創建 GenericTokenParser 對象。
      • 我們可以看到,openToken = { ,closeToken = } ,這不就是上面看到的 ${username} 和 {password} 的么。
      • 同時,我們也可以看到,handler 類型為 VariableTokenHandler ,也就是說,通過它實現自定義的處理邏輯。關於它,在 「6.1 VariableTokenHandler」 中詳細解析。
    • <2.3> ,調用 GenericTokenParser#parse(String text) 方法,執行解析。

6. TokenHandler

org.apache.ibatis.parsing.TokenHandler ,Token 處理器接口。代碼如下:

// TokenHandler.java

public interface TokenHandler {

/**
* 處理 Token
*
* @param content Token 字符串
* @return 處理后的結果
*/
String handleToken(String content);

}

TokenHandler 有四個子類實現,如下圖所示:TokenHandler 子類

  • 本文暫時只解析 VariableTokenHandler 類,因為只有它在 parsing 包中,和解析器模塊相關。

6.1 VariableTokenHandler

VariableTokenHandler ,是 PropertyParser 的內部靜態類,變量 Token 處理器。具體什么用途?上面不是已經整的明明白白啦,就不重復解釋啦。

6.1.1 構造方法

// PropertyParser.java

private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
/**
* The special property key that indicate whether enable a default value on placeholder.
* <p>
* The default value is {@code false} (indicate disable a default value on placeholder)
* If you specify the {@code true}, you can specify key and default value on placeholder (e.g. {@code ${db.username:postgres}}).
* </p>
* @since 3.4.2
*/
public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";

/**
* The special property key that specify a separator for key and default value on placeholder.
* <p>
* The default separator is {@code ":"}.
* </p>
* @since 3.4.2
*/
public static final String KEY_DEFAULT_VALUE_SEPARATOR = KEY_PREFIX + "default-value-separator";

private static final String ENABLE_DEFAULT_VALUE = "false";
private static final String DEFAULT_VALUE_SEPARATOR = ":";

// VariableTokenHandler 類里

/**
* 變量 Properties 對象
*/
private final Properties variables;
/**
* 是否開啟默認值功能。默認為 {@link #ENABLE_DEFAULT_VALUE}
*/
private final boolean enableDefaultValue;
/**
* 默認值的分隔符。默認為 {@link #KEY_DEFAULT_VALUE_SEPARATOR} ,即 ":" 。
*/
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);
}
  • 雖然看起來有一大坨的變量,但是不要怕。
  • variables 屬性,變量 Properties 對象。
  • enableDefaultValue 屬性,是否開啟默認值功能。默認為 ENABLE_DEFAULT_VALUE ,即不開啟。想要開啟,可以配置如下:

    <properties resource="org/mybatis/example/config.properties">
    <!-- ... -->
    <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/> <!-- Enable this feature -->
    </properties>
  • defaultValueSeparator 屬性,默認值的分隔符。默認為 KEY_DEFAULT_VALUE_SEPARATOR ,即 ":" 。想要修改,可以配置如下:

    <properties resource="org/mybatis/example/config.properties">
    <!-- ... -->
    <property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value="?:"/> <!-- Change default value of separator -->
    </properties>
    • 分隔符被修改成了 ?: 。

6.1.2 handleToken

// VariableTokenHandler 類里

@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);
}
}
// 無 variables ,直接返回
return "${" + content + "}";
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM