本系列文章的前两部分中,我们讨论了改进 XML 应用程序性能的一般技巧:如何正确地编写 XML 文档、重用解析器、更好地利用 SAX 和 DOM API以及使用 Xerces 的技巧。本文中将介绍如何通过使用 Xerces 本机接口(XNI)和设置 Xerces2 的特性和属性来实现应用程序性能的优化。我们还将讨论如何使用 Xerces2 来缓冲 XML 模式。
要理解如何通过 Xerces2 获得较好的性能,您需要对 Xerces2 和 XNT 有一些了解。因此本文将简要介绍 XNI,说明它和 SAX 的差别以及如何利用 XNI 加快程序执行速度。如果希望进一步了解 XNI,请阅读 Apache Xerces2 XNI 手册(请参阅 参考资料)。
Xerces2 的内部基础是 Xerces 本机接口(XNI),这种框架可以像 SAX 那样交换流文档信息,也用于构建通用解析程序及其组件。XNI 公布差不多有两年了,一直很稳定,估计将来也不会有大的变化。
从理论上讲,其他解析器也可以实现 XNI 接口,但是 Xerces2 是现有惟一支持该接口的解析器。因此,XNI 仍然是 Xerces2 解析器用于在不同组件之间通信的一种内部 API 框架。
在 Xerces2 中,SAX 和 DOM 解析器都包含 XNI 解析器配置,它定义了设置解析器的特性和属性以及启动 XML 文档解析的入口点。典型的解析器配置由一组 组件构成。其中一些组件可能串接在一起,形成 解析管道,每个组件都使用 XNI 文档处理程序接口与下一个组件通信。SAX 和 DOM 解析器连接到管道的最后一个组件,负责从收到的 XNI 流生成相应的 API(SAX 事件和 DOM 树)。
Xerces2 框架把解析管道中组件的配置和 API 生成代码分离开,从而使同一个 API 生成解析器可用于无数种不同的解析器配置,同一个解析器配置也在不同的 API 生成解析器中重用。
应用程序的性能可能会受到所选择的解析器配置的影响。默认的解析器配置(如 Xerces 2.6.2 中)支持 XML 1.0/ 1.1、Namespaces in XML 1.0/ 1.1 以及 DTD 和 W3C XML Schema 验证。如果应用程序不需要验证,使用非验证解析器配置(
org.apache.xerces.parsers.NonValidatingConfiguration )能够获得更好的性能。
完全不需要编写任何代码或者改变原来的解析器类,就可以改写 Xerces2 解析器所用的默认解析器配置。只需要使用下面这些机制之一:
- 将
org.apache.xerces.xni.parser.XMLParserConfiguration系统属性指向要使用的配置。 - 在应用程序的
JAR META-INF/services/目录中添加org.apache.xerces.xni.parser.XMLParserConfiguration文件。该文件需要包含解析器配置的类名。只要包含该文件的 JAR 文件出现在 Xerces 的 JAR 文件之前,解析器就会使用新的解析器配置。
如前所述,XNI 提供了与 SAX API 类似的框架。它也是一种基于事件的 API,解析事件(如元素的开始和结束)提交到 XNI 组件并通过回调处理程序传递给应用程序。
XNI 处理程序接口被设计成提供 XML Information Set 规范定义的所有文档信息。处理程序接口也可提供其他的信息,比如 Post-Schema Validation Infoset (PSVI) 使用一种称为 XNI
Augmentations 的结构。XNI Augmentaions 被表示成每个处理程序方法的参数。SAX 2.0.1 以及以前的 SAX 版本没有报告全部的文档信息。比如,以前的 SAX 没有用于检索解析文档的编码的方法。随着 2004 年初 SAX 2.0.2 的发布,现在可以报告 XML Information Set 中的所有信息了。现在可以通过
Locator2 接口检索编码信息。
如果您的 SAX 应用程序总是在 Xerces2 支持的环境中运行,直接使用 XNI 编程会带来很大好处。这样不仅能访问更多的信息,而且还可以改善应用程序的性能。 第 2 部分中已经提到,Xerces2 的 SAX 解析器有时候必须执行额外的处理,比如为了符合 SAX 规范,必须从解析器的内部表示中修改或者去掉名称空间声明属性。如果使用 XNI 就可以避免这种额外操作造成的代价。
与 SAX 相比,XNI 的设计对文档信息集的访问进行了更多的性能优化。要访问 SAX 中的一些信息,必须将对象强制转化成不同的接口,这样就带来了性能上的开销。比如,要查询一个属性是在文档中规定的还是使用 DTD 中的默认值,需要将
org.xml.sax.Attributes 强制转换成
org.xml.sax.ext.Attributes2 接口。要访问 XML 文档的编码和版本,则必须将
ContentHandler.setDocumentLocator(Locator) 提供的 locator 对象强制转换成
Locator2 接口。如果使用 XNI,XML 文档的编码和版本都通过
org.apache.xerces.xni.XMLDocumentHandler.xmlDecl 方法提供给应用程序。
在 SAX 中,对于每个名称空间声明,应用程序都会接收到
startPrefixMapping 调用和相应的
endPrefixMapping 调用。这意味着如果 XML 文档中的名称空间声明比较多,就会有大量额外的回调从而降低应用程序的速度。为了避免这种情况,XNI 用一个对象(
org.apache.xerces.xni.NamespaceContext )来提供名称空间信息,并通过 XNI 文档处理程序的
startDocument 方法传递给应用程序。如果以后需要访问当前名称空间信息,应用程序负责保存这个对象。
直接使用 XNI 还有其他理由吗?比方说,如果应用程序需要合并两个 XML 文档然后使用 XML Schema 验证结果文档,可以创建一个新的 XNI 组件截获 XNI 文档处理程序事件并合并两个文档。然后您可以创建一个新的配置(要保证该组件的构造函数是公共的,并且不带参数)将这个组件插入解析管道的 XML Schema 验证程序之前。否则就需要创建文档中间表示的内存结构、在内存中合并两个文档、序列化结果文档然后请求解析器再次解析并验证文档,在这里使用 XNI 可以帮助您避免反复的操作。
这一节讨论 Xerces2 解析器特有的性质和属性,以及如何设置这些性质和属性来提高应用程序的性能。
在解析器内部,文档解析的时候内存中开辟一些缓冲区保存文档块。默认情况下,阅读器缓冲区的大小是 2 KB。这意味着,一次最多从输入流中读入 2 KB。一些性能测试表明,解析大于 2 KB 的文档时增加输入缓冲区的大小能够提高性能。Xerces 2.1.0 中引入了一个用 URI
http://apache.org/xml/properties/input-buffer-size 标识的属性,允许根据 XML 文档的大小调整输入缓冲区的大小。该属性的值是
java.lang.Integer 的实例,单位是字节。
对于小型文档(一般小于 4 KB),使用默认缓冲区大小就能取得很好的性能。对于较大的文档,可以将该属性设置在 4 KB 到 8 KB 之间以便获得最佳性能。如果解析的文档小于 2 KB,也可以选择小于 2 KB 的输入缓冲区大小。对于大型文档,选择较大缓冲区带来的好处在超过 8 KB 之后反而下降,这也与传递给解析器的输入源的类型有关。
第 1 部分 中,我们说明了如何使用 SAX 定义的标准特性来配置解析器,以避免处理外部通用实体和参数实体。我们曾经说过,根据 XML 规范,验证处理程序(如 Xerces2)必须处理外部和内部 DTD 子集。Xerces2 定义了一个用 URI
http://apache.org/xml/features/nonvalidating/load-external-dtd 标识的特性,控制如果文档有外部 DTD 子集是否要将其读入。如果应用程序不需要读入那个外部子集,可以将该特性设为
false 以提高性能。以下情况下需要小心使用该特性:
- 应用程序需要查询属性类型。
- 应用程序对于解析器处理非 CDATA 属性时压缩空白敏感。
- 应用程序需要将元素内容中的空白报告成可忽略的。
如果启用验证,该特性的值将被忽略,这种情况下总是会读入外部 DTD 子集。
如果您熟悉 Xerces2 用于 XML 模式验证的设置,一定曾经注意到影响模式验证的两个特性,分别用 URI
http://apache.org/xml/features/validation/schema 和
http://apache.org/xml/features/validation/schema-full-checking 标识。当第一个特性设为
true 时启用模式验证。W3C XML 模式规范中定义的一些约束,检查起来非常耗时而且需要较大的内存空间。其中包括粒子派生(particle derivation)和惟一粒子属性(unique particle attribution)约束。第二个特性,当与第一个特性同时设为
true 时,将激活这类复杂约束的检查。默认情况下,
schema-full-checking 特性的值是
false 。如果您在开发自己的模式语法,应该将该特性设为
true ,让 Xerces2 检查所有必要的约束,确定模式是否有效。而在准备部署您的应用程序时,则应将该特性设为
false 。即使已经使用了下面所述的语法缓冲机制,避免对那些开销很大的约束进行检查也有助于提高应用程序的性能。同时应注意,禁用
schema-full-checking 特性不会影响到对实例文档的检查级别,只对模式语法本身的那些开销最大的约束有影响。
W3C XML Schema 规范定义了 XML 信息集的补充,称为 Post-Schema-Validation Infoset (PSVI)。PSVI 是作为模式断言和验证的结果生成的,包含诸如用于验证元素的类型、元素和属性的模式规范化值等属性。为了公开这些信息,Xerces2 实现了一种 XML Schema API,可通过 SAX 和 DOM 访问 PSVI。如果应用程序只能使用 JAXP 规定的 API,您就无法访问解析器报告的 PSVI。Xerces2 定义了一个用 URI
http://apache.org/xml/features/validation/schema/augment-psvi 标识的特性,规定是否在模式验证期间生成 PSVI。如果不打算从 PSVI 中读取信息,可以将该特性设为
false ,避免生成 PSVI,这样可以提高应用程序执行模式验证时的性能。
目前,很多应用程序都需要针对模式(如 DTD 或者 W3C XML Schema)来验证 XML 文档。验证的代价很高,因为解析器不仅要解析 XML 文档还要访问提供的模式,然后解析并构建模式的某种内部表示。为了简化起见,我们把预先解析模式并建立模式内部表示的过程称为 编译模式。然后解析器使用编译后的模式验证 XML 文档。
如果应用程序有一组有限的模式用于验证 XML 文档,可以考虑编译和缓冲模式,这样能够显著改善应用程序的性能。尤其是如果应用程序处理的多数 XML 文档都相对较小(小于 2 KB),模式编译可能要占用 XML 文档整个处理的一半时间。
要缓冲模式,需要一种允许您编译模式并将模式设置在解析器上的 API。不好的消息是,在 JAXP 1.3 最后定稿之前,还没有能完成这一任务的标准 API。JAXP 1.3 规范定义了一种简单的 API,允许应用程序编译和缓冲模式。但是目前如果希望缓冲模式以提供应用程序的性能,您只能使用 Xerces2 专有的 API。
默认情况下,Xerces2 解析器不对模式进行缓冲(按照 Xerces 术语,模式被称为
语法)。为了优化应用程序性能,Xerces2 定义了 XNI Grammar API(
org.apache.xerces.xni.grammars ),它允许您编译模式、创建包含这些模式的语法池并向编译器注册语法池。
多数情况下,应用程序并不需要使用完整的 XNI Grammar API 来缓冲模式。相反,您可以使用默认的 Xerces2 语法缓冲实现(
org.apache.xerces.util.XMLGrammarPoolImpl ),该实现允许您被动地编译和缓冲模式。下面是这种语法缓冲实现的原理:
- Xerces2 解析器开始解析 XML 文档。
-
验证开始之前,Xerces 验证程序(无论是 XML Schema 验证程序还是 DTD 验证程序)调用
org.apache.xerces.xni.grammars.XMLGrammarPool接口的retrieveInitialGrammarSet(String)方法,从注册的语法池中检索一组编译过的模式。这些模式保存在验证程序的 语法桶中,需要的时候在验证中使用。 - 在解析过程中,如果验证程序需要新的模式,它首先检查语法桶中是否包含需要的编译后的模式。如果使用 XML Schemas,语法桶中的模式以
targetNamespaceURI 作为关键字,DTD 则以根元素作为关键字。 - 如果语法桶中没有编译过的模式,验证程序使用
retrieveGrammar(XMLGrammarDescription)方法,要求注册的语法池提供需要的模式。如果语法池没有返回模式,验证程序尝试使用注册的实体解析程序(如org.xml.sax.EntityResolver)或者默认的实体解决方案来解析模式。 - 解析结束的时候,验证程序使用
cacheGrammars(String, Grammar[])方法将语法桶中的所有编译模式返回到注册语法池中。这些编译模式保存在语法池中,在后续的解析中提供给 Xerces2 验证程序作为初始语法(如 第 2 步所述)。
因此,使用这种默认实现就不需要事先编译模式。换句话说,应用程序启动的时候语法池是空的。解析器对验证需要的任何新模式进行编译,并在 XML 文档解析完成之后将编译过的模式交给语法池。因此,最初几个 XML 文档的处理可能比较慢。但是如果要用的模式个数有限,随着不断解析新的 XML 文档,语法池中最终将包含验证 XML 文档所需要的全部编译好的模式。这个时候,解析器就不需要再编译任何新的模式,后面的 XML 文档验证时也就不再需要模式编译所花费的性能代价了。
激活 Xerces2 默认的模式缓冲实现有两种方法:
- 指定语法缓冲解析器配置(
org.apache.xerces.parsers.XMLGrammarCachingConfiguration)。 - 使用 Xerces2 属性(URI
http://apache.org/xml/properties/internal/grammar-pool)在解析器上设置默认的语法池实现(org.apache.xerces.util.XMLGrammarPoolImpl)。
第 2 部分中曾经提到,如果预计在某种环境下应用程序的类路径中包含 Xerces2,而且应用程序使用的模式有限,则应尝试激活语法缓冲(如 清单 1所示),这样如果能够使用 Xerces2 解析器,应用程序就会取得较好的性能。
清单 1. 激活模式缓冲
...
XMLReader parser = XMLReaderFactory.createXMLReader();
try {
Class poolClass =
Class.forName("org.apache.xerces.util.XMLGrammarPoolImpl");
Object grammarPool = poolClass.newInstance();
parser.setProperty(
"http://apache.org/xml/properties/internal/grammar-pool",
grammarPool);
}
catch (Exception e) {}
|
有时候可能希望提供自己的语法池实现。比方说,假设应用程序准备处理不含重复模式的 XML 文档,您可能不希望解析器把处理过的所有模式都添加到默认的语法池中,因为这样最终可能导致虚拟机内存不够。相反,您可能希望编译一组最常用的模式然后锁住语法池(请参阅
XMLGrammarPool lockPool() 方法),禁止向语法池中增加新的模式,如
清单 2所示。
清单 2. 预编译模式
// create grammar preparser
XMLGrammarPreparser preparser = new XMLGrammarPreparser();
// register a specialized pre-parser
preparser.registerPreparser(XMLGrammarDescription.XML_SCHEMA, null);
// create grammar pool
XMLGrammarPool grammarPool = new XMLGrammarPool();
// set the grammar pool on the grammar preparser
// so that all the compiled grammars are automatically
// placed to the grammar pool
preparser.setProperty(GRAMMAR_POOL, grammarPool);
// set properties
preparser.setFeature(NAMESPACES_FEATURE_ID, true);
preparser.setFeature(VALIDATION_FEATURE_ID, true);
// parse grammar(s)
Grammar g = preparser.preparseGrammar(
XMLGrammarDescription.XML_SCHEMA,
new XMLInputSource(null, "personal.xsd", null));
// lock grammar pool
grammarPool.lockPool();
// next register the grammar pool with the parser
// and start parsing
... |
本文介绍了 Xerces Native Interface (XNI) 并说明如何直接使用这种 API 提高应用程序的性能。然后,我们讨论了 Xerces2 特有的几种特性和属性,以及如何通过调整这些特性和属性加快文档的处理。最后说明了如何利用 Xerces2 的模式缓冲特性避免重复处理模式的代价。
本系列文章所述的技术可以帮助您在编写 XML 应用程序时决定如何改善程序的性能。当然,还有很多其他的技术没有涉及到。要记住,对于某个解析器实现很好的方法可能不适用于其他实现,反之亦然,因此要获得最佳的性能需要做一点试验。
- 您可以参阅本文在 developerWorks 全球站点上的
英文原文。
- 阅读关于如何改善 XML 应用程序性能的本系列文章的前两部分:
第 1 部分 描述编写 XML 应用程序和文档的最佳实践,以及如何利用标准 SAX 和 DOM API 开发应用程序。
第 2 部分 说明如何使用 Xerces2 实现改进 SAX 和 DOM 应用程序的性能。
- 了解
Xerces2解析器。
- 在
W3C Technical Reports页面上可以找到所有的 XML 规范,包括 XML
1.0和
1.1、Namespaces in XML
1.0和
1.1以及
XML Information Set (Infoset)推荐标准。请详细阅读 W3C
Document Object Model (DOM)规范。
- 了解
Java API for XML Processing (JAXP)。
- 进一步了解
SAX。
- 参考 Apache Xerces2
XNI 手册。
- 在
developerWorks
XML 专区可以找到大量的文章、专栏、教程和技巧。
- 在
developerWorks
Developer Bookstore可以找到大量的书籍,包括
关于 XML 的书籍。
- 了解如何才能成为一名
IBM 认证的 XML 及相关技术的开发人员。
Elena Litani 是一位从事 Eclipse Modeling Framework (EMF) 的 IBM 软件开发人员。Elena 曾经作为 Apache Xerces2 项目的主要成员参与了 Xerces2 XML Schema 和 DOM Level 3 的实现,并且分析和改进解析器的性能。 Elena 还作为 IBM 在 W3C DOM 工作组中的代表,参与制定 DOM Level 3 规范。您可以通过 elitani@ca.ibm.com与她联系。
Michael Glavassevich 是一位在 IBM 多伦多实验室工作的软件开发人员。他从 2003 年开始参与 Xerces2 项目,现在已成为首席开发人员之一。您可以通过 mrglavas@ca.ibm.com与他联系。