Mybatis技术内幕(2.1):解析器模块

1.0 概述

MyBatis 的解析器模块,源码对应 parsing 包。如下图:

该模块主要提供两个功能:

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

2.0 XPathParser

org.apache.ibatis.parsing.XPathParser基于Java XPath解析器,用于解析mybatis-config.xml和XXMapper.xml等XML配置文件。
属性如下:

// XPathParser.java

/**
 * XML Document 对象
 */
private final Document document;
/**
 * 是否校验
 */
private boolean validation;
/**
 * XML 实体解析器
 */
private EntityResolver entityResolver;
/**
 * 变量 Properties 对象
 */
private Properties variables;
/**
 * Java XPath 对象
 */
private XPath xpath;
复制代码
  • document属性,XML解析后生成的org.w3c.dom.Document对象
  • entityResolver属性,org.xml.sax.EntityResolver对象,XML实体解析器。默认情况下,对XML进行校验时,会基于XML文档开始位置指定的DTD文件或XSD文件。例如说: 解析mybatis-config.xml配置文件时,会加载http://mybatis.org/dtd/mybatis-3-config.dtd这个DTD文件。但是,如果每个应用启动都从网络加载该DTD文件,势必在弱网络下体验非常差,甚至应用部署在无网络的环境下,还会导致下载不下来,那么就会出现XML校验失败的情况。所以在实际场景下,MyBatis自定义了EntityResolver的实现使用本地DTD文件,从而避免下载网络DTD文件的效果。
  • xpath属性,javax.xml.xpath.XPath对象,用于查询XML中的节点和元素。对 XPath 的使用不了解的同学,可以去《XPath教程》《Java XPath解析器》进行简单学习
  • variables属性,变量Properties对象,用来替换需要动态配置的属性值,详见《Mybatis技术内幕:初始化之properties标签解析》

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

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();
}

/**
 * 创建 Document 对象
 *
 * @param inputSource XML 的 InputSource 对象
 * @return Document 对象
 */
private Document createDocument(InputSource inputSource) {
    // important: this must only be called AFTER common constructor
    try {
        // 1> 创建 DocumentBuilderFactory 对象
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(validation); // 设置是否验证 XML

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

        // 2> 创建 DocumentBuilder 对象
        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 {
            }

        });
        // 3> 解析 XML 文件
        return builder.parse(inputSource);
    } catch (Exception e) {
        throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    }
}
复制代码

代码比较简单,主要是完成XPathParser类相关成员变量的初始化赋值操作

2.2 eval 方法族

XPathParser提供了一系列的#eval*方法,用于获得Boolean、Short、Integer、Long、Float、Double、String、Node类型的元素或节点的值。 虽然方法很多,但是都是基于 #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.3 节点属性值的动态替换

主要依赖evalString(Object root, String expression)方法,真正的替换动作由PropertyParser类完成。PropertyParser下面会讲到

  // XPathParser.java

  public String evalString(String expression) {
    return evalString(document, expression);
  }

  public String evalString(Object root, String expression) {
    String result = (String) evaluate(expression, root, XPathConstants.STRING);
    result = PropertyParser.parse(result, variables);
    return result;
  }
  
  public Integer evalInteger(Object root, String expression) {
    return Integer.valueOf(evalString(root, expression));
  }
复制代码

evalNode(String expression)方法会在后面的配置文件初始化中大量用到,返回org.apache.ibatis.parsing.XNode对象,主要为了动态值的替换

  //XPathParser
  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);
  }
  
  //XNode
  public String evalString(String expression) {
    return xpathParser.evalString(node, expression);
  }
复制代码

2.4 org.apache.ibatis.parsing.XNode

在面对一个Node时,假设我想要把Node的属性集合都以键、值对的形式,放到Properties对象里,同时把Node的body体也通过XPathParser解析出来,并保存起来(一般是Sql语句),方便程序使用,代码可能会是这样的。

private Node node;
private String body;
private Properties attributes;
private XPathParser xpathParser;
复制代码

Mybatis就把上面几个必要属性封装到一个类中,取名叫XNode。

3.0 XMLMapperEntityResolver

org.apache.ibatis.builder.xml.XMLMapperEntityResolver实现 EntityResolver 接口,用于加载本地的mybatis-3-config.dtd和mybatis-3-mapper.dtd这两个 DTD 文件。代码比较简单,代码如下:

// XMLMapperEntityResolver.java

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

    /**
     * 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);
                // 本地 mybatis-config.dtd 文件
                if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
                    return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
                // 本地 mybatis-mapper.dtd 文件
                } 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 {
                // 创建 InputSource 对象
                InputStream in = Resources.getResourceAsStream(path);
                source = new InputSource(in);
                // 设置  publicId、systemId 属性
                source.setPublicId(publicId);
                source.setSystemId(systemId);
            } catch (IOException e) {
                // ignore, null is ok
            }
        }
        return source;
    }
}
复制代码

4.0 PropertyParser

PropertyParser 前面的XPathParser小节中已经出现了,主要用于动态属性的解析,是一个提供静态方法的工具类。部分代码如下:

// PropertyParser.java

public class PropertyParser {
    // private构造器 禁止构造 PropertyParser 对象
    private PropertyParser() {
        // Prevent Instantiation
    }

    public static String parse(String string, Properties variables) {
        // 创建 VariableTokenHandler 对象
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        // 创建 GenericTokenParser 对象
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        // 执行解析
        return parser.parse(string);
    }
}
复制代码

主要代码不多,解析过程主要依赖VariableTokenHandlerGenericTokenParser对象

5.0 TokenHandler

org.apache.ibatis.parsing.TokenHandlerToken处理器接口。代码如下:

// TokenHandler.java
public interface TokenHandler {
    /**
     * 处理 Token
     * @param content Token 字符串
     * @return 处理后的结果
     */
    String handleToken(String content);
}
复制代码

