Java眼中的XML

XML 因为具有良好的数据描述能力和跨平台性,被广泛应用于数据存储和交换等,比如用作配置文件等。 在日常开发中,我们可能也需要处理 XML数据。这里就来介绍几种在 Java 中常见的对 XML 的处理方法。

使用建议: 推荐直接使用 Dom4j ,若追求性能可尝试 SAX

DOMSAX 是 Java 提供的基础的处理方法,属于 JAXPJDomDom4j 是建立其上的第三方库。

涉及 说明 优劣
DOM 基于 dom树 的处理方式,与平台无关解析方式,遵循 w3c 标准 。 以树结构处理数据,操作方便。内存一次性加载 DOM树,不利于大数据量的处理。
SAX 基于事件驱动的处理方式。 边读边处理,内存占用小,解析速度快,因为逐行解析不利于随机访问和操作。
JDom 仅使用具体类而不使用接口,API 大量使用 Collection 类。 提供的 API 非常利于编码,但性能较弱,存在内存溢出风险。
Dom4j 起源自 JDom 。使用接口和抽象基础类方法。优秀而强大的 API 。 易用性和性能兼顾的 API ,成为了许多开源框架的标配处理方式。

性能对比

1
2
3
解析耗时:JDom > DOM > Dom4j >> SAX

生成耗时: DOM > JDom > Dom4j >> SAX

认识 XML

XML (可扩展标记语言) ,一种通用的数据交换格式,由众多节点标签组成,总体呈树结构,可以有效的描述复杂的数据结构。

XML 常见节点类型 说明
document 文档对象模型
element 标签元素
attribute 属性

本文会以此 XML 为例,介绍相关功能的简单使用。

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<transaction>
<object id="1">
<name>Neo</name>
<year>1999</year>
</object>
<object id="2">
<name>NEO</name>
<year>2199</year>
</object>
</transaction>

DOM

DOM 会一次性加载将整个 XML,并将其视为dom树 ,树由节点构成。

标签、标签之间的间隔内容、属性等都被视为节点

节点类型 nodeType nodeName nodeValue
Element Node.ELEMENT_NODE 元素名 null
Attr Node.ATTRIBUTE_NODE 属性名 属性值
Text Node.TEXT_NODE #text 节点内容

解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Test
public void domReader() throws ParserConfigurationException, IOException, SAXException {

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
// 解析文件
Document document = db.parse(inputStream);

// 元素节点:通过节点名获取
NodeList nodeList = document.getElementsByTagName("object");
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
// 属性:获取所有属性
NamedNodeMap attrs = node.getAttributes();
for (int j = 0; j < attrs.getLength() ; j++) {
// 属性本身也是节点
String attrName = attrs.item(j).getNodeName(); // id
String attrValue = attrs.item(j).getNodeValue(); // 1
}
// 属性:通过属性名获取(有且仅有一个)
Element element = (Element) nodeList.item(i);
String attrValue = element.getAttribute("id"); // 1

// 节点:子节点
NodeList childNodes = node.getChildNodes(); // length : 5
for (int j = 0; j < childNodes.getLength(); j++) {
String nodeName = childNodes.item(j).getNodeName();
// 过滤出元素节点(标签和标签之间的间隔都被视为节点)
if (childNodes.item(j).getNodeType() == Node.ELEMENT_NODE) {
String no_nodeValue = childNodes.item(j).getNodeValue(); // null
String nodeValue = childNodes.item(j).getFirstChild().getNodeValue(); // Neo
// 会包含子节点文本内容
String nodeText = childNodes.item(j).getTextContent(); // Neo
}
}
}

}

生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Test
public void domWriter() throws ParserConfigurationException, TransformerException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();

Document document = db.newDocument();
// FIXME 去除 standalone ,但是会造成不换行
document.setXmlStandalone(true);
Element rootNode = document.createElement("transaction");

// 创建节点
Element objectNode = document.createElement("object");
// 设置属性
objectNode.setAttribute("id", "1");
Element nameNode = document.createElement("name");
// 设置节点值
nameNode.setTextContent("Neo");
Element yearNode = document.createElement("year");
yearNode.setTextContent("1999");
// 追加节点
objectNode.appendChild(nameNode);
objectNode.appendChild(yearNode);

