美文网首页
MyBatis源码解析 - 解析器模块

MyBatis源码解析 - 解析器模块

作者: janker0718 | 来源:发表于2020-03-11 23:10 被阅读0次

    MyBatis源码解析 - 解析器模块

    1. 前言

    在MyBatis中涉及多个xml文件,解析这些xml文件自然离不开解析器。本文就来分析一下解析器模块。

    2. 准备工作

    xml常见的解析方式分为以下三种:

    • DOM ( Document Object Model)解析方式
    • SAX (Simple APIfor XML)解析方式
    • StAX( Streaming API for XML)解析方式 - JDK 6.0版本开始,JDK开始支持

    详细的解析xml学习可以参考 Java解析XML 在这里我们需要重点看下DOM解析,DOM解析主要的好处就是易于编程,可以跟根据需求在树形结构的各个节点之间导航。

    3. XPathParser

    MyBatis 在初始化过程中处理mybatis-config.xml以及映射文件时使用的是DOM解析方式,并结合使用XPath解析XML配置文件。DOM会将整个XML文档加载到内存中形成数据结构。

    XPathParser类封装了XPath 、Document和EntityResolver 依赖关系如图所示


    XPathParser中字段含义和功能如下

        private final Document document;  //Document 对象
      private boolean validation;           //是否开启校验
      private EntityResolver entityResolver; //用于加载本地DTD文件
      private Properties variables;                 //mybatis-config.xml <properties> 标签定义的键值对集合
      private XPath xpath;                          //XPath对象
    
    • 默认情况下,对XML文档验证的时候,会根据XML文档指定的网址加载对应的DTD文件或者XSD文件。
    • 解析mybatis-config.xml文件时,默认联网加载http://mybatis.org/dtd/mybatis-3-config.dtd这个DTD文档,当网络比较慢会使加载变缓慢。其实在MyBatis中已经配置了关于DTD文件的映射关系。
    • XMLMapperEntityResolver中实现了EntityResolver接口,并配置加载本地的DTD文件。关系如图所示:
    • EntityResolver接口的核心是resolveEntity() 方法,XMLMapperEntityResolver的实现如下:


    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";
    
      /**
       * 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
       */
      //实现EntityResolver接口的resolveEntity方法
      @Override
      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());
        }
      }
        // getInputSource()方法负责读取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) {
            // ignore, null is ok
          }
        }
        return source;
      }
    }
    

    介绍完XMLMapperEntityResolver之后,我们回到XPathParser这个类上接下来我们按照组成部分挨个拆分出来。

    XPathParser构造

    XPathParser构造方法有16种,应该是满足各种各样不同使用场景下的需求吧。

    <img src="http://qiniu-cdn.janker.top/oneblog/20200105215713359.jpg" style="zoom:50%;" />

    createDocument方法

    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();
    }
    //调用createDocument发放之前一定要先调用 commonConstructor() 方法完成初始化
      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() {
            @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);
        }
      }
    
    • XPathParser.createDocument()方法中封装了创建Document对象的过程并触发了加载XML文档的过程。

    eval*()系列方法

    • XPathParser中提供了一系列的eval*()方法用于解析boolean、short、Integer、Long、Float、Sting、Double、Node等类型的信息。
    • 通过调用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>节点下配置是否开启默认值功能的对应配置项
    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 = ":";
    private PropertyParser() {
        // Prevent Instantiation
      }
    
      public static String parse(String string, Properties variables) {
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        //创建 GenericTokenParser 解析器对象 并制定其占位符为 ${}
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        return parser.parse(string);
      }
    

    • PropertyParser.parse()方法创建GenericTokenParser解析器,并将默认值的处理委托给GenericTokenParser.parse()```方法。

    4. GenericTokenParser

    GenericTokenParser是通用的占位符解析器,具体代码如下:


    package org.apache.ibatis.parsing;
    
    /**
     * 通用的占位符解析器
     * @author Clinton Begin
     */
    public class GenericTokenParser {
    
      private final String openToken; //占位符的开始标记
      private final String closeToken; //占位符的结束标记
      private final TokenHandler handler; //TokenHandler接口的实现会按照一定的逻辑解析占位符
    
      public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;
        this.closeToken = closeToken;
        this.handler = handler;
      }
    
      public String parse(String text) {
        //检测 text 是否为空
        if (text == null || text.isEmpty()) {
          return "";
        }
        // search open token
        // 查找开始标记
        int start = text.indexOf(openToken);
        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中
            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位置
            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 = text.indexOf(openToken, offset); //移动start
        }
        if (offset < src.length) {
          builder.append(src, offset, src.length - offset);
        }
        return builder.toString();
      }
    }
    
    • GenericTokenParser.parse()方法比较简单,具体实现就是顺序查找openTokencloseToken,解析得到占位符的字面值
    • 解析出来的结果交给Tokenhandler处理,然后将解析结果重新拼装成字符串返回。

    5. TokenHandler

    TokenHandler是解析占位符接口,总共有四个实现,如图:

    <img src="http://qiniu-cdn.janker.top/oneblog/20200105225624602.png" style="zoom:67%;" />

    6. PropertyParser

    PropertyParser是使用VariableTokenHandler和GenericTokenParser配合完成占位符解析。代码如下:


    package org.apache.ibatis.parsing;
    
    import java.util.Properties;
    
    /**
     * @author Clinton Begin
     * @author Kazuki Shimizu
     */
    public class 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 = ":";
    
      private PropertyParser() {
        // Prevent Instantiation
      }
    
      public static String parse(String string, Properties variables) {
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        //创建 GenericTokenParser 解析器对象 并制定其占位符为 ${}
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        return parser.parse(string);
      }
    
      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) {
          // 检测 variables 集合是否为空
          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) {
                //在variable集合中查找指定占位符
                return variables.getProperty(key, defaultValue);
              }
            }
            // 不支持默认值的功能,直接查找variables集合
            if (variables.containsKey(key)) {
              return variables.getProperty(key);
            }
          }
          return "${" + content + "}"; //variables集合为空 直接返回
        }
      }
    
    }
    

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

    7. XNode

    XPathParser.evalNode()方法返回的类型为XNode,他对org.w3c.dom.Node对象惊醒了封装和解析,具体代码如下:


    public class XNode {
    
      private final Node node;  //org.w3c.dom.Node对象
      private final String name;  //Node节点名称
      private final String body;  //节点内容
      private final Properties attributes;  //节点属性集合
      private final Properties variables; //mybatis-config.xml配置文件中<properties>节点下定义的键值对
      private final XPathParser xpathParser;  //xpathParser对象 xNode由XPathParser对象生成
    
      public XNode(XPathParser xpathParser, Node node, Properties variables) {
        this.xpathParser = xpathParser;
        this.node = node;
        this.name = node.getNodeName();
        this.variables = variables;
        this.attributes = parseAttributes(node);
        this.body = parseBody(node);
      }
    
      public XNode newXNode(Node node) {
        return new XNode(xpathParser, node, variables);
      }
    
      public XNode getParent() {
        Node parent = node.getParentNode();
        if (!(parent instanceof Element)) {
          return null;
        } else {
          return new XNode(xpathParser, parent, variables);
        }
      }
    
      public String getPath() {
        StringBuilder builder = new StringBuilder();
        Node current = node;
        while (current instanceof Element) {
          if (current != node) {
            builder.insert(0, "/");
          }
          builder.insert(0, current.getNodeName());
          current = current.getParentNode();
        }
        return builder.toString();
      }
    
      public String getValueBasedIdentifier() {
        StringBuilder builder = new StringBuilder();
        XNode current = this;
        while (current != null) {
          if (current != this) {
            builder.insert(0, "_");
          }
          String value = current.getStringAttribute("id",
              current.getStringAttribute("value",
                  current.getStringAttribute("property", null)));
          if (value != null) {
            value = value.replace('.', '_');
            builder.insert(0, "]");
            builder.insert(0,
                value);
            builder.insert(0, "[");
          }
          builder.insert(0, current.getName());
          current = current.getParent();
        }
        return builder.toString();
      }
    
      public String evalString(String expression) {
        return xpathParser.evalString(node, expression);
      }
    
      public Boolean evalBoolean(String expression) {
        return xpathParser.evalBoolean(node, expression);
      }
    
      public Double evalDouble(String expression) {
        return xpathParser.evalDouble(node, expression);
      }
    
      public List<XNode> evalNodes(String expression) {
        return xpathParser.evalNodes(node, expression);
      }
    
      public XNode evalNode(String expression) {
        return xpathParser.evalNode(node, expression);
      }
    
      public Node getNode() {
        return node;
      }
    
      public String getName() {
        return name;
      }
    
      public String getStringBody() {
        return getStringBody(null);
      }
    
      public String getStringBody(String def) {
        if (body == null) {
          return def;
        } else {
          return body;
        }
      }
    
      public Boolean getBooleanBody() {
        return getBooleanBody(null);
      }
    
      public Boolean getBooleanBody(Boolean def) {
        if (body == null) {
          return def;
        } else {
          return Boolean.valueOf(body);
        }
      }
    
      public Integer getIntBody() {
        return getIntBody(null);
      }
    
      public Integer getIntBody(Integer def) {
        if (body == null) {
          return def;
        } else {
          return Integer.parseInt(body);
        }
      }
    
      public Long getLongBody() {
        return getLongBody(null);
      }
    
      public Long getLongBody(Long def) {
        if (body == null) {
          return def;
        } else {
          return Long.parseLong(body);
        }
      }
    
      public Double getDoubleBody() {
        return getDoubleBody(null);
      }
    
      public Double getDoubleBody(Double def) {
        if (body == null) {
          return def;
        } else {
          return Double.parseDouble(body);
        }
      }
    
      public Float getFloatBody() {
        return getFloatBody(null);
      }
    
      public Float getFloatBody(Float def) {
        if (body == null) {
          return def;
        } else {
          return Float.parseFloat(body);
        }
      }
    
      public <T extends Enum<T>> T getEnumAttribute(Class<T> enumType, String name) {
        return getEnumAttribute(enumType, name, null);
      }
    
      public <T extends Enum<T>> T getEnumAttribute(Class<T> enumType, String name, T def) {
        String value = getStringAttribute(name);
        if (value == null) {
          return def;
        } else {
          return Enum.valueOf(enumType, value);
        }
      }
    
      // ** 省略 get*()方法
    
      @Override
      public String toString() {
      
        StringBuilder builder = new StringBuilder();
        toString(builder, 0);
        return builder.toString();
      }
    
      private void toString(StringBuilder builder, int level) {
        // ** 省略 toString ** 
      }
    
      private void indent(StringBuilder builder, int level) {
        for (int i = 0; i < level; i++) {
          builder.append("    ");
        }
      }
    
      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;
      }
    
      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;
      }
    
    }
    

    • XNode的构造函数会调用其parseAttributes() 方法和parseBody()方法解析org.w3c.dom.Node对象中的信息,初始化attributes集合和body```字段。
    • XNode中提供了多种get*()方法获取所需的节点信息,这些信息主要描述attributes集合、body字段、node字段。
    • 此外我们也可以使用XNode.eval*()方法结合XPath查询需要的信息,eval *() 系列方法是通过调用其封装的XPathParser对象的eval *()方法实现的。
    • eval *() 系列方法的上下文节点是当前的XNode.node

    8. 小结

    以上就是MyBatis的解析器模块的全部内容,下一篇博客我们继续分析反射模块。

    本文由 Janker 创作,采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
    <img src="http://qiniu-cdn.janker.top/oneblog/20200311150833864.jpg" style="zoom:50%;" />

    相关文章

      网友评论

          本文标题:MyBatis源码解析 - 解析器模块

          本文链接:https://www.haomeiwen.com/subject/uiikjhtx.html