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

栏目: Java · 发布时间: 5年前

内容简介:MyBatis 的解析器模块,源码对应该模块主要提供两个功能:

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

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

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

  • 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.TokenHandler Token处理器接口。代码如下:

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

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

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

本文暂时只解读 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
defaultValueSeparator
<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 进行处理,大家可以耐心看下这段逻辑,通过 源码包中相关的单元测试类 去打断点一行一行跟进


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Pro Git (Second Edition)

Pro Git (Second Edition)

Scott Chacon、Ben Straub / Apress / 2014-11-9 / USD 59.99

Scott Chacon is a cofounder and the CIO of GitHub and is also the maintainer of the Git homepage ( git-scm.com ) . Scott has presented at dozens of conferences around the world on Git, GitHub and the ......一起来看看 《Pro Git (Second Edition)》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具