在Java世界中,xml是一种重要的数据格式,很多开源框架包括Spring、MyBatis等都使用了xml文档作为配置文件,了解如何解析xml文档是非常有必要的。
常见使用JDK本身自带的API能够解析xml文件的方式一共有三种,分别是DOM(Document Object Model)、SAX(Simple API for XML)、StAX(Streaming API for API)。
在下面的示例中,都使用下示的demo.xml作为解析的样例,其内容如下:
<?xml version="1.0" encoding="UTF-8"?> <!-- 文档头,定义编码格式和xml标准版本 -->
<!DOCTYPE demo> <!-- 文档类型定义,demo表示这个xml文档的根节点标签的元素名,
一般还会在这里定义dtd文件,可以用来检验文档格式 -->
<demo> <!-- xml严格要求有开标签必须有闭标签 -->
<mobile country="China">
<company>HUAWEI</company>
<model>meta 20 Pro</model>
<price>5699</price>
<year>2018</year>
</mobile>
<mobile country="China">
<company>XIAOMI</company>
<model>小米max2</model>
<price>2899</price>
<year>2017</year>
<country>China</country>
</mobile>
<mobile country="USA">
<company>APPLE</company>
<model>iphone 7 plus</model>
<price>5799</price>
<year>2016</year>
<country>USA</country>
</mobile>
</demo>
一、DOM
DOM是文档对象模型的意思,使用这种方式解析xml文档,会将整个文件加载到内存中并构建一个DOM树,基于这颗树形结构对各个节点(Node)进行操作。
XML文档中的每个成分都是一个节点:整个文档是一个文档节点,每个标签对应一个元素节点,包含在标签中的文本是文本节点,每一个XML树形是一个属性节点,注释属于注释节点。
先读取文档,获得Document对象,代码如下所示:
// 根据XML文件路径获得Document对象
public static Document getXmlDocument(String xmlPath) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setValidating(true); // 是否指定此代码生成的解析器将在文档解析时验证文档
documentBuilderFactory.setNamespaceAware(false); // 是否指定此代码生成的解析器将为XML命名空间提供支持
documentBuilderFactory.setIgnoringComments(true); // 是否指定此代码生成的解析器将忽略注释
documentBuilderFactory.setIgnoringElementContentWhitespace(false); // 是否指定此工厂创建的解析器必须在解析XML文档时消除元素内容中的空格(有时称为“可忽略的空白”)
documentBuilderFactory.setCoalescing(false); // 是否指定此代码生成的解析器将CDATA节点转换为文本节点并将其附加到相邻(如果有的话)文本节点
documentBuilderFactory.setExpandEntityReferences(true); // 是否指定此代码生成的解析器将扩展实体引用节点
// 创建DocumentBuilder
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
// 设置异常处理对象
builder.setErrorHandler(new ErrorHandler() {
@Override
public void warning(SAXParseException exception) throws SAXException {
// TODO Auto-generated method stub
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
// TODO Auto-generated method stub
}
@Override
public void error(SAXParseException exception) throws SAXException {
// TODO Auto-generated method stub
}
});
return builder.parse(xmlPath);
}
将其放在一个工具类XMLParseUtils中,方便调用,下面使用XPath配合DOM解析xml。
public class DOMTest {
public static void main(String[] args) throws Exception {
Document doc = XMLParseUtils.getXmlDocument("src/main/java/demo.xml");
// 创建 XPathFactory
XPathFactory factory = XPathFactory.newInstance();
// 创建 XPath对象
XPath xpath = factory.newXPath();
// 编译 XPath表达式
// 表达式字符串定义了获取节点的规则
// 下面这条表达式获取model子节点值为meta 20 Pro的mobile节点下的price节点的值
XPathExpression expr = xpath.compile("//mobile[model='meta 20 Pro']/price/text()");
// 通过XPath表达式得到结果,第一个参数指定了XPath表达式进行查询的上下文节点,也就是在指定节点下查找符合XPath的节点
// 本例中的上下文节点时整个文档;第二个参数指定了XPath表达式的返回类型
Object result = expr.evaluate(doc, XPathConstants.NODESET);
System.out.println("查询型号为meta 20 Pro的手机的价格:");
NodeList nodes = (NodeList) result; // 强制类型转换,至于转换后的类型要看XPathExpression.evaluate方法returnType的设置
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}
}
}
# console:
查询型号为meta 20 Pro的手机的价格:
5699
现在想获取所有中国公司出品的手机详细信息,代码如下:
System.out.println("查询所有中国手机型号:");
NodeList nodes2 = (NodeList) xpath.evaluate("//mobile[@country='China']/model/text()", doc, XPathConstants.NODESET);
for (int i = 0; i < nodes2.getLength(); i++) {
System.out.println(nodes2.item(i).getNodeValue());
}
# console:
查询所有中国手机型号:
meta 20 Pro
小米max2
二、SAX
SAX是一种使用事件回调机制的XML解析器,事件由解析器产生并通过回调函数发送给应用程序,这种模式称为“推模式”。
跟上面的类似,首先会创建一个SAX解析器工厂对象(SAXParserFactory),工厂对象创建解析器对象(SAXParser),解析器解析文档流对象的调用方法形式为:
parse(InputStream, DefaultHandler);
# (1)InputStream是读取文档得到的流对象
# (2)DefaultHandler是一个实现了DefaultHandler接口的对象解析流对
# 象时解析流对象时会触发回调的方法都在该接口中定义,所以实现了
# 接口的类必须实现想要进行处理的事件对应的回调方法
如下所示,定义了一个DefaultHandler接口的实现类,实现了在解析文档流对象的开始和结束时触发的方法,在解析每个标签元素节点开始和结束时触发的方法,在解析文本节点时触发的方法:
static class TestSAXHandler extends DefaultHandler {
// SAX开始解析文档时会调用本方法
@Override
public void startDocument () throws SAXException {
System.out.println("parse xml document start!");
}
// SAX解析文档结束时会回调本方法
@Override
public void endDocument () throws SAXException {
System.out.println("parse xml document end!");
}
// SAX解析每个标签元素时会回调本方法
@Override
public void startElement (String uri, String localName,
String qName, Attributes attributes) throws SAXException {
System.out.println("开始解析标签:" + qName);
int length = attributes.getLength();
for (int i = 0; i < length; i++) {
String aname = attributes.getLocalName(i);
String avalue = attributes.getValue(i);
System.out.println("属性名:" + aname);
System.out.println("属性值:" + avalue);
}
}
// SAX解析每个标签结束时都会回调本方法
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
System.out.println("解析标签结束:" + qName);
}
// SAX解析每一块文本内容时都会回调本方法,空格、换行、开标签的标签头也会被视为文本
public void characters (char ch[], int start, int length) throws SAXException {
StringBuilder sb = new StringBuilder();
for (int i = 0; i <= length; i++) {
sb.append(ch[start + i]);
}
System.out.println("\"" + sb.toString() + "\"");
}
}
将文档转换为流对象,在XMLParseUtils中新增一个静态方法,代码如下:
public static InputStream getXmlInputStream(String xmlPath) {
InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(xmlPath);
return inputStream;
}
在SAXTest的main方法中解析xml的代码如下:
public class SAXTest {
public static void main(String[] args) throws Exception {
InputStream inputStream = XMLParseUtils.getXmlInputStream("demo.xml");
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(inputStream, new TestSAXHandler());
}
# console:
parse xml document start!
开始解析标签:demo
"
<"
开始解析标签:mobile
属性名:country
属性值:China
"
<"
开始解析标签:company
"HUAWEI<"
解析标签结束:company
"
<"
开始解析标签:model
"meta 20 Pro<"
解析标签结束:model
"
<"
开始解析标签:price
"5699<"
解析标签结束:price
"
<"
开始解析标签:year
"2018<"
解析标签结束:year
"
<"
解析标签结束:mobile
"
<"
开始解析标签:mobile
属性名:country
属性值:China
"
<"
开始解析标签:company
"XIAOMI<"
解析标签结束:company
"
<"
开始解析标签:model
"小米max2<"
解析标签结束:model
"
<"
开始解析标签:price
"2899<"
解析标签结束:price
"
<"
开始解析标签:year
"2017<"
解析标签结束:year
"
<"
开始解析标签:country
"China<"
解析标签结束:country
"
<"
解析标签结束:mobile
"
<"
开始解析标签:mobile
属性名:country
属性值:USA
"
<"
开始解析标签:company
"APPLE<"
解析标签结束:company
"
<"
开始解析标签:model
"iphone 7 plus<"
解析标签结束:model
"
<"
开始解析标签:price
"5799<"
解析标签结束:price
"
<"
开始解析标签:year
"2016<"
解析标签结束:year
"
<"
开始解析标签:country
"USA<"
解析标签结束:country
"
<"
解析标签结束:mobile
"
<"
解析标签结束:demo
parse xml document end!
三、StAX
StAX与SAX类似,也是基于文档流对象和产生事件解析XML的模式,不过事件不同与SAX的回调通知方式,需要应用程序自行遍历判断事件类型,从中筛选出要获取的节点的信息,所有事件类型都在XMLStreamConstants中定义,常见的有:
事件 | 字典 |
---|---|
解析开标签节点事件 | XMLStreamConstants.START_ELEMENT |
闭标签节点事件 | XMLStreamConstants.END_ELEMENT |
文本节点事件 | XMLStreamConstants.CHARACTERS |
StAX这种解析的策略也被成为“拉模式”。
同样的,StAX首先要获取XML文档流对象,然后创建解析器工厂对象(XMLInputFactory),根据工厂对象创建解析器对象(XMLStreamReader),这里的解析器实际上就是一个迭代器,根据迭代器可以顺序获取事件类型,并根据事件类型去调用解析器的其他方法获取节点内容进行处理,使用样例如下所示:
public class StAXTest {
public static void main(String[] args) throws Exception {
InputStream in = XMLParseUtils.getXmlInputStream("inventory.xml");
XMLInputFactory factory = XMLInputFactory.newFactory();
XMLStreamReader parser = factory.createXMLStreamReader(in);
while (parser.hasNext()) {
int event = parser.next();
// 解析开标签
if (event == XMLStreamConstants.START_ELEMENT) {
System.out.println("解析标签元素: " + parser.getLocalName());
int attCount = parser.getAttributeCount();
System.out.println("该标签属性数量: " + attCount);
for (int i = 0; i < attCount; i++) {
System.out.println("属性名:" + parser.getAttributeLocalName(0));
System.out.println("属性值:" + parser.getAttributeValue(0));
}
System.out.println();
continue;
}
// 解析文本
if (event == XMLStreamConstants.CHARACTERS) {
System.out.println("文本内容: \"" + parser.getText() + "\"");
System.out.println();
continue;
}
// 解析闭标签
if (event == XMLStreamConstants.END_ELEMENT) {
System.out.println("解析标签元素结束: " + parser.getLocalName());
System.out.println();
}
}
}
}
# console:
解析标签元素: demo
该标签属性数量: 0
文本内容: "
"
解析标签元素: mobile
该标签属性数量: 1
属性名:country
属性值:China
文本内容: "
"
解析标签元素: company
该标签属性数量: 0
文本内容: "HUAWEI"
解析标签元素结束: company
文本内容: "
"
解析标签元素: model
该标签属性数量: 0
文本内容: "meta 20 Pro"
解析标签元素结束: model
文本内容: "
"
解析标签元素: price
该标签属性数量: 0
文本内容: "5699"
解析标签元素结束: price
文本内容: "
"
解析标签元素: year
该标签属性数量: 0
文本内容: "2018"
解析标签元素结束: year
文本内容: "
"
解析标签元素结束: mobile
文本内容: "
"
解析标签元素: mobile
该标签属性数量: 1
属性名:country
属性值:China
文本内容: "
"
解析标签元素: company
该标签属性数量: 0
文本内容: "XIAOMI"
解析标签元素结束: company
文本内容: "
"
解析标签元素: model
该标签属性数量: 0
文本内容: "小米max2"
解析标签元素结束: model
文本内容: "
"
解析标签元素: price
该标签属性数量: 0
文本内容: "2899"
解析标签元素结束: price
文本内容: "
"
解析标签元素: year
该标签属性数量: 0
文本内容: "2017"
解析标签元素结束: year
文本内容: "
"
解析标签元素: country
该标签属性数量: 0
文本内容: "China"
解析标签元素结束: country
文本内容: "
"
解析标签元素结束: mobile
文本内容: "
"
解析标签元素: mobile
该标签属性数量: 1
属性名:country
属性值:USA
文本内容: "
"
解析标签元素: company
该标签属性数量: 0
文本内容: "APPLE"
解析标签元素结束: company
文本内容: "
"
解析标签元素: model
该标签属性数量: 0
文本内容: "iphone 7 plus"
解析标签元素结束: model
文本内容: "
"
解析标签元素: price
该标签属性数量: 0
文本内容: "5799"
解析标签元素结束: price
文本内容: "
"
解析标签元素: year
该标签属性数量: 0
文本内容: "2016"
解析标签元素结束: year
文本内容: "
"
解析标签元素: country
该标签属性数量: 0
文本内容: "USA"
解析标签元素结束: country
文本内容: "
"
解析标签元素结束: mobile
文本内容: "
"
解析标签元素结束: demo
四、三种方式的比较
DOM 的优点在于面向节点树编程比较简单,也比较好理解,在解析DOM时就已经完整加载了文档树,对节点的遍历和导航(包括父节点、子节点、兄弟节点)比较方便,也易于添加和删除节点,但是在文档内容比较大的时候,性能消耗比较大,处理效率较低,不过一般都用作配置文件,内容不多,因此忽略不计。
如果xml文件本身内容较多,而且在很多情况下只想解析某一个节点而不想加载全部节点浪费资源,这时可考虑使用流机制的解析器SAX和StAX,能够降低性能消耗,提高效率。
SAX 的缺点非常明显,没有加载完整的文档结构,对节点信息的获取和处理依赖回调函数,当处理逻辑涉及多个多层节点之间的关系时,回调函数的逻辑会非常复杂和难以维护;而且流处理方式只允许从上往下处理,不允许回溯已经处理过的节点,另外SAX也不支持修改XML。
StAX 具有跟SAX一样的流处理的缺点,StAX包括了两套处理XML文档的API:一种是基于指针的API,效率高但是抽象化程度低;另一种是基于事件迭代器的API,效率低但是抽象化程序高。开发者可以根据需求做平衡和选择。