解析器模块

1. 简介

​ 解析器模块,对应的包是parsing,如下图所示:

解析器模块,主要提供了两个功能:

  • 对XPath进行封装,为Mybatis初始化时解析mybatis-config.xml配置文件以及mapper映射配置文件提供支持。
  • 为处理动态SQL语句中的占位符提供支持。

2. XML解析方式

在mybatis中涉及到多个XML配置文件,因此我们首先要介绍XML解析的相关内容。XML解析的常见的方式有三种:

  • DOM(document object model)解析方式
  • SAX(simple api for xml)解析方式
  • Java6版本开始,JDK致辞的StAX(streaming api for xml)解析方式。

DOM是基于树形结构的XML解析方式,它将整个XML文档读入内存并构建成一个DOM树,基于这棵树形结构对各个节点(node)进行操作。XML文档中的每一个成分都是一个节点:整个文档是一个文档节点,每个XML标签对应一个元素节点,包含在XML标签中的文本是文本节点,每一个XML属性是一个属性节点,注释属于注释节点。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

解析后得到的树形结构为:

3. XPath

​ mybatis在初始化过程中处理mybatis-config.xml配置文件以及映射文件时,使用的是DOM解析方式,并结合XPath解析xml配置文件,正如前文所述,DOM会对整个XML文档加载到内存中形成树形数据结构,而xpth是一种为查询XML文档而设计的语言,它可以与DOM解析方式配合使用,实现对XML文档的解析。xpath之于XML就好比SQL语言之于数据库。

​ 代码示例:

import org.apache.ibatis.io.Resources;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.*;
import java.io.IOException;
import java.io.InputStream;

/**
 * XPath解析测试 {@link javax.xml.xpath.XPath}
 * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
 * @since : 2019/11/25 10:11
 */
public class XPathTest {
  public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException, XPathExpressionException {
    //创建DOM解析器工厂
    DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
    //开启验证
    documentBuilderFactory.setValidating(true);
    documentBuilderFactory.setNamespaceAware(false);
    documentBuilderFactory.setIgnoringComments(true);
    documentBuilderFactory.setIgnoringElementContentWhitespace(false);
    documentBuilderFactory.setCoalescing(false);
    documentBuilderFactory.setExpandEntityReferences(true);
    //得到一个DOM解析器对象
    DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
    documentBuilder.setErrorHandler(new ErrorHandler() {
      @Override
      public void warning(SAXParseException exception) throws SAXException {
        System.out.println("warning" + exception.getMessage());
      }

      @Override
      public void error(SAXParseException exception) throws SAXException {
        System.out.println("error" + exception.getMessage());
      }

      @Override
      public void fatalError(SAXParseException exception) throws SAXException {
        System.out.println("fatalError" + exception.getMessage());
      }
    });
    String resource = "resources/xpath-demo.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    //解析xml
    Document document = documentBuilder.parse(inputStream);

    XPathFactory xPathFactory = XPathFactory.newInstance();
    XPath xPath = xPathFactory.newXPath();

    XPathExpression xPathExpression = xPath.compile("/configuration/mappers/mapper");
    NodeList evaluate = (NodeList) xPathExpression.evaluate(document, XPathConstants.NODESET);
    for (int i = 0; i < evaluate.getLength() ; i++) {
      Node node = evaluate.item(i);
      String nodeName = node.getNodeName();
      if (("mapper").equals(node.getNodeName())){
        Node attributeNode = node.getAttributes().getNamedItem("resource");
        String nodeValue1 = attributeNode.getNodeValue();
        System.out.println(nodeName +"......"+nodeValue1);
      }
    }
  }
}

运行结果:

mapper......org/mybatis/example/BlogMapper.xml
mapper......org/mybatis/example/UserMapper.xml

4. XPathParser

Mybatis提供的org.apache.ibatis.parsing.XPathParser类封装了前面涉及到的XPathDocumentEntityResolver,如图所示:

