2.2.将 Mybatis 全局配置文件对应的 DOM 转换为 XNODE 对象

优质
小牛编辑
145浏览
2023-12-01

在上文中我们完成了XmlConfigBuilder对象的构建工作,准备好了解析XML文件的基础环境。

所以接下来就是调用XmlConfigBuilder暴露的parse()方法来完成mybatis配置文件的解析工作了。

public Configuration parse() {
    if (parsed) {
        // 第二次调用XMLConfigBuilder
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    // 重置XMLConfigBuilder的解析标志,防止重复解析
    parsed = true;
    // 此处开始进行Mybatis配置文件的解析流程
    // 解析 configuration 配置文件,读取【configuration】节点下的内容
    parseConfiguration(parser.evalNode("/configuration"));
    // 返回Mybatis的配置实例
    return configuration;
}

在没有解析过的前提下,mybatis会调用parseConfiguration(XNode root)方法来完成Configuration对象的构建操作。

parseConfiguration(XNode root)方法的入参是一个XNode类型的对象实例,该对象的产生是通过调用我们上文创建的XPathParserXNode evalNode(String expression)方法来完成的。

parseConfiguration(parser.evalNode("/configuration"));

evalNode方法接收的是一个XPath地址表达式,字符串"/configuration"中的/表示从根节点获取元素,所以"/configuration"则表示获取配置文件的根元素configuration.

  • configuration是Mybaits主配置文件的根节点,我们通常这样使用:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
    
    <configuration>
    ...
    </configuration>
    
/**
  * 根据表达式解析Document对象获取对应的节点
  *
  * @param expression Xpath地址表达式
  * @return XNode
  */
 public XNode evalNode(String expression) {
     // 从document对象解析出指定的节点
     return evalNode(document, expression);
 }

evalNode方法中,将上文获取到的XPathParser的类属性document作为参数,传递给他的重载方法:

/**
 *  根据表达式获取节点
 * @param root 根节点
 * @param expression xpath表达式
 * @return XNode
 */
public XNode evalNode(Object root, String expression) {
    // 获取DOM节点
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
        return null;
    }
    // 包装成XNode节点
    return new XNode(this, node, variables);
}

在重载的evalNode方法内部,将获取表达式对应的DOM节点的工作委托给了evaluate方法来完成,如果解析出了对应的DOM节点,将会以XPathParser对象本身和解析出来的DOM节点对象,以及用户传入的变量作为参数构造出一个XNode对象实例返回给方法的调用方。

被委托的evaluate方法利用XPath解析器来完成将表达式解析成指定对象的工作。

private Object evaluate(String expression, Object root, QName returnType) {
     try {
         // 在指定的上下文中计算XPath表达式,并将结果作为指定的类型返回。
         return xpath.evaluate(expression, root, returnType);
     } catch (Exception e) {
         throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
     }
 }

XNode类中定义了六个常量,这六个常量的初始化赋值操作都是在XNode节点的构造方法中完成的。

/**
 * XNode
 *
 * @param xpathParser XPath解析器
 * @param node        被包装的节点
 * @param variables   用户传入的变量
 */
public XNode(XPathParser xpathParser, Node node, Properties variables) {
    // 初始化节点对应的解析器
    this.xpathParser = xpathParser;
    // 初始化DOM 节点
    this.node = node;
    // 初始化节点名称
    this.name = node.getNodeName();
    // 初始化用户定义的变量
    this.variables = variables;
    // 解析节点中的属性配置
    this.attributes = parseAttributes(node);
    // 解析节点包含的内容
    this.body = parseBody(node);
}

其中attributesbody属性的取值操作需要分别通过parseAttributesparseBody方法来完成。

/**
 * 解析节点中的属性值
 *
 * @param n 节点
 * @return 属性集合
 */
private Properties parseAttributes(Node n) {
    // 定义 Properties对象
    Properties attributes = new Properties();
    // 获取属性节点
    NamedNodeMap attributeNodes = n.getAttributes();
    if (attributeNodes != null) {
        for (int i = 0; i < attributeNodes.getLength(); i++) {
            Node attribute = attributeNodes.item(i);
            // 针对每个属性的值进行一次占位符解析替换的操作
            String value = PropertyParser.parse(attribute.getNodeValue(), variables);
            // 保存
            attributes.put(attribute.getNodeName(), value);
        }
    }
    return attributes;
}

/**
 * 解析节点中的内容
 *
 * @param node 节点
 * @return 节点中内容
 */
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;
}

/**
 * 获取CDATA节点和TEXT节点中的内容
 *
 * @param child 节点
 */
private String getBodyData(Node child) {
    if (child.getNodeType() == Node.CDATA_SECTION_NODE
            || child.getNodeType() == Node.TEXT_NODE) {
        // 获取CDATA节点和TEXT节点中的内容
        String data = ((CharacterData) child).getData();
        // 执行占位符解析操作
        data = PropertyParser.parse(data, variables);
        return data;
    }
    return null;
}

