[MyBatis源码详解 - 解析器模块 - 组件一] XNode

一、功能

  常用的解析XML的方式有三种,详见:Java解析xml的三种方式,其中最常用的是DOM,在DOM中每个XML的节点都是一个Node(org.w3c.dom.Node),Node通常跟XPath配合使用,提供了解析节点元素名属性名属性值节点文本内容嵌套节点等功能,要想熟练地使用这些功能,需要对XPath表达式解析的语法非常熟悉。
  XNode封装了Node,提供了常见的解析一个Node节点需要的功能和方法。

二、属性

  XNode的属性组成如图:

XNode_field.jpg

  node:被包装的org.w3c.dom.Node对象
  name:节点名
  body:节点内容
  attributes:节点属性集合
  variables:mybatis-config.xml配置文件中节点下引入或定义的键值对
  xpathParser:封装了XPath解析器,XNode对象由XPathParser对象生成,并提供了解析XPath表达式的功能

eg:



 
    
        
        
        
        
    

     
        
    

  以节点为例子,生成一个对应的XNode节点后,name值为"typeAlias",由于没有文本内容也没有子节点所以body为空,attributes值为{alias=role, type=com.learn.ssm.chapter4.pojo.Role}的一个Properties对象,variables{database.driver=com.mysql.jdbc.Driver, database.url=jdbc:mysql://localhost:3306/ssm?useSSL=false, database.username=root, database.password=root},xpathParser是构造函数传进来的一个参数,只需要知道它提供了解析XPath表达式的功能即可。

三、构造函数
    public XNode(XPathParser xpathParser, Node node, Properties variables) {
        this.xpathParser = xpathParser;
        this.node = node;
        this.name = node.getNodeName();  // name是调用Node的方法获取
        this.variables = variables;      // variables是外部传入的构造参数
        this.body = parseBody(node);
        this.attributes = parseAttributes(node);
    }
四、方法

  解析节点内容调用的是parseBody方法,先从该方法入手分析。

1、private String parseBody(Node node)
    // 提供解析节点文本内容的功能
    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;
    }

【功能】提供解析节点文本内容的功能
【源码分析】getBodyData方法可以获取节点文本内容,只有文本类型的节点才能返回字符串值,否则返回空,如果一个节点形如aaa,则返回"aaa",后面parseBody直接返回即可;如果非文本节点,则进入下面的if分支,分支中先去获取该节点下的所有子节点,并且逐个获取子节点的文本内容(假如子节点时文本节点的话),一旦获取到第一个文本子节点的文本内容,并且内容非空,则跳出循环返回,如下所示,A会在解析第二个子节点C时拿到其文本内容cbody跳出循环返回。


  
  cbody
  dbody


2、private String getBodyData(Node child)
    private String getBodyData(Node child) {
        /**
         *  只处理文本类型的节点: Node.CDATA_SECTION_NODE、Node.TEXT_NODE
         *                 Node.COMMENT_NODE(不需要判断,因为XPathParser加载XML时已经设置了忽略注释)
         */
        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;
    }

【功能】获取文本节点文本内容
【源码分析】代码很简单,PropertyParser.parse(data, variables);只是为了解析带占位符的变量的值,比如解析到的文本内容为${database.driver},则data会被进一步解析成com.mysql.jdbc.Driver,此处暂时不深究PropertyParser是怎么实现的。



  解析节点属性调用的是parseAttributes方法,接下来分析parseAttributes方法。

3、private Properties parseAttributes(Node node)
    // 解析节点属性键值对,并将其放入Properties对象中,对外提供根据属性名差属性值功能时用到
    private Properties parseAttributes(Node node) {
        Properties attributes = new Properties();
        NamedNodeMap attributeNodes = node.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;
    }

【功能】获取所有节点属性
【源码分析】先通过Node.getAttributes()获取到了包含所有节点属性的NamedNodeMap对象,接着遍历该对象,拿到属性名和属性值,放入要返回的Properties对象中。


4、public XNode getParent()
    // 获取当前节点的父节点
    public XNode getParent() {
        Node parent = node.getParentNode();
        if (parent == null || !(parent instanceof Element)) {
            return null;
        } else {
            return new XNode(xpathParser, parent, variables);
        }
    }

【功能】获取当前节点的父节点并包装为XNode
【源码分析】如果是顶层节点或非元素节点,则返回空,否则创建。


5、public String getPath()
    // 获取节点路径
    public String getPath() {
        StringBuilder builder = new StringBuilder();
        Node current = node;
        while (current != null && current instanceof Element) {
            if (current != node) {
                builder.insert(0, "/");
            }
            builder.insert(0, current.getNodeName());
            current = current.getParentNode();
        }
        return builder.toString();
    }

