1.总览

解析器模块代码总览
img

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

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

下面,我们就来看看具体的源码。

2.XPathParser

XPathParser是基于 Java XPath 解析器,用于解析 MyBatis mybatis-config.xml**Mapper.xml 等 XML 配置文件。

  /**
   * XML被解析后生产的Document对象
   */
  private final Document document;
  /**
   * 是否校验 XML 。一般情况下,值为true
   */
  private boolean validation;
  /**
   * XML 实体解析器。
   */
  private EntityResolver entityResolver;
  /**
   * 变量 Properties 对象,用来替换需要动态配置的属性值。
   */
  private Properties variables;
  /**
   * 用于查询 XML 中的节点和元素。
   */
  private XPath xpath;
  • entityResolver属性,XML 实体解析器。默认情况下,对 XML 进行校验时,会基于 XML 文档开始位置指定的 DTD 文件或 XSD 文件。例如说,解析 mybatis-config.xml 配置文件时,会加载 http://mybatis.org/dtd/mybatis-3-config.dtd 这个 DTD 文件。但是,如果每个应用启动都从网络加载该 DTD 文件,势必在弱网络下体验非常差,甚至说应用部署在无网络的环境下,还会导致下载不下来,那么就会出现 XML 校验失败的情况。所以,在实际场景下,MyBatis 自定义了 EntityResolver 的实现,达到使用本地 DTD 文件,从而避免下载网络 DTD 文件的效果。
  • variables属性,用于替换动态配置的属性值,如:
    <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
    </dataSource>
    
    • variables的来源,即可以在常用的 Java Properties 文件中配置,也可以使用 MyBatis <property />标签中配置。
      <properties resource="org/mybatis/example/config.properties">
          <property name="username" value="123"/>
          <property name="password" value="456"/>
      </properties>
      
      • 这里配置的 usernamepassword 属性,就可以替换上面的 ${username}${password} 这两个动态属性。

2.1 构造方法

XPathParser 的构造方法有 16 个之多,当然基本都非常相似,我们来挑选其中一个。代码如下:

// XPathParser.java

/**
 * 构造 XPathParser 对象
 *
 * @param xml XML 文件地址
 * @param validation 是否校验 XML
 * @param variables 变量 Properties 对象
 * @param entityResolver XML 实体解析器
 */
public XPathParser(String xml, boolean validation, Properties variables, EntityResolver entityResolver) {
    commonConstructor(validation, variables, entityResolver);
    this.document = createDocument(new InputSource(new StringReader(xml)));
}

调用 #commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) 方法,公用的构造方法逻辑。代码如下:

// XPathParser.java

private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
    this.validation = validation;
    this.entityResolver = entityResolver;
    this.variables = variables;
    // 创建 XPathFactory 对象
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();
}

调用 #createDocument(InputSource inputSource) 方法,将 XML 文件解析成 Document 对象。代码如下:

  /**
   * 创建文档
   * @param inputSource
   * @return
   */
  private Document createDocument(InputSource inputSource) {
    // important: this must only be called AFTER common constructor
    try {
      // 创建并配置工厂属性
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      // 校验xml
      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);
    }
  }

2.2 eval 方法族

XPathParser 提供了一系列的 #eval* 方法,用于获得 BooleanShortIntegerLongFloatDoubleStringNode 类型的元素或节点的“值”。当然,虽然方法很多,但是都是基于 #evaluate(String expression, Object root, QName returnType) 方法,代码如下:

// XPathParser.java

/**
 * 获得指定元素或节点的值
 * @param expression 表达式
 * @param root 指定节点
 * @param returnType 返回类型
 * @return 值
 */
private Object evaluate(String expression, Object root, QName returnType) {
    try {
        return xpath.evaluate(expression, root, returnType);
    } catch (Exception e) {
        throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
    }
}

2.2.1 eval 元素

eval 元素的方法,用于获得 BooleanShortIntegerLongFloatDoubleString 类型的元素的值。我们以 #evalString(Object root, String expression) 方法为例子,代码如下:

// XPathParser.java