XPathParser中各个字段的含义和功能如下所示:

  /**
   * xml document 对象
   */
  private final Document document;
  /**
   * 是否开启校验xml
   */
  private boolean validation;
  /**
   * 用于加载本地DTD文件
   *
   * xml实体解析器,默认情况下,对XML校验是,会基于xml文档开始位置定义的DTD文件或者XSD文件,
   * 例如:
   *    解析mybatis-config.xml配置文件时,会加载http://mybatis.org/dtd/mybatis-3-config.dtd这个DTD文件。
   *    但是,如果如果每个应用启动都从网络加载该DTD文件,势必在弱网络下体验非常下,甚至说应用部署在无网络的环境下,
   *    还会导致下载不下来,那么就会出现XML校验失败的情况 ,所以,在实际场景下,MyBatis自定义了EntityResolver
   *    的实现,达到使用本地DTD文件,从而避免下载网络 DTD 文件的效果。
   *
   * @see org.apache.ibatis.builder.xml.XMLMapperEntityResolver
   */
  private EntityResolver entityResolver;
  /**
   * 变量 Properties对象
   */
  private Properties variables;
  /**
   * java XPath对象
   *    用于查询xml中的节点和元素
   */
  private XPath xpath;

默认情况下,对XML文档进行验证时,会根据XML文档开始位置指定的网址加载对应的DTD文件或者XSD文件。如果是解析mybatis-config.xml配置文件时,默认会加载http://mybatis.org/dtd/mybatis-3-config.dtd这个DTD文件。但是,如果如果每个应用启动都从网络加载该DTD文件,势必在弱网络下体验非常下,甚至说应用部署在无网络的环境下,还会导致下载不下来,那么就会出现XML校验失败的情况 ,所以,在实际场景下,MyBatis自定义了EntityResolver的实现,达到使用本地DTD文件,从而避免下载网络 DTD 文件的效果。XMLMapperEntityResolvermybatis提供的EntityResolver接口的实现类,如下图所示:

EntityResolver接口的核心是resolveEntity()方法,XMLMapperEntityResolver是实现如下所示:

package org.apache.ibatis.builder.xml;

import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;

import org.apache.ibatis.io.Resources;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * XML映射器实体解析器
 * Offline entity resolver for the MyBatis DTDs.
 *
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class XMLMapperEntityResolver implements EntityResolver {
  /**
   * 指定mybatis-config.xml文件和映射文件对应的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";

  /**
   * 指定mybatis-config.xml文件和映射文件对应的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";

  /**
   * {@link #resolveEntity(String, String)}是{@link EntityResolver} 接口中定义的方法,具体实现如下所示:
   *
   * 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);
        /**
         *  查找systemId指定的DTD文档,并调用{@link #getInputSource(String, String, String)}方法读取DTD文档
         */
        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());
    }
  }

  /**
   * 读取DTD文档并形成InputSource对象
   * @param path  文件所在路径
   * @param publicId    公共标志符
   * @param systemId    systemId
   * @return InputSource
   */
  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;
  }

}

介绍完XMLMapperEntityResolver之后,回到XPathParser的分析。在XPathParser.createDocument()方法中封装了前面介绍的创建Document对象的过程并触发了加载XML文档的过程,具体实现如下:

/**
   * 调用这个方法之前一定要先调用{@link #commonConstructor(boolean, Properties, EntityResolver)}方法完成初始化。
   * 将xml解析为Document对象
   * @param inputSource
   * @return
   */
  private Document createDocument(InputSource inputSource) {
    // important: this must only be called AFTER common constructor
    try {
      //创建DocumentBuilderFactory对象
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      //对DocumentBuilderFactory进行一系列的配置
      factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
      factory.setValidating(validation);

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

      //创建DocumentBuilder对象并进行配置
      DocumentBuilder builder = factory.newDocumentBuilder();
      //设置EntityResolver接口对象
      builder.setEntityResolver(entityResolver);
      builder.setErrorHandler(new ErrorHandler() {
        //其中实现的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 {
          // NOP
        }
      });
      //加载xml文件
      return builder.parse(inputSource);
    } catch (Exception e) {
      throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    }
  }

/**
   * 通用构造:
   *    就是通用的赋值操作,主要是的得到一个xpath解析器对document对象进行解析
   * @param validation
   * @param variables
   * @param entityResolver
   */
  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();
  }

