XML 和 Java 技术:数据绑定,第 3 部分: JiBX 体系结构

JavaGG 发布于 2009/05/22 12:25
阅读 738
收藏 0
企业 Java 技术专家 Dennis Sosnoski 向您介绍了他用于 Java 应用程序中 XML 数据绑定的 JiBX 框架。在第 1 部分介绍了当前的框架并在第 2 部分比较了性能之后,他现在将深入研究 JiBX 设计的细节,这种设计给 XML 和 Java 对象之间的映射带来了极佳的性能和巨大的灵活性。JiBX 是如何做到的呢?关键在于内部结构……

本系列文章的 第 1 部分介绍了为什么您会愿意将数据绑定用于 XML 的背景知识,并概述了可用于数据绑定的 Java 框架。 第 2 部分显示了一些样本文档所用的几个框架的性能。在这第 3 部分中,您会看到新的 JiBX 框架的细节,这个 JiBX 框架在测试中的表现相当优秀。一开始我将扼要回顾一下第 2 部分中 JiBX 简介的一些基本内容。

JiBX 起源于我研究 Java 技术中 XML 数据绑定性能极限的实验。由于它的发展远远超出了实验阶段所需,因此我决定把它作为试验床,来测试用于数据绑定的名为 以 Java 为中心的方法 - 与此相对的是从 XML 语法生成对象模型时所用的 以 XML 为中心的方法。由于这些选择,所以 JiBX 与已建立的数据绑定框架在几个方面都有明显的不同。

这些差别的核心是对文档进行数据编出时所使用的解析技术。已建立的框架都基于广泛使用的 SAX2 推(push)解析器 API。而 JiBX 使用一个更新的 拉出(pull)解析器 API,它提供了一个更自然的接口来处理文档中的元素序列。因为使用了这个不同的 API,所以 JiBX 能够使用相对简单的代码来处理数据编出。

这个比较简单的数据编出代码是 JiBX 和其它框架之间第二个主要差别的基础。其它框架要么生成它们自己的数据类(然后要求您在应用程序中使用它们);要么使用称为 反射(reflection)的技术,通过使用 Java 虚拟机(JVM)所知的类信息提供对数据的间接运行时访问。而 JiBX 使用它添加到现有类的代码,直接使用由 Java 编译器生成的二进制类文件。在保持能够灵活地处理您所控制的类的同时,由于这是直接访问数据,还使速度加快了。

第三个 JiBX 差别与您现有类的使用有关。JiBX 读取 绑定定义文件以控制它添加到您类中的代码如何在实例和 XML 之间进行相互转换。JiBX 并不是支持这类方法的唯一数据绑定框架(例如,Castor 也可以处理它),但是在允许您更改类和 XML 文档之间的关系方面,JiBX 比其它框架领先很多。

我将在本文其余部分详细阐述这其中的每一点。即使您正规划对您的项目使用另一种框架,您也会通过学习对 JiBX 的描述的过程中发现一些有用的“精神食粮”。

通过拉出来提高性能

JiBX 和其它数据绑定框架之间的一个最大区别在于它们处理输入 XML 文档的方式。大多数 Java 语言的 XML 解析器实现 SAX2API,以供应用程序使用。这个 API 已经过了多年的精心改进,许多不同的解析器实现都支持它,包括当今可用的功能最齐全的解析器。

数据绑定字典

以下是由一些术语组成的微型词典,我将在本文中使用这些术语:

数据编组(marshalling)是在内存中生成对象(可能包含从原始对象上链接的对象)的 XML 表示的过程。

数据编出(unmarshalling)是数据编组的逆向过程,它根据 XML 表示在内存中构建对象(以及可能是链接对象图)。

映射(mapping)是一个将对象数据编组成 XML 文档以及将 XML 文档数据编出成对象的规则集。根据语法生成代码的数据绑定方法通常在构造好的对象中已经内置了隐式映射,但是 JiBX(以及其它几个框架)却使用显式绑定定义来控制映射。

语法(grammar)是定义 XML 文档系列结构的规则集。有一种语法是由 XML 规范定义的文档类型定义(Document Type Definition,DTD)格式,另一种语法是日益普及的 W3C XML Schema(Schema)格式。

