2023-03-13  阅读(5)
原文作者:青花鱼罐头 原文地址:https://blog.csdn.net/qq_32782279/article/details/107826317

1.概述

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

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

源码对应 parsing 包。

2. XPathParser

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

    /**
    XML Document 对象
    */
      private final Document document;
      /**
      是否检验
      */
      private boolean validation;
        /**
      XML 实体解析器
      */
      private EntityResolver entityResolver;
        /**
    变量 Properties 对象
      */
      private Properties variables;
      /**
     * Java XPath 对象
     */
      private XPath xpath;
2.1 构造方法
    // 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 对象。代码如下:

    // XPathParser.java
    
    /**
     * 创建 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);
        }
    }
2.2 eval 方法族

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

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

<1> 处,调用 #evaluate(String expression, Object root, QName returnType) 方法,获得值。其中,returnType 方法传入的是 XPathConstants.STRING ,表示返回的值是 String 类型。
<2> 处,调用 PropertyParser#parse(String string, Properties variables) 方法,基于 variables 替换动态值,如果 result 为动态值。这就是 MyBatis 如何替换掉 XML 中的动态值实现的方式。

3. XMLMapperEntityResolver

org.apache.ibatis.builder.xml.XMLMapperEntityResolver ,实现 EntityResolver 接口,MyBatis 自定义 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";
    
        /**
         * 本地 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";
    
        /**
         * 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. GenericTokenParser

org.apache.ibatis.parsing.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 "";
            }
            // search open token
            // 寻找开始的 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] == '\\') {
                    // this open token is escaped. remove the backslash and continue.
                    // 因为 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] == '\\') {
                            // this close token is escaped. remove the backslash and continue.
                            // 因为 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) {
                        // close token was not found.
                        // closeToken 未找到,直接拼接
                        builder.append(src, start, src.length - start);
                        // 修改 offset
                        offset = src.length;
                    } else {
                        // <x> 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();
        }
    
    }

5. PropertyParser

org.apache.ibatis.parsing.PropertyParser ,动态属性解析器。代码如下:

    // PropertyParser.java
    
    public class PropertyParser {
    
        // ... 省略部分无关的
    
        private PropertyParser() { // <1>
            // Prevent Instantiation
        }
    
        public static String parse(String string, Properties variables) { // <2>
            // <2.1> 创建 VariableTokenHandler 对象
            VariableTokenHandler handler = new VariableTokenHandler(variables);
            // <2.2> 创建 GenericTokenParser 对象
            GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
            // <2.3> 执行解析
            return parser.parse(string);
        }
        
    }

6. TokenHandler

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

    // TokenHandler.java
    
    public interface TokenHandler {
    
        /**
         * 处理 Token
         *
         * @param content Token 字符串
         * @return 处理后的结果
         */
        String handleToken(String content);
    
    }
6.1.1 构造方法
    // PropertyParser.java
    
    private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
    /**
     * The special property key that indicate whether enable a default value on placeholder.
     * <p>
     *   The default value is {@code false} (indicate disable a default value on placeholder)
     *   If you specify the {@code true}, you can specify key and default value on placeholder (e.g. {@code ${db.username:postgres}}).
     * </p>
     * @since 3.4.2
     */
    public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";
    
    /**
     * The special property key that specify a separator for key and default value on placeholder.
     * <p>
     *   The default separator is {@code ":"}.
     * </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 = ":";
    
    // VariableTokenHandler 类里
    
    /**
     * 变量 Properties 对象
     */
    private final Properties variables;
    /**
     * 是否开启默认值功能。默认为 {@link #ENABLE_DEFAULT_VALUE}
     */
    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);
    }
6.1.2 handleToken
    // VariableTokenHandler 类里
    
    @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 + "}";
    }

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文