TokenHandler 有四个子类实现,如下图所示:

本文暂时只解读VariableTokenHandler

##5.1 VariableTokenHandler VariableTokenHandler是PropertyParser的内部静态类,变量 Token 处理器。代码如下:

// PropertyParser.java
    private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
  /**
   * @since 3.4.2
   */
  public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";

  /**
   * @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 = ":";
  
private static class VariableTokenHandler implements TokenHandler {
    private final Properties variables;
    //是否开启默认值功能。默认为 {@link #ENABLE_DEFAULT_VALUE false}
    private final boolean enableDefaultValue;
    //默认值的分隔符。默认为 {@link #KEY_DEFAULT_VALUE_SEPARATOR} ,即 ":"
    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);
            }
        }
        // 无 variables ,直接返回
        return "${" + content + "}";
    }
  }
复制代码

代码比较简单,在3.4.2版本以后开始支持默认值功能(默认和spring一致),可以通过mybatis-config.xml配置修改

  • enableDefaultValue 属性,是否开启默认值功能。默认为 ENABLE_DEFAULT_VALUE(false) ,即不开启
  • defaultValueSeparator属性,默认值的分隔符。默认为 KEY_DEFAULT_VALUE_SEPARATOR ,即 ":"
<properties resource="org/mybatis/example/config.properties">
  <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>
  <property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value="?:"/> 
</properties>
复制代码

6.0 GenericTokenParser

GenericTokenParser通用的Token解析器,代码如下:

// GenericTokenParser.java

public class GenericTokenParser {
    /**
     * 开始的 Token 字符串
     */
    private final String openToken;
    /**
     * 结束的 Token 字符串
     */
    private final String closeToken;
    private final TokenHandler handler;

    public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;
        this.closeToken = closeToken;
        this.handler = handler;
    }

    public String parse(String text) {
        if (text == null || text.isEmpty()) {
            return "";
        }
        // 寻找开始的 openToken 的位置
        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; // 匹配到 openToken 和 closeToken 之间的表达式
        // 循环匹配
        while (start > -1) {
            // 转义字符
            if (start > 0 && src[start - 1] == '\\') {
                // 因为 openToken 前面一个位置是 \ 转义字符,所以忽略 \
                // 添加 [offset, start - offset - 1] 和 openToken 的内容,添加到 builder 中
                builder.append(src, offset, start - offset - 1).append(openToken);
                // 修改 offset
                offset = start + openToken.length();
            // 非转义字符
            } else {
                // found open token. let's search close token.
                // 创建/重置 expression 对象
                if (expression == null) {
                    expression = new StringBuilder();
                } else {
                    expression.setLength(0);
                }
                // 添加 offset 和 openToken 之间的内容,添加到 builder 中
                builder.append(src, offset, start - offset);
                // 修改 offset
                offset = start + openToken.length();
                // 寻找结束的 closeToken 的位置
                int end = text.indexOf(closeToken, offset);
                while (end > -1) {
                    // 转义
                    if (end > offset && src[end - 1] == '\\') {
                        // 因为 endToken 前面一个位置是 \ 转义字符,所以忽略 \
                        // 添加 [offset, end - offset - 1] 和 endToken 的内容,添加到 builder 中
                        expression.append(src, offset, end - offset - 1).append(closeToken);
                        // 修改 offset
                        offset = end + closeToken.length();
                        // 继续,寻找结束的 closeToken 的位置
                        end = text.indexOf(closeToken, offset);
                    // 非转义
                    } else {
                        // 添加 [offset, end - offset] 的内容,添加到 builder 中
                        expression.append(src, offset, end - offset);
                        break;
                    }
                }
                // 拼接内容
                if (end == -1) {
                    // closeToken 未找到,直接拼接
                    builder.append(src, start, src.length - start);
                    // 修改 offset
                    offset = src.length;
                } else {
                    // closeToken 找到,将 expression 提交给 handler 处理 ,并将处理结果添加到 builder 中
                    builder.append(handler.handleToken(expression.toString()));
                    // 修改 offset
                    offset = end + closeToken.length();
                }
            }
            // 继续,寻找开始的 openToken 的位置
            start = text.indexOf(openToken, offset);
        }
        // 拼接剩余的部分
        if (offset < src.length) {
            builder.append(src, offset, src.length - offset);
        }
        return builder.toString();
    }
}
复制代码

代码比较冗长,但是就一个#parse(String text)方法,循环(因为可能不只一个 ),解析以 openToken 开始,以closeToken结束的Token,并提交给指定handler 进行处理,大家可以耐心看下这段逻辑,通过源码包中相关的单元测试类去打断点一行一行跟进

参考和推荐如下文章

徐郡明 《MyBatis 技术内幕》 的 「2.1 解析器模块」 小节
祖大俊 《Mybatis3.3.x技术内幕(七):Mybatis初始化之六个工具》

失控的阿甘,乐于分享,记录点滴

猜你喜欢

转载自juejin.im/post/5c7cd4e9e51d453742770e9a