mybatis源码学习-3-解析器模块

1. 目录结构

  1. XNode类

    • 作用:XNode 类表示XML文档中的一个节点(Element或Node),它用于封装XML节点的信息,例如标签名、属性和子节点等。
    • 使用场景:MyBatis使用 XNode 来解析XML配置文件中的各种元素,如<select>, <update>, <insert>, <delete>等,并提供了一种方便的方式来访问这些元素的属性和子元素。
  2. PropertyParser类

    • 作用:PropertyParser 类是MyBatis用于解析属性表达式的工具类。属性表达式是在XML配置文件中引用JavaBean属性的方式,通常用于动态SQL。
    • 使用场景:MyBatis在解析动态SQL语句或属性表达式时,会使用 PropertyParser 来解析${}#{}中的属性表达式,并将其替换为实际的属性值或SQL参数。
  3. GenericTokenParser类

    • 作用:GenericTokenParser 类是MyBatis用于解析通用占位符(例如${}#{})的工具类。它允许你指定一个TokenHandler接口的实现,用于处理占位符内的内容。
    • 使用场景:MyBatis在解析SQL语句中的占位符时,会使用 GenericTokenParser 来查找和替换占位符内的内容,然后将处理后的SQL语句返回。
  4. ParsingException类

    • 作用:ParsingException 类是MyBatis中的自定义异常类,用于表示解析过程中的异常情况。它通常用于捕获和处理解析XML配置文件或SQL语句时可能发生的错误。
    • 使用场景:当MyBatis在解析配置文件或SQL语句时遇到不合法的语法或结构时,会抛出 ParsingException 异常,以便开发者识别和处理问题。
  5. TokenHandler接口

    • 作用:TokenHandler 接口是一个回调接口,定义了如何处理占位符内的内容。它用于与 GenericTokenParser 类一起工作,允许开发者自定义处理占位符内部内容的逻辑。
    • 使用场景:当MyBatis使用 GenericTokenParser 解析占位符时,它会调用 TokenHandler 接口的实现来处理占位符内的内容。开发者可以实现自定义的 TokenHandler 来定义处理逻辑,例如从配置文件或参数中获取属性值。
  6. XPathParser类

    • 作用:XPathParser 类是MyBatis中的XML解析工具,用于解析XML配置文件和映射文件。它提供了一种方便的方式来处理XML文档,包括节点遍历、属性获取、XPath表达式解析等。
    • 使用场景:XPathParser 类通常用于加载和解析MyBatis的XML配置文件,例如mybatis-config.xml和映射文件。它允许MyBatis以一种结构化的方式访问和操作XML配置文件中的内容。

2. XPathParser类

  1. 参数

XPathParser 类通常用于加载和解析MyBatis的XML配置文件,例如mybatis-config.xml和映射文件。它允许MyBatis以一种结构化的方式访问和操作XML配置文件中的内容。

java 复制代码
public class XPathParser {
    private final Document document;
    private boolean validation;
    private EntityResolver entityResolver;
    private Properties variables;
    private XPath xpath;
    //省略
}
  • document属性,XML 被解析后,生成的org.w3c.dom.Document对象。

  • validation 属性,是否校验 XML 。一般情况下,值为 true

  • 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](# 3. XMLMapperEntityResolver) 的实现,达到使用本地DTD 文件,从而避免下载网络DTD 文件的效果.

  • xpath 属性,javax.xml.xpath.XPath 对象,用于查询 XML 中的节点和元素。如果对 XPath 的使用不了解的胖友,请先跳转 《Java XPath 解析器 - 解析 XML 文档》 中,进行简单学习,灰常简单。

  • variables 属性,变量 Properties 对象,用来替换需要动态配置的属性值。例如:

    xml 复制代码
    <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 配置文件中配置,也可以使用 MyBatis <property /> 标签进行配置。例如:

      xml 复制代码
      <properties resource="org/mybatis/example/config.properties">
        <property name="username" value="dev_user"/>
        <property name="password" value="F2Fa3!33TYyg"/>
      </properties>
      • 这里配置的 usernamepassword 属性,就可以替换上面的 ${username}${password} 这两个动态属性。
      • 具体如何实现的,可以看下面的 PropertyParser#parse(String string, Properties variables) 方法。
  1. 构造方法

解释完属性后,我们可以看见后面有一长串的构造方法,挑选其中一个

java 复制代码
/**
 * 构造 XPathParser 对象
 *
 * @param xml XML 文件地址
 * @param validation 是否校验 XML
 * @param variables 变量 Properties 对象
 * @param entityResolver XML 实体解析器
 */
public XPathParser(String xml, boolean validation, Properties variables, EntityResolver entityResolver) {
    //1. 公共构造方法
    commonConstructor(validation, variables, entityResolver);
    //2. 创建document对象,将xml文件解析成一个document对象
    this.document = createDocument(new InputSource(new StringReader(xml)));
}
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();
}
Java 复制代码
/**
 * 创建 Document 对象,这里就是简单的调用别人提供的java xml api,就像我们调用netty提供的api那样
 *
 * @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();
        // 设置是否验证 XML
        factory.setValidating(validation); 

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

至此,我们可以通过构造方法创建一个对象对xml文件进行解析

例如,在org.apache.ibatis.parsing.XPathParserTest#shouldTestXPathParserMethods方法中,我们可以通过测试代码解析nodelet_test.xml中的元素,通过构造方法后获得的XPathParser对象已经对xml文件进行解析,如下

  1. eval元素

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

java 复制代码
public String evalString(Object root, String expression) {
    // 1. 获得指定元素或节点的值
    String result = (String) evaluate(expression, root, XPathConstants.STRING);
    // 2. 基于 variables 替换动态值,如果 result 为动态值,还记得variables是什么吗?Properties 对象,用来替换需要动态配置的属性值。
    result = PropertyParser.parse(result, variables);
    return result;
}
Java 复制代码
/**
 * 获得指定元素或节点的值,例如在上例中箭头所指表达式为"/employee/@id" , 节点为"[#document:null]" , 返回类型为String,那么返回的结果就是result = "${id_var}"
 *
 * @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);
    }
}

在这里没有使用动态替换,因此即便result = "${id_var}",在经过PropertyParser#parse(String string, Properties variables)进行动态解析后,结果依然保持不变.

除了上面的vealString之类的方法,还有一个evalNode()方法,用户获得Node类型的节点的值,代码如下

java 复制代码
//返回 Node 数组
public List<XNode> evalNodes(String expression) { 
    return evalNodes(document, expression);
}

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

//返回 Node 对象
public XNode evalNode(String expression) { 
    return evalNode(document, expression);
}

public XNode evalNode(Object root, String expression) {
    //1. 获得 Node 对象
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
        return null;
    }
    //2. 封装成 XNode 对象
    return new XNode(this, node, variables);
}

1处,返回结果有 Node 对象数组 两种情况,根据方法参数 expression 需要获取的节点不同。

2处,会将结果Node封装为org.apache.ibatis.parsing.XNode对象,主要为了动态值的替换 ,例如测试方法org.apache.ibatis.parsing.XPathParserTest#shouldTestXPathParserMethods调用parser.evalNode("/employee/@id").toString().trim()这条语句时,会产生下面的一个XNode对象,也就是获取xml文件中id节点的值了

3. XMLMapperEntityResolver

MyBatis 自定义 EntityResolver 实现类,用于加载本地的 mybatis-3-config.dtdmybatis-3-mapper.dtd 这两个 DTD 文件。

简单介绍一下DTD文件的作用,菜🐤教程

  • DTD文件用于验证和定义相应的XML文件的结构和规则,以确保XML文件的正确性和一致性。解析器会根据publicIdsystemId来决定使用哪个DTD文件进行验证。
    • mybatis-config.dtd 定义了MyBatis配置文件中允许的元素和属性,包括数据源配置、事务管理器配置、映射器配置等。
    • mybatis-mapper.dtd 定义了映射文件中允许的元素和属性,包括SQL语句、参数映射、结果映射等。
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";

    /**
     * 本地 mybatis-config.dtd 文件
     */
    private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
    /**
     * 本地 mybatis-mapper.dtd 文件
     */
    private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";

    /**
     * 将公共DTD转换为本地DTD
     *
     * @param publicId publicId 是DTD声明中的 PUBLIC 标识符。它用于指定一个公共标准的标识符,通常用于引用在公共资源中定义的DTD。
     * @param systemId 是DTD声明中的 SYSTEM 标识符。它用于指定一个系统标准的标识符,通常用于引用本地文件系统或远程资源中的DTD。
     * @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. GenericTokenParser

在初识mybatis源码时,我会产生这样的疑惑,这里的Token指的是什么,与SpringSecurity中的Token的区别是什么?

在GPT之后😂,给出如下解释

  • 在 MyBatis 中,"token" 通常指的是通用标记解析器(GenericTokenParser)中的占位符标记,用于解析 SQL 语句或其他文本中的占位符,例如 ${variable}#{parameter}。这些占位符标记允许你在 SQL 语句中插入变量或参数值,以动态生成 SQL 查询语句。通用标记解析器会将这些标记替换为实际的值,从而生成最终的 SQL 语句。MyBatis 使用这种机制来支持参数化查询和动态 SQL。
  • 在 Spring Security 中,"token" 通常指的是认证令牌(Authentication Token)或访问令牌(Access Token)的概念。这些令牌用于身份验证和授权,用于验证用户的身份并控制用户对资源的访问。Spring Security 使用这种机制来保护应用程序的安全性,实现认证和授权功能。
Java 复制代码
public class GenericTokenParser {

    // 开放令牌,例如 "${"
    private final String openToken;
    // 关闭令牌,例如 "}"
    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 "";
        }
        // 搜索开放令牌的位置
        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] == '\\') {
                // 添加转义的开放令牌
                builder.append(src, offset, start - offset - 1).append(openToken);
                offset = start + openToken.length();
            } else {
                // 找到开放令牌,开始搜索关闭令牌
                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) {
                    // 如果没有找到关闭令牌,添加剩余的文本
                    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();
    }
}

测试案例org.apache.ibatis.parsing.GenericTokenParserTest

像上面两幅图那样,就可以把我们xml文件中诸如#{id}通过反射去转换为具体的id内容了,再去看看带有转义字符的

5. PropertyParser

Java 复制代码
public class PropertyParser {

    private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
    /**
     * 特殊属性键,指示是否在占位符上启用默认值。
     * <p>
     * 默认值为false (表示禁用占位符上的默认值)如果指定true ,则可以在占位符上指定键和默认值(例如${db.username:postgres} )。
     * </p>
     *
     * @since 3.4.2
     */
    public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";

    /**
     * 特殊属性键,用于指定占位符上键和默认值的分隔符。
     * <p>
     * 默认分隔符是":" 。
     * </p>
     *
     * @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 PropertyParser() {
        // Prevent Instantiation
    }
	//1.
    public static String parse(String string, Properties variables) {
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        return parser.parse(string);
    }
	//2.
    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 + "}";
        }
	}

}

先看看1处的代码,它主要是

  • 创建一个VariableTokenHandler对象

  • 创建一个[GenericTokenParser](# 4. GenericTokenParser)

  • 调用org.apache.ibatis.parsing.GenericTokenParser#parse方法对对象进行动态替换,从而完成动态映射的功能

再来看看2处的代码,它是org.apache.ibatis.parsing.TokenHandler的实现类,主要用于定义一些动态字符串的映射规则,例如将id->123456.

测试代码org.apache.ibatis.parsing.PropertyParserTest

写在后面,这里会有很多借鉴的内容,有以下三个原因

  1. 本博客只是作为本人学习记录并用以分享,并不是专业的技术型博客
  2. 笔者是位刚刚开始尝试阅读源码的人,对源码的阅读流程乃至整体架构并不熟悉,观看他人博客可以帮助我快速入门
  3. 如果只是笔者自己观看,难免会有很多弄不懂乃至理解错误的地方,观看他人的体会能有效改善这个问题
相关推荐
Caarlossss19 分钟前
mybatis
java·数据库·tomcat·maven·mybatis·mybatis-spring
YJlio22 分钟前
PsPing 学习笔记(14.1):ICMP Ping 进阶——替代系统 ping 的正确姿势
windows·笔记·学习
BMS小旭35 分钟前
CubeMx-GPIO学习
单片机·学习
hunjinYang1 小时前
使用嵌入式 Tomcat 创建Java Web应用程序
java·tomcat
liuc03171 小时前
AI下调用redis并调用deepseek
数据库·redis·mybatis
雨中飘荡的记忆3 小时前
MyBatis缓存模块详解
mybatis
東雪木4 小时前
编程算法学习——数组与排序算法
学习·算法
代码游侠4 小时前
复习—sqlite基础
linux·网络·数据库·学习·sqlite
坚持学习前端日记5 小时前
2025年的个人和学习年度总结以及未来期望
java·学习·程序人生·职场和发展·创业创新
汉堡包0015 小时前
【面试总结】--安服实习岗(2)
学习·安全·web安全·网络安全·面试