Mybatis(三):架构及解析器模块详解

1,429 阅读7分钟

在前面的文章中我们了解了Mybatis的基本使用及其执行流程。相信你已经对Mybatis有了简单的了解。

今天我们正式进去源码学习阶段,在这篇文章中,我们首先介绍下Mybatis的结构,之后再学习下解析器模块的内容。

首先自行去github上下载其源码,地址为:github.com/mybatis/myb…

为方便查看,我们将下载的源码导入到idea中,Mybaits的项目结构还是比较清晰明了的,目录如下所示:

image-20210819180007905

1 架构

通过Mybatis的源码我们发现,Mybatis将其模块分成了一个个的包,我们可以很清楚的知道其有哪些模块组成,接下来我们依次简单的介绍下这些模块:

  • annotations 注解 这个没什么可说的,Mybatis中的一些注解定义在了这个包里,例如:@Select @Insert @Update...
  • io 这个模块主要负责资源的加载,在上篇文章中提到的Resources类便是定义在这个模块下。
  • parsing 解析器 XPathParser封装了对xml文件的解析。
  • builder 配置解析模块 解析Mybatis配置文件及映射文件,将里面的信息封装到Configuration对象中。
  • parsing 解析器 这个模块的功能是负责我们定义的配置文件及映射文件的解析。XPathParser
  • binding 这个模块是负责将Mapper中的方法同xml文件中相关的SQL进行关联,MapperRegistryMypperProxyFactoryMypperProxyMapperMethod都是定义在这个模块下。
  • cache 缓存 使用Mybatis的同学应该都知道其有一级和二级缓存,这些功能便定义在这个模块中,有了缓存的存在,能够提高一些查询的性能,但如果在开发中不注意的话也会导致一些问题。
  • cursor 游标
  • datasource 数据源模块
  • executor 执行器模块 负责SQL的执行及结果的映射。
  • transaction 事务
  • logging 日志模块
  • mapping mapper对应的java类
  • plugin 插件
  • reflection 反射
  • scripting 动态sql解析
  • type 类型处理器
  • session sql会话

Mybatis的架构由接口层、核心处理层、基础支持层三部分组成。各层的组成模块如下图所示。

image-20210819212430029

2 解析器模块

解析器模块为Mybatis初始化时加载配置文件和mapper映射文件解析及动态SQL占位符的处理提供了支持。

Mybatis解析器模块的目录如下:

image-20210826201954683

2.1 XPathParser

XPathParserMybatis中的解析器,其对XPath进行了封装,其属性字段如下:

  • Document document Document对象
  • boolean validation 是否校验xml
  • EntiryResolver entityResolver
  • Properties variables 属性
  • XPath xpath Xpath对象

2.1.1 构造方法

XPathParser中提供了很多个构造方法,这里就不进行列举了,在这些方法中没有过多的逻辑,就是创建DocumentXPath对象并设置其他属性字段。

在其构造方法中会调用到commonConstructorcreateDocument两个方法,其源码如下:

private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
    // 设置是否校验XML
    this.validation = validation;
    // EntityResolver用于解析本地DTD或XSD
    this.entityResolver = entityResolver;
    // 配置文件中配置的properties
    this.variables = variables;
    // 创建XPath对象
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();
}
​
private Document createDocument(InputSource inputSource) {
    // important: this must only be called AFTER common constructor
    try {
        // 创建DocumentBuilderFactory对象
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        // 是否校验
        factory.setValidating(validation);
        // 设置其他一些属性
        factory.setNamespaceAware(false);
        factory.setIgnoringComments(true);
        factory.setIgnoringElementContentWhitespace(false);
        factory.setCoalescing(false);
        factory.setExpandEntityReferences(true);
        // 创建DocumentBuilder对象
        DocumentBuilder builder = factory.newDocumentBuilder();
        // 设置EntityResolver
        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 {
                // NOP
            }
        });
        // 创建Document对象并返回
        return builder.parse(inputSource);
    } catch (Exception e) {
        throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    }
}