public String evalString(Object root, String expression) {
    // 获得值
    String result = (String) evaluate(expression, root, XPathConstants.STRING);
    // 基于 variables 替换动态值,如果 result 为动态值
    result = PropertyParser.parse(result, variables);
    return result;
}

2.2.2 eval 节点

eval 元素的方法,用于获得 Node 类型的节点的值。代码如下:

  /**
   * 解析Node数组
   * @param expression
   * @return
   */
  public List<XNode> evalNodes(String expression) {
    // 从根文档开始解析
    return evalNodes(document, expression);
  }

  public List<XNode> evalNodes(Object root, String expression) {
    List<XNode> xnodes = new ArrayList<XNode>();
    // 获取节点数组
    NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
    // 封装问XNode数组
    for (int i = 0; i < nodes.getLength(); i++) {
      xnodes.add(new XNode(this, nodes.item(i), variables));
    }
    return xnodes;
  }

  /**
   * 解析单个节点
   * @param expression
   * @return
   */
  public XNode evalNode(String expression) {
    return evalNode(document, expression);
  }

  public XNode evalNode(Object root, String expression) {
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
      return null;
    }
    return new XNode(this, node, variables);
  }

3. XMLMapperEntityResolver

org.apache.ibatis.builder.xml.XMLMapperEntityResolver ,实现 EntityResolver 接口,MyBatis 自定义 EntityResolver 实现类,用于加载本地的 mybatis-3-config.dtdmybatis-3-mapper.dtd 这两个 DTD 文件。代码逻辑比较简单,简单看下即可:

/**
 * MyBatis dtd的离线实体解析器
 * 
 * @author Clinton Begin
 * @author Eduardo Macarron
 */
public class XMLMapperEntityResolver implements EntityResolver {

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

  // 配置文件路径
  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转换为本地DTD
   * 
   * @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);
        if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
          // mybatis
          return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
        } else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
          // ibatis
          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;
  }

}

4. GenericTokenParser