尽管 SAX2 API 已被广泛使用,但它对于许多类型的 XML 处理还存在一些严重缺陷。这些缺陷是由 SAX2( 解析方法)实现的接口样式引起的。在推解析中,您在代码中定义了一些方法,这些方法实现了由解析器 API 定义的接口。然后您将这些方法(以 处理程序的形式)以及要解析的文档一起传递给解析器。解析器遍历文档的文本并根据 XML 规则来解释它,当它在文本中发现文档组件(元素和字符数据内容等)时就调用处理程序方法以报告这些文档组件。解析器处理完输入文档之后,就将控制权交还给应用程序,但此时解析器仍受控制。

这个方法存在的问题是,它需要处理程序代码始终知道解析器位于文档的哪个位置,并需要它正确地解释组件。在最基本的级别上,将包含简单文本值的元素报告为三个(或更多)独立组件:第一个是开始标记,接着是文本(可能分成好几个片段),以及最后是结束标记。处理程序一般在报告结束标记之前,需要做累积文本的工作,随后对文本做某些操作。它所做的“某些操作”可能要受其它项(例如开始标记的属性)的影响,因此需要掌握更多信息。最终结果是用于推解析的处理程序代码往往要包含许多 String ,这些 String 与链接的 if 语句(或使用 java.util.Hashtable 的等价语句)中的元素名称相匹配。这样在编写时就会很杂乱,而且维护时也会很杂乱,更不要提什么效率了。

拉出解析将解析事件报告转了向。解析器不必调用处理程序中的方法来报告文档组件,而可以调用解析器依次获取每个组件 - 解析器在本质上成为遍历文档组件的迭代器。当您使用这个方法编写代码时,状态信息实际上是代码所固有的。就以包含文本的元素为例:由于您知道正处理的元素包含文本内容,所以您可以编写代码,并直接处理它 - 实际上,第一个调用获取开始标记,第二个获取内容,而第三个获取结束标记。

而更好的是,您可以编写一个处理包含文本值的 任何元素的方法 - 只要用期望的元素名调用该方法,它就能返回文本内容(如果没有希望的元素,则返回一个错误)。在从 XML 构建对象(就是数据编出)时尤其方便。大多数 XML 文档使用顺序固定的子元素,因此处理特定元素的多个子元素就象依次调用这个方法那么简单。这比起 SAX2 样式的处理程序方法来简单又迅速。

 




回页首


停留在上下文中

JiBX 广泛地使用将多个解析器操作组合在单一方法中的这种技术。它将内部拉出解析器接口包装在定义各种元素和属性访问方法的 数据编出上下文类中。这些方法处理解析并将数据转换成基本类型,还处理专门的操作类型,例如跟踪拥有唯一标识的对象。以下是这些方法的一些示例:

  • parsePastStartTag(ns, name) - 解析完元素的开始部分,它必须是所能看到的除字符数据以外的下一个解析组件。使解析的位置保留在开始标记之后。
  • attributeText(ns, name) - 从当前的开始标记获取所需属性的文本值。
  • attributeInt(ns, name, dflt) - 从当前的开始标记获取可选属性的 int 值(如果该属性不存在,那么返回缺省值)。
  • parseContentText(ns, name) - 解析完当前元素的尾部,它必须只包含字符数据内容。返回其内容。
  • parseElementText(ns, name) - 解析下一个元素,它必须只包含字符数据内容。返回其内容。

将解析器包装在数据编出上下文内,这样就可以用最少的代码定义各种各样的访问方法。它还提供了与正在使用的实际解析器的隔离。由此,所生成的添加到类中的代码不是特定于特定解析器的,或者甚至不是特定于特定的解析器类型。这在今后可能是一个重要问题。此时对数据编出上下文编码以使用实现 XMLPull接口的解析器,该接口是由拉出解析领域中的一些主要开发人员定义的非正式标准。在将来,它很可能成为拉出解析器的 Java 技术标准。当该标准可用时,JiBX 将能够使用该标准,只不过会对数据编出上下文代码作少量更改。

JiBX 使用的第二种上下文是 数据编组上下文,用于在进行数据编组时编写 XML 文档。就象数据编出上下文包装解析器并为数据编出提供各种专门的简便方法,数据编组上下文包装输出流并提供它自己的便利方法集。这些方法提供了每次一个组件(component-at-a-time)的接口,用于构造输出文档。以下是基本方法的简化列表:

  • startTag(nsi, name) - 生成不带属性的开始标记。
  • startTagAttributes(nsi, name) - 生成要添加属性的开始标记。
  • attribute(nsi, name, value) - 将属性添加到当前的开始标记(值可能是 String 或基本类型)。
  • closeStartEmpty() - 关闭不包含任何内容的开始标记(空元素)。
  • closeStartContent() - 关闭其后有内容的开始标记。
  • textContent(value) - 将字符数据内容添加到当前元素( String 或基本类型)。
  • endTag(nsi, name) - 生成元素的结束标记。