其实这些代码也没什么可说的,这些都是创建XpathDocument对象时固定的代码。

2.1.2 解析方法

XPathParser中提供了多个eval*方法,用于解析XML获取到不同类型的数据,提供了如下的方法

  • evalString 获取字符串数据
  • evalBoolean 获取布尔数据
  • evalShort 获取short类型数据
  • evalInteger 获取Integer类型数据
  • evalLong 获取Long类型数据
  • evalFloat 获取Float类型数据
  • evalDouble 获取Double类型数据
  • evalNodes 获取XNode列表数据
  • evalNode 获取XNode数据

这些方法最后都调用到evaluate方法,在这个方法中调用了XPath.evaluate方法,其源码如下:

private Object evaluate(String expression, Object root, QName returnType) {
    try {
        // 调用XPath.evaluate
        return xpath.evaluate(expression, root, returnType);
    } catch (Exception e) {
        throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
    }
}

这里我们看下evalStringevalNodeevalNodes方法。

evalString的源码如下如下:

public String evalString(Object root, String expression) {
    // 解析出字符串值
    String result = (String) evaluate(expression, root, XPathConstants.STRING);
    // 处理字符串中的占位符
    result = PropertyParser.parse(result, variables);
    return result;
}

evalNodeevalNodes的源码如下:

// 获取XNode列表对象
public List<XNode> evalNodes(Object root, String expression) {
    // XNode列表
    List<XNode> xnodes = new ArrayList<>();
    // XPath.evaluate方法中获取到的NodeList列表
    NodeList nodes = (NodeList) evaluate(expression, root, XPathConstants.NODESET);
    // 遍历NodeList获取其中的Node对象
    for (int i = 0; i < nodes.getLength(); i++) {
        // 封装成一个XNode对象并添加到列表中
        xnodes.add(new XNode(this, nodes.item(i), variables));
    }
    // 返回XNode列表对象
    return xnodes;
}
// 获取XNode对象
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);
}

通过这两段源码我们可以看到,在XPathParser中解析出字符串后会再调用PropertyParser.parse方法,该方法是用来处理配置文件中的占位符,对org.w3c.dom.Node类进行了封装,接下来我们看看占位符的处理及XNode对象。

2.2 占位符处理

在上面解析源码中,我们看到获取到XML中的字符串后调用PropertyParser.parser方法替换其中的占位符,其逻辑如下:

private static final String KEY_PREFIX = "org.apache.ibatis.parsing.PropertyParser.";
// 通过这个配置设置是否支持默认值
public static final String KEY_ENABLE_DEFAULT_VALUE = KEY_PREFIX + "enable-default-value";
// 通过这个配置自定义分隔符
public static final String KEY_DEFAULT_VALUE_SEPARATOR = KEY_PREFIX + "default-value-separator";
// 默认是否支持默认值为false
private static final String ENABLE_DEFAULT_VALUE = "false";
// 默认分隔符为:
private static final String DEFAULT_VALUE_SEPARATOR = ":";
​
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);
}

PropertyParser.parser方法中创建了VariableTokenHandlerGenericTokenParser对象,最后调用GenericTokenParser.parse方法。

  • GenericTokenParser 负责解析出占位符中的字面值。
  • VariableTokenHandler 负责从Properties中获取占位符所代表的值。

GenericTokenParser.parser方法,是解析出字符串中的占位符,然后调用VariableTokenHandler.tokenHandler方法获取到其对应的值。

VariableTokenHandlerPropertyParser中的一个内部类,该类实现了TokenHandler接口。其源码如下:

private static class VariableTokenHandler implements TokenHandler {
    // 配置文件中的Properties
    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);
    }
​
    // 根据key查找对应的value   如果不存在返回默认值
    private String getPropertyValue(String key, String defaultValue) {
        return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue);
    }
