IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope:Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  XML  >

提高 XML 应用程序的性能,第 2 部分

通过 Xerces2 SAX 和 DOM 实现重用解析器实例

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 中级

Elena Litani (elitani@ca.ibm.com), 软件开发人员, IBM
Michael Glavassevich (mrglavas@ca.ibm.com), 软件开发人员, IBM

2004 年 8 月 01 日

介绍编写 XML 应用程序最佳实践的这一系列的文章共有三部分,作者 Elena Litani 和 Michael Glavassevich 在本文中解释了如何使用 Xerces2 实现改进 SAX 和 DOM 应用程序的性能。文中还包括一些示例代码,向您展示了如何通过重用解析器实例改进应用程序的性能。

目前有很多应用程序使用 Java API for XML Processing (JAXP) 来获得 SAX 或者 DOM 解析器。根据所用 JAXP 版本和 Java 2 Platform, Standard Edition (J2SE) 厂商的不同,应用程序能够得到的解析器实现也不同。比方说,Sun J2SE 1.4.x 包括 Crimson 解析器,而 IBM® J2SE 1.4.x 和 Sun J2SE 1.5 则包括 Xerces2 解析器。如果对直接使用 Xerces2 解析器感兴趣,那么您可以使用 JAXP 工厂插件机制,为 JAXP 指定能够在哪儿找到 Xerces2 解析器实现。

即使您的应用程序不能指定这种解析器实现,您可能仍然想预先知道在哪些环境中 Xerces2 解析器是可用的解析器实现,从而可以设置可能会影响性能的解析器特性和性质。如果 Xerces2 解析器不可用,设置 Xerces2 解析器特性或性质就会失败,但是因为解析器的初始化只会发生一次(如 所述),因此也不会影响应用程序的性能。

本文提供了在使用 Xerces2 解析器时提高 SAX 或者 DOM 应用程序性能的一些建议。我们建议使用最新的 Xerces2 可用版本,因为基本上每个 Xerces2 版本都包括一些性能上的改进。此外,我们还将讨论为何重用解析器能够显著地改进应用程序的性能。要记住,特别适用于 Xerces 的方法可能对其他解析器没有多少效果,反之亦然。

SAX Xerces2——专门的技巧

在上一篇文章中,我们提供了编写 SAX 应用程序方面的一些一般性的性能技巧。如果您在使用 Xerces2 SAX 解析器,也许您还希望多了解一点。这一部分我们将讨论使用 Xerces2 SAX 解析器可能对应用程序性能的哪些方面产生影响。

名称空间声明

在内部,Xerces 将名称空间声明和在元素起始标签中指定的其他属性存储在一起。默认情况下,SAX 解析器在 startElement 回调函数中报告的属性不包括名称空间声明。为了与 SAX API 一致,解析器必须遍历所有的属性来删除名称空间声明,即使在起始标签中没有指定名称空间声明,它也会这样做。

由特性 URI http://xml.org/sax/features/namespace-prefixes 标识的一个特性控制着是否报告名称空间声明。为了获得较好的性能,应将该特性设为 true,这样解析器将报告和其他属性放在一起的名称空间声明。但是,解析器报告的名称空间声明并不像 Namespaces in XML Recommendation 勘误表(请参阅 参考资料)中所规定的那样,被绑定到名称空间名 http://www.w3.org/2000/xmlns/ 。该规范的第一版并没有为名称空间声明分配名称空间。在 SAX 2.0.2 版本之前,报告名称空间时总是认为 Xerces 没有名称空间。Xerces 在内部将这类属性绑定到一个名称空间,因此在将名称空间声明作为属性报告给应用程序之前,解析器必须从属性所在的名称空间中解除对这类属性的绑定。与此类似,解析器必须遍历属性集来 定位所有的名称空间声明。

SAX 2.0.2 引入了一个新的特性,使用特性 URI http://xml.org/sax/features/xmlns-uris 标志。它允许指定一个首选项,说明报告的名称空间声明是否有名称空间。如果将该特性设为 true, Xerces 2.7.0(撰写本文时仍然处在开发之中)将不再进一步处理属性集,因为名称空间声明已经具有期望的形式。通过这种方式配置解析器可以提高处理属性的速度。

按索引读取属性

在 Xerces2 的 org.xml.sax.Attributes 实现中,属性是保存在数组中的,以便能够按照索引快速对其进行访问。SAX 帮助器类 org.xml.sax.helpers.AttributeListImpl (Crimson 解析器也使用这个类并对其进行了扩展)以类似的方式保存属性。如果以这种方式存储属性,按索引而不是按名称处理属性可以取得更好的性能。在这种情况下,按名称查找属性会进行线性搜索。随着为元素指定的属性个数的增加,平均查找时间也将增加。当按名称查找一个属性并分析它的多种性质时,最好首先按照名称找到该属性的索引,然后使用索引获得您感兴趣的属性,比如属性的值和类型。