rootNode.appendChild(objectNode);
document.appendChild(rootNode);

TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
// 设置输出属性: 换行
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
// 输出文件
transformer.transform(new DOMSource(document), new StreamResult(new File("domTest.xml")));
}

domTest.xml

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?><transaction>
<object id="001">
<name>Neo</name>
<year>1999</year>
</object>
</transaction>

SAX

SAX 使用 Hander 边解析边处理,在对应位置会触发执行对应方法。

Handler 常用方法 说明
startDocument() 文档开始时触发。
endDocument() 文档结束时触发。
startElement(String uri, String localName, String qName, Attributes attributes) 元素节点开始时触发。
endElement(String uri, String localName, String qName) 元素节点结束时触发。
characters(char[] ch, int start, int length) 可以接收元素内字符数据的方法。

解析

1
2
3
4
5
6
7
8
@Test
public void saxReader() throws ParserConfigurationException, SAXException, IOException {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
// 自定义一个 Handler
SaxParserHandler handler = new SaxParserHandler();
saxParser.parse(inputStream, handler);
}

ParserHandler

自定义一个 Handler 用于解析处理 ,需要继承 org.xml.sax.helpers.DefaultHandler

characters() 可以拿到节点值,但需要注意:

  • 和 DOM 一样 空白也被识别为节点。
  • 因为 SAX 会将 dom 分块解析 (默认2k),所以直接获取的节点值不一定是完整的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class SaxParserHandler extends DefaultHandler {

/**
* 记录当前节点元素名
*/
private String currentNode;

private StringBuilder sb = new StringBuilder();

/**
* 解析Dom开始
* @throws SAXException
*/
@Override
public void startDocument() throws SAXException {
super.startDocument();
}

/**
* 解析Dom结束
* @throws SAXException
*/
@Override
public void endDocument() throws SAXException {
super.endDocument();
}

/**
* 解析标签开始
* @param uri
* @param localName
* @param qName 便签名
* @param attributes 属性集合
* @throws SAXException
*/
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
super.startElement(uri, localName, qName, attributes);
currentNode = qName;

if ("object".equals(qName)){
// 属性:通过属性名获取
String attrValue = attributes.getValue("id"); // 1

// 属性:遍历,通过索引获取
for (int i = 0; i < attributes.getLength(); i++) {
String attrName = attributes.getQName(i); // id
String attrVlaue = attributes.getValue(i); // 1
}
}
}

/**
* 解析标签结束
* @param uri
* @param localName
* @param qName
* @throws SAXException
*/
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
super.endElement(uri, localName, qName);

if (StringUtils.isNotEmpty(currentNode)){
System.out.println(currentNode + ":" + sb.toString()); // name:NEO
}

currentNode = "";
sb.delete( 0, sb.length());
}


/**
* 获取节点值 (和 DOM 一样 空白也被识别为节点)
* @param ch dom块内容 , 默认2k
* @param start
* @param length
* @throws SAXException
*/
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
super.characters(ch, start, length);

// FIXME 由于 SAX 分块解析,直接获取的可能不完整
String value = new String(ch, start, length); // Neo (理想状态)

if (StringUtils.isNotEmpty(currentNode)
&& StringUtils.isNotEmpty(value.trim())){
sb.append(value);
}
}
}

生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Test
public void saxWriter() throws TransformerConfigurationException, SAXException {
SAXTransformerFactory factory = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
TransformerHandler handler = factory.newTransformerHandler();
// 输出设置 (编码,换行)
Transformer transformer = handler.getTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");

Result result = new StreamResult(new File("saxTest.xml"));
// FIXME 配置需要在此之前
handler.setResult(result);

// start 必须在 setResult 之后
handler.startDocument();
AttributesImpl attr = new AttributesImpl();
attr.addAttribute("", "", "id", "", "1");
handler.startElement("", "", "transaction", attr);
attr.clear();

handler.startElement("", "", "name", attr);
handler.characters("Neo".toCharArray(), 0, "abc".length());
handler.endElement("", "", "name");

handler.startElement("", "", "year", null);
handler.characters("1999".toCharArray(), 0, "1999".length());
handler.endElement("", "", "year");

handler.endElement("", "", "transaction");
handler.endDocument();
}

