Java DOM 操作经验总结
提示:本文修订于2023年7月13日
虽然在面试过程中,很少有考官提问Java DOM相关内容,但是掌握Java DOM操作却是很有必要,尤其是当你想做自己软件作品的时候,利用Xml实现配置信息是必不可少的环节。本文记录了我在做个人作品(分布式消息中间件)过程中的若干Java DOM经验总结,以备后续复习使用。
Java DOM API给我的总体感觉是:入门简单,但是深入很难。在Java的DOM API中,主要有五种类型的对象,如下所示:
- Document:代表整个XML文档
- Element:代表一个XML元素
- Attr:代表一个元素的某个属性
- Text:表示元素的文本内容
- Comment:表示注释的内容
这些DOM对象都很容易理解,看似简单,实则并非如此,因为很多人不知道如何去选择,选择哪种类型的对象去操作Xml。这就是我上文提到的“入门简单,深入很难”的原因所在。
正因为DOM API给我们的东西很多,导致我们出现了选择困难,搞不清楚到底那种操作方式才是最合理的,但实际上我们只需要极少东西就可以实现自己的目的。我通过经验发现只需要两类对象就可以实现基本的Xml操作:Document
和Element
。关于Document
和Element
使用介绍可以见下文。当然,要想达到这种境界,需要深入研究Java Dom源码才能领悟。幸好,本文中也带有源码级的解读,方便参考。
1、Document的使用介绍
Document必不可少,因为它是Xml文件的入口。Document用于文档的初始化,主要分四步:
第一步:创建一个DocumentBuilderFactory对象。
第二步:通过DocumentBuilderFactory对象创建一个DocumentBuilder对象。
第三步:通过DocumentBuilder对象的parse()方法解析XML文档,得到一个Document对象。
第四步:通过Document对象获取XML文档中的元素、属性、文本节点等。
以上四步对应的代码如下所示:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse("src/main/resources/dom2.xml");
NodeList nodes = doc.getChildNodes();
Document自己独有的方法有两个:
public NodeList getElementsByTagName(String tagname);
public Element getElementById(String elementId);
仔细品读这两个方法,你会发现它们设计的非常有意思:
(1)getElementsByTagName
方法得到的是NodeList,而Node是所有对象(Document、Element、Attr、Text、Comment)的父类,此处是抽象化思维的极致体现,函数返回Node类这样的设计思想十分合理!
(2)getElementById
方法得到的是Element对象,而非Node对象。不同于上面的极致抽象思想,因为Node对象也包括了Attr、Text、Comment这类对象,而像Attr、Text、Comment这类对象,自身压根就没有ID属性,不可能通过ID来获取,所以返回Node对象则有点不精准的感觉。另外,从这个函数的设计也可以看出来,在所有Node类的派生类中,Element类独树一帜,是个非常重要的东西。
2、Element的使用介绍
Element自身的方法有两个:
public String getAttribute(String name);
public NodeList getElementsByTagName(String name);
上面的两个方法分别用于获取自身属性和子元素。因为Element继承自Node类,所以还可以使用Node类的一个重要方法:
备注:Node类有两个非常容易引发误解的函数:getNodeValue和getTextContent,见下文介绍。
public String getTextContent()
因为Element具备了上述三个方法,所以可以大杀四方,通吃一切,以下面的代码为例:
<element attr="attr">text<subelement></subelement></element>
我们完全可以只使用Element实现所有的操作:
(1)使用element.getAttribute("attr")
就可以获得属性的值
(2)使用element.getTextContent()
就可以获得文本text的值
(3)使用element.getElementsByTagName("subelement")
获得subelement元素
3、Node的误区提示
Node是所有对象(Document、Element、Attr、Text、Comment)的父类,在它里面有两个函数非常容易引发误解:
public String getNodeValue();
public String getTextContent()
还是以上面代码为例:
<element attr="attr">text<subelement></subelement></element>
element这个Node是没有nodeValue的,"text"不是它的nodeValue,而是个单独的Node。我们只能通过getTextContent获得element这个Node的文本内容。
4、经验总结
当我深入学习和使用Java DOM操作之后,越发感觉到Java设计者在处理Xml文档方面的繁琐。同时,也理解了为什么Jdom和dom4J能后来者居上。在Java的DOM API中有很多东西,但实际上我们只需要极少东西就可以实现操作,导致我们出现了选择困难,搞不清楚到底那种操作方式才是最合理的,其实我们只需要牢记Document和Element就可以化解这个问题。
5、Java DOM 源码解读
5.1、Node接口
Node是DOM的基础,它表示文档树中的某个节点。所有的节点对象都起源于Node接口,并派生为不同的类型:Document、Element、Attr、Text、Comment。Node的作用主要有三个:
(1)不用向下转型成具体的派生接口就可以获得nodeName、nodeValue等基本信息。
(2)实现DOM的遍历:
public Node getParentNode();
public NodeList getChildNodes();
public Node getFirstChild();
public Node getLastChild();
public Node getPreviousSibling();
public Node getNextSibling();
(3)获取节点的属性,仅限于Element节点:
public NamedNodeMap getAttributes();
备注:NamedNodeMap本质是个工具类,它并非派生自Node接口,顾名思义,便可知它是一个存放Node的Map容器。需要注意的是,它只在Node接口中使用了一次,只用于获取属性的方法中,所以可以知道这个容器只存放Attr派生类。
5.2、Document接口
见上文介绍。
5.3、Element接口
见上文介绍。
5.4、Attr接口
Attr接口派生自Node接口,即:
interface Attr extends Node
Attr接口的常用方法有两个,如下所示:
public String getName();
public String getValue();
5.5、CharacterData接口
CharacterData接口派生自Node,并且是Text接口和Comment接口的父类,如下所示:
interface Text extends CharacterData
interface Comment extends CharacterData
Text接口和Comment接口在编程中遇到的机会很少,所以不再赘述。
5.6、NodeList接口,就是一个工具类而已。
public interface NodeList {
public Node item(int index);
public int getLength();
}
6、Java DOM 实战解读
对于Java DOM 实战操练,我是从两个角度进行解读。第一个角度是nodeValue,第二个角度是textContent。
对于下面的xml节点来说:
<user id='000'>Tom</user>
它有两个node,分别是user和Tom。user这个node只有name,没有value,而Tom这个node正好相反。这是从nodeValue角度看到的情况,而从textContent角度,则只需要关注user这个node即可,它的文本内容是“Tom”,不需要关注Tom这个node。
书归正传,继续我们的实战操作,此番实战对象为下面的xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<users>
<user id="1">
<firstname>Peter</firstname>
<lastname>Brown</lastname>
<occupation>programmer</occupation>
</user>
<user id="2">
<firstname>Martin</firstname>
<lastname>Smith</lastname>
<occupation>accountant</occupation>
</user>
<user id="3">
<firstname>Lucy</firstname>
<lastname>Gordon</lastname>
<occupation>teacher</occupation>
</user>
</users>
6.1、通过nodeValue的形式解析文件。
需要明白一点:对于任意节点而言,有nodeName则无nodeValue,反之亦然,两者不可得兼。
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class Dom1 {
public static void main(String[] args) throws SAXException, IOException, ParserConfigurationException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse("src/main/resources/dom2.xml");
NodeList nodes = doc.getChildNodes();
readXml(nodes);
}
public static void readXml(NodeList nodes) {
for (int i = 0; i < nodes.getLength(); i++) {
if (nodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
System.out.println("该节点的名称为:" + nodes.item(i).getNodeName());
String value = nodes.item(i).getFirstChild().getNodeValue();
if (value != null && !value.trim().equals("")) {
System.out.println("该节点的值为:" + value);
}
String attrValue = ((Element) (nodes.item(i))).getAttribute("id");
if (attrValue != null && !attrValue.equals("")) {
System.out.println("该节点的id为:" + attrValue);
}
System.out.println("");
}
if (nodes.item(i).getChildNodes().getLength() != 0) {
readXml(nodes.item(i).getChildNodes());
}
}
}
}
6.2、通过textContent的形式解析文件。
import java.io.File;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class Dom2 {
public static void main(String argv[]) throws SAXException, IOException, ParserConfigurationException {
File xmlFile = new File("src/main/resources/dom2.xml");
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = factory.newDocumentBuilder();
Document doc = dBuilder.parse(xmlFile);
System.out.println("Root element: " + doc.getDocumentElement().getNodeName());
NodeList nList = doc.getElementsByTagName("user");
for (int i = 0; i < nList.getLength(); i++) {
Node node = nList.item(i);
System.out.println("\nCurrent Element: " + node.getNodeName());
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element elem = (Element) node;
String uid = elem.getAttribute("id");
Node node1 = elem.getElementsByTagName("firstname").item(0);
String fname = node1.getTextContent();
Node node2 = elem.getElementsByTagName("lastname").item(0);
String lname = node2.getTextContent();
Node node3 = elem.getElementsByTagName("occupation").item(0);
String occup = node3.getTextContent();
System.out.printf("User id: %s%n", uid);
System.out.printf("First name: %s%n", fname);
System.out.printf("Last name: %s%n", lname);
System.out.printf("Occupation: %s%n", occup);
}
}
}
}
6、Java DOM 小结
虽然Java DOM API给我们的东西很多,导致我们出现了选择困难,但实际上我们只需要牢记Document和Element就可以化解这个问题。纸上得来终觉浅,须知此事要躬行,通过不断的练习和体会,我们才能发现Java DOM API设计者的良苦用心。