这个生成 XML 的接口比那些由许多其它框架所使用的接口简单。它主要被设计成供由 JiBX 框架所添加的代码使用,而不是为手工编写的代码使用,所以它避免了状态检查和类似的安全措施。它 确实能够正确地将特殊字符转义作为文本中的实体处理,但除此之外,则需要调用程序正确使用该接口。

这里显示的形式中,这个接口和名称空间索引一起工作。数据编组上下文保留名称空间前缀的内部数组,所以在生成输出时查询前缀只需对该数组建立索引(与此相对的是需要映射操作,如果传递了实际名称空间 URI,那么就有必要执行这样的操作)。这个方法是个折衷,它简化了处理,同时又没有丧失灵活性。较早的接口版本需要静态构造元素和属性的限定名(如果需要,还包括名称空间前缀)。这很适合于生成文本输出,但是在转换成其它输出形式(例如用于构建文档的 DOM 表示的解析事件流)时会很困难。更新的接口形式使转换容易得多。

 




回页首


以 Java 为中心 vs 以 XML 为中心

大多数用于 Java 语言程序的数据绑定框架都注重于根据 W3C XML Schema 语法生成代码。使用这种方法,您一开始就对要处理的文档应用 Schema 语法,然后使用一个属于数据绑定框架的程序,来生成与 Schema 相对应的类集合(对象模型)的 Java 语言源代码。对于许多应用程序,这都能正常工作,尤其适用于那些要提前定义 Schema 语法而且不会在项目过程中进行更改的应用程序。对于这类应用程序,根据 Schema 构造的对象模型能提供一种非常快速的开始使用文档的方法。

所生成的对象模型方法的主要缺点是它使您的代码与 XML 文档结构直接相关。由此,我称之为 以 XML 为中心的方法。使用所生成的对象模型,应用程序代码被强制使用反射 XML 结构的接口,而不是使用应用程序可能想要强加于文档中数据的结构的接口。这意味着 XML 和应用程序之间没有任何隔离 - 如果 XML 文档结构(Schema)在任何重要的方面发生更改,那么您将需要重新生成对象模型并更改应用程序代码,以与之相匹配。

有几个数据绑定框架(包括 JiBX)已经实现了另一种方法,通常称为 映射绑定。这就是我称为 以 Java 为中心的方法。它使用您在应用程序中定义的类,不必强制使用一组生成的类。尽管这使开发人员很方便,但它使框架的工作更复杂。框架出于某种原因需要以一致的方式与应用程序类交换数据,而不是强制应用程序类遵循严格的模型。这是 JiBX 和其它数据绑定框架之间存在的另一个差别。

反射的性能

当框架需要与各种 Java 语言类交换数据时,通常使用 反射。例如,Castor 在它的映射绑定支持中采用了此方法。它是在运行时访问有关 Java 语言类信息的一种方法。它能用于访问类实例中的字段和方法,从而提供了不需要类之间的任何源代码链接就能在运行时将类动态地挂钩在一起的方法。

反射是许多种使用 Java 类的框架的重要基础,但是在用于数据绑定时它确实存在一些缺陷。首先,它一般需要您在类中使用公共字段或方法以允许反射正确运行。这意味着您需要提供对内部状态信息的访问,并将它作为公共 API 的一部分,而事实上这些信息应当只能由绑定框架来访问。与直接在已编译代码中调用方法或访问字段相比,反射在性能上也处于劣势。

由于存在这些局限性,所以我想要避免将反射用于 JiBX 中的数据绑定。Java 数据对象(Java Data Object,JDO)规范的开发人员在 Java 语言对象和数据库之间移动数据时已经遇到了类似的问题。在他们的情况中,通过直接处理类文件,同时向编译器所生成的类添加代码以便为框架提供直接访问,来避免使用反射。对 JiBX 我选择使用这一方法。

 




回页首


增强类文件

