Mybatis 源码学习-解析器模块

Posted 凉茶方便面

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybatis 源码学习-解析器模块相关的知识,希望对你有一定的参考价值。

XPath

XPath 是通过表达式在 XML 文件中选择节点的表达式语言,通过 XPath 可以将 XML 中的节点,通过特定的规则将 DOM 对象,转化为 Boolean、Double、String 等 Java 对象。

表达式含义
nodename选取指定节点的子节点
/从根节点开始选取指定节点
//根据指定的表达式,在整个文档中选取匹配的节点(不考虑节点的位置)
.选取当前节点
选取当前节点的父节点
@选取属性
*匹配任意节点元素
@*匹配任意属性节点
node()匹配任何类型的节点
text()匹配文本节点
|选取若干路径
[]指定某个条件,用于查找某个特定节点

简单的举例来说,给定如下 xml 文件内容:

<foo>
  <bar/>
  <bar/>
  <bar/>
</foo>

选择所有的bar 元素://bar
使用通配符选择所有的 foo 元素的子元素:/foo/*
根据 include 属性值选择 foo 元素://foo[@include='true']

Xpath 的核心类包括:

XPath:提供利用 xpath 语法进行处理的入口;
Node:根据选择表达式获取的单个元素节点(如果命中多个节点,node.getNextSibling 会指向后续的节点,直至最后一个);
NodeList:Node的列表;
XPathConstants:作为 XPath.evaluate 的参数,标记 Node 的类型,XPathConstants.NODESET(节点集合,对应类型为 NodeList)、XPathConstants.NODE(节点,对应类型为 Node);XPathConstants.STRING(字符串值,对应类型为String);XPathConstants.BOOLEAN(布尔值,对应类型为 Boolean);XPathConstants.NUMBER(数值,对应类型为 Double);
XPathExpression:预编译的 xpath 表达式,可以被多次使用,方便提高性能。

举例来看:

public class XPathExample 
    private static final String xml =
            "<inventory>\\n" +
            "    <book year=\\"2000\\">\\n" +
            "        <title>Snow Crash</title>\\n" +
            "        <author>Neal Stephenson</author>\\n" +
            "        <publisher>Spectra</publisher>\\n" +
            "        <isbn>0553380958</isbn>\\n" +
            "        <price>14.95</price>\\n" +
            "    </book>\\n" +
            "    <book year=\\"2005\\">\\n" +
            "        <title>Burning Tower</title>\\n" +
            "        <author>Larry Niven</author>\\n" +
            "        <author>Jerry Pournelle</author>\\n" +
            "        <publisher>Pocket</publisher>\\n" +
            "        <isbn>0743416910</isbn>\\n" +
            "        <price>5.99</price>\\n" +
            "    </book>\\n" +
            "    <book year=\\"1995\\">\\n" +
            "        <title>Zodiac</title>\\n" +
            "        <author>Neal Stephenson</author>\\n" +
            "        <publisher>Spectra</publisher>\\n" +
            "        <isbn>0553573862</isbn>\\n" +
            "        <price>7.50</price>\\n" +
            "    </book>\\n" +
            "</inventory>";

    // 参考文档:https://docs.oracle.com/javase/10/docs/api/javax/xml/xpath/package-summary.html
    // 直接使用 xpath 读取文件内容
    @Test
    public void javaSDKEg() throws Exception 
        // parse the XML as a W3C Document
        DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        Document document = builder.parse(new InputSource(new StringReader(xml)));

        //Get an XPath object and evaluate the expression
        XPath xpath = XPathFactory.newInstance().newXPath();
        String expression = "/inventory/book";
        Node widgetNode = (Node) xpath.evaluate(expression, document, XPathConstants.NODE);

        System.out.println(widgetNode.getNodeName());
        // log.info("", JsonUtils.serialize(widgetNode));
    

    @Test
    public void book() throws Exception 
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();

        // 开启验证
        // documentBuilderFactory.setValidating(true);
        documentBuilderFactory.setNamespaceAware(false);
        documentBuilderFactory.setIgnoringComments(true);
        documentBuilderFactory.setIgnoringElementContentWhitespace(false);
        documentBuilderFactory.setCoalescing(false);
        documentBuilderFactory.setExpandEntityReferences(true);

        // 创建 DocumentBuilder
        DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder(); //设置异常处理对象
        builder.setErrorHandler(new ErrorHandler() 
            @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());
            

            @Override
            public void warning(SAXParseException exception) throws SAXException 
                System.out.println("WARN:" + exception.getMessage());
            
        );

        // 将文档加载到一个 Document 对象中
        Document doc = builder.parse(new InputSource(new StringReader(xml)));

        // 创建 XPathFactory
        XPathFactory factory = XPathFactory.newInstance(); //创建 XPath 对象
        XPath xpath = factory.newXPath();

        // 编译 XPath 表达式
        XPathExpression expr =
                xpath.compile("//book[author='Neal Stephenson']/title/text()");

        // 通过XPath表达式得到结果,第一个参数指定了 XPath 表达式进行查询的上下文节点,也就是在指定
        // 节点下查找符合 XPath 的节点。本例中的上下文节点是整个文档;第二个参数指定了 XPath 表达式
        // 的返回类型 。
        Object result = expr.evaluate(doc, XPathConstants.NODESET);
        System.out.println("查询作者为 Neal Stephenson 的图书的标题:");
        NodeList nodes = (NodeList) result; // 强制类型转换
        for (int i = 0; i < nodes.getLength(); i++) 
            System.out.println(nodes.item(i).getNodeValue());
        

        System.out.println("查询 1997 年之后的图书的标题:");
        nodes = (NodeList) xpath.evaluate("//book[@year>1997]/title/text()", doc, XPathConstants.NODESET);
        for (int i = 0; i < nodes.getLength(); i++) 
            System.out.println(nodes.item(i).getNodeValue());
        

        System.out.println("查询 1997 年之后的图书的属性和标题:");
        nodes = (NodeList) xpath
                .evaluate("//book[@year>1997]/@* | //book[@year>1997]/title/text()", doc, XPathConstants.NODESET);
        for (int i = 0; i < nodes.getLength(); i++) 
            System.out.println(nodes.item(i).getNodeValue());
        
    

总的来说,XPath 是使用选择器选择 XML 文件内元素的工具,在 Mybatis 中解析 config 和 mapper 文件均使用该工具。

XPathParser

XPathParser 提供了对 XML 文件解析的能力,它将 XPath、Document 对象做了封装,便于进行对象操作,此外,XPathParser 还对原始方法做了升级,使得原本只能处理 String、Integer、Double 类型的数据,升级为能够处理 String、Boolean、Short、Integer、Long、Float、Double。

public class XPathParser 

  private final Document document; // Document 对象
  private boolean validation; // 是否开启验证
  private EntityResolver entityResolver; // 用于加载本地 DTD 文件
  private Properties variables; // mybatis-config.xml 中<propteries>标签定义的键位对集合
  private XPath xpath; // XPath 对象 

  public XPathParser()  // 多个重载的构造函数,用于加载 xml 文件
    commonConstructor(); // 通用构造器
    this.document = createDocument(); // 创建 Document 对象
  

  // 设置外界参数,同时初始化 XPath 对象
  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(); // 初始化 XPath 对象
  

  // 调用 createDocument ()方法之前一定要先调用 commonConstructor()方法完成初始化 
  private Document createDocument(InputSource inputSource) 
    try 
      //创建 DocumentBuilderFactory 对象
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      // 为 Document 设置验证参数
      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);
      // 设置 ErrorHandler  处理器,所有的方法都是空方法
      builder.setErrorHandler(new ErrorHandler() );
      //加载 XML 文件,构造 Document 对象
      return builder.parse(inputSource);
     catch (Exception e) 
      throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    
  

  public void setVariables(Properties variables) // 设置外部 properties

  public String evalString() // 重载函数,处理 String
  public Boolean evalBoolean() // 重载函数,处理 Boolean
  public Short evalShort() // 重载函数,处理 Short(通过 Short.valueOf(evalString(…)))
  public Integer evalInteger()  // 重载函数,处理 Integer(通过 Integer.valueOf(evalString(…))
  public Long evalLong() // 重载函数,处理 Long(通过 Long.valueOf(evalString(…))
  public Float evalFloat()  // 重载函数,处理 Float(通过 Float.valueOf(evalString(…))
  public Double evalDouble() // 重载函数,处理 Double

  public List<XNode> evalNodes()  // 重载函数,将原始 NodeList 构造为 List<XNode>
  public XNode evalNode() // 重载函数,将原始 Node 构造为 XNode

从上边的代码可以看出,初始化 XPathParser 时,会通过 createDocument 方法触发对 XML 文件的加载,并创建 Document 对象,此外在调用 createDocument 之前会要求必须执行 commonConstructor 方法以初始化 xpath 对象。除了初始化部分,XPathParser 提供了一系列 eval* 方法,这些方法被用于解析特定的数据类型,它们背后实际上调用的还是 xpath.evaluate(…) 方法。

总的来说,XPathParser 是Mybatis 中解析 XML 文件的工具,它封装了 XPath 的方法,使得解析的代码更加清晰、简单。

EntityResolver

默认情况下,在对 XML 文件进行验证时,会从网络下载 XML 文件对应的 DTD 文件或 XSD 文件,如,解析 mybatis-config.xml 时会从网络下载 http://mybatis.org/dtd/mybatis-3-config.dtd,但是某些情况下,运行时,是没有网络,或者每次都网络下载,其开销也是非常大的,为了避免从网络下载验证文件,则需要使用 EntityResolver 加载本地验证文件。XMLMapperEntityResolver 是 MyBatis提供的 EntityResolver 接口的实现类。

EntityResolver 接口中仅存在一个方法:resolveEntity,程序会在读取任何外部资源之前,都会首先调用该方法。XMLMapperEntityResolver 中设置了网络文件与本地 jar 包中的 DTD 文件的路径对应关系,在加载文件时,会自动用本地文件替代网络文件。

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

  // resolveEntity ()方法是 EntityResolver 接 口中 定义的方法,
  public InputSource resolveEntity(String publicId, String systemId) throws SAXException 
    try 
      if (systemId != null) 
        String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
        //查找 systemId 指定的 DTD 文档 , 并调用 getInputSource ()方法读取 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 对象 
  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) 
      
    
    return source;
  

总的来说,EntityResolver 是用来用本地 DTD 文件替换远程网络文件,辅助验证 XML 文件的工具。

PropertyParser

在 XPathParser.evalString 方法中,存在对 PropertyParser 方法的调用,用于对节点的默认值进行处理。

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>节点下是否开启默认值功能的对应配置项 
public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";
// 配置占位符与默认值之间的默认分隔符的对应配置项
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 是通用的 token 解析器,用于根据参数,将特定的占位符去除并用属性值替换。

public static String parse(String string, Properties variables) 
  VariableTokenHandler handler = new VariableTokenHandler(variables);
  // 创建 GenericTokenParser 对象,并指定其处理的占位符格式为”$ ” 
  GenericTokenParser parser = new GenericTokenParser("$", "", handler);
  return parser.parse(string);

对于 GenericTokenParser 而言,它的核心方法就是 parse 方法,它会根据 openToken 和 closeToken 在待解析的字符串中使用 TokenHandler 处理占位符,处理完成后,并重构成新的字符串。

public class GenericTokenParser 
   // … 初始化方法

  public String parse(String text) 
    // … 判断 text 是否是空的

    // 检查 openToken 是否在 text 中出现过,未出现(indexOf == -1)则直接返回 text
    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] == '\\\\') 
        // 遇到转义的开始标记,则直接将前面的字符串以及开始标记追加到 builder 中 
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
       else 
        // 查找到开始标记,且未转义
        if (expression == null) 
          expression = new StringBuilder();
         else 
          // 如果已经存在 expression 则重置 expression 的长度,使得 expression 的数据为空
          expression.setLength(0);
        
        // 将前面的字符串追加到 builder 中,其中 offset 是上次的开始下标,start 本次查询到的位置
        builder.append(src, offset, start - offset);
        // 修改 offset 的位置,设置为 openToken 的位置 + openToken.length,即 openToken 之后的第一个位置
        offset = start + openToken.length();
        // 从 offset 向后查询 closeToken
        int end = text.indexOf(closeToken, offset);
        while (end > -1) 
          if (end > offset && src[end - 1] == '\\\\') 
            // 处理转义的结束标记,并去除转义字符
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            // 继续向后搜索 closeToken
            end = text.indexOf(closeToken, offset);
         

以上是关于Mybatis 源码学习-解析器模块的主要内容,如果未能解决你的问题,请参考以下文章

浩哥解析MyBatis源码——Parsing解析模块之通用标记解析器(GenericTokenParser)与标记处理器(TokenHandler)

mybatis-解析器模块

Mybatis 源码学习-反射工具(Reflector)

浩哥解析MyBatis源码——Type类型模块之类型处理器注册器(TypeHandlerRegistry)

Mybatis框架基础支持层——解析器模块

Mybatis 源码学习(12)-资源加载