【功能】获取节点路径
【源码分析】获取从当前节点到顶层节点的路径,在while循环中每次current都会获取其父节点,一层层向上追溯,直到顶层节点,比如,对C节点来说节点路径就是A/B/C


6、public String getValueBasedIdentifier()
    // 获取节点值的识别码,优先级: id > value > property
    public String getValueBasedIdentifier() {
        StringBuilder builder = new StringBuilder();
        XNode current = this;
        while (current != null) {
            if (current != this) {
                builder.insert(0, "_");
            }
            String value = current.getStringAttribute("id", 
                            current.getStringAttribute("value", 
                             current.getStringAttribute("property", null)));
            if (value != null) {
                value = value.replace('.', '_');
                builder.insert(0, "]");
                builder.insert(0, value);
                builder.insert(0, "[");
            }
            builder.insert(0, current.getName());
            current = current.getParent();
        }
        return builder.toString();
    }

【功能】获取节点值的识别码,优先级: id > value > property
【源码分析】获取一个能唯一标识节点的字符串,如下面的C节点,返回的唯一标识字符串为A_B[bid]_C[cid],类似于获取节点路径,也会一层层追溯到顶层节点。


  
    
  



6、eval*()系列方法
    public String evalString(String expression) {
        return xpathParser.evalString(node, expression);
    }
    
    public Boolean evalBoolean(String expression) {
        return xpathParser.evalBoolean(node, expression);
    }
    
    public Double evalDouble(String expression) {
        return xpathParser.evalDouble(node, expression);
    }
    
    public XNode evalNode(String expression) {
        return xpathParser.evalNode(node, expression);
    }
    
    public List evalNodes(String expression) {
        return xpathParser.evalNodes(node, expression);
    }

【功能】调用XPathParser方法在当前节点下寻找符合表达式条件的节点,通常是文本节点,并将其值转化为指定的类型,如果值无法转化为指定类型会报错。
【支持数据类型】 String、Boolean、Double、Node、List
【源码分析】简单调用XPathParser提供的方法


7、get*Body()系列方法(以getBooleanBody为例)
    public Boolean getBooleanBody() {
        return getBooleanBody(null);
    }
    
    public Boolean getBooleanBody(Boolean def) {
        if (body == null) {
            return def;
        } else {
            return Boolean.valueOf(body);
        }
    }

【功能】获取文本节点内容并将其转化为指定的数据类型
【支持的数据类型】 String、Boolean、Integer、Long、Double、Float
【源码分析】简单判断body是否为空,不为空则做类型转换


7、get*Attribute()系列方法(以getStringAttribute为例)
    // 获取属性值,如果没有返回null
    public String getStringAttribute(String name) {
        return getStringAttribute(name, null);
    }
    
    // 获取属性是,没有没有使用默认值
    public String getStringAttribute(String name, String def) {
        String value = attributes.getProperty(name);
        if (value == null) {
            return def;
        } else {
            return value;
        }
    }

【功能】获取节点指定属性的属性值并将其转化为指定的数据类型
【支持的数据类型】 Enum、String、Boolean、Integer、Long、Double、Float
【源码分析】从attributes中根据属性名取出属性值再坐简单类型转化


8、public List getChildren()
    // 获取子节点,对Node.getChildNodes()做相应的封装得到List
    public List getChildren() {
        List children = new ArrayList();
        NodeList nodeList = node.getChildNodes();
        if (nodeList != null) {
            // 我的写法
            /*for (int i = 0; i < nodeList.getLength(); i++) {
                Node node = nodeList.item(i);
                if (node instanceof Element) {
                    children.add(new XNode(xpathParser, node, variables));
                }
            }*/
            
            // 源码的写法
            for (int i = 0, n = nodeList.getLength(); i < n; i++) {
                Node node = nodeList.item(i);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    children.add(new XNode(xpathParser, node, variables));
                }
            }
        }
        return children;
    }

【功能】获取子节点,对Node.getChildNodes()做相应的封装得到List
【源码分析】取出包装的Node对象的所有子元素节点,并对其包装成XNode列表返回


9、public Properties getChildrenAsProperties()
    // 获取所有子节点的name、value属性键值对
    public Properties getChildrenAsProperties() {
        Properties properties = new Properties();
        for (XNode child : getChildren()) {
            String name = child.getStringAttribute("name");
            String value = child.getStringAttribute("value");
            if (name != null && value != null) {
                properties.put(name, value);
            }
        }
        return properties;
    }

【功能】获取所有子节点的name、value属性键值对
【源码分析】先调用getChildren()获得节点的所有子XNode,然后逐一遍历获取其name、value,放在Properties对象中返回。


五、测试案例
1、测试XML文件


    
        evalString1_text
        ${evalString2}
        false
        3.14156
        Node
        Node
        Node
        Node
        100
        20000000000
        6.667