通用的token解析器,主要是提取被openTokencloseToken包裹的token,并交由`处理替换。

代码逻辑不复杂,主要流程就是找到合适的openToken,再查询合适的closeToken,然后再交由handler来处理token内容。

  // 关键属性
  /**
   * 包裹token的左表达式
   */
  private final String openToken;
  /**
   * 包裹token的右表达式
   */
  private final String closeToken;
  /**
   * token的替换处理类
   */
  private final TokenHandler handler;


  // 关键方法

  /**
   * 替换方法
   * @param text 需要解析替换的文本
   * @return
   */
  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] == '\\') {
        // 转义字符,不应该被标记为openToken,并把转义的斜杠“\”去掉
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // 找到openToken后,开始处理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] == '\\') {
            // 跟上面一样,判断是否为转义
            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,说明不符合表达式,不进行TokenHandler处理
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          // 找到对应的token,由TokenHandler处理替换逻辑
          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();
  }

5. PropertyParser

参数解析器的静态工具类,用于解析动态属性,用于解析型如:${xxx:yyy}的参数,关键方法如下:

  /**
   * 转换方法
   * @param string
   * @param variables
   * @return
   */
  public static String parse(String string, Properties variables) {
    // 变量替换的处理器
    VariableTokenHandler handler = new VariableTokenHandler(variables);
    // token提取,提取后交由handler进行匹配替换
    GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
    return parser.parse(string);
  }

GenericTokenParser在上面已经解析过,这里就是替换${xxx}(是不是很熟悉)的变量,然后交给VariableTokenHandler来处理,那我们就看看这个处理类做了什么吧。

VariableTokenHandler是PropertyParser内部类,实现了TokenHandler接口,具体代码如下:

  /**
   * 参数变量提取处理器
   */
  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);
    }

    /**
     * 用于解析${key:defaultValue},默认分割符为“:”,可通过配置修改
     * @param content
     * @return
     */
    @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 + "}";
    }
  }

代码逻辑很清晰,其中包含了两个私有的配置项:

  • enableDefaultValue:是否使用默认值,如果找不到配置值,则使用默认值替换,默认是false
  • defaultValueSeparator:默认值的分隔符,默认是:,可以通过配置项修改

 

6. TokenHandler

该接口只定义了一个通用方法,用于解析token

public interface TokenHandler {
  /**
   * 处理token
   * @param content
   * @return
   */
  String handleToken(String content);
}

TokenHandler 的实现类有4个,前面已经VariableTokenHandler,剩余的在后续模块中再解析,敬请期待。

 

7.XNode

扩展了dom的Node节点,提供了便捷的获取父子节点,动态参数替换后的属性、元素体等,是MyBatis解析器的基础,也是解析器的结果。

关键属性如下:

  /**
   * 原始文档节点
   */
  private final Node node;
  /**
   * 节点名称
   */
  private final String name;
  /**
   * 节点的内容
   */
  private final String body;
  /**
   * 节点属性
   */
  private final Properties attributes;
  /**
   * 属性
   */
  private final Properties variables;
  /**
   * xpathParser解析器
   */
  private final XPathParser 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);
    // 解析body
    this.body = parseBody(node);
  }

在构建XNode的时候,会把node节点上的参数和body先解析好,看看具体的解析方法:

  • 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;
  }

逻辑和简单,把节点上的属性转换为Properties,其中参数的值使用PropertyParser来处理。

  • body的解析:
  private String parseBody(Node node) {
    // 获取内容
    String data = getBodyData(node);
    if (data == null) {
      // 如果获取到的数据为空,这查询子节点的body数据
      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;
  }

  /**
   * 获取解析后的节点的文本内容(忽略子元素)
   * @param child
   * @return
   */
  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提供了便捷的获取父子节点,动态参数替换后的属性、元素体等,我们具体来看几个方法:

  • 获取子节点信息:
  /**
   * 获取子节点列表
   * @return
   */
  public List<XNode> getChildren() {
    List<XNode> children = new ArrayList<XNode>();
    NodeList nodeList = node.getChildNodes();
    if (nodeList != null) {
      for (int i = 0, n = nodeList.getLength(); i < n; i++) {
        Node node = nodeList.item(i);
        if (node.getNodeType() == Node.ELEMENT_NODE) {
          children.add(new XNode(xpathParser, node, variables));
        }
      }
    }
    return children;
  }

  /**
   * 把子节点转化的属性对象返回
   * @return
   */
  public Properties getChildrenAsProperties() {
    Properties properties = new Properties();
    for (XNode child : getChildren()) {
      String name = child.getStringAttribute("name");
      String value = child.getStringAttribute("value");
      if (name != null && value != null) {
        properties.setProperty(name, value);
      }
    }
    return properties;
  }

  public String getStringAttribute(String name) {
    return getStringAttribute(name, null);
  }

  public String getStringAttribute(String name, String def) {
    String value = attributes.getProperty(name);
    if (value == null) {
      return def;
    } else {
      return value;
    }
  }

首先获取所有子节点封装为XNode的数组,再从这些XNode的attributes中提取namevalue信息,汇总为Properties输出。

  • 获取父节点信息:
  /**
   * 获取父节点的XNode对象
   * @return
   */
  public XNode getParent() {
    Node parent = node.getParentNode();
    if (parent == null || !(parent instanceof Element)) {
      return null;
    } else {
      return new XNode(xpathParser, parent, variables);
    }
  }

  • 获取根节点到当前节点的路径:
  /**
   * 获取根节点到当前节点的路径
   * 如返回root/node
   * @return
   */
  public String getPath() {
    StringBuilder builder = new StringBuilder();
    Node current = node;
    while (current != null && current instanceof Element) {
      if (current != node) {
        builder.insert(0, "/");
      }
      // 在最前面插入当前循环的节点的名称
      builder.insert(0, current.getNodeName());
      // 往上循环,直到根节点
      current = current.getParentNode();
    }
    return builder.toString();
  }

  • 从元素的属性中获取id值
  /**
   * 从该元素的属性中获取id值,优先级分别为:id 》 value 》 property
   * 如节点:<namespace id="test"><sql id="sql.testSql"></sql></namespace>
   * 则返回:[test]_sql_testSql
   * 注意:这里的“.”会被替换为“_”
   * @return
   */
  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();
  }

除了上面提到的方法外,XNode还提供了获取各种类型的参数和eval*方法,都是简单的类型转换,这里就不一一列出了。