提要
SAX API在运行中的各方面表现都优于DOM API。下文将探索用SAX将XML文档解析为Java对象。SAX用起来不像DOM那样直观,所以我们要先熟悉一下SAX的用法。 (3,000 字(译注:英文原文三千字))
Robert Hustead 作
现在XML很火爆. 因为XML是一种自定义数据(译注:self-describing data,这个词在英文文献中很常见,XML有DTD和其它方法描述它本身或某一部分内容的性质和格式),所以它能存储不同编码方式的数据。人们常将XML用作在异构系统中交换数据的媒介。XML格式的数据能很容易地用诸如COBOL程序、C++程序等的各种系统输出。
不过,用XML建立系统会有两个难题:首先,生成XML数据是一个简单的过程,但是反过来从一个程序里调用这些数据就不是了。其次,现今的XML技术都容易被处置不当,这会导致速度慢且内存消耗大的程序出现。在以XML作为基本数据交换格式的系统中,速度慢和内存消耗大被证明是两个瓶颈。
在当前的各种通用XML处理工具中,有的相对比较好。SAX API就有一些对于性能要求高的代码很有帮助的特点。在这篇文章中,我们要制定一些SAX API编码模式。用这种模式,你就能写出速度快、内存消耗小的XML-JAVA映射代码,甚至对某些相当复杂的XML结构(但不包括递归结构)它们也能应付。
在第二部分,我们将解决含有递归的XML结构。这种结构的某些XML元素表示的是一个列表的列表。我们还要开发一个用SAX API处理数据导航的类库。这个库可以简化基于SAX的XML解析。
解析代码就像编译代码
写XML解析程序就像写编译器一样。你看,几乎所有的编译器都分三步把源代码变为可执行程序。首先,语法模块将字符组成编译器能识别的字词——就是所谓的词法分析。第二个模块调用解析器,分析各组字词以识别合法的语法。最后的第三个模块处理一系列合法语句结构生成可执行代码。有时,源文件解析和可执行代码生成是交织进行的。
要用Java解析XML数据,我们也得经过一个相似的流程。首先我们要分析XML文档中的每个字符以识别合法的XML组成,诸如起始标签、属性、结束标签、CDATA部分。
然后我们证实这些组成可以形成合法的XML结构。如果完全由符合XML 1.0要求的合法结构组成,那么它就是结构良好的XML文档。比如最基本的,我们要确定所有的标签都有起始与结束标签相匹配,同时所有属生都以正确的形式存在于起始标签中。
此外,如果有对应的DTD,我们能选择性地通过验证解析到的XML结构符合DTD描述,来确定XML文档是结构良好的。
最后,我们用XML文档里的数据做一些有意义的事情——我管这个叫“XML映射到JAVA对象”(mapping XML into Java)
XML解析器
幸运的是,有一些现成的组件——XML解析器——可以用来完成类似编译的工作。XML解析器处理所有的语法分析和解析。现在的很多基于Java的XML解析器都依照两种解析标准:SAX和DOM API(译注:解析器一般会选择一种标准,并非两种同时在一个解析器内实现)
有了这些现成的XML解析器好像在Java中使用XML就没什么别的困难了,其实使用这些XML解析器也是一件很棘手的事情。
SAX和DOM API
SAX API是基于事件的。实现了SAX API的XML解析器跟据解析到的XML文档的不同特征产生事件。通过在Java代码中捕捉这些事件,就可以写出由XML数据驱动的程序。
DOM API是一种基于对象的API。实现DOM的XML解析器在内存中生成代表XML文档内容的一般对象模型。XML解析器一旦完成解析,内存中也就有了一个同时包含XML文档的结构和内容信息的DOM对象树。
DOM的概念来自HTML流览器界,HTML流览器通常用一个普通文档对象来表示所装载的HTML文件。这样象JavaScript之类的脚本语就可以访问到这些HTML DOM对象。HTML DOM 在这方面的应用是很成功的。
DOM的不足
乍一看,DOM API像有更加丰富的特色,因此也比SAX API更优秀。但是,DOM在用于对性能要求高的程序设计时却存在严重的不足。
目前支持DOM的XML解析器都使用一种对象存储的方式,也就是创建很多小的代表DOM节点的对象,这些节点对象还包含了文本或嵌套其它DOM节点。这看似顺理成章,但却造成了性能的下降。Java中最为影响性能的操作之一是new操作符。对应于new操作符的每一次执行,在对所得对象的所有引用都消失后,垃圾收集器都要负责将这个对象从内存中清除。DOM API的众多小对象一般在解析完后被立即抛弃,这几乎耗光了JVM的所有内存。
DOM的另一个不足是它把整个的XML文档都装入内存。对于大的文档,这就成了一个问题。再一次地,因为DOM是基于许多小对象实现的,所以在保存XML文档的同时,JVM还要用额外的几字节来保存关于所有这些对象的信息,这样一来,内存使用就变得比XML文档本身还要大。
还有一个很麻烦的,就是很多Java程序实际上并没有用到DOM这种一般形式的对象结构。而是在DOM结构一装入内存就立即将数据拷贝到对应它们的特定问题域的对象结构中——一个繁杂而多余的过程。
DOM API的另一个不易察觉的问题是,使用它写成的代码要扫描XML文档两次。第一次将DOM结构读进内存,第二次定位感兴趣的数据。理所当然,定位不同的数据块就要在DOM结构中来回地移动。相反,SAX编程模式支持一趟同时定位和收集XML数据。
这些问题中有的可以通过设计一个更好的底层数据结构在内部表示DOM对象得以解决。而像使用多次扫描和一般—特定对象模型转换这样的问题则无法在XML解析器内部解决。
求肋于SAX
相对DOM API,SAX API是一种颇有吸引力的解决方案。SAX没有一般的对象模型,所以在内存消耗和性能问题上对new操作符的滥用没有顾忌。同时如果你要设计自己特定问题域的对象模型,SAX也就没有冗余的对象模型。并且,SAX一遍就能处理好XML文档,它所需的处理时间大大减少。
SAX确实也有它的不足,但这些不足大都与程序员有关,并非API本身的性能问题。我们先来大致看一下。
第一个缺点是概念上的。程序员们习惯于通过定位获取数据。(译注:作者指程序员都喜欢自已主动获取数据,想要什么数据就立即去取,而不是SAX这种数据被依次抛出,再由程序员处理的方式。)为了找到服务器上的一个文件,你通过改变目录来定位。相似地,为了得到一个数据库里的数据,你将写一个SQL查询语句。对于SAX,这种模式是相反的。也就是,你先建立起自己的代码来监听每列有效的XML数据片。这段代码只有当感兴趣的XML数据出现时才被调用。SAX API乍看起来很别扭,但是用不了多久,这种思考方式就会成为习惯。
第二个缺点就有点危险了。对于SAX的代码,那种天真的草率行事的做法会很快的引火烧身,因为在收集数据的同时也彻底地把XML结构过滤了一遍。大多数的人只注意数据解析而忽视了数据流动是有顺序的这一方面。如果你不在自已的代码中考虑到数据流将会出现的顺序,在进行SAX解析过程中进行定位的代码就会发散失控并产生很多复杂的相互耦合(或称牵制)。这个问题就有点像一般程序中对全局变量过分依赖所产生的那些问题。但是如果你学会正确地构建优雅的SAX代码,它甚到比DOM API还要直观。(译注:我在理解这个地方时遇到很大的麻烦,曾直接向Robert求教。反复阅读后才明白了一点。SAX解析XML并非没有数据出现的顺序,而是数据出现的顺序仅可预测不可改变的,所以在处理数据时要时刻牢记这一点。要构建所谓的优雅的代码,我的办法是不要试图在收集数据的同时进行过于复杂的操作,不要一心想将已经出现的事件回卷以获取“从前”的数据。以下是Mr. Robert的答复:-- The point I'm making is that the navigational aspects of coding a SAX based solution exist whether you are aware of them or not. The fact that they exist will affect how you code. To directly address is to acknowledge the presence and impact of the navigational aspects explicitly during design. The opposite would be to ignore the aspects and instead have the navigational aspects just show up in little pockets of code in unrelated areas of the application.)
基本的SAX
当前SAX API有两个版本。我们用第二版(见资源)来做示例。第二版中的类名和方法名与第一版都有出入,但是代码的结构是一样的。
SAX是一套API,不是一个解析器,所以这个代码在XML解析器中是通用的。要让示例跑起来,你将需要一个支持SAX v2的XML解析器。我用Apache的Xerces解析器。(见资源)参照你的解析器的getting-started文档来获得调用一个SAX解析器的资料。
SAX API 的说明书通俗易读。它包含了很多的详细内容。而使用SAX API的主要任务就是创建一个实现ContentHandler接口,一个供XML 解析器调用以将分析XML文档时所发生的SAX事件分发给处理程序的回调接口。
方便起见,SAX API也提供了一个已经实现了ContentHandler接口的DefaultHandler适配器类。
一但实现了ContentHandler或者扩展了DefaultHandler类,你只需直接将XML解析器解析一个特定的文档即可。
我们的第一个例子扩展DefaultHandler将每个SAX事件打印到控制台。这将给你一个初步的映象,以说明什么SAX事件将会发生及以怎样的顺序发生。
作为开始,以下是将在我们的第一个示例中用到的XML文档样本:
<?xml version="1.0"?>
<simple date="7/7/2000" >
<name> Bob </name>
<location> New York </location>
</simple>
接下来,我们看看第一个XML解析例子的代码:
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import java.io.*;
public class Example1 extends DefaultHandler {
// 重载DefaultHandler类的方法
// 以拦截SAX事件通知。
//
// 关于所有有效事件,见org.xml.sax.ContentHandler
//
public void startDocument( ) throws SAXException {
System.out.println( "SAX Event: START DOCUMENT" );
}
public void endDocument( ) throws SAXException {
System.out.println( "SAX Event: END DOCUMENT" );
}
public void startElement( String namespaceURI,
String localName,
String qName,
Attributes attr ) throws SAXException {
System.out.println( "SAX Event: START ELEMENT[ " +
localName + " ]" );
// 如果有属性,我们也一并打印出来...
for ( int i = 0; i < attr.getLength(); i++ ){
System.out.println( " ATTRIBUTE: " +
attr.getLocalName(i) +
" VALUE: " +
attr.getValue(i) );
}
}
public void endElement( String namespaceURI,
String localName,
String qName ) throws SAXException {
System.out.println( "SAX Event: END ELEMENT[ " +
localName + " ]" );
}
public void characters( char[] ch, int start, int length )
throws SAXException {
System.out.print( "SAX Event: CHARACTERS[ " ];
try {
OutputStreamWriter outw = new OutputStreamWriter(System.out);
outw.write( ch, start,length );
outw.flush();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println( " )" );
}
public static void main( String[] argv ){
System.out.println( "Example1 SAX Events:" );
try {
// 建立SAX 2解析器...
XMLReader xr = XMLReaderFactory.createXMLReader();
// 安装ContentHandler...
xr.setContentHandler( new Example1() );
// 解析文件...
xr.parse( new InputSource(
new FileReader( "Example1.xml" )) );
}catch ( Exception e ) {
e.printStackTrace();
}
}
}
最后,就得到了运行第一个例子解析我们的XML样本文档所产生的输出:
Example1 SAX Events:
SAX Event: START DOCUMENT
SAX Event: START ELEMENT[ simple ]
ATTRIBUTE: date VALUE: 7/7/2000
SAX Event: CHARACTERS[
]
SAX Event: START ELEMENT[ name ]
SAX Event: CHARACTERS[ Bob ]
SAX Event: END ELEMENT[ name ]
SAX Event: CHARACTERS[
]
SAX Event: START ELEMENT[ location ]
SAX Event: CHARACTERS[ New York ]
SAX Event: END ELEMENT[ location ]
SAX Event: CHARACTERS[
]
SAX Event: END ELEMENT[ simple ]
SAX Event: END DOCUMENT
如你所见,SAX解析器会为每个在XML文档中出现的SAX事件调用正确的ContentHandler成员方法。
(未完待续)
本文地址:http://com.8s8s.com/it/it18159.htm