​
    @Override
    public String handleToken(String content) {
        // 判断Properties是否为空
        if (variables != null) {
            String key = content;
            // 是否支持默认值
            if (enableDefaultValue) {
                final int separatorIndex = content.indexOf(defaultValueSeparator);
                String defaultValue = null;
                if (separatorIndex >= 0) {
                    // 分隔符前是Properties中的key
                    key = content.substring(0, separatorIndex);
                    // 分隔符后是默认值
                    defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
                }
                // 默认值存在,先从Properties中查找   没有找到则返回默认值
                if (defaultValue != null) {
                    return variables.getProperty(key, defaultValue);
                }
            }
            // 如果存在这个配置则返回
            if (variables.containsKey(key)) {
                return variables.getProperty(key);
            }
        }
        return "${" + content + "}";
    }
}

TokenHandler还有其他的三个实现类,在之后的内容中我们会进行介绍,其实现类如下

image-20210828160048333

2.3 XNode

XNodeMybatis中用来表示节点的数据,其对org.w3c.dom.Node进行了封装,该类的属性字段如下:

  • Node node
  • String name
  • String body
  • Properties attributes
  • Properties variabes
  • XPathParser xpathParser

构造方法如下:

public XNode(XPathParser xpathParser, Node node, Properties variables) {
    this.xpathParser = xpathParser;
    this.node = node;
    // 获取Node名称   标签类型
    this.name = node.getNodeName();
    // 传递过来的Properties
    this.variables = variables;
    // 解析节点中的属性字段   存储到一个Properties对象
    this.attributes = parseAttributes(node);
    this.body = parseBody(node);
}

parseAttributes是解析出org.w3c.dom.NOde中的属性字段存储到一个Properties对象中,其源码如下:

private Properties parseAttributes(Node n) {
    Properties attributes = new Properties();
    // 获取NamedNodeMap
    NamedNodeMap attributeNodes = n.getAttributes();
    if (attributeNodes != null) {
        // 遍历NamedNodeMap
        for (int i = 0; i < attributeNodes.getLength(); i++) {
            Node attribute = attributeNodes.item(i);
            // 解析配置的value值
            String value = PropertyParser.parse(attribute.getNodeValue(), variables);
            // 存储到Properties中
            attributes.put(attribute.getNodeName(), value);
        }
    }
    return attributes;
}

parseBody方法如下:

private String parseBody(Node node) {
    // 获取文本数据
    String data = getBodyData(node);
    if (data == null) {
        // 如果不是文本节点   获取子节点
        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;
}
​
private String getBodyData(Node child) {
    // 处理文本节点
    if (child.getNodeType() == Node.CDATA_SECTION_NODE
        || child.getNodeType() == Node.TEXT_NODE) {
        // 获取内容
        String data = ((CharacterData) child).getData();
        // 处理占位符
        data = PropertyParser.parse(data, variables);
        return data;
    }
    return null;
}

XNode中还提供了一些geteval方法,这些逻辑也比较简单,这里就不一一粘贴了,大家自行查看源码吧。

3 总结

至此今天的内容就结束了,在今天的文章中我们介绍了Mybatis的架构及一个基础模块-解析器。通过对解析器的分析,我们可以看到其作用如下:

  • 封装DocumentXPathXML配置文件的解析提供了支持。
  • 处理配置文件中的占位符。

该模块所包含的类如下:

  • XPathParser 解析器,里面维护了DocumentXpath对象,提供了一系列的eval方法用于配置文件解析。
  • XNode 节点对象,维护了org.w3c.Node对象。
  • PropertyParser 用来处理占位符的入口
  • PropertyParser.VariableTokenHandler 继承自TokenHandler,用于从Properties中获取占位符对应的值。
  • GenericTokenParser 获取字符串中的占位符。

这个模块的逻辑不复杂,大家多跟着源码看看相信都能看明白的。

如果感觉对您有帮助,欢迎关注下公众号,您的关注是我更新的最大动力~

qrcode_for_gh_8febd60b14c9_258.jpg