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*()方法,信息来源于上述属性

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值