Mybatis源碼學習之路二解析器模塊

一 前言

  1. Mybatis在初始化過程中處理mybatis-config.xml核心配置文件時,使用的是DOM解析方式,並結合使用XPath解析XML配置文件
  2. Mybatis對XML解析功能進行了封裝,由解析器模塊提供支持,其中核心類爲XPathParser,它封裝了XPath、Document以及EntityResolver等

二 基礎知識

  1. XML解析常見的三種方式:
    1. DOM(Document Object Model)解析方式
    2. SAX(Simple API for XML)解析方式
    3. StAX(Streaming API for XML)解析方式
  2. DOM:
    1. 基於樹形結構的解析方式,將整個XML文檔讀入到內存並構建一個DOM樹,基於這顆樹形結構對各個節點Node進行操作
    2. 優點:
      1. 易於編程,可以在根據需求在各個節點之間進行導航
    3. 缺點:
      1. 將整個XML加載到內存中並構建樹形結構,對於比較大的XML文檔,消耗很大
  3. SAX:
    1. SAX基於事件模型進行解析,它每次只是加載一部分文檔到內存,即可開始解析。
    2. 當SAX解析器解析到某類型節點時,會觸發註冊在該節點上的回調函數,開發人員可以註冊相應的回調函數進行操作,一般情況下繼承SAX提供的DefaultHandler基類,重寫相應的事件處理器並進行註冊即可
    3. 優點:
      1. 不會將整個XML文檔加載到內存,資源消耗較少
      2. 當程序處理過程滿足條件時可以即刻停止,不必解析剩餘的XML文檔
    4. 缺點:
      1. 基於事件驅動進行解析,不會將整個XML文檔加載到內存,需要開發人員自己維護業務邏輯涉及的各個節點
  4. StAX:
    1. 它可以很好的支持DOM和SAX解析方式,和SAX解析方式類似,但是不同之處在於StAX採用的是"拉模式",即應用程序通過調用解析器推進解析的過程
    2. StAX提供了兩套API:
      1. 基於指針的API,一種底層的API,優勢是效率高,缺點是抽象度低不易使用
      2. 基於迭代器的API,允許應用程序將XML文檔作爲一系列的事件對象來處理,缺點是效率低,但是易於使用
  5. XPath簡介
    1. XPath是一種爲查詢XML文檔而設計的語言,可以和DOM解析方式配合使用,實現對XML文檔的解析。它使用路徑表達式來選取XML文檔中指定的節點或者節點集合
    2. XPath表達式的查找結果類型有五種,在XPathConstants類中提供:
      1. nodeset、boolean、number、string和Node

三 核心接口解析

  1. 相關類:XPathParser、XNode、PropertyParser、GenericTokenParser
    1. XNode對象由XPathParser創建,並且每個XNode都持有其對應XPathParser的引用
  2. 相關接口及其實現類:EntityResolver及其實現類XMLMapperEntityResolver,TokenHandler及其實現類VariableTokenHandler
  3. XPathParser類
    1. 各個字段的含義和功能
    2. //Document對象
      private final Document document;
      //是否開啓驗證
      private boolean validation;
      //用於加載本地dtd文件
      private EntityResolver entityResolver;
      //核心配置文件中<properties>標籤中定義的鍵值對
      private Properties variables;
      //XPath對象,用於查找節點
      private XPath xpath;

       

    3. createDocument()方法用於根據文件流創建文檔對象,在構造器中會被調用
    4. 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);
          }
        }

       

    5. 提供了一系列的eval*()方法用於解析boolean、short、long等類型信息:
      1. 底層通過調用XPath.evaluate()方法查找指定路徑的節點或屬性,並進行相應的類型轉換
      2. 注意evalString()方法調用了PropertyParser.parser()方法,該類在後面會介紹
    6. commonConstructor()方法,在構造器中會調用該方法設置相應屬性:
    7. 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();
        }

       

    8.  
  4. EntityResolver接口及其子類XMLMapperEntityResolver
    1. 注:EntityResolver接口由JDK的org.xml.sax包提供,mybatis只是實現了該接口
    2. EntityResolver接口實現類對象的作用:
      1. 加載本地的DTD文件,避免互聯網加載DTD文件。比如在解析mybatis的核心配置文件mybatis-config.xml時,默認互聯網記載http://mybatis.org/dtd/mybatis-3-config.dtd這個DTD文檔,但是當網絡比較慢時會導致驗證過程緩慢
    3. 代碼:
    4. //指定了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;
        }

       

  5. PropertyParser類
    1. 核心方法parser(),其中通過GenericTokenParser類找到字符串的佔位符,然後通過VariableTokenHandler處理佔位符中的內容。有點類似於策略模式
    2. public static String parse(String string, Properties variables) {
          VariableTokenHandler handler = new VariableTokenHandler(variables);
          GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
          return parser.parse(string);
        }

       

    3. 內部類VariableTokenHandler實現了接口TokenHandler,裏面會判斷配置文件中有沒有配置開啓默認值,若開啓了,在獲取屬性爲空時返回默認值,否者直接返回
    4. 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 + "}";
          }
        }

       

  6. GenericTokenParser類
    1. 用於解析佔位符,包含三種屬性:開始佔位符、結束佔位符、具體的處理算法
    2. private final String openToken;
      private final String closeToken;
      private final TokenHandler handler;

       

    3. 核心方法parse(),用於處理字符串,將佔位符通過TokenHandler替換爲實際值並返回
    4. 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();
        }

       

  7. XNode類
    1. 對org.w3c.dom.Node進行了封裝,屬性包含如下
    2. //封裝的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;

       

    3. 提供了很多get*()方法,信息來源於上述屬性

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章