虽然以数组形式保存属性是一种典型实现,但元素的属性是一个无序集,因此其他解析器实现可以以更有效的方式保存属性。这可能意味着要以和文档中出现顺序不同的方式存储属性。在这种情况下,按名称查找属性可能会获得比按索引查找更好的性能。





回页首


DOM Xerces2——特殊的技巧

在上一期文章中,我们讨论了编写 DOM 应用程序的一些一般性能技巧,这一部分我们将讨论 Xerces2 DOM 的设计和影响应用程序性能的特性。

指定一种 DOM 实现

DOM API 包括几个规范。这些规范定义的特性标识了特定规范所涉及的领域。默认的 Xerces DOM 实现支持 DOM Core、 XML、 Mutation Events、 Range 和 Traversal 等特性。如果应用程序只需要 DOM Level 2 (或者 3) Core Recommendation 的实现,那么通过使用 http://apache.org/xml/properties/dom/document-class-name 属性将 org.apache.xerces.dom.CoreDocumentImpl 类指定为 org.w3c.dom.Document 接口的实现可以提高性能。该实现支持 DOM Level 2、 Level 3 Core、 Level 3 Load 和 Save Recommendations。它没有实现其他的 DOM 规范,因此性能更好一些。

延迟 DOM

默认情况下,Xerces2 解析器使用一种紧凑的数组结构来构建 DOM,该结构就是延迟 DOM。与在解析器期间完全展开整棵 DOM 树相比,这样做可以使解析器更快地返回文档,并且提高存储效率。当遍历树时,Xerces2 DOM 实现利用该数组结构中存储的信息创建 DOM 节点。

一般而言,如果应用程序需要处理大型文档或者不大可能遍历整棵树时,应该使用这种延迟实现。但是一些性能测试表明,将延迟节点展开的 Xerces2 DOM 用于较小的文档(0K-10K)时,性能反而会降低,并会消耗更多的内存。

因此,将 Xerces2 DOM 用于较小的文档时,为了获得较好的性能,应该通过特性 URI http://apache.org/xml/features/dom/defer-node-expansion 禁用延迟节点展开特性。对于较大的文档(大约 100K 或者更大),延迟 DOM 的性能要好于非延迟 DOM,但是要使用更多的内存。

遍历 Xerces2 DOM

应坚持使用 getValueorg.w3c.dom.Attr 接口)方法检索属性节点的字符串值,要避免使用那些检索属性节点的子节点的方法。虽然 DOM 实现必须为属性字符串值创建 Text 节点,但 Xerces2 DOM 实现会延迟 Text 节点的创建,除非要检索属性节点的子节点。如果应用程序使用 getValue 方法检索属性值,就不需要创建 Text 节点,从而可以节约空间并提高 DOM 的遍历性能。

很多应用程序选择使用 getChildNodes()NodeList 遍历 DOM 树。但是像 清单 1 那样使用 getFirstChildgetLastChildgetNextSiblinggetPreviousSibling 方法遍历树的代价更小。


清单 1. 遍历 Xerces2 DOM
Element root = document.getDocumentElement();
// Avoid traversing using NodeList:
NodeList children = root.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
   Node n = children.item(i);
}
// Instead, use the following methods:
Node child = root.getFirstChild();
while (child != null) {
   child = child.getNextSibling();
}

DOM 串行化

虽然可以使用 Java 对象串行化来串行化 Xerces2 DOM,但我们建议只要有可能,就将 DOM 串行化为 XML,而避免使用对象串行化。Xerces2 DOM 实现不能保证不同版本的 Xerces 的 DOM Java 对象串行化之间的互操作性。此外,一些粗略的测量表明,XML 串行化的性能要好于 Java 对象串行化,而且 XML 实例文档和对象串行化的 DOM 相比需要更少的存储空间。





回页首


解析器重用

编写 XML 应用程序有一种常见的错误观念,即创建解析器实例不会造成很大的性能开销。恰恰相反,创建解析器实例包括创建、初始化和设置解析器在后续的 XML 文档解析中需要的和不断使用的多种对象,这些初始化和设置操作的代价非常高。

此外,如果使用 JAXP API 创建解析器,可能需要更高的代价。为了通过这种 API 得到解析器,首先需要取得相应的解析器工厂,比如 SAXParserFactory ,然后再使用工厂创建解析器。为了取得解析器工厂,JAXP 使用一种搜索机制首先查找 ClassLoader (根据环境的不同,这可能是一种代价很高的操作),然后尝试定位可以在 JAXP 系统属性( jaxp.property 文件)中指定的解析器工厂实现,或者使用 Jar Service Provider 机制进行定位。使用 Jar Service Provider 机制进行查找可能特别费时,而且还可能要搜索类路径中的所有 JAR 文件,如果 ClassLoader 最终决定在网络上进行搜索,那么情况会变得更糟。