XPathParser中提供了一系列的eval*()方法用于解析booleanshortlongintStringNode等类型的信息,它通过前面介绍代码中介绍的XPath.evaluate()方法查找指定路径的节点或属性,并进行相应的类型转换。具体的代码比较简单,就不贴出来了,这里需要注意的是XPathParser.evalString()
方法,其中会调用PropertyParser.parse()方法处理节点中相应的默认值,具体实现如下所示:

 public String evalString(Object root, String expression) {
    String result = (String) evaluate(expression, root, XPathConstants.STRING);
    //处理节点中相应的默认值
    result = PropertyParser.parse(result, variables);
    return result;
  }

PropertyParser中指定了是否开启默认值的功能以及默认的分隔符,相应字段如下所示:

 private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
  /**
   * 在mybatis-config.xml中的<properties></properties>节点下配置是否开启默认值功能的对应配置项
   * 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 = ":";

PropertyParser.parse()方法中会创建GenericTokenParser解析器,并将默认值的处理委托给GenericTokenParser.parse()方法,实现如下:

/**
   * 当前方法中会创建{@link GenericTokenParser}解析器,并将默认值的处理委托给{@link GenericTokenParser#parse(String)}
   * @param string
   * @param variables
   * @return
   */
  public static String parse(String string, Properties variables) {
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    //创建GenericTokenParser对象,并指定其处理的占位符格式为"${}"
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }

GenericTokenParser是一个通用的占位符解析器,其字段的含义如下:

/**
   * 占位符的开始标记
   */
  private final String openToken;
  /**
   * 占位符的结束标记
   */
  private final String closeToken;
  /**
   * {@link TokenHandler}接口的实现会按照一定的逻辑解析占位符
   */
  private final TokenHandler handler;

GenericTokenParser.parse()方法的逻辑并不复杂,它会顺序查找openTokencloseToken,解析得到的占位符的字面值,并将其交给TokenHandler处理,然后将解析结果重新拼装成字符串并返回。该方法的实现如下:

/**
   * 此方法的逻辑并不复杂,它会顺序查找{@link #openToken}和{@link #closeToken} ,解析得到占位符的字面值,
   * 将将其交给{@link TokenHandler}进行处理,然后将解析结果重新拼装成字符串并返回。
   * @param text  拼装之前的字符串
   * @return 拼装之后后字符串
   */
  public String parse(String text) {
    //检测text是否为空
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    // 查找开始标记
    int start = text.indexOf(openToken);
    // 检测start是否为-1
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    // builder是用来记录解析后的字符串
    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中
        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中
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        // 从offset向后继续查找结束标记
        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中保存
            expression.append(src, offset, end - offset);
            break;
          }
        }
        //未找到结束标记
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //将占位符的字面值交给TokenHandler处理,并将处理结果追加到builder中保存
          //最终拼凑出解析后的完整内容
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      //移动start
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

占位符有TokenHandler接口的实现进行解析,TokenHandler接口总共有四个实现,如下图所示:

通过对PropertyParser.parse()方法的介绍,我们知道了PropertyParser是使用VariableTokenHandlerGenericTokenParser配合完成占位符解析的,VariableTokenHandlerPropertyReslover中的一个私有静态内部类,其字段的含义如下所示:

 /**
     * <properties><properties/>节点下定义的键值对,用于替换占位符
     */
    private final Properties variables;
    /**
     * 是否支持占位符中使用默认值的功能
     */
    private final boolean enableDefaultValue;
    /**
     * 指定占位符和默认值之间的分隔符
     */
    private final String defaultValueSeparator;

VariableTokenHandler实现了TokenHandler接口中的handlerToken()方法,该实现首先会按照defaultValueSeparator字段指定的分隔符对整个占位符切分,得到占位符的名称和默认值,然后按照切分得到的占位符名称查找对应的值,如果在<properties>节点下未定义相应的键值对,则将切分得到的默认值作为解析结果返回。