JDO 团队提出了一个极佳的术语来描述修改(munge)(请参阅 参考资料)由编译器生成的类文件的过程:“字节码的增强”。与说成类文件正“被破坏”或“被植入”相比,这个词似乎更好,不是吗?我认为这个术语相当好,所以我就“厚着脸皮”还将它用于 JiBX 框架。

实际的类文件修改是由称为 绑定编译器的 JiBX 组件完成的。程序组装时执行绑定编译器 - 在您已经将 Java 语言源代码编译成类文件之后,但是在打包或执行这些类文件之前。绑定编译器读取一个或多个绑定定义文档(请参阅 定义绑定)。对于任何绑定中包含的每个类,它都添加了适当的数据编组或数据编出代码。

一般而言,对于每个包含该类的数据编组或数据编出绑定,都会在被绑定类文件中添加一对方法。但是也有一些例外:如果在多个绑定中以完全相同的方式来处理一个类,则这些绑定重用一组方法,而且有些绑定操作类型可能需要添加更多或更少的一些方法。但是,一般而言每个绑定有四个方法(两个用于数据编组,两个用于数据编出)。

绑定编译器在生成一些添加的方法时,还会生成一些添加的支持类。基本上会为任何绑定中包含的每个类添加一个小型助手类,并为每个绑定添加一个单独的类。在某种特殊情况中,即数据编出由标识值所标识的对象向前引用时,一旦数据编出向前引用的对象,那么还要生成等价的小型内部类以填充该对象的值。

通常所添加类的总数和所添加代码的总长度十分小。您可以从 JiBX 站点下载样本,它包含一个基于第 2 部分测试代码的示例(请参阅 参考资料)。这个示例生成 6 个添加的类和 23 个添加的方法,添加的总长度是 9 KB,以处理涉及 5 个类的绑定。代码量似乎很大,但是在 图 1中将它与基于根据语法而生成代码的其它框架作比较时,却并非如此。下图显示了在与其它框架所生成代码总长度相比时,JiBX 的代码总长度(包括基本类和添加的绑定代码)。只有 Quick 的代码长度比 JiBX 小。

图 1. 绑定代码比较
绑定代码比较

 




回页首


定义绑定

上一节中描述的 JiBX 绑定编译器根据您提供的绑定定义进行工作。绑定定义本身是 XML 文档,在下载中包含了 DTD 语法。与大多数其它数据绑定框架不同的是,JiBX 支持组合使用任意数量的绑定定义。例如,您可以使用一个绑定数据编出文档,进行更改,然后使用另一个绑定将数据对象编组成输出文档。

绑定定义让您可以控制基本细节,例如类的特定简单特性(如基本类型值,或 String )是否被映射成 XML 文档中的元素或属性,以及该元素或属性所用的名称。您还可以告知 JiBX 如何访问该特性 - 无论是作为字段(能包含任何访问限定符,包括 private)还是作为带有 get 和 set 方法的 JavaBean 样式的特性。您甚至可以将值指定为可选,并定义格式化程序用于简单值与文本之间的转换。

但是,JiBX 不仅仅是这种简单的映射形式。其它数据绑定框架通常强制每个带有复杂内容(属性或子元素)的 XML 元素与一个单独的对象相互映射,以便对象模型直接与 XML 文档的布局相匹配。有了 JiBX,您会更灵活:可以使用对象的特性子集定义子元素,而且所引用对象的特性可以作为当前元素的简单子元素或属性直接包含。这让您可以对类结构或 XML 文档结构中的一个作多种更改,而不必同时更改这两个结构以求匹配。

结构映射

我将这类 XML 元素不直接对应 Java 语言对象的映射称为 结构映射。对于它如何与 JiBX 一起使用的简单示例,请考虑以下 XML 文档:


清单 1. 样本 XML 文档

<customer>
<name>
<first-name>John</first-name>
<last-name>Smith</last-name>
</name>
<street1>12345 Happy Lane</street1>
<city>Plunk</city>
<state>WA</state>
<zip>98059</zip>
<phone>888.555.1234</phone>
</customer>

 

大多数数据绑定框架支持将这个文档映射成包含以下代码行的类实例:


清单 2. 与文档结构相匹配的类

public class Customer {
public Name name;
public String street1;
public String city;
public String state;
public String zip;
public String phone;
}
public class Name {
public String firstName;
public String lastName;
}

 