这两个方法比较简单,唯一需要注意的就是在处理属性值和body内容的时候,调用了PropertyParser.parse(String string, Properties variables)方法对属性值和body体中的占位符进行了替换操作。

  • 关于PropertyParser

    PropertyParser在mybatis中担任着一个替换变量占位符的角色。主要作用就是将${变量名}类型的占位符替换成对应的实际值。

    PropertyParser只对外暴露了一个String parse(String string, Properties variables)方法,该方法的作用是替换指定的占位符为变量上下文中对应的值,该方法有两个入参:一个是String类型的可能包含了占位符的文本内容,一个是Properties类型的变量上下文。

    /**
     * 替换占位符
     * @param string    文本内容
     * @param variables 变量上下文
     */
    public static String parse(String string, Properties variables) {
        // 占位符变量处理器
        VariableTokenHandler handler = new VariableTokenHandler(variables);
        // 占位符解析器
        GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
        // 返回闭合标签内的内容
        return parser.parse(string);
    }
    

    parse方法中涉及到了两个类,VariableTokenHandlerGenericTokenParser.

    VariableTokenHandlerTokenHandler接口的一个实现类,TokenHandler定义了一个String handleToken(String content);方法,该方法主要用来对客户端传入的内容进行一些额外的处理。

    TokenHandler也是策略模式的一种体现,它定义了文本统一处理的接口,其子类负责提供不同的处理策略。

    具体到VariableTokenHandler中,该方法的作用就是替换传入的文本内容中的占位符。

    VariableTokenHandler的构造方法需要一个Properties类型的variables参数,该参数中定义的变量将用于替换占位符。

    VariableTokenHandler的占位符解析操作允许用户以${key:defaultValue}的形式为指定的key提供默认值,即如果变量上下文中没有匹配key的变量值,则以defaultValue作为key的值。

    占位符中取默认值时使用分隔符默认是:,如果需要修改,可以通过在variables参数中添加org.apache.ibatis.parsing.PropertyParser.default-value-separator="自定义分隔符"进行配置。

    在占位符中使用默认值的操作默认是关闭的,如果需要开启,可以在variables参数中添加org.apache.ibatis.parsing.PropertyParser.enable-default-value=true进行配置。

    下面是VariableTokenHandler的构造方法:

    private VariableTokenHandler(Properties variables) {
              this.variables = variables;
              // 是否允许使用默认值比如${key:aaaa}
              this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE));
              // 默认值分隔符
              this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR);
          }
    

    GenericTokenParser是一个通用的占位符解析器,他的构造方法有三个入参,分别是占位符的开始标签,结束标签,以及针对占位符内容的处理策略对象。

    /**
      * GenericTokenParser
      * @param openToken 开始标签
      * @param closeToken 结束标签
      * @param handler 内容处理器
      */
     public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
         this.openToken = openToken;
         this.closeToken = closeToken;
         this.handler = handler;
     }
    

    GenericTokenParser对外提供了一个parse(String text)方法,该方法将会寻找匹配占位符的内容并调用TokenHandler对其进行处理,如果未匹配到占位符对应的内容,则返回始原内容。

在完成XNode对象的创建工作之后,就可以使用该对象调用parseConfiguration(XNode root)方法来进行真正的配置文件解析操作了:

parseConfiguration(parser.evalNode("/configuration"));

在解析配置文件之前,我们先简单了解一下mybatis全局配置文件的DTD定义:

<!ELEMENT configuration (
properties?
, settings?
, typeAliases?
, typeHandlers?
, objectFactory?
, objectWrapperFactory?
, reflectorFactory?
, plugins?
, environments?
, databaseIdProvider?
, mappers?
)>

参考上面的DTD文件,我们可以发现configuration节点下允许出现11种类型的子节点,这些节点都是可选的,这就意味着Mybatis的全局配置文件可以不配置任何子节点(参考单元测试:org.apache.ibatis.builder.XmlConfigBuilderTest#shouldSuccessfullyLoadMinimalXMLConfigFile)。

回头继续看方法parseConfiguration,在该方法中,对应着configuration的子节点,解析配置的工作被拆分成了多个子方法来完成:

/**
 * 解析Configuration节点
 */
private void parseConfiguration(XNode root) {
    try {
        //issue #117 read properties first
        // 加载资源配置文件,并覆盖对应的属性[properties节点]
        propertiesElement(root.evalNode("properties"));
        // 将settings标签内的内容转换为Property,并校验。
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        // 根据settings的配置确定访问资源文件的方式
        loadCustomVfs(settings);
        // 根据settings的配置确定日志处理的方式
        loadCustomLogImpl(settings);
        // 别名解析
        typeAliasesElement(root.evalNode("typeAliases"));
        // 插件配置
        pluginElement(root.evalNode("plugins"));
        // 配置对象创建工厂
        objectFactoryElement(root.evalNode("objectFactory"));
        // 配置对象包装工厂
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        // 配置反射工厂
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        // 通过settings配置初始化全局配置
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        // 加载多环境源配置,寻找当前环境(默认为default)对应的事务管理器和数据源
        environmentsElement(root.evalNode("environments"));
        // 数据库类型标志创建类,Mybatis会加载不带databaseId以及当前数据库的databaseId属性的所有语句,有databaseId的
        // 语句优先级大于没有databaseId的语句
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        // 注册类型转换器
        typeHandlerElement(root.evalNode("typeHandlers"));
        // !!注册解析Dao对应的MapperXml文件
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

在上面代码中多次出现了root.evalNode(String)方法,该方法的作用是:根据传入的表达式,获取到对应的XNode节点对象。

public XNode evalNode(String expression) {
      return xpathParser.evalNode(node, expression);
  }

具体的实现实际上是委托给了xpathParser解析器的XNode evalNode(Object root, String expression)方法来完成。

/**
 *  根据表达式获取节点
 * @param root 根节点
 * @param expression xpath表达式
 * @return XNode
 */
public XNode evalNode(Object root, String expression) {
    // 获取DOM节点
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
        return null;
    }
    // 包装成XNode节点
    return new XNode(this, node, variables);
}

该方法我们在上文中已经看过了,这里不再赘述,现在以propertiesElement(root.evalNode("properties"));为例,解释一下xpath表达式"properties"的作用: 该表达式表示获取properties元素及其所有子元素。