/**
     * 实现了{@link TokenHandler}接口中的{@link TokenHandler#handleToken(String)}方法,该实现:
     *  1. 首先会按照{@link #defaultValueSeparator}字段指定的分隔符对整个占位符切分,得到占位符的名称和默认值;
     *  2. 然后按照切分得到的占位符名称查找对应的值,如果在<properties><properties/>节点中未定义对应的键值对,
     *     则将切分得到的默认值作为解析结果返回。
     * @param content
     * @return
     */
    @Override
    public String handleToken(String content) {
      // 检测variable集合是否为空
      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) {
            //在variables集合中查找指定的占位符
            return variables.getProperty(key, defaultValue);
          }
        }
        // 不支持默认值的功能,则直接查找variables集合
        if (variables.containsKey(key)) {
          return variables.getProperty(key);
        }
      }
      //variables集合为空,直接返回
      return "${" + content + "}";
    }
  }

GenericTokenParser此类不仅用于默认值解析PropertyParser#parse(String, Properties),还会在动态SQL语句的解析中用到。很明显,GenericTokenParser只是查找到指定的占位符,而具体的解析行为会根据持有的TokenHandler实现的不同而不同,这有点策略模式的意思。
回到对XPathParser的分析,XPathParser.evalNode()方法返回值类型是XNode,它对org.w3c.dom.Node对象做了封装和解析,其各个字段的含义如下:


  /**
   * org.w3c.dom.Node对象
   */
  private final Node node;
  /**
   * Node节点名称
   */
  private final String name;
  /**
   * Node节点内容
   */
  private final String body;
  /**
   * 节点属性集合
   */
  private final Properties attributes;
  /**
   * mybatis-config.xml配置文件中<properties><properties/>节点下定义的键值对
   */
  private final Properties variables;
  /**
   * XPathParser对象,该Node对象由此XPathParser对象生成
   */
  private final XPathParser xpathParser;

XNode的构造函数中会调用其parseAttributes()方法和parseBody()方法解析org.w3c.dom.Node对象中的信息,初始化attributes集合和body字段,具体初始化过程如下:

/**
   * 解析Node对象中的Attributes属性集合
   * @param n 节点对象
   * @return
   */
  private Properties parseAttributes(Node n) {
    Properties attributes = new Properties();
    //获取节点的属性结合
    NamedNodeMap attributeNodes = n.getAttributes();
    if (attributeNodes != null) {
      for (int i = 0; i < attributeNodes.getLength(); i++) {
        Node attribute = attributeNodes.item(i);
        //使用PropertyParser处理每一个属性中的占位符
        String value = PropertyParser.parse(attribute.getNodeValue(), variables);
        attributes.put(attribute.getNodeName(), value);
      }
    }
    return attributes;
  }

/**
   * 解析Node对象中的信息body字段
   * @param node
   * @return
   */
  private String parseBody(Node node) {
    String data = getBodyData(node);
    //当前节点不是文本节点
    if (data == null) { 
      //处理子节点
      NodeList children = node.getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        Node child = children.item(i);
        data = getBodyData(child);
        if (data != null) {
          break;
        }
      }
    }
    return data;
  }

  private String getBodyData(Node child) {
    //只处理文本内容
    if (child.getNodeType() == Node.CDATA_SECTION_NODE
        || child.getNodeType() == Node.TEXT_NODE) {
      String data = ((CharacterData) child).getData();
      //使用PropertyParser处理文本节点中的占位符
      data = PropertyParser.parse(data, variables);
      return data;
    }
    return null;
  }

XPathParser测试用例

import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;

import java.io.IOException;
import java.util.List;

/**
 * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
 * @since : 2019/11/25 16:11
 */
public class XPathParserTest {
  public static void main(String[] args) throws IOException {
    String resource = "resources/xpath-demo.xml";
    XPathParser xPathParser = new XPathParser(Resources.getResourceAsReader(resource),true,null,new XMLMapperEntityResolver());
    XNode xNode = xPathParser.evalNode("/configuration");
    List<XNode> children = xNode.getChildren();
    for (int i = 0; i < children.size(); i++) {
      System.out.println(children.get(i).getName());
    }
  }
}


Mybatis源码分析   基础支持层      Mybatis源码分析

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!