saxTest.xml

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?><transaction id="1">
<name>Neo</name>
<year>1999</year>
</transaction>

JDom

JDom 的 API 使用集合来存储节点信息,操作起来十分便利。

添加 jdom 依赖,如下:

1
2
3
4
5
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom2</artifactId>
<version>2.0.6</version>
</dependency>

解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
public void jDomReader() throws IOException, JDOMException {
SAXBuilder saxBuilder = new SAXBuilder();
Document document = saxBuilder.build(new InputStreamReader(inputStream, "UTF-8"));

// 根节点
Element rootElement = document.getRootElement();
// 子节点
List<Element> elementList = rootElement.getChildren();
for (Element element : elementList) {
// 获取元素索引位
int index = elementList.indexOf(element);

// 属性:遍历
List<Attribute> attrs = element.getAttributes();
for (Attribute attr : attrs) {
attr.getName(); // id
attr.getValue(); // 1
}
// 属性:通过属性名获取
String attrValue = element.getAttributeValue("id"); // 1

// 元素
List<Element> childElements = element.getChildren();
for (Element childElement : childElements) {
childElement.getName(); // name
childElement.getValue(); // Neo
}
}
}

生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Test
public void jDomWriter() throws IOException {
Element rootNode = new Element("transaction");

Element objectNode = new Element("object");
objectNode.setAttribute("id", "1");

Element nameNode = new Element("name");
nameNode.setText("Neo");
objectNode.addContent(nameNode);

Element yearNode = new Element("year");
yearNode.setText("1999");
objectNode.addContent(yearNode);

rootNode.addContent(objectNode);
Document document = new Document(rootNode);

// 设置输出格式(换行缩进,字符)
Format format = Format.getCompactFormat();
format.setIndent(" ");
format.setEncoding("UTF-8");

XMLOutputter outputter = new XMLOutputter(format);
outputter.output(document, new FileOutputStream(new File("jDomTest.xml")));
}

jDomTest.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<transaction>
<object id="1">
<name>Neo</name>
<year>1999</year>
</object>
</transaction>

Dom4j

Dom4j 中需要通过迭代器来遍历相关信息。

添加 dom4j 依赖,如下:

1
2
3
4
5
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>

解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Test
public void dom4jReader() throws DocumentException {
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 根节点
Element rootElement = document.getRootElement();
// 子节点 (此处效果等同以下)
Iterator<Element> iterator = rootElement.elementIterator();
// 元素:通过元素名获取
Iterator<Element> object = rootElement.elementIterator("object");
while (object.hasNext()) {
Element element = object.next();

// 属性:遍历
List<Attribute> attrList = element.attributes();
for (Attribute attribute : attrList) {
attribute.getName(); // id
attribute.getValue(); // 1
}
// 属性:通过属性名获取
element.attributeValue("id"); // 1

// 元素:遍历
Iterator<Element> childIter = element.elementIterator();
while (childIter.hasNext()) {
Element childElement = childIter.next();

childElement.getName(); // name
childElement.getStringValue(); // Neo
}
}
}

生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Test
public void dom4jWriter() {
Document document = DocumentHelper.createDocument();
Element rootNode = document.addElement("transaction");

Element objectNode = rootNode.addElement("object");
objectNode.addAttribute("id", "1");
Element nameNode = objectNode.addElement("name");
nameNode.setText("Neo");
Element yearNode = objectNode.addElement("year");
yearNode.setText("1999");

// 设置输出格式 (换行缩进,字符)
OutputFormat format = OutputFormat.createPrettyPrint();
format.setEncoding("UTF-8");
XMLWriter writer = null;
try {
writer = new XMLWriter(new FileOutputStream("dom4jTest.xml"), format);
// 设置为不转义 (默认转义)
// writer.setEscapeText(false);
writer.write(document);
} catch (IOException e) {
e.printStackTrace();
} finally {
writer.close();
}
}

dom4jTest.xml

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>

<transaction>
<object id="1">
<name>Neo</name>
<year>1999</year>
</object>
</transaction>