10.4 XML和JSON处理
对于部署在 Java EE 应用服务器上的 servlet 应用来说,它们的输出会在浏览器中显示,而返回给用户的数据几乎总是 HTML。本节涵盖了一些如何处理这些数据交换的最佳实践。
程序之间交换数据也可以使用应用服务器,特别是通过 HTTP。Java EE 支持多种基于 HTTP 的数据传输:成熟的 Web Service 使用 JAX-WS,RESTful 使用 JAX-RS,甚至你可以自己调用 HTTP。这些 API 的共同点是,它们都使用基于本文的数据传输(基于 XML 或 JSON)。虽然 XML 和 JSON 的数据呈现有很大的不同,但 Java 处理它们的方式是类似的,并且性能的考量点也是类似的。
这并不意味着两种呈现之间没有功能上的重要差别。一般来说,选择哪种呈现还应该依据算法和可编程性上的考虑,而不仅仅是性能。如果目的是与其他系统交互,那选择何种方式就取决于接口定义。对于复杂应用来说,处理 Java 对象通常要比遍历文档树要腰容易得多;这种情况下,JAXB(即采用 XML)是更好的选择,至少可以节省时间:Java EE 7 遵循 JSR 353(提供文档模型的标准解析)只支持 JSON-P。编写本书时,JSON-B JSR(JSON 中支持类似 JAXB 的特性)还没有得到认可(但将来可能会)。
除了上述差别之外,XML 和 JSON 还有其他重要差别。所以,本节比较两者性能的真实目的在于理解如何尽可能地获得最佳性能,而不是在特定环境下如何选择优化,无论选择的是哪种呈现。
10.4.1 数据大小
10.1 节“Web 容器的基本性能”显示了数据大小对整体性能的影响。在分布式网络环境中,数据大小是很重要的。关于这方面,通常都认为 JSON 比 XML 小,虽然差别通常不大。在本节的测试中,我从 eBay 请求获取最畅销的 20 件商品,并用 XML 和 JSON 返回。例子中的 XML 有 23 031 字节,JSON 比较小,只有 16 078 字节。但 JSON 数据之间没有空格,所以易读性差,不过可读性并不是目标,所以并不碍事。XML 则与之不同,结构明晰,有许多空格,去掉空格后可以缩减为 20 556 字节。不过与 JSON 相比,字节数仍然有 25% 的差别,绝大部分是因为 XML 元素的结束标记。通常来说,这些结束标记总会使输出的 XML 较大。值得注意的是,有许多网站可以将 XML 自动转换成 JSON。
样本数据有效负载
贯穿本节的样本数据来自 eBay。像许多公司一样,eBay 为开发人员提供接口,以便他们在自己的应用中使用。通常来说,数据以 XML 或 JSON 的格式获取。
比如,获取 eBay 上销量排行榜前 20 位商品的列表。下面是简化后的 XML 样本数据:
<xml version="1.0" encoding="UTF-8"?><FindPopularItemsResponse xmlns="urn:ebay:apis:eBLBaseComponents"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="urn:ebay:apis:eBLBaseComponents docs/xsd/ebay.xsd"><Timestamp>2013-03-29T01:57:46.530Z</Timestamp><Ack>Success</Ack><Build>E815_CORE_APILW2_15855352_R1</Build><Version>815</Version><ItemArray><Item><ItemID>140356481394</ItemID>……其他17个属性 ……</Item>…… 其他相同结构的19个元素 ……</ItemArray></FindPopularItemsResponse>JSON 数据与此类似(实际上它没有空格,此处只是为了增加可读性):
{"Timestamp":"2013-03-29T02:17:14.898Z","Ack":"Success","Build":"E815_CORE_APILW2_15855352_R1","Version":"815","ItemArray":{"Item":[{"ItemID":"140356481394", …… 其他17个属性 …… }],…… 其他相同结构的19个元素 ……},}
无论采用哪种格式传输,数据压缩都能带来巨大的好处。实际上,两种格式压缩之后的大小非常接近:JSON 压缩后的大小为 3471 字节,XML 压缩后的大小为 3742 字节。如此一来,数据大小上的差异就不那么重要了,而传递压缩数据也和传递其他压缩后的 HTTP 数据一样,都有好处。
快速小结
和 HTML 数据一样,程序中的数据也能从减少空格和压缩中获得巨大的益处。
10.4.2 解析和编组概述
给定一组 XML 或 JSON 字符串,程序必须将其转换成适合 Java 处理的数据。依据程序的上下文和输出结果,这个过程被称为编组(marshal)或解析。反过来——从数据生成 XML 或 JSON 串——则被称为解组(unmarshal)。
一般来说,处理这些数据涉及以下四种技术。
标识符解析器(Token parser)
解析器遍查输入数据中的标识符,当发现标识符时则回调相应对象上的方法。
拉模式解析器(Pull parser)
输入的数据与解析器关联,程序从解析器中请求(或拉取)标识符。
文档模型(Document model)
输入数据被转换成文档风格的对象,以便程序在查找数据片段时可以遍历。
对象呈现(Object representation)
通过与输入数据对应的预定义类,可以将数据转换成一个或多个 Java 对象(例如,可以用预定义的 Person 类来转换关于人的数据)。
虽然上述技术大体上按照性能从慢到快的顺序排列,但它们之间最主要的差别是功能而不是性能。前两种技术在功能上没有很大差别:它们都适用于大多数只需扫描一次就能提取信息的算法。不过解析器所能做的只是简单的扫描。解析器模式并不非常适合那种需要随机访问的数据,或者需多次遍历的数据。为了应对这些情况,使用简单解析器的程序应该构建内部的数据结构,虽然这只是个简单的编程问题,但文档对象模型和 Java 对象模型已经提供了结构化的数据,可能比你自己定义新结构要容易。
实际上,这就是使用解析器和数据编组之间的真实差别。前面两种是纯粹的解析器模式,取决于如何用解析器提供的方式处理数据逻辑。下面两项是数据编组器:它们必须使用解析器处理数据,但它们所提供的数据呈现可以用在更复杂的程序逻辑中。
所以,采用何种技术首先取决于应用是什么样的。如果程序只需要简单地过一遍数据,那么简单地使用最快的解析器是最有效的。如果数据需要保存为应用所定义的简单结构,那么直接使用解析器也是合适的;例如,示例数据中商品条目的价格需要保存为 ArrayList,以便应用的其他部分进行处理。
数据格式为重的时候,使用文档模型更合适。如果必须保留数据格式,那文档的格式转换就会很容易:数据读入后转成文档格式,用某种方法更改,然后可以很容易地写到新的数据流中。
为了尽可能便利,对象模型提供 Java 语言层面的数据呈现。数据可以通过对象及其属性的方式来操作。数据编组时所增加的复杂性(绝大部分)对开发人员透明,并且会使应用略微慢一些,但开发人员代码生产率上的提高可以抵消这个问题。
本节的示例读取有 20 个条目的 XML 或者 JSON 文档,并将条目 ID 保存到 ArrayList 中。对某些测试来说,只需要前 10 个条目。这是为了模拟真实世界里经常发生的事,即返回的数据总是超过实际所需要的数据。在 Web 服务的设计考量中,这是很好的点:调用的建立需要一些时间,用更少的远程调用(即便需要大量数据)而不是大量小的远程调用。
尽管所有的示例都展示了这类常见操作,但关键点并不在于直接比较这部分任务的性能。而是说,每个示例展示的是如何在所选择的框架下最有效地执行操作,因为框架的选择并不单是考虑解析和数据编组的性能。
快速小结
1. Java EE 应用中有很多办法处理程序所需要的数据。
2. 虽然这些技术给开发人员提供了很多功能,但数据处理本身的代价也增加了。不要因此影响你在应用中选择正确处理数据的方法。
10.4.3 选择解析器
编程中所用的所有数据都必须能被解析。对应用来说,选择直接使用解析器,还是通过序列化框架间接使用解析器,对于数据操作的整体性能至关重要。
1. 拉模式解析器
从开发者的角度来看,拉模式的解析器最容易使用。在 XML 的世界中,广为人知的拉模式解析器就是 StAX(Streaming API for XML)解析器。JSON-P 只提供拉模式解析器。
拉模式解析器依据需要从流中获取数据。本节测试所用的基本拉模式解析器的主逻辑就是下面的这个循环:
XMLStreamReader reader = staxFactory.createXMLStreamReader(ins);while (reader.hasNext()) {reader.next();int state = reader.getEventType();switch (state) {case XMLStreamConstants.START_ELEMENT:String s = reader.getLocalName();if (ITEM_ID.equals(s)) {isItemID = true;}break;case XMLStreamConstants.CHARACTERS:if (isItemID) {String id = reader.getText();isItemID = false;if (addItemId(id)) {return;}}break;default:break;}}
解析器返回一组 token。这个例子中的大多数 token 都会被丢弃。当遇到起始类型的 token 时,就会检查是不是 ITEM_ID。如果是,则下一个字符 token 就是应用所需要保存的 ID。ID 可通过 addItemId() 保存,如果成功保存 ID 则返回 true。一旦发生,循环就会返回,不再处理输入流中剩下的数据。
从概念上说,JSON 解析器的工作方式与此一模一样,只是有一些 API 调用上的变化:
while (parser.hasNext()) {Event event = parser.next();switch (event) {case KEY_NAME:String s = parser.getString();if (ITEM_ID.equals(s)) {isItemID = true;}break;case VALUE_STRING:if (isItemID) {if (addItemId(parser.getString())) {return;}isItemID = false;}continue;default:continue;}}
只处理必要的数据可以给性能带来可预见的好处。表 10-3 列出了解析样本文档的平均时间(毫秒),假设条件从解析 10 个条目后即可退出循环,到处理整个文档。解析 10 个条目后退出并没有节约 50% 的时间(因为文档的其他段落也需要解析),但差别还是很显著的。
表10-3:拉模式解析器的性能
| 处理的条目数 | XML解析器(毫秒) | JSON解析器(毫秒) |
|---|---|---|
| 10 | 143 | 68 |
| 20 | 265 | 146 |
2. 推模式解析器
标准的 XML 解析器是 SAX(Simple API for XML)解析器。SAX 解析器是一种推模式解析器:读入数据,当发现 token 时,就会执行类中处理该 token 的回调方法。下面测试中的解析逻辑与之前相同,不过现在逻辑放在了类所定义的回调方法中:
protected class CustomizedInnerHandler extends DefaultHandler {public void startElement(String space, String name,String raw, Attributes atts) {if (name.length() == 0)name = raw;if (name.equalsIgnoreCase(ITEM_ID))isItemID = true;}public void characters(char[] ch, int start,int length) throws SAXDoneException {if (isItemID) {String s = new String(ch, start, length);isItemID = false;if (addItemId(s)) {throw new SAXDoneException("Done");}}}}
这里程序逻辑上唯一的差别是,必须以抛出异常的方式来通知解析完成了,因为这是 XML 推模式解析框架检测到解析应该停止的唯一方法。这个例子中,应用所抛出的异常是 SAXDoneException。一般来说,任何 SAXException 都可以抛出,这个例子中使用的是该异常的子类,使得程序其他部分的逻辑可以区分哪个是实际错误,哪个是通知解析终止的信号。
SAX 解析器比 StAX 快,虽然性能上的差别很小——选择哪种解析器应该取决于开发中哪种模型更容易。表 10-4 展示了推模式解析器和拉模式解析器在处理时间上的差异。
表10-4:推模式解析器的性能
| 条目数 | XML StAX解析器(毫秒) | XML SAX解析器(毫秒) |
|---|---|---|
| 10 | 143 | 132 |
| 20 | 265 | 231 |
JSON-P 没有相应的推模式解析器模型。
3. 其他解析机制的实现和解析器工厂
XML 和 JSON 规范定义了解析器的标准接口。JDK 提供了 XML 解析器的参考实现,JSON-P 项目则提供了 JSON 解析器的参考实现。应用可以使用任意解析器(当然,只要该解析器实现了所需要的接口)。
解析器是从解析器工厂中获得的。将解析器工厂设置成返回所需解析器的实例(而不是默认解析器),就可以使用不同的解析器实现。这其中隐含着某些性能问题。
工厂初始化的代价昂贵:确保可以通过全局(或至少是线程本地变量)引用的方式重用工厂。
工厂可通过多种不同的方式进行配置,其中一些配置(包括默认配置)从性能的角度来看并不是最优的。
其他的解析器实现可能比默认的更快。
工厂和解析器的重用
XML 和 JSON 解析器工厂的创建代价很高。幸运的是,工厂是线程安全的,所以很容易保存在全局静态变量中,可以在需要的时候重用。
但一般来说,解析器无法重用,也不是线程安全的。因此,解析器通常是因需而建。
SAX 解析器的一个优点是可以重用解析器对象。重用时,只需要在使用解析器之前调用
reset()方法即可。不过解析器仍然不是线程安全的,所以务必确保同一时间只在单个线程中重用解析器。
让我们依次来看上述几点。
为求平均的解析速度,这些测试解析了 1 百万次数据(10 000 次预热解析之后)。下面的示例代码确保只构造一次工厂,在测试开始时调用的初始化方法中完成。每轮测试中的解析器实例则由工厂因需而创建。由此,SAX 测试包含的代码如下所示:
SAXParserFactory spf;// 只在程序初始化时调用一次protected void engineInit(RunParams rp) throws IOException {spf = SAXParserFactory.newInstance();}// 每轮迭代时调用protected XMLReader getReader() Throws SAXException {return spf.newSAXParser().getXMLReader();}
StAX 解析器与此类似,调用 XMLFactory.newInstance() 获得工厂(类型为 XMLInputFactory),然后调用工厂的 createStreamReader() 方法获得 StreamReader。对于 JSON,相应的调用方法是 Json.createParserFactory() 和 createParser()。
如果要用另一种解析器实现,我们必须用另一种工厂,才能使工厂的调用返回所需要的实现。这就是关于工厂配置的第二点:确保所用的工厂是经过优化设定的。
可以通过 3 种方法设定 XML 工厂。此处所用的工厂(javax.xml.stream.XMLInputFactory)默认设定的是 StAX 解析器。为了覆盖默认的 SAX 解析器,需设置成 javax.xml.parsers.SAXParserFactory。
为确定使用的是哪种工厂,需要按以下顺序查找选项。
使用由系统属性
-Djavax.xml.stream.XMLInput Factory=my.factory.class指定的工厂。JAVA/jre/lib 下的文件 jaxp.properties 内所指定的工厂,类似这样一行:
javax.xml.stream.XMLInputFactory=my.factory.class
在 classpath 上搜索文件 META-INF/services/javax.xml.stream.XMLInputFactory。该文件需要包含单独的一行
my.factory.class。使用 JDK 定义的默认工厂。
上面第 3 种方法有明显的性能问题,特别是在环境设置了很长的 classpath 的时候。为了查看某个备选的实现是否已被设定,必须扫描整个 classpath,搜索每个入口中的 META-INF/services 目录下的特定文件。而且,每次创建工厂时都会重复这个查找过程。所以,如果类加载器没有缓存资源的查找结果(大多数类加载器没有缓存),初始化工厂的代价就非常高。
更好的做法是用前两种办法配置应用。系统会依上述列表的顺序查找工厂,一旦找到,搜索过程就会停止。
这两种方法的不足之处在于,它们是全局的,会影响应用服务器上的所有代码。如果两个不同的企业应用部署到了同一个服务器上,并且需要不同的解析器工厂,那服务器就要必须依靠在 classpath 上搜索工厂的技术了(可能会很慢)。
发现解析器工厂的方法甚至还影响了默认工厂:JDK 必须要搜索完 classpath 后才知道使用默认工厂。因此,即便你使用默认工厂,你也应该通过配置全局系统属性或 Java 运行时环境(JRE)属性文件来指向默认实现。否则,只有在第 3 步花费了昂贵代价搜索之后,才会使用默认工厂(列表中的第 4 项)。
对 JSON 来说,配置有少许不同:指定其他实现的唯一办法是,在 META-INF/services 下指定一个名为 javax.json.spi.JsonProvider 的包含新 JSON 解析器实现类的类名的文件。不幸的是,查找 JSON 工厂时,没有办法避免搜索整个 classpath。
选择解析器的最后一个性能考量点是备选实现的性能。本节只是对一些解析器实现性能的快速浏览,没有必要在意表面上的结果。不同的实现之间总有差异。就性能而言,不同实现之间可能各有千秋。某些时候,备选实现会比参考实现快(到 JDK 新的发布版或者新的 JSON-P 参考实现时,参考实现可能就会超过备选实现)。
比如说,在编写本书时,Woodstox 的 StAX 解析器(http://woodstox.codehaus.org/)就比 JDK 7 和 8 所带的解析器要快一些(见表 10-5)。
表10-5:StAX解析器的性能
| 数据条数 | JDK StAX解析器(毫秒) | Woodstox StAX解析器(毫秒) |
|---|---|---|
| 10 | 143 | 125 |
| 20 | 265 | 237 |
而 JSON 解析器的状况要混乱得多。在编写本书时,JSON-P 规范的最终稿刚刚制定好,但还没有 JSR 353 兼容的 JSON 解析器实现。对于其他 JSON 解析器来说,最后遵循 JSR 353 API 只是一个时间问题。
具体应视情况而定,所以查找其他 JSON 实现,看看它们是否有更好的性能不失为一个好主意。一种实现是 Jackson JSON 处理器(目前不兼容 JSR 353),它已经实现了基本的拉模式解析器(准确来说,并不是 JSR 353 的 API 调用)。参见表 10-6。
表10-6:JSON解析器的性能
| 数据条数 | Java EE JSON解析器(毫秒) | Jackson JSON解析器(毫秒) |
|---|---|---|
| 10 | 68 | 40 |
| 20 | 146 | 74 |
新的 JSR 参考实现通常也是这样,就像 JDK 7 XML 解析器比前一个版本快很多一样,新的 JSON-P 解析器的性能预计也会有巨大的提升。(实际上,本节测试所用的 JSON 解析器版本为 1.0.2,比初始的 1.0 版本快约 65%。)
快速小结
1. 选择的解析器是否合适,对应用的性能有巨大的影响。
2. 推模式解析器通常比拉模式解析器快。
3. 查找解析器工厂的算法非常耗时;如果可能的话,应该通过系统属性直接指定工厂而不是用现有的实现。
4. 在不同的时间点上,最快的解析器实现的赢家可能会不同。适当的时候,应该从备选的解析器中找。
10.4.4 XML验证
解析器可依据一个 schema(意为“模式”)对 XML 数据进行验证,拒绝语法不正确的文档——指缺少某些必要的信息,或者包含了不该有的信息的文档。此处所说的“语法正确”是指文档内容,如果文档有语法错误(比如文档没有包含在 XML 标签中,或者缺少 XML 闭合标签等),所有解析器都不会接收该文档。
这种验证是 XML 相比 JSON 所具有的一个优点。解析 JSON 文档时你可以自己提供验证逻辑,但解析 XML 时,解析器能替你做这些验证。但这个好处是有性能代价的。
XML 验证是依据一个或多个 schema 或 DTD 文件进行的。虽然 DTD 的验证更快,但 XML schema 更灵活,现在是 XML 世界中的主流。schema 比 DTD 慢的一个原因是,schema 通常在多个文件中设定。所以减少验证成本的第一个方法就是整合 schema 文件:schema 文件越多,验证的代价越高。需要在多个文件的可维护性和性能收益之间进行权衡。不幸的是,由于 schema 文件维护了不同的命名空间,所以整合起来并不容易(就像 CSS 或 JavaScript 文件那样)。
从何处装载 schema 文件对性能有极大的影响。如果必须反复从网络上装载 schema 或 DTD,性能就会变糟糕。理想情况下,schema 文件应该随着应用代码一起分发,这样就能从本地文件系统装载了。
对常见的 SAX 验证来说,只需要用代码为 SAX 解析器工厂设置一些属性即可(这只对 SAX 解析器有效;对 StAX 解析器而言,除非使用本节后面讨论的 Validator 对象,否则验证依据的是 DTD 文件而不是 schema)。
SAXParserFactory spf = SAXParserFactory.newInstance();spf.setValidating(true);spf.setNamepsaceAware(true);SAXParser parser = spf.newSAXParser();// 注意:创建解析器时可以执行上面的几行代码// 如果重用该解析器,而不是调用parser.reset(),则需要设置属性parser.setProperty(JAXPConstants.JAXP_SCHEMA_LANGUAGE,XMLConstants.W3C_XML_SCHEMA_NS_URI);XMLReader xr = parser.getXMLReader();xr.setErrorHandler(new MyCustomErrorHandler());
解析器默认没有验证,所以得先调用 setValidating(),然后设置属性,将验证所依据的语言告诉解析器——本例中为 W3C XML schema 语言(例如 XSD 文件)。最后,设置解析器在验证出错时的处理程序。
这种处理方式——XML 文档的默认处理方式——会在每次解析新文档时重读 schema,即便解析器本身已经重用。为了更好的性能,可以考虑重用 schema。
即便从文件系统装载 schema,重用 schema 也会有很大的好处。装载 schema 时,必须解析和处理它(毕竟它自己也是 XML 文档)。保留处理结果并重用可以极大地提升 XML 的处理效率。这在绝大多数应用场景下都是正确的:应用接收和处理成千上万的 XML 文档,而所有这些文档都遵循相同的一个(或一组)schema。
重用 schema 的方法有两种,第一种(只对 SAX 解析器有效)是创建 schema 对象并与 SAXParserFactory 关联:
SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);StreamSource ss = new StreamSource(rp.getSchemaFileName());Schema schema = sf.newSchema(new Source[]{ss});SAXParserFactory spf = SAXParserFactory.newInstance();spf.setValidating(false);spf.setNamespaceAware(true);spf.setSchema(schema);parser = spf.newSAXParser();
请注意,这个例子中调用 setValidating() 时参数为 false。setSchema() 和 setValidating() 是两种互相矛盾的文档验证方法 1。
1因为 setValidating() 只包括 DTD 的验证。——译者注
重用 schema 对象的第二种方法是使用 Validator,将解析与验证分离,使得两种操作可以在不同时间进行。用 StAX 解析器解析时,可以在流的验证过程中嵌入特定的 reader 来进行验证。
使用 Validator 时,首先要创建特定的 reader。reader 的处理逻辑和之前相同:查找起始元素(start element)itemID,找到之后保存这些 ID。不过,这些操作必须委托给默认的 StAX 流 reader:
private class MyXMLStreamReader extends StreamReaderDelegate() {XMLStreamReader reader;public MyXMLStreamReader(XMLStreamReader xsr) {reader = xsr;}public int next() throws XMLStreamException {int state = super.next();switch (state) {case XMLStreamConstants.START_ELEMENT:……处理起始元素Item ID……break;case XMLStreamConstants.CHARACTERS:……如果是item id,则保存当前的字符。break;}return state;}}
接下来,将这个 reader 与 Validator 所用的输入流关联。
SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);StreamSource ss = new StreamSource(rp.getSchemaFileName());Schema schema = sf.newSchema(new Source[]{ss});XMLInputFactory staxFactory = XMLInputFactory.newInstance();staxFactory.setProperty (XMLInputFactory.IS_VALIDATING, Boolean.FALSE);XMLStreamReader xsr = staxFactory.createXMLStreamReader(ins);XMLStreamReader reader = new MyXMLStreamReader(xsr);Validator validator = schema.newValidator();validator.validate(new StAXSource(new StaxSource(reader)));
validate() 在进行常规验证时还会调用 StreamReaderDelegate(此处为 MyXMLStreamReader),它会从输入数据中解析所需要的信息(实质上是验证过程的意外收获)。
这种方法的不足之处在于,处理过程无法在读取若干条数据之后干净利索地终止。next() 依旧可以在抛出异常之后捕获该异常——就像前面的 SAXDoneException。此处有个问题,即默认的 schema 监听器在处理遇到异常时会打印错误信息。
表 10-7 列出了所有这些操作的影响。与简单的解析(不进行验证的)相比,带有默认验证的解析会付出更大的性能代价。重用 schema 可以弥补一些性能损耗,并能确保 XML 文档是合乎语法规范的,但验证总会有些显著的代价。
表10-7:XML文档验证的性能
| 解析模式 | SAX(毫秒) | StAX(毫秒) |
|---|---|---|
| 无验证 | 231 | 265 |
| 默认验证 | 730 | N/A |
| 重用 schema 的验证 | 649 | 1392 |
快速小结
1. 如果业务需要进行 schema 验证,那就用它。只是要留意,验证对解析数据的性能会带来显著的损耗。
2. 总是重用 schema,以将验证对性能的影响降至最低。
10.4.5 文档模型
构建文档对象模型(Document Object Model,DOM)或 JSON 对象只需要一组相对简单的方法调用。对象本身是随着底层的解析器而创建的,所以优化性能的一个重要举措是配置好解析器(DOM 默认使用 StAX 解析器)。
DOM 对象是随着 DocumentBuilder 对象而创建的,而 DocumentBuilder 来自 DocumentBuilderFactory。默认的 DocumentBuilderFactory 可通过属性 javax.xml.parsers.DocumentBuilderFactory(或者文件 META-INF/services/javax.xml.parsers.DocumentBuilderFactory)来指定。由于创建解析器时所配置的属性对性能优化很重要,所以创建 DocumentBuilder 时配置的系统属性也就变得很重要了。
和 SAX 解析器一样,只要在使用前调用 reset() 方法就能重用 DocumentBuilder 对象。
JSON 对象是随着 JsonReader 对象而创建的,而 JsonReader 可直接来自 Json 对象(通过调用 Json.createReader())或来自 JsonReaderFactory 对象(通过调用 Json.createJsonReaderFactory())。虽然 JSR 353 RI 现在还没有支持任何配置选项,但通过属性 Map 可以配置 JsonReaderFactory。JsonReader 对象不能重用。
如表 10-8 所示,与简单解析数据相比,DOM 的创建代价比较高。
表10-8:DOM与JSON解析的性能对比
| 操作 | XML(毫秒) | JSON(毫秒) |
|---|---|---|
| 解析数据 | 265 | 146 |
| 构建文档 | 348 | 197 |
构建文档的时间包括解析的时间加上创建文档对象结构的时间——所以从这张表可以推算出,对 XML 而言构建文档结构的时间大约占了总时间的 33%,对 JSON 而言则占了 25%。对更复杂的文档来说,构建文档模型所占用的时间百分比会更大。
之前的解析测试有时候可能只关心前 10 个条目。如果对象展现类似地也包括这前 10 个元素,那就有两种选择。首先创建对象,调用各种方法遍历对象,并丢弃不需要的条目。这是 JSON 对象唯一的选择。
DOM 对象可以用 DOM 级别 3 的属性建立过滤解析器。首先要创建一个解析过滤器:
private class InputFilter implements LSParserFilter {private boolean done = false;private boolean itemCountReached;public short acceptNode(Node node) {if (itemCountReached) {String s = node.getNodeName();if ("ItemArray".equals(s)) {return NodeFilter.FILTER_ACCEPT;}if (done) {return NodeFilter.FILTER_SKIP;}// 我们不需要元素</Item>if ("Item".equals(s)) {done = true;}}return NodeFilter.FILTER_ACCEPT;}public int getWhatToShow() {return NodeFilter.SHOW_ALL;}public short startElement(Element element) {if (itemCountReached) {return NodeFilter.FILTER_ACCEPT;}String s = element.getTagName();if (ITEM_ID.equals(element.getTagName())) {if (addItemId(element.getNodeValue())) {itemCountReached = true;}}return NodeFilter.FILTER_ACCEPT;}}
每个元素会调用两次解析过滤器:解析元素开始时,调用 startElement(),结束元素结束时,调用 acceptNode()。如果元素不应在最终的 DOM 文档中出现,就应在上述两个方法之一中返回 fiLTER_SKIP。在这个例子中,startElement() 用来追踪有多少条目已经被处理了,而 acceptNode() 则用来判定是否整个元素应该被跳过。请注意,acceptNode() 中也需要追踪结尾的 标签,以免跳过。也请注意,只有类型 ItemArray 的子节点元素才会被跳过,XML 文档的其他元素则不应该被跳过。
为了设定输入过滤器,可以使用以下代码:
System.setProperty(DOMImplementationRegistry.PROPERTY,"com.sun.org.apache.xerces.internal.dom.DOMImplementationSourceImpl");DOMImplementationRegistry registry =DOMImplementationRegistry.newInstance();DOMImplementation domImpl = registry.getDOMImplementation("LS 3.0");domLS = (DOMImplementationLS) domImpl;LSParser lsp = domLS.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS,"http://www.w3.org/2001/XMLSchema");lsp.setFilter(new InputFilter());LSInput lsi = domLS.createLSInput();lsi.setByteStream(is);Document doc = lsp.parse(lsi);
最后创建 Document 对象,如同它没有过滤输入——不过在这个例子中,结果文档要小得多。这是过滤的关键点:在实际的解析和过滤过程中,生成过滤的文档所花费的时间比生成包含所有原始信息的文档花费的时间要多。因为文档占用的内存更小,且能减少对垃圾收集器的压力,所以这在文档需要长时间存活(或者有许多这样的文档在使用)时很有用。
表 10-9 显示的是构造只有一半(10 个)条目的 DOM 对象时,解析一般的 XML 文件的速度差异。
表10-9:过滤DOM文档所造成的影响
| 标准DOM | 过滤DOM | |
|---|---|---|
| 创建 DOM 所用时间 | 348 毫秒 | 417 毫秒 |
| DOM 的大小 | 101 440 字节 | 58 824 字节 |
快速小结
1. 使用 DOM 和
JsonObject比用简单解析器要强大得多,但构造模型所花的时间长度会很显著。2. 过滤模型数据比构造默认模型要花费更多的时间,但对于长时间运行或者很大的文档来说,仍然是值得的。
10.4.6 Java对象模型
处理文本数据的最后一种选择是在解析相关的数据之后创建一组 Java 类实例。JSR 有此类的 JSON 建议(proposal),但没有标准。对 XML 来说,这是通过 JAXB 来实现的。
JAXB 底层用的是 StAX 解析器,所以为你的平台选择最佳的 StAX 解析器配置有助于提高 JAXB 的性能。JAXB 通过创建 Unmarshaller 对象来创建 Java 对象:
JAXBContext jc = JAXBContext.newInstance("net.sdo.jaxb");Unmarshaller u = jc.createUnmarshaller();
创建 JAXBContext 的代价比较昂贵。幸运的是,它是线程安全的:可以创建单个全局的 JAXBContext,然后重用(在多个线程间共享)。但 Unmarshaller 对象不是线程安全的,所以每个线程必须创建一个新对象。不过 Unmarshaller 对象可以重用,所以将它保存在线程本地变量中(或者对象池中),将有助于提高处理大量文档时的性能。
通过 JAXB 创建对象的代价要比解析或创建 DOM 文档的代价昂贵。但权衡下来,使用这些对象还是要比遍历文档快得多(甚至,使用对象就仅仅是写常规的 Java 代码,而不是用错综复杂的 API 来访问)。此外,依据 JAXB 文档编写 XML 要比直接从文档编写 XML 快得多。表 10-10 显示了性能差异,示例文档有 20 个条目。
表10-10:JAXB编组和解组的性能
| 编组(毫秒) | 解组(毫秒) | |
|---|---|---|
| DOM | 348 | 298 |
| JAXB | 414 | 232 |
过滤 XML 和 JAX-WS
即便使用 JAXB,你需要处理的通常也只是部分 XML 数据。而一般来说,JAX-WS 会基于 JAXB 将整个 XML 转换成 Java 对象。从易用角度来看,这种方法很好,它使得应用代码更容易编写和维护。但是,如果只需要访问部分 XML,用 JAXB 处理整个文档就太奢侈了,并且所有这些 JAXB 对象会消耗太多堆内存。
在这个例子中,XML 数据应该作为 SOAP 消息的附件发送(MIME 类型为 application/xml)。附件不会被转换成 JAXB 对象,你可以用 DOM builder 过滤,或者用简单的 StAX 解析器只处理你所关心的文档部分。
快速小结
1. JAXB 将 XML 文档生成 Java 对象,以最简单的编程模型访问和使用数据。
2. 创建 JAXB 对象的代价比创建 DOM 对象的昂贵。
3. JAXB 写 XML 数据的速度要快于 DOM 对象。
快速小结