Mybatis技術內幕(2.1):解析器模塊

1.0 概述

MyBatis 的解析器模塊,源碼對應 parsing 包。以下圖: html

該模塊主要提供兩個功能:

  • 1.對Java XPath 進行封裝,爲MyBatis初始化時解析mybatis-config.xml配置文件以及映射配置文件提供支持。
  • 2.爲處理動態 SQL 語句中的佔位符提供支持

2.0 XPathParser

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

// 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對象
  • 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文件的效果。
  • xpath屬性,javax.xml.xpath.XPath對象,用於查詢XML中的節點和元素。對 XPath 的使用不了解的同窗,能夠去《XPath教程》《Java XPath解析器》進行簡單學習
  • variables屬性,變量Properties對象,用來替換須要動態配置的屬性值,詳見《Mybatis技術內幕:初始化之properties標籤解析》

2.1 構造方法

XPathParser 的構造方法重載了16個之多,基本都很是類似,咱們挑選其中一個。代碼以下:node

// 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)));
}

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();
}

/** * 建立 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);
    }
}
複製代碼

代碼比較簡單,主要是完成XPathParser類相關成員變量的初始化賦值操做spring

2.2 eval 方法族

XPathParser提供了一系列的#eval*方法,用於得到Boolean、Short、Integer、Long、Float、Double、String、Node類型的元素或節點的值。 雖然方法不少,可是都是基於 #evaluate(String expression, Object root, QName returnType) 方法。代碼以下:express

// 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);
    }
}
複製代碼

2.3 節點屬性值的動態替換

主要依賴evalString(Object root, String expression)方法,真正的替換動做由PropertyParser類完成。PropertyParser下面會講到apache

// XPathParser.java

  public String evalString(String expression) {
    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;
  }
  
  public Integer evalInteger(Object root, String expression) {
    return Integer.valueOf(evalString(root, expression));
  }
複製代碼

evalNode(String expression)方法會在後面的配置文件初始化中大量用到,返回org.apache.ibatis.parsing.XNode對象,主要爲了動態值的替換bash

//XPathParser
  public XNode evalNode(String expression) {
    return evalNode(document, expression);
  }

  public XNode evalNode(Object root, String expression) {
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
      return null;
    }
    return new XNode(this, node, variables);
  }
  
  //XNode
  public String evalString(String expression) {
    return xpathParser.evalString(node, expression);
  }
複製代碼

2.4 org.apache.ibatis.parsing.XNode

在面對一個Node時,假設我想要把Node的屬性集合都以鍵、值對的形式,放到Properties對象裏,同時把Node的body體也經過XPathParser解析出來,並保存起來(通常是Sql語句),方便程序使用,代碼可能會是這樣的。網絡

private Node node;
private String body;
private Properties attributes;
private XPathParser xpathParser;
複製代碼

Mybatis就把上面幾個必要屬性封裝到一個類中,取名叫XNode。mybatis

3.0 XMLMapperEntityResolver

org.apache.ibatis.builder.xml.XMLMapperEntityResolver實現 EntityResolver 接口,用於加載本地的mybatis-3-config.dtd和mybatis-3-mapper.dtd這兩個 DTD 文件。代碼比較簡單,代碼以下:app

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

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

    /** * 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.0 PropertyParser

PropertyParser 前面的XPathParser小節中已經出現了,主要用於動態屬性的解析,是一個提供靜態方法的工具類。部分代碼以下:

// PropertyParser.java

public class PropertyParser {
    // private構造器 禁止構造 PropertyParser 對象
    private PropertyParser() {
        // Prevent Instantiation
    }

    public static String parse(String string, Properties variables) {
        // 建立 VariableTokenHandler 對象
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        // 建立 GenericTokenParser 對象
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        // 執行解析
        return parser.parse(string);
    }
}
複製代碼

主要代碼很少,解析過程主要依賴VariableTokenHandlerGenericTokenParser對象

5.0 TokenHandler

org.apache.ibatis.parsing.TokenHandlerToken處理器接口。代碼以下:

// TokenHandler.java
public interface TokenHandler {
    /** * 處理 Token * @param content Token 字符串 * @return 處理後的結果 */
    String handleToken(String content);
}
複製代碼

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

本文暫時只解讀VariableTokenHandler

##5.1 VariableTokenHandler VariableTokenHandler是PropertyParser的內部靜態類,變量 Token 處理器。代碼以下:

// PropertyParser.java
    private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
  /** * @since 3.4.2 */
  public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";

  /** * @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 = ":";
  
private static class VariableTokenHandler implements TokenHandler {
    private final Properties variables;
    //是否開啓默認值功能。默認爲 {@link #ENABLE_DEFAULT_VALUE false}
    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);
    }

    @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 + "}";
    }
  }
複製代碼

代碼比較簡單,在3.4.2版本之後開始支持默認值功能(默認和spring一致),能夠經過mybatis-config.xml配置修改

  • enableDefaultValue 屬性,是否開啓默認值功能。默認爲 ENABLE_DEFAULT_VALUE(false) ,即不開啓
  • defaultValueSeparator屬性,默認值的分隔符。默認爲 KEY_DEFAULT_VALUE_SEPARATOR ,即 ":"
<properties resource="org/mybatis/example/config.properties">
  <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>
  <property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value="?:"/> 
</properties>
複製代碼

6.0 GenericTokenParser

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 "";
        }
        // 尋找開始的 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] == '\\') {
                // 由於 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] == '\\') {
                        // 由於 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) {
                    // closeToken 未找到,直接拼接
                    builder.append(src, start, src.length - start);
                    // 修改 offset
                    offset = src.length;
                } else {
                    // 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 進行處理,你們能夠耐心看下這段邏輯,經過源碼包中相關的單元測試類去打斷點一行一行跟進

參考和推薦以下文章

徐郡明 《MyBatis 技術內幕》 的 「2.1 解析器模塊」 小節
祖大俊 《Mybatis3.3.x技術內幕(七):Mybatis初始化之六個工具》

失控的阿甘,樂於分享,記錄點滴

相關文章
相關標籤/搜索