大多数框架还允许您更改字段的名称,例如使用 customerName 来替代 name ,以及使用 first 来替代 firstName 。有些框架还使用 get/set 方法,而不是这里显示的公共字段,但原理是相同的。

大多数框架 允许您将 name 元素内的值带入对应于 customer 元素的对象。换言之,您不能将 XML 映射成以下这个类:


清单 3. 单一类中的相同数据

public class Customer {
public String firstName;
public String lastName;
public String street1;
public String city;
public String state;
public String zip;
public String phone;
}

 

您也不能将 XML 映射成以下两个类:

清单 4. 结构不同的类

public class Customer {
public String firstName;
public String lastName;
public Address address;
public String phone;
}
public class Address {
public String street1;
public String city;
public String state;
public String zip;
}

 

JiBX 确实允许您进行这种映射。这意味着对象的结构与 XML 的结构没有联系 - 您可以重新构造对象类,而不必更改用于外部数据传送的 XML 格式。我将在本系列的下一篇文章中详细讨论这一点(包括上面两个示例的实际映射文件)。

 




回页首


结束语

JiBX 的基本体系结构与其它用于 Java 应用程序的 XML 数据绑定框架的体系结构迥然不同。这导致在将 JiBX 与其它这些框架作比较时,它优劣并存。在其优势方面,JiBX 的优点是运行非常快,运行时很紧凑而且 XML 文档格式和 Java 语言对象结构之间的隔离相当好。在其劣势方面,它基于一种相对来说未经证实的解析器技术,而且不支持有些其它框架(特别是 JAXB)所提供的验证灵活性。

JiBX 和其它框架之间的一些其它差异不是十分明显。例如,JiBX 的类文件增强技术提供的优点是使源代码保持干净 - 编译后就添加绑定代码,所以您从不需要直接处理它 - 但是所付出的代价是在构建过程增加了一个步骤,并且在追踪数据编组或数据编出期间所访问代码的问题时有可能产生混乱。对于某些用户和应用程序,这些因素中的一个或另一个将比其它的更重要。

本系列的第 4 部分将详细阐述在应用程序中使用 JiBX 的细节。在我向您显示如何使用它之后,我还是要回过头讨论我对 JiBX 较弱方面的看法,并继续提出未来能增强这些薄弱环节的可行方法。阅读第 4 部分以了解 JiBX 故事的其余内容!


参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • 了解有关用于映射绑定的新 JiBX 框架的更多信息。


  • 本系列是关于数据绑定的, 第 1 部分提供了为什么您会希望将数据绑定用于 XML 的背景,并概述了可用于数据绑定的 Java 框架。 第 2 部分比较了各种数据绑定框架(包括新 JiBX 框架)的性能( developerWorks,2003 年 1 月)。


  • 查阅作者以前的“ Data Binding with Castor”一文,其中描述了使用 Castor 的映射数据绑定技术( developerWorks,2002 年 4 月)。


  • 如果你需要 XML 的背景知识,请尝试阅读 developerWorksIntroduction to XML 教程”(2002 年 8 月)。


  • 重温作者先前的 developerWorks文章,它们讨论了 Java XML 文档模型的 性能(2001 年 9 月)和 用法(2002 年 2 月)的对比。


  • 到 source(Forge) 查找广泛使用的用于推解析 XML 的 SAX2 解析器 API


  • 了解由一些主要的拉出解析器研究人员开发的快速且灵活的 XmlPull 解析器 API


  • 了解 Streaming API for XML (StAX) 的 Java 规范请求(Java Specification Request)进展情况。


  • 阅读 Brett McLaughlin 在“ Converting between Java objects and XML with Quick”中对 Quick 所作的概述,其中向您显示了在不使用其它数据绑定框架所需的类生成语义的情况下,如何使用这个框架来迅速且轻松地将 Java 代码转换成 XML 文档( developerWorks,2002 年 8 月)。

  • 查找有关 Java Architecture for XML Binding (JAXB) 的更多信息,它是用于 Java 平台数据绑定的正在不断发展的标准。
  • 更深入地研究 Castor 框架,它支持映射绑定和生成绑定。
  • Quick 框架基于一系列 Java 平台和 XML 之前的开发成果。它提供了极其灵活的框架来处理 Java 平台上的 XML。
  • 研究 Zeus 的细节,它(类似于 Quick)基于 XML 文档的 DTD 描述而生成代码,但是它比 Quick 更易使用但也更受限制。
加载中
返回顶部
顶部