因此为了获得较好的性能,我们强烈建议应用程序只在一个地方创建解析器,然后重用这个解析器实例。

一般有两种类型的应用程序:

  • 需要使用具有一组相同特性和性质的解析器的应用程序。
  • 可能需要在后续解析操作之前改变解析器特性和性质的应用程序。

对于第一种类型的应用程序,很容易实现解析器的重用。通常只需要首先创建一个解析器,然后设置适当的特性和性质,就可以使用同一个解析器实例解析所有的 XML 文档了,如 清单 2所示:


清单 2. 重用解析器实例
// Use JAXP to retrieve SAX factory
SAXParserFactory factory = SAXParserFactory.newInstance();
// create a parser instance
SAXParser parser = factory.newSAXParser();
// set features and properties
parser.getXMLReader().setFeature(
    "http://xml.org/sax/features/validation", 
    true);
parser.getXMLReader().setProperty(
    "http://xml.org/sax/properties/lexical-handler", 
     myHandler);
parser.getXMLReader().setErrorHandler(myErrorHandler);
DefaultHandler myHandler = new DefaultHandler();
// use the same parser instance to parse XML documents
for (int i=0; i < args.length; i++){
   parser.parse(args[i], myHandler);
}

对于第二种类型的应用程序,仅仅使用一个解析器实例实现解析器缓冲可能更加麻烦。虽然这样做也是有可能的,但是必须记得将特性和性质重新设为默认状态,比如 清单 3中所示:


清单 3. 重新设置特性和性质值
// record the features used by application and their default values
HashMap defaultFeatureValues = new HashMap();
defaultFeatureValues.put("http://xml.org/sax/features/validation",
          Boolean.FALSE);
...
// record features that are set for this scenario
Vector currentFeatures = new Vector();
currentFeatures.add("http://xml.org/sax/features/validation");
// set features on the parser
parser.getXMLReader().setFeature(
        "http://xml.org/sax/features/validation", 
        true);
DefaultHandler myHandler = new DefaultHandler();
// use the same parser instance to parse XML documents
for (int i=0; i < args.length; i++){
   parser.parse(args[i], myHandler);
}
// reset parser features 
for (int i=0; i < currentFeatures.size(); i++){
   String feature = (String) currentFeatures.get(i);
   parser.getXMLReader().setFeature(
       feature, 
       ((Boolean)defaultFeatureValues.get(feature)).booleanValue());
}

没有一个用来重置解析器特性的 API,在某些情况下,可能无法重新设置特性。比如,如果试图将一个性质的值设为空,那么 Xerces2 version 2.6.2 将抛出 NullPointerException 异常。因此,这种情况下最好使用多个解析器实例。

可以事先定义一个解析器池接口,并通过应用程序注册其实现。假定有一组特性和性质,解析器池应该从内部池中返回一个解析器,如果内部池中没有解析器,那么解析器池应该创建并保存一个新的解析器实例。应用程序应当在需要获得解析器时,与解析器池进行交互。

如果在多线程环境中运行应用程序,那么您需要确保解析器池是同步的。就是说,解析器池不仅要定义 get 方法,还要定义 release 方法,使线程能够将解析器实例返回到缓冲池中,继续供其他线程使用。 清单 4 是一种可能的 SAX 解析器池接口。为了确保实现是线程安全的,实现该接口的类应该在方法上使用 synchronized 关键字,或者在方法内部使用同步锁。


清单 4. 在多线程环境中重用 SAX 解析器的接口
public interface XMLParserPool
{
  /**
   * Retrieves a parser from the pool given specified properties 
   * and features.
   * If parser can't be created using specified properties 
   * or features, an exception can be thrown.
   */
  public SAXParser get(Map features, Map properties) 
           throws ParserConfigurationException, SAXException;
  /**
   * Returns the parser to the pool.
   */
  public void release(SAXParser parser, 
            Map features, 
            Map properties);
}





回页首


结束语

本文讨论了在使用 Xerces2 SAX 和 DOM 实现时如何改进应用程序的性能,并介绍了如何通过重用和缓冲解析器提高应用程序的性能。本系列文章的第三部分将继续讨论如何使用 Xerces 独特的特性和性质改进性能,该文将简要地讨论 Xerces Native Interface (XNI) ,并将其和 SAX 进行比较,然后探讨 Xerces2 语法缓冲 API,这种机制可以显著改善需要对 DTD 或者 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和他联系。




对本文的评价

太差! (1)
需提高 (2)
一般;尚可 (3)
好文章 (4)
真棒!(5)

建议?




回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款