Java-XML-和-JSON-教程-全-

Java XML 和 JSON 教程(全)

原文:Java XML and JSON

协议:CC BY-NC-SA 4.0

一、XML 简介

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1916-4_​1) contains supplementary material, which is available to authorized users.

应用程序通常使用 XML 文档来存储和交换数据。XML 定义了以格式编码文档的规则,这种格式既可读机器可读。本章介绍 XML,浏览 XML 语言特性,并讨论格式良好的有效文档。

什么是 XML?

XML(可扩展标记语言)是一种用于定义词汇(自定义标记语言)的元语言(一种用于描述其他语言的语言),这是 XML 的重要性和普及性的关键。基于 XML 的词汇表(如 XHTML)让您能够以有意义的方式描述文档。

XML 词汇表文档类似于 HTML(参见 http://en.wikipedia.org/wiki/HTML )文档,因为它们是基于文本的,由标记(文档逻辑结构的编码描述)和内容(不被解释为标记的文档文本)组成。标记通过标签(尖括号分隔的语法结构)来证明,每个标签都有一个名称。此外,一些标签具有属性(名称-值对)。

Note

XML 和 HTML 是标准通用标记语言(SGML)的后代,SGML 是创建词汇表的原始元语言。XML 本质上是 SGML 的限制形式,而 HTML 是 SGML 的应用。XML 和 HTML 之间的关键区别在于,XML 让您使用自己的标记和规则创建自己的词汇表,而 HTML 为您提供一个预先创建的词汇表,它有自己固定的标记和规则集。XHTML 和其他基于 XML 的词汇表都是 XML 应用程序。创建 XHTML 是为了更清晰地实现 HTML。

如果您以前没有接触过 XML,您可能会对它的简单性和它的词汇与 HTML 的相似程度感到惊讶。学习如何创建 XML 文档并不需要成为火箭科学家。为了证明这一点,请查看清单 1-1 。

<recipe>
   <title>
      Grilled Cheese Sandwich
   </title>
   <ingredients>
      <ingredient qty="2">
         bread slice
      </ingredient>
      <ingredient>
         cheese slice
      </ingredient>
      <ingredient qty="2">
         margarine pat
      </ingredient>
   </ingredients>
   <instructions>
      Place frying pan on element and select medium heat. For each bread slice, smear one pat of margarine on one side of bread slice. Place cheese slice between bread slices with margarine-smeared sides away from the cheese. Place sandwich in frying pan with one margarine-smeared side in contact with pan. Fry for a couple of minutes and flip. Fry other side for a minute and serve.
   </instructions>
</recipe>
Listing 1-1.XML-Based Recipe for a Grilled Cheese Sandwich

清单 1-1 展示了一个 XML 文档,描述了制作烤奶酪三明治的食谱。这个文档类似于 HTML 文档,因为它由标签、属性和内容组成。然而,相似之处也就到此为止了。这种非正式的菜谱语言呈现了自己的<recipe><ingredients>和其他标签,而不是 HTML 标签,比如<html><head><img><p>

Note

虽然清单 1-1 的<title></title>标签也可以在 HTML 中找到,但是它们与它们的 HTML 对应物不同。Web 浏览器通常会在标题栏中显示这些标签之间的内容。相比之下,清单 1-1 的<title></title>标签之间的内容可能会显示为菜谱标题、大声朗读或以其他方式呈现,这取决于解析该文档的应用程序。

语言特色之旅

XML 提供了几种用于定义自定义标记语言的语言功能:XML 声明、元素和属性、字符引用和 CDATA 节、名称空间以及注释和处理指令。在本节中,您将了解这些语言特性。

XML 声明

XML 文档通常以 XML 声明开始,XML 声明是一种特殊的标记,告诉 XML 解析器该文档是 XML。清单 1-1 中缺少 XML 声明表明这种特殊的标记不是强制性的。当 XML 声明存在时,它前面不能出现任何内容。

XML 声明看起来至少类似于<?xml version="1.0"?>,其中非可选的version属性标识文档符合的 XML 规范的版本。该规范的初始版本(1.0)于 1998 年推出,并得到了广泛的实现。

Note

维护 XML 的万维网联盟(W3C)在 2004 年发布了 1.1 版。该版本主要支持使用 EBCDIC 平台上使用的行尾字符(见 http://en.wikipedia.org/wiki/EBCDIC )和使用 Unicode 3.2 中没有的脚本和字符(见 http://en.wikipedia.org/wiki/Unicode )。与 XML 1.0 不同,XML 1.1 没有被广泛实现,应该只由那些需要其独特特性的人使用。

XML 支持 Unicode,这意味着 XML 文档完全由 Unicode 字符集中的字符组成。文档的字符被编码成字节以便存储或传输,编码是通过 XML 声明的可选属性encoding指定的。一种常见的编码是 UTF-8(见 http://en.wikipedia.org/wiki/UTF-8 ),它是 Unicode 字符集的可变长度编码。UTF-8 是 ASCII 的严格超集(见 http://en.wikipedia.org/wiki/ASCII ),这意味着纯 ASCII 文本文件也是 UTF-8 文档。

Note

如果没有 XML 声明或者 XML 声明的encoding属性不存在,XML 解析器通常会在文档的开头寻找一个特殊的字符序列来确定文档的编码。该字符序列被称为字节顺序标记(BOM ),由编辑程序(如 Microsoft Windows 记事本)根据 UTF-8 或其他编码保存文档时创建。例如,十六进制序列EF BB BF表示编码为 UTF-8。同样,FE FF表示 UTF-16 大端(参见 https://en.wikipedia.org/wiki/UTF-16 ),FF FE表示 UTF-16 小端,00 00 FE FF表示 UTF-32 大端(参见 https://en.wikipedia.org/wiki/UTF-32 ),而FF FE 00 00表示 UTF-32 小端。当没有物料清单时,假定为 UTF-8。

如果除了 ASCII 字符集之外,您从来不使用字符,那么您可能会忘记encoding属性。但是,当您的母语不是英语,或者当您被要求创建包含非 ASCII 字符的 XML 文档时,您需要正确地指定encoding。例如,当您的文档包含来自非英语西欧语言(如法语、葡萄牙语和其他语言中使用的 cedilla)的 ASCII plus 字符时,您可能希望选择ISO-8859-1作为encoding属性的值——以这种方式编码的文档可能比使用 UTF-8 编码的文档更小。清单 1-2 向您展示了生成的 XML 声明。

<?xml version="1.0" encoding="ISO-8859-1"?>
<movie>
   <name>Le Fabuleux Destin d’Amélie Poulain</name>
   <language>français</language>
</movie>
Listing 1-2.An Encoded Document Containing Non-ASCII Characters

可以出现在 XML 声明中的最后一个属性是standalone。这个可选属性只与 dtd 相关(稍后讨论),它决定了是否有外部标记声明影响从 XML 处理器(一个解析器)传递到应用程序的信息。它的值默认为no,暗示有,或者可能有这样的声明。一个yes值表示没有这样的声明。有关更多信息,请查看( www.xmlplease.com/xml/xmlquotations/standalone )上的文章“独立伪属性仅在使用 DTD 时相关”。

元素和属性

XML 声明之后是元素的层次(树)结构,其中元素是由开始标签(如<name>)和结束标签(如</name>)分隔的文档的一部分,或者是空元素标签(名称以正斜杠(/)结尾的独立标签,如<break/>)。开始标签和结束标签包围内容和可能的其他标记,而空元素标签不包围任何东西。图 1-1 展示了清单 1-1 的 XML 文档树结构。

A394211_1_En_1_Fig1_HTML.jpg

图 1-1。

Listing 1-1’s tree structure is rooted in the recipe element

与 HTML 文档结构一样,XML 文档的结构锚定在根元素(最顶层的元素)中。在 HTML 中,根元素是html(<html></html>标签对)。与 HTML 不同,您可以为 XML 文档选择根元素。图 1-1 显示根元素为recipe

与其他有父元素的元素不同,recipe没有父元素。还有,recipeingredients有子元素:recipe的子元素是titleingredientsinstructions;而ingredients子代是ingredient的三个实例。titleinstructionsingredient元素没有子元素。

元素可以包含子元素、内容或混合内容(子元素和内容的组合)。清单 1-2 揭示了movie元素包含namelanguage子元素,还揭示了这些子元素中的每一个都包含内容(例如,language包含français)。清单 1-3 展示了另一个例子,展示了混合内容以及子元素和内容。

<?xml version="1.0"?>
<article title="The Rebirth of JavaFX" lang="en">
   <abstract>
      JavaFX 2 marks a significant milestone in the history of JavaFX. Now that Sun Microsystems has passed the torch to Oracle, we have seen the demise of JavaFX Script and the emergence of Java APIs (such as <code-inline>javafx.application.Application</code-inline>) for interacting with this technology. This article introduces you to this new flavor of JavaFX, where you learn about JavaFX 2 architecture and key APIs.
   </abstract>
   <body>
   </body>
</article>
Listing 1-3.An abstract Element Containing Mixed Content

这个文档的根元素是article,它包含了abstractbody子元素。abstract元素将内容与包含内容的code-inline元素混合在一起。相反,body元素是空的。

Note

与清单 1-1 和 1-2 一样,清单 1-3 也包含空格(不可见的字符,比如空格、制表符、回车符和换行符)。XML 规范允许在文档中添加空白。内容中出现的空白(如单词之间的空格)被视为内容的一部分。相反,解析器通常会忽略结束标记和下一个开始标记之间出现的空白。这样的空白不被认为是内容的一部分。

XML 元素的开始标记可以包含一个或多个属性。例如,清单 1-1 的<ingredient>标签有一个qty(数量)属性,清单 1-3 的<article>标签有titlelang属性。属性提供了关于元素的附加细节。例如,qty表示可以添加的成分的量,title表示文章的标题,lang表示文章使用的语言(en表示英语)。属性可以是可选的。例如,当未指定qty时,假定默认值为1

Note

元素和属性名称可以包含英语或其他语言的任何字母数字字符,还可以包含下划线(_)、连字符(-)、句点(。),以及冒号(:)标点字符。冒号应该只用于名称空间(将在本章后面讨论),并且名称不能包含空格。

字符引用和 CDATA 节

某些字符不能出现在开始标记和结束标记之间的内容中,也不能出现在属性值中。例如,不能在开始标记和结束标记之间放置文字字符<,因为这样做会使 XML 解析器误以为遇到了另一个标记。

这个问题的一个解决方案是用字符引用替换原义字符,字符引用是代表字符的代码。字符引用分为数字字符引用或字符实体引用:

  • 数字字符引用通过其 Unicode 码位来引用字符,并遵循格式&# nnnn ;(不限于四位)或&#x hhhh ;(不限于四位),其中 nnnn 提供码位的十进制表示,hhhh 提供十六进制表示。例如,&#0931;&#x03A3;代表希腊文大写字母 sigma。虽然 XML 要求&#x hhhh ;中的x是小写的,但是它很灵活,前导零在两种格式中都是可选的,并且允许您为每个 h 指定大写或小写字母。因此,&#931;&#x3A3;&#x03a3;也是希腊大写字母 sigma 的有效表示。
  • 字符实体引用通过实体名称(别名数据)引用字符,该实体将所需字符指定为其替换文本。字符实体引用由 XML 预定义,格式为& name ;,其中 name 是实体的名称。XML 预定义了五个字符实体引用:&lt;(<)&gt;(>)&amp;(&)&apos;()和" ( ")。

考虑一下<expression>6 < 4</expression>。你可以用数字参考&#60;代替<,产生<expression>6 &#60; 4</expression>,或者更好的是用&lt;,产生<expression>6 &lt; 4</expression>。第二种选择更清晰,更容易记忆。

假设您想在一个元素中嵌入一个 HTML 或 XML 文档。为了使嵌入的文档能够被 XML 解析器接受,您需要用它的&lt;&amp;预定义的字符实体引用来替换每个文字<(标签的开始)和&(实体的开始)字符,这是一项繁琐且可能容易出错的工作——您可能会忘记替换其中的一个字符。为了避免繁琐和潜在的错误,XML 以 CDATA(字符数据)部分的形式提供了一种替代方法。

CDATA 部分是由前缀<![CDATA[和后缀]]>包围的文字 HTML 或 XML 标记和内容的一部分。您不需要在 CDATA 部分中指定预定义的字符实体引用,如清单 1-4 所示。

<?xml version="1.0"?>
<svg-examples>
   <example>
      The following Scalable Vector Graphics document describes a blue-filled and black-stroked rectangle.
      <![CDATA[<svg width="100%" height="100%" version="1.1"
           >
         <rect width="300" height="100"
               style="fill:rgb(0,0,255);stroke-width:1; stroke:rgb(0,0,0)"/>
      </svg>]]>
   </example>
</svg-examples>
Listing 1-4.Embedding an XML Document in Another Document’s CDATA Section

清单 1-4 嵌入了一个可缩放的矢量图形(SVG[参见 SVG 示例文档的example元素中的 https://en.wikipedia.org/wiki/Scalable_Vector_Graphics ) XML 文档。SVG 文档放在一个CDATA部分,避免了用&lt;预定义的字符实体引用替换所有<字符的需要。

命名空间

创建结合不同 XML 语言特性的 XML 文档是很常见的。当元素和其他 XML 语言特性出现时,命名空间用于防止名称冲突。如果没有名称空间,XML 解析器就无法区分同名元素或其他具有不同含义的语言特性,比如来自两种不同语言的两个同名title元素。

Note

名称空间不是 XML 1.0 的一部分。它们是在这个规范发布一年后出现的。为了确保向后兼容 XML 1.0,名称空间利用了冒号字符,这是 XML 名称中的合法字符。不识别名称空间的解析器返回包含冒号的名称。

名称空间是基于统一资源标识符(URI)的容器,它通过为包含的标识符提供唯一的上下文来帮助区分 XML 词汇表。命名空间 URI 通过指定(通常在 XML 文档的根元素中)单独的xmlns属性(表示默认命名空间)或xmlns:前缀属性(表示被标识为前缀的命名空间),并将 URI 分配给该属性,与命名空间前缀(URI 的别名)相关联。

Note

一个命名空间的作用域从声明它的元素开始,应用于该元素的所有内容,除非被另一个具有相同前缀名称的命名空间声明覆盖。

当 prefix 被指定时,前缀和冒号字符被添加到属于该名称空间的每个元素标签的名称之前(参见清单 1-5 )。

<?xml version="1.0"?>
<h:html xmlns:h="http://www.w3.org/1999/xhtml"
        xmlns:r="http://www.javajeff.ca/">
   <h:head>
      <h:title>
         Recipe
      </h:title>
   </h:head>
   <h:body>
   <r:recipe>
      <r:title>
         Grilled Cheese Sandwich
      </r:title>
      <r:ingredients>
         <h:ul>
         <h:li>
         <r:ingredient qty="2">
            bread slice
         </r:ingredient>
         </h:li>
         <h:li>
         <r:ingredient>
            cheese slice
         </r:ingredient>
         </h:li>
         <h:li>
         <r:ingredient qty="2">
            margarine pat
         </r:ingredient>
         </h:li>
         </h:ul>
      </r:ingredients>
      <h:p>
      <r:instructions>
         Place frying pan on element and select medium heat. For each bread slice, smear one pat of margarine on one side of bread slice. Place cheese slice between bread slices with margarine-smeared sides away from the cheese. Place sandwich in frying pan with one margarine-smeared side in contact with pan. Fry for a couple of minutes and flip. Fry other side for a minute and serve.
      </r:instructions>
      </h:p>
   </r:recipe>
   </h:body>
</h:html>
Listing 1-5.Introducing a Pair of Namespaces

清单 1-5 描述了一个结合了 XHTML(参见 http://en.wikipedia.org/wiki/XHTML )元素和菜谱语言元素的文档。所有与 XHTML 相关的元素标签都以h:为前缀,所有与菜谱语言相关的元素标签都以r:为前缀。

h:前缀与 www.w3.org/1999/xhtml URI 相关联,r:前缀与 www.javajeff.ca URI 相关联。XML 不要求 URIs 指向文档文件。它只要求它们是惟一的,以保证惟一的名称空间。

该文档将菜谱数据与 XHTML 元素分离开来,这使得保留该数据的结构成为可能,同时还允许符合 XHTML 的 web 浏览器(如 Mozilla Firefox)通过网页呈现菜谱(见图 1-2 )。

A394211_1_En_1_Fig2_HTML.jpg

图 1-2。

Mozilla Firefox presents the recipe data via XHTML tags

当标签的属性属于元素时,这些属性不需要加上前缀。例如,<r:ingredient qty="2">中没有前缀qty。但是,属于其他名称空间的属性需要前缀。例如,假设您想要向文档的<r:title>标签添加一个 XHTML style属性,以便在通过应用程序显示时为菜谱标题提供样式。您可以通过在title标签中插入一个 XHTML 属性来完成这项任务,如下所示:

<r:title h:style="font-family: sans-serif;">

XHTML style属性带有前缀h:,因为该属性属于 XHTML 语言名称空间,而不属于 recipe 语言名称空间。

当涉及多个名称空间时,将其中一个名称空间指定为默认名称空间会很方便,这样可以减少输入名称空间前缀的繁琐。考虑列出 1-6 。

<?xml version="1.0"?>
<html 
      xmlns:r="http://www.javajeff.ca/">
   <head>

      <title>
         Recipe
      </title>
   </head>
   <body>
   <r:recipe>
      <r:title>
         Grilled Cheese Sandwich
      </r:title>
      <r:ingredients>
         <ul>
         <li>
         <r:ingredient qty="2">
            bread slice
         </r:ingredient>
         </li>
         <li>
         <r:ingredient>
            cheese slice
         </r:ingredient>
         </li>
         <li>
         <r:ingredient qty="2">
            margarine pat
         </r:ingredient>
         </li>
         </ul>
      </r:ingredients>
      <p>
      <r:instructions>
         Place frying pan on element and select medium heat. For each bread slice, smear one pat of margarine on one side of bread slice. Place cheese slice between bread slices with margarine-smeared sides away from the cheese. Place sandwich in frying pan with one margarine-smeared side in contact with pan. Fry for a couple of minutes and flip. Fry other side for a minute and serve.
      </r:instructions>
      </p>
   </r:recipe>
   </body>
</html>

Listing 1-6.Specifying a Default Namespace

清单 1-6 指定了 XHTML 语言的默认名称空间。没有 XHTML 元素标签需要以h:为前缀。然而,配方语言元素标签仍然必须以前缀r:为前缀。

注释和处理说明

XML 文档可以包含注释,注释是以<!--开始,以-->结束的字符序列。例如,您可以将<!-- Todo -->放在清单 1-3 的body元素中,以提醒自己需要完成该元素的编码。

注释用于阐明文档的各个部分。它们可以出现在 XML 声明之后的任何地方,除了在标记内,不能嵌套,不能包含双连字符(--),因为这样做可能会使 XML 解析器混淆,认为注释已经结束,出于同样的原因,不应该包含连字符(-),并且通常在处理过程中被忽略。评论不内容。

XML 还允许存在处理指令。处理指令是对解析文档的应用程序可用的指令。指令以<?开始,以?>结束。<?前缀后面是一个名为目标的名字。该名称通常标识处理指令所针对的应用程序。处理指令的其余部分包含适合应用程序格式的文本。以下是处理指令的两个示例:

  • <?xml-stylesheet href="modern.xsl" type="text/xml"?>将可扩展样式表语言(XSL)样式表与 XML 文档相关联(参见 http://en.wikipedia.org/wiki/XSL )。
  • <?php /* PHP code */ ?>将一段 PHP 代码片段传递给应用程序(参见 http://en.wikipedia.org/wiki/PHP )。尽管 XML 声明看起来像一个处理指令,但事实并非如此。

Note

XML 声明不是处理指令。

格式良好的文档

HTML 是一种松散的语言,在这种语言中,可以无序地指定元素,可以省略结束标记,等等。web 浏览器页面布局代码的复杂性部分是由于需要处理这些特殊情况。相比之下,XML 是一种更严格的语言。为了使 XML 文档更容易解析,XML 要求 XML 文档遵循某些规则:

  • 所有元素必须有开始和结束标记,或者由空元素标记组成。例如,不像 HTML <p>标签那样经常没有对应的</p></p>也必须从 XML 文档的角度出现。
  • 标记必须正确嵌套。例如,虽然您可能在 HTML 中指定了<b><i>XML</b></i>,但是 XML 解析器会报告一个错误。相比之下,<b><i>XML</i></b>不会导致错误,因为嵌套的标签对相互镜像。
  • 所有属性值都必须用引号括起来。单引号()或双引号(")都是允许的(尽管双引号是更常见的指定引号)。省略这些引号是错误的。
  • 空元素必须正确格式化。例如,HTML 的<br>标签在 XML 中必须被指定为<br/>。您可以在标签名称和/字符之间指定一个空格,尽管空格是可选的。
  • 小心大小写。XML 是一种区分大小写的语言,其中大小写不同的标签(如<author><Author>)被认为是不同的。将不同大小写的开始和结束标签混在一起是错误的,例如,<author></Author>

意识到名称空间的 XML 解析器执行两个额外的规则:

  • 每个元素和属性名称不得包含一个以上的冒号字符。
  • 实体名称、处理指令目标或符号名称(稍后讨论)都不能包含冒号。

符合这些规则的 XML 文档是格式良好的。该文档具有逻辑清晰的外观,并且更易于处理。XML 解析器只会解析格式良好的 XML 文档。

有效文件

对于一个 XML 文档来说,格式良好并不总是足够的;在许多情况下,文件也必须是有效的。有效的文档遵守约束。例如,可以在列出 1-1 的食谱文档时设置一个约束,以确保ingredients元素总是在instructions元素之前;也许一个申请必须首先处理ingredients

Note

XML 文档验证类似于编译器分析源代码,以确保代码在机器上下文中有意义。例如,intcount=1;都是有效的 Java 字符序列,但是1 count ; int =不是有效的 Java 结构(而int count = 1;是有效的 Java 结构)。

一些 XML 解析器执行验证,而其他解析器不执行验证,因为验证解析器更难编写。执行验证的解析器将 XML 文档与语法文档进行比较。对语法文档的任何偏离都会被报告为应用程序的错误 XML 文档是无效的。应用程序可以选择修复错误或拒绝 XML 文档。与格式良好性错误不同,有效性错误不一定是致命的,解析器可以继续解析 XML 文档。

Note

默认情况下,验证 XML 解析器通常不进行验证,因为验证非常耗时。必须指导他们执行验证。

语法文件是用一种特殊的语言写的。两种常用的语法语言是文档类型定义和 XML 模式。

文档类型定义

文档类型定义(DTD)是规定 XML 文档语法的最古老的语法语言。DTD 语法文档(称为 DTD)是根据一种严格的语法编写的,该语法规定了什么元素可以出现在文档的什么部分,元素中包含什么(子元素、内容或混合内容)以及可以指定什么属性。例如,DTD 可能会指定一个recipe元素必须有一个ingredients元素,后跟一个instructions元素。

清单 1-7 给出了用于构建清单 1-1 文档的配方语言的 DTD。

<!ELEMENT recipe (title, ingredients, instructions)>
<!ELEMENT title (#PCDATA)>
<!ELEMENT ingredients (ingredient+)>
<!ELEMENT ingredient (#PCDATA)>
<!ELEMENT instructions (#PCDATA)>
<!ATTLIST ingredient qty CDATA "1">
Listing 1-7.The Recipe Language’s DTD

这个 DTD 首先声明配方语言的元素。元素声明采用的形式是<!ELEMENT name content-specifier >,其中 name 是任何合法的 XML 名称(例如,它不能包含空格),content-specifier 标识元素中可以出现的内容。

第一个元素声明声明 XML 文档中只能出现一个recipe元素——这个声明并不意味着recipe是根元素。此外,这个元素必须包含titleingredientsinstructions子元素中的一个,并且按照这个顺序。子元素必须指定为逗号分隔的列表。此外,列表总是用括号括起来。

第二个元素声明声明title元素包含解析的字符数据(非标记文本)。第三个元素声明声明至少一个ingredient元素必须出现在ingredients中。+字符是表示一个或多个的正则表达式的一个例子。其他可能使用的表达式有*(零或更多)和?(一次或根本不使用)。第四个和第五个元素声明与第二个类似,声明ingredientinstructions元素包含解析的字符数据。

Note

元素声明支持其他三种内容说明符。您可以指定<!ELEMENT名称ANY>来允许任何类型的元素内容,或者指定<!ELEMENT名称EMPTY>来禁止任何元素内容。要声明一个元素包含混合内容,您可以指定#PCDATA和一个元素名称列表,用竖线(|)分隔。例如,<!ELEMENT ingredient (#PCDATA | measure | note)*>表示ingredient元素可以包含已解析的字符数据、零个或多个measure元素以及零个或多个note元素。它没有指定被解析的字符数据和这些元素出现的顺序。但是,#PCDATA必须是列表中指定的第一项。在此上下文中使用正则表达式时,它必须出现在右括号的右侧。

清单 1-7 的 DTD 最后声明了菜谱语言的属性,其中只有一个:qty。属性声明的形式是<!ATTLISTename aname type default-value>,其中 ename 是属性所属元素的名称,aname 是属性的名称,type 是属性的类型,default-value 是属性的默认值。

属性声明将qty标识为ingredient的属性。它还说明了qty的类型是CDATA(任何不包括&符号、小于或大于符号或双引号的字符串都可能出现;这些字符可以分别通过&amp;&lt;&gt;&quot;来表示,并且qty是可选的,当不存在时采用默认值1

More About Attributes

DTD 允许您指定附加的属性类型:ID(为标识元素的属性创建唯一的标识符)、IDREF(属性值是位于文档中其他位置的元素)、IDREFS(值由多个IDREF组成)、ENTITY(可以使用外部二进制数据或未解析的实体)、ENTITIES(值由多个实体组成)、NMTOKEN(值限于任何有效的 XML 名称)、NMTOKENS(值由多个 XML 名称组成)、NOTATION(值已经通过 DTD 表示法声明指定)值用竖线分隔)。

您可以不逐字指定缺省值,而是指定#REQUIRED来表示属性必须始终具有某个值(<!ATTLIST名称名称类型#REQUIRED>),#IMPLIED来表示属性是可选的并且不提供缺省值(<!ATTLIST名称名称类型#IMPLIED>),或者#FIXED来表示属性是可选的并且在使用时必须始终采用 DTD 分配的缺省值(<!ATTLIST名称名称类型#FIXED "value">)。

您可以在一个ATTLIST声明中指定属性列表。例如,<!ATTLISTename aname1 type 1 default-value 1 aname2 type 2 default-value 2>声明了标识为 aname 1 和 aname 2 的两个属性。

基于 DTD 的验证 XML 解析器在验证文档之前,要求文档包含一个标识 DTD 的文档类型声明,该 DTD 指定了文档的语法。

Note

文档类型定义和文档类型声明是两回事。DTD 首字母缩略词标识文档类型定义,从不标识文档类型声明。

文档类型声明紧跟在 XML 声明之后,并以下列方式之一指定:

清单 1-8 显示了带有内部 DTD 的清单 1-1 (减去<recipe></recipe>标签之间的子元素)。

<?xml version="1.0"?>
<!DOCTYPE recipe [

   <!ELEMENT recipe (title, ingredients, instructions)>
   <!ELEMENT title (#PCDATA)>
   <!ELEMENT ingredients (ingredient+)>
   <!ELEMENT ingredient (#PCDATA)>
   <!ELEMENT instructions (#PCDATA)>
   <!ATTLIST ingredient qty CDATA "1">
]>
<recipe>
   <!-- Child elements removed for brevity. -->
</recipe>
Listing 1-8.The Recipe Document with an Internal DTD

Note

文档可以有内部和外部 DTDs 比如<!DOCTYPE recipe SYSTEM " http://www.javajeff.ca/dtds/recipe.dtd " [ <!ELEMENT ...>]>。内部 DTD 称为内部 DTD 子集,外部 DTD 称为外部 DTD 子集。任何一个子集都不能覆盖另一个子集的元素声明。

您还可以在 dtd 中声明符号、一般实体和参数实体。符号是一段任意的数据,通常描述未解析的二进制数据的格式,通常具有形式<!NOTATION name SYSTEM uri >,其中 name 标识符号,uri 标识某种插件,该插件可以代表解析 XML 文档的应用程序处理数据。例如,<!NOTATION image SYSTEM "psp.exe">声明了一个名为image的符号,并将 Windows 可执行文件psp.exe标识为处理图像的插件。

使用符号通过媒体类型指定二进制数据类型也很常见(参见 https://en.wikipedia.org/wiki/Media_type )。例如,<!NOTATION image SYSTEM "image/jpeg">声明了一个图像符号,它标识了联合图像专家组图像的image/jpeg媒体类型。

通用实体是通过通用实体引用从 XML 文档内部引用的实体,格式为&name;。例如预定义的ltgtampaposquot角色实体,其&lt;&gt;&amp;&apos;&quot;角色实体引用分别是角色<>&"的别名。

一般实体分为内部实体和外部实体。内部一般实体是其值存储在 DTD 中的一般实体,其形式为<!ENTITY name value >,其中 name 标识实体,value 指定其值。例如,<!ENTITY copyright "Copyright &copy; 2016 Jeff Friesen. All rights reserved.">声明了一个名为copyright的内部通用实体。这个实体的值可能包括另一个声明的实体,比如&copy;(版权符号的 HTML 实体),并且可以通过指定&copyright;从 XML 文档中的任何地方引用。

外部一般实体是其值存储在 DTD 外部的一般实体。该值可能是文本数据(如 XML 文档),也可能是二进制数据(如 JPEG 图像)。外部通用实体分为外部已解析通用实体和外部未解析通用实体。

外部解析的通用实体引用存储该实体的文本数据的外部文件,当在文档中指定了通用实体引用时,该文件将被插入到文档中并由验证解析器进行解析,并且该文件具有形式<!ENTITY name SYSTEM uri >,其中 name 标识实体,uri 标识外部文件。例如,<!ENTITY chapter-header SYSTEM " http://www.javajeff.ca/entities/chapheader.xml ">chapheader.xml标识为存储要插入到 XML 文档中&chapter-header;出现的任何地方的 XML 内容。可以指定替代的<!ENTITY名称PUBLIC fpi uri >形式。

Caution

因为外部文件的内容可能被解析,所以该内容必须是格式良好的。

一个外部未解析的通用实体引用一个存储实体二进制数据的外部文件,其格式为<!ENTITY name SYSTEM uri NDATA nname >,其中 name 标识实体,uri 定位外部文件,NDATA标识名为 nname 的符号声明。该符号通常标识用于处理二进制数据或该数据的互联网媒体类型的插件。例如,<!ENTITY photo SYSTEM "photo.jpg" NDATA image>将名称photo与外部二进制文件photo.png和符号image相关联。可以指定替代的<!ENTITY名称PUBLIC fpi uri NDATA名称>形式。

Note

XML 不允许对外部通用实体的引用出现在属性值中。例如,您不能在属性值中指定&chapter-header;

参数实体是通过参数实体引用从 DTD 内部引用的实体,格式为%name;。它们有助于消除元素声明中的重复内容。例如,您正在为一家大公司创建一个 DTD,这个 DTD 包含三个元素声明:<!ELEMENT salesperson (firstname, lastname)><!ELEMENT lawyer (firstname, lastname)><!ELEMENT accountant (firstname, lastname)>。每个元素都包含重复的子元素内容。如果你需要添加另一个子元素(比如middleinitial,你需要确保所有的元素都被更新;否则,您将面临 DTD 格式错误的风险。参数实体可以帮你解决这个问题。

参数实体分为内部实体和外部实体。内部参数实体是其值存储在 DTD 中的参数实体,其形式为<!ENTITY % name value >,其中 name 标识实体,value 指定其值。例如,<!ENTITY % person-name "firstname, lastname">声明了一个名为person-name的参数实体,其值为firstname, lastname。一旦声明,这个实体可以在前面的三个元素声明中被引用,如下:<!ELEMENT salesperson (%person-name;)><!ELEMENT lawyer (%person-name;)><!ELEMENT accountant (%person-name;)>。不是像以前那样将middleinitial添加到salespersonlawyeraccountant中,而是像在<!ENTITY % person-name "firstname, middleinitial, lastname">中那样将这个子元素添加到person-name中,并且这个更改将被应用到这些元素声明中。

外部参数实体是其值存储在 DTD 外部的参数实体。它的形式是<!ENTITY % name SYSTEM uri>,其中name标识实体,uri定位外部文件。例如,<!ENTITY % person-name SYSTEM " http://www.javajeff.ca/entities/names.dtd ">names.dtd标识为存储要插入到 DTD 中%person-name;出现的任何地方的firstname, lastname文本。可以指定替代的<!ENTITY %名称PUBLIC fpi uri >形式。

Note

这个讨论总结了 DTD 的基础。另外一个没有涉及的主题(为了简洁)是条件包含,它允许您指定 DTD 中可供解析器使用的部分,通常与参数实体引用一起使用。

XML 模式

XML Schema 是一种语法语言,用于声明 XML 文档的结构、内容和语义(含义)。这种语言的语法文档被称为模式,模式本身就是 XML 文档。模式必须符合 XML 模式 DTD(参见 www.w3.org/2001/XMLSchema.dtd )。

W3C 引入了 XML Schema 来克服 DTD 的局限性,比如 DTD 缺乏对名称空间的支持。此外,XML Schema 提供了一种面向对象的方法来声明 XML 文档的语法。这种语法语言提供了比 DTD 的 CDATA 和 PCDATA 类型更多的基本类型。例如,整数、浮点、各种日期和时间以及字符串类型都是 XML 模式的一部分。

Note

XML Schema 预定义了 19 种原语类型,通过以下标识符来表示:anyURIbase64BinarybooleandatedateTimedecimaldoubledurationfloathexBinarygDaygMonthgMonthDaygYeargYearMonthNOTATIONQNamestringtime

XML Schema 提供了限制(通过约束减少允许值的集合)、列表(允许值的序列)和联合(允许从几种类型中选择值)派生方法,用于从这些原始类型创建新的简单类型。比如 XML Schema 通过限制从decimal派生出 13 个整数类型;这些类型通过以下标识符表示:byteintintegerlongnegativeIntegernonNegativeIntegernonPositiveIntegerpositiveIntegershortunsignedByteunsignedIntunsignedLongunsignedShort。它还支持从简单类型创建复杂类型。

熟悉 XML 模式的一个好方法是通过一个例子,比如为清单 1-1 的菜谱语言文档创建一个模式。创建这个配方语言模式的第一步是识别它的所有元素和属性。要素有recipetitleingredientsinstructionsingredientqty是孤属性。

下一步是根据 XML Schema 的内容模型对元素进行分类,该模型指定了元素中可以包含的子元素和文本节点的类型(参见 [http://en.wikipedia.org/wiki/Node_(computer_science](http://en.wikipedia.org/wiki/Node_(computer_science) ))。当元素没有子元素或文本节点时,该元素被认为是空的;当只接受文本节点时,该元素被认为是简单的;当只接受子元素时,该元素被认为是复杂的;当接受子元素和文本节点时,该元素被认为是混合的。清单 1-1 的元素都没有空的或混合的内容模型。然而,titleingredientinstructions元素具有简单的内容模型;并且recipeingredients元素具有复杂的内容模型。

对于具有简单内容模型的元素,我们可以区分有属性的元素和没有属性的元素。XML Schema 将具有简单内容模型并且没有属性的元素分类为简单类型。此外,它将具有简单内容模型和属性的元素或者来自其他内容模型的元素分类为复杂类型。此外,XML Schema 将属性分类为简单类型,因为它们只包含文本值——属性没有子元素。清单 1-1 的titleinstructions元素及其qty属性是简单类型。它的recipeingredientsingredient元素是复杂类型。

此时,您可以开始声明模式了。以下代码片段展示了介绍性的schema元素:

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">

元素介绍了语法。它还将常用的xs名称空间前缀分配给标准 XML 模式名称空间;xs:随后被添加到 XML 模式元素名称的前面。

接下来,使用element元素声明titleinstructions简单类型元素,如下所示:

<xs:element name="title" type="xs:string"/>
<xs:element name="instructions" type="xs:string"/>

XML Schema 要求每个元素都有一个名称,并且(与 DTD 不同)与一个类型相关联,该类型标识元素中存储的数据类型。例如,第一个element声明通过其name属性将title标识为名称,通过其type属性将string标识为类型(字符串或字符数据出现在<title></title>标记之间)。xs:string中的xs:前缀是必需的,因为string是预定义的 W3C 类型。

继续,现在使用attribute元素声明qty简单类型属性,如下所示:

<xs:attribute name="qty" type="xs:unsignedInt" default="1"/>

这个attribute元素声明了一个名为qty的属性。我选择unsignedInt作为这个属性的type,因为数量是非负值。此外,我指定了1作为没有指定qty时的default值— attribute元素默认声明可选属性。

Note

元素和属性声明的顺序在模式中并不重要。

既然已经声明了简单类型,就可以开始声明复杂类型了。首先,声明recipe如下:

<xs:element name="recipe">
   <xs:complexType>
      <xs:sequence>
         <xs:element ref="title"/>
         <xs:element ref="ingredients"/>
         <xs:element ref="instructions"/>
      </xs:sequence>
   </xs:complexType>
</xs:element>

该声明声明recipe是一个复杂类型(通过complexType元素),由一个title元素、一个ingredients元素和一个instructions元素组成(通过sequence元素)。这些元素中的每一个都由一个不同的element声明,这个不同的element由它的elementref属性引用。

下一个要声明的复杂类型是ingredients。下面的代码片段提供了它的声明:

<xs:element name="ingredients">
   <xs:complexType>
      <xs:sequence>
         <xs:element ref="ingredient" maxOccurs="unbounded"/>
      </xs:sequence>
   </xs:complexType>
</xs:element>

这个声明声明ingredients是一个复杂类型,由一个或多个ingredient元素组成。“或更多”是通过包含elementmaxOccurs属性并将该属性的值设置为unbounded来指定的。

Note

maxOccurs属性标识一个元素可以出现的最大次数。一个类似的minOccurs属性标识了一个元素出现的最小次数。每个属性可以被赋予0或一个正整数。此外,您可以为maxOccurs指定unbounded,这意味着元素的出现次数没有上限。每个属性的默认值为1,这意味着当两个属性都不存在时,一个元素只能出现一次。

最后要声明的复杂类型是ingredient。虽然ingredient只能包含文本节点,这意味着它应该是一个简单的类型,但正是qty属性的存在使它变得复杂。查看以下声明:

<xs:element name="ingredient">
   <xs:complexType>
      <xs:simpleContent>
         <xs:extension base="xs:string">
            <xs:attribute ref="qty"/>
         </xs:extension>
      </xs:simpleContent>
   </xs:complexType>
</xs:element>

名为ingredient的元素是一个复杂类型(因为它有可选的qty属性)。simpleContent元素表示ingredient只能包含简单的内容(文本节点),extension元素表示ingredient是一个新类型,扩展了预定义的string类型(通过base属性指定),意味着ingredient继承了string的所有属性和结构。此外,ingredient被赋予了一个附加的qty属性。

清单 1-9 将前面的例子组合成一个完整的模式。

<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="title" type="xs:string"/>
<xs:element name="instructions" type="xs:string"/>
<xs:attribute name="qty" type="xs:unsignedInt" default="1"/>
<xs:element name="recipe">
   <xs:complexType>
      <xs:sequence>
         <xs:element ref="title"/>
         <xs:element ref="ingredients"/>
         <xs:element ref="instructions"/>
      </xs:sequence>
   </xs:complexType>
</xs:element>
<xs:element name="ingredients">
   <xs:complexType>
      <xs:sequence>
         <xs:element ref="ingredient" maxOccurs="unbounded"/>
      </xs:sequence>
   </xs:complexType>
</xs:element>
<xs:element name="ingredient">
   <xs:complexType>
      <xs:simpleContent>
         <xs:extension base="xs:string">
            <xs:attribute ref="qty"/>
         </xs:extension>
      </xs:simpleContent>
   </xs:complexType>
</xs:element>
Listing 1-9.The Recipe Document’s Schema

创建模式后,您可以从配方文档中引用它。通过在文档的根元素开始标记(<recipe>)上指定xmlns:xsixsi:schemaLocation属性来完成这个任务,如下所示:

<recipe 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.javajeff.ca/schemas recipe.xsd">

xmlns属性将 http://www.javajeff.ca/ 标识为文档的默认名称空间。无前缀的元素及其无前缀的属性属于此命名空间。

xmlns:xsi属性将传统的xsi (XML 模式实例)前缀与标准的 http://www.w3.org/2001/XMLSchema-instance 名称空间相关联。文档中唯一以xsi:为前缀的项目是schemaLocation

schemaLocation属性用于定位模式。该属性的值可以是多对空格分隔的值,但在本例中被指定为一对这样的值。第一个值( http://www.javajeff.ca/schemas )标识模式的目标名称空间,第二个值(recipe.xsd)标识模式在该名称空间中的位置。

Note

符合 XML 模式语法的模式文件通常被指定为文件扩展名.xsd

如果一个 XML 文档声明了一个名称空间(xmlns default 或xmlns:prefix),那么该名称空间必须对模式可用,以便验证解析器可以解析对该名称空间的元素和其他模式组件的所有引用。您还需要提到模式描述了哪个名称空间,通过在schema元素中包含targetNamespace属性可以做到这一点。例如,假设您的配方文档声明了一个默认的 XML 名称空间,如下所示:

<?xml version="1.0"?>
<recipe >

至少,您需要修改清单 1-9 的schema元素,以包含targetNameSpace和配方文档的默认名称空间作为targetNameSpace的值,如下所示:

<xs:schema targetNamespace="http://www.javajeff.ca/"
           xmlns:xs="http://www.w3.org/2001/XMLSchema">

Exercises

以下练习旨在测试您对第一章内容的理解。

Define XML.   True or false: XML and HTML are descendents of SGML.   What language features does XML provide for use in defining custom markup languages?   What is the XML declaration?   Identify the XML declaration’s three attributes. Which attribute is nonoptional?   True or false: An element always consists of a start tag followed by content followed by an end tag.   Following the XML declaration, an XML document is anchored in what kind of element?   What is mixed content?   What is a character reference? Identify the two kinds of character references.   What is a CDATA section? Why would you use it?   Define namespace.   What is a namespace prefix?   True or false: A tag’s attributes don’t need to be prefixed when those attributes belong to the element.   What is a comment? Where can it appear in an XML document?   Define processing instruction.   Identify the rules that an XML document must follow to be considered well formed.   What does it mean for an XML document to be valid?   A parser that performs validation compares an XML document to a grammar document. Identify the two common grammar languages.   What is the general syntax for declaring an element in a DTD?   Which grammar language lets you create complex types from simple types?   Create a books.xml document file with a books root element. The books element must contain one or more book elements, where a book element must contain one title element, one or more author elements, and one publisher element (and in that order). Also, the book element’s <book> tag must contain isbn and pubyear attributes. Record Advanced C++/James Coplien/Addison Wesley/0201548550/1992 in the first book element, Beginning Groovy and Grails/Christopher M. Judd/Joseph Faisal Nusairat/James Shingler/Apress/9781430210450/2008 in the second book element, and Effective Java/Joshua Bloch/Addison Wesley/0201310058/2001 in the third book element.   Modify books.xml to include an internal DTD that satisfies the previous exercise’s requirements.

摘要

应用程序经常使用 XML 文档来存储和交换数据。XML 定义了以格式编码文档的规则,这种格式既可读机器可读。它是一种定义词汇表的元语言,这是 XML 的重要性和受欢迎程度的关键。

XML 提供了几种用于定义自定义标记语言的语言功能。这些特性包括 XML 声明、元素和属性、字符引用和 CDATA 节、名称空间以及注释和处理指令。

HTML 是一种松散的语言,其中元素可以无序指定,结束标记可以省略,等等。相比之下,XML 文档格式良好,因为它们符合特定的规则,这使得它们更容易处理。XML 解析器只解析格式良好的 XML 文档。

在许多情况下,XML 文档也必须是有效的。有效的文档遵循语法文档所描述的约束。语法文档是用语法语言编写的,比如常用的文档类型定义和 XML Schema。

第二章介绍了 Java 解析 XML 文档的 SAX API。

二、使用 SAX 解析 XML 文档

Java 为解析 XML 文档提供了几个 API。这些 API 中最基本的是 SAX,这是本章的重点。

什么是萨克斯?

Simple API for XML (SAX)是一个基于事件的 Java API,用于从头到尾顺序解析 XML 文档。当面向 SAX 的解析器遇到文档信息集(描述 XML 文档信息的抽象数据模型;参见 http://en.wikipedia.org/wiki/XML_Information_Set ),它通过调用应用程序的一个处理程序(解析器调用其方法以使事件信息可用的对象)中的一个方法,使该项目作为事件对应用程序可用,应用程序先前已经向解析器注册了该处理程序。然后,应用程序可以通过以某种方式处理 infoset 项来使用该事件。

SAX 解析器比 DOM 解析器更节省内存(参见第三章),因为它不需要将整个文档放入内存。这个好处变成了使用 XPath(见第五章)和 XSLT(见第六章)的一个缺点,它们需要将整个文档存储在内存中。

Note

根据其官方网站( www.saxproject.org ),SAX 起源于 Java 的 XML 解析 API。然而,SAX 并不是 Java 的专利。微软也支持 SAX。NET 框架(参见 http://saxdotnet.sourceforge.net )。

探索 SAX API

SAX 有两个主要版本。Java 通过javax.xml.parsers包的抽象SAXParserSAXParserFactory类实现 SAX 1,通过org.xml.sax包的XMLReader接口和org.xml.sax.helpers包的XMLReaderFactory类实现 SAX 2。org.xml.saxorg.xml.sax.extorg.xml.sax.helpers包提供了各种类型来增强这两种 Java 实现。

Note

我只研究 SAX 2 实现,因为 SAX 2 提供了关于 XML 文档的附加信息集项目(比如注释和 CDATA 节通知)。

获得 SAX 2 解析器

实现XMLReader接口的类描述了基于 SAX 2 的解析器。这些类的实例是通过调用XMLReaderFactory类的createXMLReader()类方法获得的。例如,下面的代码片段调用该类的XMLReader createXMLReader()类方法来创建并返回一个XMLReader对象:

XMLReader xmlr = XMLReaderFactory.createXMLReader();

方法调用返回一个XMLReader实现类的实例,并将其引用分配给xmlr

Note

在幕后,createXMLReader()试图根据一个查找过程从系统默认值创建一个XMLReader对象,该过程首先检查org.xml.sax.driver系统属性以查看它是否有值。如果是这样,这个属性的值被用作实现XMLReader的类的名称。此外,还尝试实例化该类并返回实例。当createXMLReader()不能获得一个合适的类或者实例化该类时,抛出org.xml.sax.SAXException类的一个实例。

巡视 XMLReader 方法

返回的XMLReader对象提供了几种配置解析器和解析文档内容的方法。这些方法如下:

  • ContentHandler getContentHandler()返回当前内容处理程序,它是一个实现org.xml.sax.ContentHandler接口的类的实例,或者当没有注册时返回null
  • DTDHandler getDTDHandler()返回当前的 DTD 处理程序,它是一个实现了org.xml.sax.DTDHandler接口的类的实例,或者当没有注册时返回null
  • EntityResolver getEntityResolver()返回当前实体解析器,它是一个实现org.xml.sax.EntityResolver接口的类的实例,或者当没有注册时返回null
  • ErrorHandler getErrorHandler()返回当前的错误处理程序,它是一个实现org.xml.sax.ErrorHandler接口的类的实例,或者当没有注册时返回null
  • boolean getFeature(String name)返回对应于由name标识的特征的布尔值,它必须是全限定的 URI。当名字没有被识别为特性时,这个方法抛出org.xml.sax.SAXNotRecognizedException,当名字被识别但在调用getFeature()时不能确定相关值时,抛出org.xml.sax.SAXNotSupportedExceptionSAXNotRecognizedExceptionSAXNotSupportedExceptionSAXException的子类。
  • Object getProperty(String name)返回name标识的属性对应的java.lang.Object实例,必须是全限定的 URI。当名字没有被识别为属性时,这个方法抛出SAXNotRecognizedException,当名字被识别但在调用getProperty()时不能确定相关值时,抛出SAXNotSupportedException
  • void parse (InputSource input)解析 XML 文档,直到文档被解析后才返回。input参数存储了对一个org.xml.sax.InputSource对象的引用,该对象描述了文档的来源(比如一个java.io.InputStream对象,或者甚至是一个基于java.lang.String的系统标识符 URI)。当无法读取源代码时,该方法抛出java.io.IOException,当解析失败时抛出SAXException,这可能是由于违反了格式良好性。
  • void parse(String systemId)通过执行parse(new InputSource(systemId));来解析 XML 文档。
  • void setContentHandler (ContentHandler handler)向解析器注册由handler标识的内容处理器。ContentHandler接口提供了 11 个回调方法,调用这些方法来报告各种解析事件(比如元素的开始和结束)。
  • void setDTDHandler( DTDHandler handler )向解析器注册handler标识的 DTD 处理程序。DTDHandler接口提供了一对回调方法,用于报告符号和外部未解析的实体。
  • void setEntityResolver( EntityResolver resolver )向解析器注册resolver标识的实体解析器。EntityResolver接口为解析实体提供了单一的回调方法。
  • void setErrorHandler( ErrorHandler handler )向解析器注册handler标识的错误处理程序。ErrorHandler接口提供了三个回调方法,用于报告致命错误(阻止进一步解析的问题,比如违反了格式良好性)、可恢复错误(不阻止进一步解析的问题,比如验证失败)和警告(不需要解决的错误,比如在元素名前面加上 W3C 保留的前缀xml)。
  • void setFeature( String name, boolean value)value分配给name标识的特征,该特征必须是完全合格的 URI。当名字没有被识别为特性时,这个方法抛出SAXNotRecognizedException,当名字被识别但是在调用setFeature()时不能设置相关值时,抛出SAXNotSupportedException
  • void setProperty( String name, Object value)value分配给name标识的房产,该房产必须是完全合格的 URI。当名字没有被识别为属性时,这个方法抛出SAXNotRecognizedException,当名字被识别但在调用setProperty()时不能设置相关值时,抛出SAXNotSupportedException

如果没有安装处理程序,所有与该处理程序相关的事件都会被忽略。不安装错误处理程序可能会有问题,因为正常的处理可能无法继续,应用程序也不会意识到有什么地方出错了。如果没有安装实体解析器,解析器会执行自己的默认解析。在这一章的后面我会有更多关于实体解析的内容。

Note

通常在解析文档之前安装新的内容处理程序、DTD 处理程序、实体解析程序或错误处理程序,但也可以在解析文档时安装。当下一个事件发生时,解析器开始使用处理程序。

设置功能和属性

获得一个XMLReader对象后,您可以通过设置其特性和属性来配置该对象。特性是描述解析器模式的名称-值对,比如验证。相反,属性是一个名称-值对,它描述了解析器接口的一些其他方面,例如词法处理程序,它通过提供用于报告注释、CDATA 分隔符和一些其他语法结构的回调方法来增强内容处理程序。

特性和属性有名称,名称必须是以http://前缀开头的绝对 URIs。一个特性的值总是一个布尔值true / false。相反,属性值是一个任意的对象。下面的代码片段演示了如何设置功能和属性:

xmlr.setFeature("http://xml.org/sax/features/validation", true);
xmlr.setProperty("http://xml.org/sax/properties/lexical-handler",
                 new LexicalHandler() { /* ... */ });

setFeature()调用启用了validation特性,以便解析器执行验证。特征名称以 http://xml.org/sax/features/ 为前缀。

Note

解析器必须支持namespacesnamespace-prefixes特性。namespaces决定是否将 URIs 和本地名字传递给ContentHandlerstartElement()endElement()方法。默认为true—这些名字都是经过的。当false. namespace-prefixes决定名称空间声明的xmlnsxmlns:prefix属性是否包含在传递给startElement()org.xml.sax.Attributes列表中时,解析器可以传递空字符串,并且还决定限定名是否作为方法的第三个参数传递——限定名是一个前缀加一个本地名。它默认为false,意味着不包括xmlnsxmlns:prefix,也意味着解析器不必传递限定名。没有强制的属性。JDK 文档的org.xml.sax包页面列出了标准的 SAX 2 特性和属性。

setProperty()调用将实现org.xml.sax.ext.LexicalHandler接口的类的实例分配给lexical-handler属性,这样就可以调用接口方法来报告注释、CDATA 部分等等。物业名称以 http://xml.org/sax/properties/ 为前缀。

Note

ContentHandlerDTDHandlerEntityResolverErrorHandler不同,LexicalHandler是一个扩展(它不是核心 SAX API 的一部分),这就是为什么XMLReader没有声明void setLexicalHandler(LexicalHandler handler)方法。如果你想安装一个词法处理程序,你必须使用XMLReadersetProperty()方法来安装这个处理程序作为 http://xml.org/sax/properties/lexical-handler 属性的值。

要素和属性可以是只读的,也可以是读写的。(在极少数情况下,功能或属性可能是只写的。)设置或读取特性或属性时,可能会抛出SAXNotSupportedExceptionSAXNotRecognizedException。例如,如果你试图修改一个只读的特征/属性,就会抛出一个SAXNotSupportedException类的实例。此外,如果在解析过程中调用setFeature()setProperty(),可能会抛出这个异常。试图为不执行验证的解析器设置验证特性是一种抛出SAXNotRecognizedException类实例的情况。

浏览处理程序和解析器接口

setContentHandler()setDTDHandler()setErrorHandler()安装的基于接口的处理程序;setEntityResolver()安装的实体解析器;并且lexical-handler属性描述的处理程序提供了各种回调方法。您需要理解这些方法,然后才能对它们进行编码,以便有效地响应解析事件。

旅游内容处理程序

ContentHandler声明了以下面向内容的信息回调方法:

  • void characters(char[] ch, int start, int length)通过ch数组报告一个元素的字符数据。传递给startlength的参数标识数组中与该方法调用相关的部分。字符通过一个char[]数组传递,而不是通过一个String对象传递,以优化性能。解析器通常将大量文档存储在一个数组中,并反复将对该数组的引用以及更新后的startlength值传递给characters()
  • void endDocument()报告已到达文档的结尾。应用程序可能会使用此方法来关闭输出文件或执行一些其他清理。
  • void endElement (String uri, String localName, String qName)报告已经到达一个元素的末尾。uri标识元素的名称空间 URI,或者当没有名称空间 URI 或名称空间处理尚未启用时为空。localName标识元素的本地名称,即没有前缀的名称(例如htmlh:html中的html)。qName引用限定名,例如,当没有前缀时,h:htmlhtml。当检测到结束标签时,调用endElement(),或者当检测到空元素标签时,紧接着startElement()调用。
  • void endPrefixMapping( String prefix)报告已经到达名称空间前缀映射的结尾(例如xmlns:h),而prefix报告这个前缀(例如h)。
  • void ignorableWhitespace( char[] ch, int start, int length)报告可忽略的空白(位于 DTD 不允许混合内容的标签之间的空白)。这个空白通常用于缩进标签。这些参数与characters()方法的目的相同。
  • void processingInstruction (String target, String data)报告一条处理指令,其中target标识指令指向的应用,data提供指令的数据(无数据时为空引用)。
  • void setDocumentLocator (Locator locator)报告一个org.xml.sax.Locator对象(实现Locator接口的类的一个实例),可以调用它的int getColumnNumber()int getLineNumber()String getPublicId()String getSystemId()方法来获得任何文档相关事件结束位置的位置信息,即使解析器没有报告错误。这个方法在startDocument()之前被调用,是保存Locator对象的好地方,这样就可以从其他回调方法中访问它。
  • void skippedEntity( String name)报告所有跳过的实体。验证解析器解析所有一般的实体引用,但是非验证解析器可以选择跳过它们,因为非验证解析器不读取声明这些实体的 dtd。如果非验证解析器不读取 DTD,它就不知道实体是否被正确声明。非验证解析器不是试图读取 DTD 并报告实体的替换文本,而是用实体的名称调用skippedEntity()
  • void startDocument()报告已到达文档的开头。应用程序可能使用此方法来创建输出文件或执行一些其他初始化。
  • void startElement( String uri, String localName, String qName, Attributes attributes)报告已经到达一个元素的开始。uri标识元素的名称空间 URI,或者当没有名称空间 URI 或者名称空间处理还没有启用时为空。localName标识元素的本地名,qName引用其限定名,attributes引用元素的属性列表——当没有属性时,该列表为空。当检测到开始标签或空元素标签时,调用startElement()
  • void startPrefixMapping( String prefix, String uri)报告已经到达名称空间前缀映射的开始处(例如xmlns:h=" http://www.w3.org/1999/xhtml "),其中prefix报告该前缀(例如h),而uri报告该前缀映射到的 URI(例如 http://www.w3.org/1999/xhtml )。

除了setDocumentLocator()之外,每个方法都被声明为抛出SAXException,重载的回调方法可能会选择在检测到问题时抛出。

巡回演出

DTDHandler声明了以下面向 DTD 的信息回调方法:

  • void notationDecl( String name, String publicId, String systemId)报告一个批注声明,其中name提供该声明的name属性值,publicId提供该声明的public属性值(该值不可用时为空引用),systemId提供该声明的system属性值。
  • void unparsedEntityDecl( String name, String publicId, String systemId, String notationName)报告外部未解析的实体声明,其中name提供该声明的name属性的值,publicId提供public属性的值(该值不可用时为空引用),systemId提供system属性的值,notationName提供NDATA名称。

每个方法都被声明为抛出SAXException,重载的回调方法可能会选择在检测到问题时抛出。

旋转误差处理程序

ErrorHandler声明了以下面向错误的信息回调方法:

  • void error(SAXParseException exception)报告发生了可恢复的解析器错误(通常是文档无效);细节通过传递给exception的参数指定。此方法通常被重写以通过命令窗口报告错误,或者将其记录到文件或数据库中。
  • void fatalError( SAXParseException exception)报告出现不可恢复的解析器错误(文档格式不正确);细节是通过传递给exception的参数指定的。此方法通常被重写,以便应用程序可以在停止处理文档之前记录错误(因为文档不再可靠)。
  • void warning(SAXParseException e)报告发生了非严重错误(如以保留的xml字符序列开头的元素名);细节通过传递给exception的参数指定。此方法通常被重写以通过控制台报告警告,或者将其记录到文件或数据库中。

每个方法都被声明为抛出SAXException,重载的回调方法可能会选择在检测到问题时抛出。

旅游实体解决方案

EntityResolver声明了下面的回调方法:

  • 调用InputSource resolveEntity(String publicId, String systemId)让应用程序通过返回基于不同 URI 的自定义InputSource对象来解析外部实体(如外部 DTD 子集)。该方法被声明为在检测到面向 SAX 的问题时抛出SAXException,并且还被声明为在遇到 I/O 错误时抛出IOException,这可能是为了响应为正在创建的InputSource创建一个InputStream对象或java.io.Reader对象。

旋转字典处理程式

LexicalHandler声明了以下附加的面向内容的信息回调方法:

  • void comment(char[] ch, int start, int length)通过ch数组报告注释。传递给startlength的参数标识了数组中与该方法调用相关的部分。
  • void endCDATA()报告 CDATA 段的结尾。
  • void endDTD()报告 DTD 的结束。
  • void endEntity(String name)报告由name标识的实体的结束。
  • void startCDATA()报告 CDATA 段的开始。
  • void startDTD(String name, String publicId, String systemId)报告由name. publicId标识的 DTD 的开始指定外部 DTD 子集声明的公共标识符,或者当没有声明时为空引用。类似地,systemId为外部 DTD 子集指定声明的系统标识符,或者当没有声明时为空引用。
  • void startEntity(String name)报告由name标识的实体的开始。

每个方法都被声明为抛出SAXException,重载的回调方法可能会选择在检测到问题时抛出。

因为在每个接口中实现所有方法可能很繁琐,所以 SAX API 方便地提供了org.xml.sax.helpers.DefaultHandler适配器类来减轻您的负担。DefaultHandler实现ContentHandlerDTDHandlerEntityResolverErrorHandler。SAX 也提供了org.xml.sax.ext.DefaultHandler2,它继承了DefaultHandler,也实现了LexicalHandler

演示 SAX API

清单 2-1 向SAXDemo展示了源代码,这是一个演示 SAX API 的应用程序。该应用程序由一个SAXDemo入口点类和一个DefaultHandler2Handler子类组成。

import java.io.FileReader;
import java.io.IOException;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import org.xml.sax.helpers.XMLReaderFactory;

public class SAXDemo
{
   public static void main(String[] args)
   {
      if (args.length < 1 || args.length > 2)
      {
         System.err.println("usage: java SAXDemo xmlfile [v]");
         return;
      }
      try
      {
         XMLReader xmlr = XMLReaderFactory.createXMLReader();
         if (args.length == 2 && args[1].equals("v"))
            xmlr.setFeature("http://xml.org/sax/features/validation", true);
         xmlr.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
         Handler handler = new Handler();
         xmlr.setContentHandler(handler);
         xmlr.setDTDHandler(handler);
         xmlr.setEntityResolver(handler);
         xmlr.setErrorHandler(handler);
         xmlr.setProperty("http://xml.org/sax/properties/lexical-handler",
                          handler);
         xmlr.parse(new InputSource(new FileReader(args[0])));
      }
      catch (IOException ioe)
      {
         System.err.println("IOE: " + ioe);
      }
      catch (SAXException saxe)
      {
         System.err.println("SAXE: " + saxe);
      }
   }
}

Listing 2-1.
SAXDemo

SAXDemomain()方法首先验证是否指定了一个或两个命令行参数(XML 文档的名称,后面可选地跟着小写字母v,它告诉SAXDemo创建一个验证解析器)。然后它创建一个XMLReader对象;有条件地使能validation特性并使能namespace-prefixes特性;实例化伴生的Handler类;安装这个Handler对象作为解析器的内容处理程序、DTD 处理程序、实体解析程序和错误处理程序。安装这个Handler对象作为lexical-handler属性的值;创建输入源以从文件中读取文档;并解析文档。

清单 2-2 中展示了Handler类的源代码。

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXParseException;

import org.xml.sax.ext.DefaultHandler2;

public class Handler extends DefaultHandler2
{
   private Locator locator;

   @Override
   public void characters(char[] ch, int start, int length)
   {
      System.out.print("characters() [");
      for (int i = start; i < start + length; i++)
         System.out.print(ch[i]);
      System.out.println("]");
   }

   @Override
   public void comment(char[] ch, int start, int length)
   {

      System.out.print("characters() [");
      for (int i = start; i < start + length; i++)
         System.out.print(ch[i]);
      System.out.println("]");
   }

   @Override
   public void endCDATA()
   {
      System.out.println("endCDATA()");
   }

   @Override
   public void endDocument()
   {
      System.out.println("endDocument()");
   }

   @Override
   public void endDTD()
   {
      System.out.println("endDTD()");
   }

   @Override
   public void endElement(String uri, String localName, String qName)
   {
      System.out.print("endElement() ");
      System.out.print("uri=[" + uri + "], ");
      System.out.print("localName=[" + localName + "], ");
      System.out.println("qName=[" + qName + "]");
   }

   @Override
   public void endEntity(String name)
   {
      System.out.print("endEntity() ");
      System.out.println("name=[" + name + "]");
   }

   @Override
   public void endPrefixMapping(String prefix)
   {
      System.out.print("endPrefixMapping() ");
      System.out.println("prefix=[" + prefix + "]");
   }

   @Override
   public void error(SAXParseException saxpe)
   {
      System.out.println("error() " + saxpe);
   }

   @Override
   public void fatalError(SAXParseException saxpe)
   {
      System.out.println("fatalError() " + saxpe);
   }

   @Override
   public void ignorableWhitespace(char[] ch, int start, int length)
   {
      System.out.print("ignorableWhitespace() [");
      for (int i = start; i < start + length; i++)
         System.out.print(ch[i]);
      System.out.println("]");
   }

   @Override
   public void notationDecl(String name, String publicId, String systemId)
   {
      System.out.print("notationDecl() ");
      System.out.print("name=[" + name + "]");
      System.out.print("publicId=[" + publicId + "]");
      System.out.println("systemId=[" + systemId + "]");
   }

   @Override
   public void processingInstruction(String target, String data)
   {
      System.out.print("processingInstruction() [");
      System.out.println("target=[" + target + "]");
      System.out.println("data=[" + data + "]");
   }

   @Override
   public InputSource resolveEntity(String publicId, String systemId)
   {
      System.out.print("resolveEntity() ");
      System.out.print("publicId=[" + publicId + "]");
      System.out.println("systemId=[" + systemId + "]");
      // Do not perform a remapping.
      InputSource is = new InputSource();
      is.setPublicId(publicId);
      is.setSystemId(systemId);
      return is;
   }

   @Override
   public void setDocumentLocator(Locator locator)
   {
      System.out.print("setDocumentLocator() ");
      System.out.println("locator=[" + locator + "]");
      this.locator = locator;
   }

   @Override
   public void skippedEntity(String name)
   {
      System.out.print("skippedEntity() ");
      System.out.println("name=[" + name + "]");
   }

   @Override
   public void startCDATA()
   {
      System.out.println("startCDATA()");
   }

   @Override
   public void startDocument()
   {
      System.out.println("startDocument()");
   }

   @Override
   public void startDTD(String name, String publicId, String systemId)
   {
      System.out.print("startDTD() ");
      System.out.print("name=[" + name + "]");
      System.out.print("publicId=[" + publicId + "]");
      System.out.println("systemId=[" + systemId + "]");
   }

   @Override
   public void startElement(String uri, String localName, String qName,
                            Attributes attributes)
   {
      System.out.print("startElement() ");
      System.out.print("uri=[" + uri + "], ");
      System.out.print("localName=[" + localName + "], ");
      System.out.println("qName=[" + qName + "]");
      for (int i = 0; i < attributes.getLength(); i++)
         System.out.println("  Attribute: " + attributes.getLocalName(i) +
                            ", " + attributes.getValue(i));
      System.out.println("Column number=[" + locator.getColumnNumber() +
                         "]");
      System.out.println("Line number=[" + locator.getLineNumber() + "]");
   }

   @Override
   public void startEntity(String name)
   {
      System.out.print("startEntity() ");
      System.out.println("name=[" + name + "]");
   }

   @Override
   public void startPrefixMapping(String prefix, String uri)
   {
      System.out.print("startPrefixMapping() ");
      System.out.print("prefix=[" + prefix + "]");
      System.out.println("uri=[" + uri + "]");
   }

   @Override
   public void unparsedEntityDecl(String name, String publicId,
                                  String systemId, String notationName)
   {
      System.out.print("unparsedEntityDecl() ");
      System.out.print("name=[" + name + "]");
      System.out.print("publicId=[" + publicId + "]");
      System.out.print("systemId=[" + systemId + "]");
      System.out.println("notationName=[" + notationName + "]");
   }

   @Override
   public void warning(SAXParseException saxpe)
   {
      System.out.println("warning() " + saxpe);
   }
}

Listing 2-2.The Handler Class

Handler子类非常简单;它根据特性和属性设置输出关于 XML 文档的每一条可能的信息。您会发现这个类非常便于探索事件发生的顺序以及各种特性和属性。

假设基于清单 2-1 和 2-2 的文件位于同一个目录中,编译如下:

javac SAXDemo.java

执行以下命令来解析清单 1-4 的svg-examples.xml文档:

java SAXDemo svg-examples.xml

SAXDemo通过显示以下输出做出响应(哈希码可能不同):

setDocumentLocator() locator=[com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser$LocatorProxy@6d06d69c]
startDocument()
startElement() uri=[], localName=[svg-examples], qName=[svg-examples] 

Column number=[15]
Line number=[2]
characters() [
   ]
startElement() uri=[], localName=[example], qName=[example]
Column number=[13]
Line number=[3]
characters() [
      The following Scalable Vector Graphics document describes a ]
characters() [
      blue-filled and black-stroked rectangle.
      ]
startCDATA()
characters() [<svg width="100%" height="100%" version="1.1"
           >
         <rect width="300" height="100"
               style="fill:rgb(0,0,255);stroke-width:1; stroke:rgb(0,0,0)"/>
      </svg>]
endCDATA()
characters() [
   ]
endElement() uri=[], localName=[example], qName=[example]
characters() [
]
endElement() uri=[], localName=[svg-examples], qName=[svg-examples]
endDocument()

第一个输出行证明先调用setDocumentLocator()。它还标识了Locator对象,当调用startElement()时,调用该对象的getColumnNumber()getLineNumber()方法来输出解析器位置——这些方法返回从 1 开始的列号和行号。

也许您对以下输出的三个实例感到好奇:

characters() [
   ]

跟在endCDATA()输出后面的这个输出的实例报告了一个回车/换行符组合,这个组合没有包含在前面的characters()方法调用中,传递给它的是 CDATA 部分的内容减去这些行结束符。相比之下,在对svg-examplesstartElement()调用之后和对exampleendElement()调用之后的输出实例就有些奇怪了。<svg-examples><example>之间没有内容,</example></svg-examples>之间也没有内容,还是有?

您可以通过修改svg-examples.xml来包含一个内部 DTD 来满足这种好奇心。在 XML 声明和<svg-examples>开始标记之间放置下面的 DTD(表示一个svg-examples元素包含一个或多个example元素,一个example元素包含解析的字符数据):

<!DOCTYPE svg-examples [
<!ELEMENT svg-examples (example+)>
<!ELEMENT example (#PCDATA)>
]>

继续,执行以下命令:

java SAXDemo svg-examples.xml

这一次,您应该会看到以下输出(尽管 hashcode 可能会有所不同):

setDocumentLocator() locator=[com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser$LocatorProxy@6d06d69c]
startDocument()
startDTD() name=[svg-examples]publicId=[null]systemId=[null]
endDTD()
startElement() uri=[], localName=[svg-examples], qName=[svg-examples]
Column number=[15]
Line number=[6]

ignorableWhitespace() [

   ] 

startElement() uri=[], localName=[example], qName=[example]
Column number=[13]
Line number=[7]
characters() [
      The following Scalable Vector Graphics document describes a
      blue-filled and black-stroked rectangle.]
characters() [
      ]
startCDATA()
characters() [<svg width="100%" height="100%" version="1.1"
           >
         <rect width="300" height="100"
               style="fill:rgb(0,0,255);stroke-width:1; stroke:rgb(0,0,0)"/>
      </svg>]
endCDATA()
characters() [
   ]
endElement() uri=[], localName=[example], qName=[example]

ignorableWhitespace() [

]

endElement() uri=[], localName=[svg-examples], qName=[svg-examples]
endDocument()

这个输出揭示了在svg-examplesstartElement()之后和exampleendElement()之后调用了ignorableWhitespace()方法。产生奇怪输出的前两个对characters()的调用报告了可忽略的空白。

回想一下,我以前将可忽略的空白定义为位于 DTD 不允许混合内容的标签之间的空白。例如,DTD 指出svg-examples应该只包含example元素,不包含example元素和解析的字符数据。然而,<svg-examples>标签后面的行结束符和<example>前面的前导空格是解析的字符数据。解析器现在通过调用ignorableWhitespace()来报告这些字符。

这一次,以下输出只出现了两次:

characters() [
   ]

第一次出现时,将行结束符与example元素的文本分开报告(在 CDATA 部分之前);它以前没有这样做,这证明了用元素的全部或部分内容调用了characters()。再次,第二次出现报告 CDATA 部分后面的行结束符。

让我们在没有之前给出的内部 DTD 的情况下验证svg-examples.xml。您可以通过执行下面的命令来做到这一点——不要忘记包含v命令行参数,否则文档将无法验证:

java SAXDemo svg-examples.xml v

在它的输出中有几行以error()为前缀的代码,如下所示:

error() org.xml.sax.SAXParseException; lineNumber: 2; columnNumber: 14; Document is invalid: no grammar found.
error() org.xml.sax.SAXParseException; lineNumber: 2; columnNumber: 14; Document root element "svg-examples", must match DOCTYPE root "null".

这几行表明还没有找到 DTD 语法。此外,解析器报告了svg-examples(它认为第一个遇到的元素是根元素)和null(它认为在没有 DTD 的情况下null是根元素的名称)之间的不匹配。这两种违规都不被认为是致命的,这就是为什么叫error()而不是fatalError()的原因。

将内部 DTD 添加到svg-examples.xml并重新执行java SAXDemo svg-examples.xml v。这一次,您应该在输出中看不到带有error()前缀的行。

Tip

SAX 2 验证默认为根据 DTD 进行验证。相反,要根据基于 XML 模式的模式进行验证,请将带有 http://www.w3.org/2001/XMLSchema 值的schemaLanguage属性添加到XMLReader对象中。通过在xmlr.parse(new InputSource(new FileReader(args[0])));前指定xmlr.setProperty("http://java.sun.com/xml/jaxp/properties/schemaLanguage", "http://www.w3.org/2001/XMLSchema");SAXDemo完成此任务。

创建自定义实体解析程序

在第一章探索 XML 时,我向您介绍了实体的概念,它是别名数据。然后,我讨论了一般实体和参数实体的内部和外部变体。

不同于其值在 DTD 中指定的内部实体,外部实体的值在 DTD 之外指定,并通过公共和/或系统标识符来标识。系统标识符是 URI,而公共标识符是正式的公共标识符。

XML 解析器通过连接到适当系统标识符的InputSource对象读取外部实体(包括外部 DTD 子集)。在许多情况下,您向解析器传递一个系统标识符或InputSource对象,让解析器发现在哪里可以找到从当前文档实体引用的其他实体。

但是,出于性能或其他原因,您可能希望解析器从不同的系统标识符中读取外部实体的值,例如本地 DTD 副本的系统标识符。您可以通过创建一个实体解析器来完成这项任务,该实体解析器使用公共标识符来选择不同的系统标识符。当遇到外部实体时,解析器调用自定义实体解析器来获取这个标识符。

考虑清单 2-3 对清单 1-1 的烤奶酪三明治配方的正式说明。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE recipeml PUBLIC "-//FormatData//DTD RecipeML 0.5//EN"
"http://www.formatdata.com/recipeml/recipeml.dtd">
<recipeml version="0.5">
   <recipe>
      <head>
         <title>Grilled Cheese Sandwich</title>
      </head>
      <ingredients>
         <ing>
            <amt><qty>2</qty><unit>slice</unit></amt>
            <item>bread</item>
         </ing>
         <ing>
            <amt><qty>1</qty><unit>slice</unit></amt>
            <item>cheese</item>
         </ing>
         <ing>
            <amt><qty>2</qty><unit>pat</unit></amt>
            <item>margarine</item>
         </ing>
      </ingredients>
      <directions>
         <step>Place frying pan on element and select medium heat.</step>
         <step>For each bread slice, smear one pat of margarine on one side 

of bread slice.</step>
         <step>Place cheese slice between bread slices with margarine-smeared sides away from the cheese.</step>
         <step>Place sandwich in frying pan with one margarine-smeared size in contact with pan.</step>
         <step>Fry for a couple of minutes and flip.</step>
         <step>Fry other side for a minute and serve.</step>
      </directions>
   </recipe>
</recipeml>
Listing 2-3.XML-Based Recipe for a Grilled Cheese Sandwich Specified in Recipe Markup Language

清单 2-3 用食谱标记语言(RecipeML)指定了烤奶酪三明治食谱,Recipe ml 是一种基于 XML 的标记食谱的语言。(一家名为 FormatData 的公司在 2000 年发布了这种格式; www.formatdata.com。)

文档类型声明将-//FormatData//DTD RecipeML 0.5//EN报告为正式的公共标识符,将 http://www.formatdata.com/recipeml/recipeml.dtd 报告为系统标识符。让我们将这个正式的公共标识符映射到recipeml.dtd,一个 DTD 文件本地副本的系统标识符,而不是保留默认的映射。

要创建一个定制的实体解析器来执行这种映射,您需要声明一个类,该类根据它的InputSource resolveEntity(String publicId, String systemId)方法来实现EntityResolver接口。然后使用传递的publicId值作为指向所需systemId值的映射的键,然后使用这个值创建并返回一个定制的InputSource。清单 2-4 展示了结果类。

import java.util.HashMap;
import java.util.Map;

import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

public class LocalRecipeML implements EntityResolver
{
   private Map<String, String> mappings = new HashMap<>();

   LocalRecipeML()
   {
      mappings.put("-//FormatData//DTD RecipeML 0.5//EN", "recipeml.dtd");
   }

   @Override
   public InputSource resolveEntity(String publicId, String systemId)
   {
      if (mappings.containsKey(publicId))
      {
         System.out.println("obtaining cached recipeml.dtd");
         systemId = mappings.get(publicId);
         InputSource localSource = new InputSource(systemId);
         return localSource;
      }
      return null;
   }
}

Listing 2-4.
LocalRecipeML

列表 2-4 声明LocalRecipeML。该类的构造函数在映射中存储 RecipeML DTD 的正式公共标识符和该 DTD 文档的本地副本的系统标识符。

Note

尽管在这个例子中没有必要使用映射(一个if (publicId.equals("-//FormatData//DTD RecipeML 0.5//EN")) return new InputSource("recipeml.dtd") else return null;语句就足够了),但我还是选择了使用映射,以防将来我想要扩展映射的数量。在另一种情况下,您可能会发现地图非常方便。例如,使用映射比在自定义实体解析器中使用一系列if语句更容易,它映射 XHTML 的严格、过渡和框架集正式公共标识符,并且还将其各种实体集映射到这些文档文件的本地副本。

覆盖的resolveEntity()方法使用publicId的参数在映射中定位相应的系统标识符——忽略systemId参数值,因为它从不引用recipeml.dtd的本地副本。找到映射后,会创建并返回一个InputSource对象。如果找不到映射,将返回null

要在SAXDemo中安装这个自定义实体解析器,请在parse()方法调用之前指定xmlr.setEntityResolver(new LocalRecipeML());。重新编译源代码后,执行以下命令:

java SAXDemo gcs.xml

在这里,gcs.xml店铺列表 2-3 的正文。在结果输出中,您应该在调用startEntity()之前看到消息“obtaining cached recipeml.dtd”。

Tip

SAX API 包括一个org.xml.sax.ext.EntityResolver2接口,为解析实体提供了改进的支持。如果你更喜欢实现EntityResolver2而不是EntityResolver,那么用一个特性名为use-entity-resolver2setFeature()调用代替setEntityResolver()调用来安装实体解析器(不要忘记 http://xml.org/sax/features/ 前缀)。

Exercises

以下练习旨在测试您对第二章内容的理解。

Define SAX.   How do you obtain a SAX 2-based parser?   What is the purpose of the XMLReader interface?   How do you tell a SAX parser to perform validation?   Identify the four kinds of SAX-oriented exceptions that can be thrown when working with SAX.   What interface does a handler class implement to respond to content-oriented events?   Identify the three other core interfaces that a handler class is likely to implement.   Define ignorable whitespace.   True or false: void error(SAXParseException exception) is called for all kinds of errors.   What is the purpose of the DefaultHandler class?   What is an entity? What is an entity resolver?   Apache Tomcat is an open-source web server developed by the Apache Software Foundation. Tomcat stores usernames, passwords, and roles (for authentication purposes) in its tomcat-users.xml configuration file. Create a DumpUserInfo application that uses SAX to parse the user elements in the following tomcat-users.xml file and, for each user element, dump its username, password, and roles attribute values to standard output in a key = value format:

<?xml version='1.0' encoding='utf-8'?>

<tomcat-users>
  <role rolename="dbadmin"/>
  <role rolename="manager"/>
  <user username="JohnD" password="password1" roles="dbadmin,manager"/>
  <user username="JillD" password="password2" roles="manager"/>
</tomcat-users>

Create a SAXSearch application that searches Exercise 1-21’s books.xml file for those book elements whose publisher child elements contain text that equals the application’s single command-line publisher name argument. Once there is a match, output the title element’s text followed by the book element’s isbn attribute value. For example, java SAXSearch Apress should output title = Beginning Groovy and Grails, isbn = 9781430210450, whereas java SAXSearch "Addison Wesley" should output title = Advanced C++, isbn = 0201548550 followed by title = Effective Java, isbn = 0201310058 on separate lines. Nothing should output when the command-line publisher name argument doesn’t match a publisher element’s text.   Use Listing 2-1’s SAXDemo application to validate Exercise 1-22’s books.xml content against its DTD. Execute java SAXDemo books.xml -v to perform the validation.

摘要

SAX 是一个基于事件的 Java API,用于从头到尾顺序解析 XML 文档。当面向 SAX 的解析器遇到文档信息集中的一个项目时,它通过调用应用程序的一个处理程序中的一个方法,将这个项目作为一个事件提供给应用程序,应用程序之前已经向解析器注册了这个处理程序。然后,应用程序可以通过以某种方式处理 infoset 项来使用该事件。

SAX 有两个主要版本。Java 通过javax.xml.parsers包的抽象SAXParserSAXParserFactory类实现 SAX 1,通过org.xml.sax包的XMLReader接口和org.xml.sax.helpers包的XMLReaderFactory类实现 SAX 2。org.xml.saxorg.xml.sax.extorg.xml.sax.helpers包提供了各种类型来增强这两种 Java 实现。

XMLReader提供了几种配置解析器和解析文档内容的方法。其中一些方法获取并设置内容处理程序、DTD 处理程序、实体解析器和错误处理程序,这些由ContentHandlerDTDHandlerEntityResolverErrorHandler接口描述。在了解了XMLReader的方法和这些接口之后,您了解了非标准的LexicalHandler接口以及如何创建一个定制的实体解析器。

第三章介绍了 Java 解析/创建 XML 文档的 DOM API。

三、使用 DOM 解析和创建 XML 文档

SAX 可以解析 XML 文档,但不能创建它们。相比之下,DOM 可以解析和创建 XML 文档。本章向您介绍 DOM。

什么是 DOM?

文档对象模型(DOM)是一个 Java API,用于将 XML 文档解析成内存中的节点树,并从节点树创建 XML 文档。在 DOM 解析器创建一棵树后,应用程序使用 DOM API 导航并从树的节点中提取信息集项目。

DOM 相对于 SAX 有两大优势:

  • DOM 允许随机访问文档的信息集项,而 SAX 只允许串行访问。
  • DOM 还允许您创建 XML 文档,而您只能用 SAX 解析文档。

但是,SAX 优于 DOM,因为它可以解析任意大小的文档,而由 DOM 解析或创建的文档的大小受到用于存储文档的基于节点的树结构的可用内存量的限制。

Note

DOM 起源于 Netscape Navigator 3 和 Microsoft Internet Explorer 3 web 浏览器的对象模型。这些实现统称为 DOM Level 0。因为每个供应商的 DOM 实现彼此之间只有轻微的兼容性,W3C 随后负责 DOM 的开发以促进标准化,并且到目前为止已经发布了 DOM 级别 1、2 和 3(级别 4 正在开发中)。Java 8 通过其 DOM API 支持所有三个 DOM 级别。

节点树

DOM 将 XML 文档视为由几种节点组成的树。该树只有一个根节点,除了根节点之外,所有节点都有一个父节点。此外,每个节点都有一个子节点列表。当该列表为空时,子节点称为叶节点。

Note

DOM 允许不属于树结构的节点存在。例如,元素节点的属性节点不被视为元素节点的子节点。此外,可以创建节点,但不能将其插入树中;它们也可以从树中删除。

每个节点都有一个节点名,对于有名称(如元素或属性的前缀名)的节点来说是完整的名称,对于未命名的节点来说是#节点类型,其中节点类型是cdata-sectioncommentdocumentdocument-fragmenttext中的一个。节点也有本地名称(没有前缀的名称)、前缀和名称空间 URIs(尽管这些属性对于某些类型的节点可能是空的,比如 comments)。最后,节点有字符串值,恰好是文本节点、评论节点,以及类似的面向文本的节点的内容;属性的规范化值;其他的都是空的。

DOM 将节点分为 12 种类型,其中 7 种类型可以视为 DOM 树的一部分。所有这些类型描述如下:

  • 属性节点:元素的属性之一。它有一个名称、一个本地名称、一个前缀、一个命名空间 URI 和一个规范化的字符串值。该值通过解析任何实体引用以及将空白序列转换为单个空白字符来规范化。属性节点有子节点,这些子节点是构成其值的文本和任何实体引用节点。属性节点不被视为其关联元素节点的子节点。
  • CDATA 节节点:CDATA 节的内容。它的名字是#cdata-section,它的值是 CDATA 部分的文本。
  • 注释节点:文档注释。它的名字是#comment,它的值是注释文本。注释节点有一个父节点,它是包含注释的节点。
  • 文档节点:DOM 树的根。它的名字是#document,它总是有一个单元素子节点,当文档有文档类型声明时,它也有一个文档类型子节点。此外,它还可以有额外的子节点,这些子节点描述出现在根元素的开始标记之前或之后的注释或处理指令。树中只能有一个文档节点。
  • 文档片段节点:一个可选的根节点。它的名字是#document-fragment,包含一个元素节点可以包含的任何东西(比如其他元素节点,甚至注释节点)。解析器从不创建这种类型的节点。但是,当应用程序提取 DOM 树的一部分并将其移动到其他地方时,它可以创建文档片段节点。文档片段节点允许您使用子树。
  • 文档类型节点:文档类型声明。它的名称是由根元素的文档类型声明指定的名称。此外,它还有一个(可能为空的)公共标识符、一个必需的系统标识符、一个内部 DTD 子集(可能为空)、一个父节点(包含文档类型节点的文档节点)以及 DTD 声明的符号和一般实体的列表。它的值总是设置为 null。
  • 元素节点:文档的元素。它有一个名称、一个本地名称、一个前缀(可能为空)和一个名称空间 URI,当元素不属于任何名称空间时,该名称空间为空。元素节点包含子节点,包括文本节点,甚至注释和处理指令节点。
  • 实体节点:在文档的 DTD 中声明的已解析和未解析的实体。当一个解析器读取一个 DTD 时,它会将一个实体节点映射(由实体名索引)附加到文档类型节点上。实体节点有一个名称和一个系统标识符,如果在 DTD 中出现了一个公共标识符,它也可以有一个公共标识符。最后,当解析器读取实体时,实体节点会得到一个包含实体替换文本的只读子节点列表。
  • 实体引用节点:对 DTD 声明的实体的引用。每个实体引用节点都有一个名称,当解析器没有用它们的值替换实体引用时,它就会包含在树中。解析器从不包含字符引用的实体引用节点(如&amp;Σ),因为它们被各自的字符替换并包含在文本节点中。
  • 符号节点:DTD 声明的符号。读取 DTD 的解析器将符号节点的映射(由符号名索引)附加到文档类型节点。每个符号节点都有一个名称和一个公共标识符或系统标识符,无论哪个标识符用于在 DTD 中声明符号。符号节点没有子节点。
  • 处理指令节点:出现在单据中的处理指令。它有一个名称(指令的目标)、一个字符串值(指令的数据)和一个父节点(它的包含节点)。
  • 文本节点:文档内容。它的名字是#text,当必须创建一个中间节点(比如注释)时,它代表元素内容的一部分。通过字符引用在文档中表示的字符,如<&,将被它们所表示的文字字符替换。当这些节点被写入文档时,这些字符必须被转义。

尽管这些节点类型存储了关于 XML 文档的大量信息,但是也有一些限制,比如不能在根元素之外暴露空白。此外,大多数 DTD 或模式信息,比如元素类型(<!ELEMENT...>)和属性类型(<xs:attribute...>,都不能通过 DOM 访问。

DOM Level 3 解决了 DOM 的各种限制。例如,尽管 DOM 没有为 XML 声明提供节点类型,但是 DOM Level 3 使得通过文档节点的属性访问 XML 声明的versionencodingstandalone属性值成为可能。

Note

非根节点从不孤立存在。例如,元素节点永远不会不属于文档或文档片段。即使当这些节点与主树断开连接时,它们仍然知道它们所属的文档或文档片段。

探索 DOM API

Java 通过javax.xml.parsers包的抽象DocumentBuilderDocumentBuilderFactory类以及非抽象FactoryConfigurationErrorParserConfigurationException类实现 DOM。org.w3c.domorg.w3c.dom.bootstraporg.w3c.dom.eventsorg.w3c.dom.lsorg.w3c.dom.views包提供了各种类型来增强这种实现。

获取 DOM 解析器/文档构建器

DOM 解析器也称为文档构建器,因为它在解析和创建 XML 文档方面有双重作用。通过首先实例化DocumentBuilderFactory,调用它的一个newInstance()类方法,可以获得一个 DOM 解析器/文档构建器。例如,下面的代码片段调用了DocumentBuilderFactoryDocumentBuilderFactory newInstance()类方法:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

在幕后,newInstance()遵循一个有序的查找过程来识别要加载的DocumentBuilderFactory实现类。这个过程首先检查javax.xml.parsers.DocumentBuilderFactory系统属性,最后在找不到其他类时选择 Java 平台的默认DocumentBuilderFactory实现类。如果一个实现类不可用(也许由javax.xml.parsers.DocumentBuilderFactory系统属性标识的类不存在)或者不能被实例化,newInstance()抛出一个FactoryConfigurationError类的实例。否则,它实例化该类并返回其实例。

获得一个DocumentBuilderFactory实例后,可以调用各种配置方法来配置工厂。例如,您可以用一个true参数调用DocumentBuilderFactoryvoid setNamespaceAware(boolean awareness)方法,告诉工厂任何返回的文档构建器必须提供对 XML 名称空间的支持。您还可以使用true作为参数调用void setValidating(boolean validating)来根据文档的 dtd 验证文档,或者调用void setSchema(Schema schema)来根据由schema标识的javax.xml.validation.Schema实例验证文档。

Validation API

Schema是 Java 的验证 API 的一员,它将文档解析与验证解耦,使应用程序更容易利用支持其他模式语言的专用验证库(如 Relax NG——参见 http://en.wikipedia.org/wiki/RELAX_NG ),也更容易指定模式的位置。

验证 API 与javax.xml.validation包相关联,该包还包括SchemaFactorySchemaFactoryLoaderTypeInfoProviderValidator,而ValidatorHandler. Schema是中心类,代表一个语法的不可变内存表示。

DOM 通过DocumentBuilderFactoryvoid setSchema(Schema schema)Schema getSchema()方法支持验证 API。类似地,SAX 1.0 支持通过 j avax.xml.parsers.SAXParserFactoryvoid setSchema(Schema schema)Schema getSchema()方法进行验证。SAX 2.0 和 StAX(参见第四章)不支持验证 API。

以下代码片段演示了 DOM 上下文中的验证 API:

// Parse an XML document into a DOM tree.
DocumentBuilder parser =
   DocumentBuilderFactory.newInstance().newDocumentBuilder();

Document document = parser.parse(new File("instance.xml"));
// Create a SchemaFactory capable of understanding W3C XML Schema (WXS).
   SchemaFactory factory =
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
// Load a WXS schema, represented by a Schema instance.
Source schemaFile = new StreamSource(new File("mySchema.xsd"));
Schema schema = factory.newSchema(schemaFile);
// Create a Validator instance, which is used to validate an XML document.
Validator validator = schema.newValidator();
// Validate the DOM tree.
try
{
   validator.validate(new DOMSource(document));
}
catch (SAXException saxe)
{
   // XML document is invalid!
}

这个例子引用了 XSLT 类型,比如Source。我在第六章探索 XSLT。

配置完工厂后,调用它的DocumentBuilder newDocumentBuilder()方法返回一个支持配置的文档生成器,如下所示:

DocumentBuilder db = dbf.newDocumentBuilder();

如果不能返回一个文档构建器(也许工厂不能创建一个支持 XML 名称空间的文档构建器),这个方法抛出一个ParserConfigurationException实例。

解析和创建 XML 文档

假设您已经成功地获得了一个文档生成器,接下来会发生什么取决于您是想要解析还是创建一个 XML 文档。

DocumentBuilder提供了几个重载的parse()方法,用于将 XML 文档解析成节点树。这些方法在获取文档的方式上有所不同。例如,Document parse(String uri)解析由基于字符串的 URI 参数标识的文档。

Note

null作为方法的第一个参数传递时,每个parse()方法抛出java.lang.IllegalArgumentException,当出现输入/输出错误时抛出java.io.IOException,当文档无法解析时抛出org.xml.sax.SAXException。最后一种异常类型表明DocumentBuilderparse()方法依赖 SAX 来处理实际的解析工作。因为 DOM 解析器更多地参与构建节点树,所以通常被称为文档构建器。

DocumentBuilder还声明了创建文档树的抽象Document newDocument()方法。

返回的org.w3c.dom.Document对象通过像DocumentType getDoctype()这样的方法提供了对已解析文档的访问,这使得文档类型声明通过org.w3c.dom.DocumentType接口可用。从概念上讲,Document是文档节点树的根。它还声明了各种“create”和其他创建节点树的方法。例如,Element createElement(String tagName)创建一个名为tagName的元素,返回一个具有指定名称的新的org.w3c.dom.Element对象,但是其本地名称、前缀和名称空间 URI 设置为null

Note

除了DocumentBuilderDocumentBuilderFactory和其他几个类,DOM 是基于接口的,其中DocumentDocumentType就是例子。在幕后,DOM 方法(如parse()方法)返回其类实现这些接口的对象。

Document和所有其他描述不同类型节点的org.w3c.dom接口都是org.w3c.dom.Node接口的子接口。因此,它们继承了Node的常量和方法。

Node声明 12 个常数,表示各种节点;ATTRIBUTE_NODEELEMENT_NODE就是例子。要识别给定的Node对象所代表的节点类型,调用Nodeshort getNodeType()方法,并将返回值与这些常量之一进行比较。

Note

使用getNodeType()和这些常量而不是使用instanceof和一个类名的基本原理是,DOM(对象模型,而不是 Java DOM API)被设计成语言独立的,像 AppleScript 这样的语言没有等同的instanceof

Node声明了几个获取和设置公共节点属性的方法。这些方法包括String getNodeName()String getLocalName()String getNamespaceURI()String getPrefix()void setPrefix(String prefix)String getNodeValue()void setNodeValue(String nodeValue),它们允许您获取和(对于某些属性)设置节点的名称(如#text)、本地名称、名称空间 URI、前缀和规范化的字符串值。

Note

各种Node方法(比如setPrefix()getNodeValue())在出错时抛出org.w3c.dom.DOMException类的一个实例。例如,当prefix参数包含非法字符、节点是只读的或者参数格式不正确时,setPrefix()会抛出这个异常。类似地,当getNodeValue()返回的字符多于实现平台上的DOMString(W3C 类型)变量所能容纳的字符时,getNodeValue()抛出DOMExceptionDOMException声明了一系列常量(如DOMSTRING_SIZE_ERR)对异常原因进行分类。

Node声明了几种导航节点树的方法。这里描述了它的三种导航方法:

  • 当一个节点有子节点时,boolean hasChildNodes()返回true
  • Node getFirstChild()返回节点的第一个子节点。
  • Node getLastChild()返回节点的最后一个子节点。

对于有多个子节点的节点,您会发现NodeList getChildNodes()方法非常方便。该方法返回一个org.w3c.dom.NodeList实例,其int getLength()方法返回列表中的节点数,其Node item(int index)方法返回列表中第index个位置的节点(或者当index的值无效时返回null——小于零或者大于等于getLength()的值)。

Node声明了通过插入、移除、替换和追加子节点来修改树的四种方法:

  • Node insertBefore (Node newChild, Node refChild)refChild指定的现有节点前插入newChild并返回newChild
  • Node removeChild (Node oldChild)从树中删除由oldChild标识的子节点,并返回oldChild
  • Node replaceChild (Node newChild, Node oldChild)newChild替换oldChild并返回oldChild
  • Node appendChild (Node newChild)newChild添加到当前节点子节点的末尾,并返回newChild

最后,Node声明了几个实用方法,包括Node cloneNode(boolean deep)(创建并返回当前节点的副本,当true传递给deep时递归克隆其子树),以及void normalize()(从给定节点开始向下遍历树,合并所有相邻的文本节点,删除那些空的文本节点)。

Tip

要获得元素节点的属性,首先调用NodeNamedNodeMap getAttributes()方法。当节点表示一个元素时,该方法返回一个org.w3c.dom.NamedNodeMap实现;否则,它返回null。除了通过名字声明访问这些节点的方法(比如Node getNamedItem (String name))之外,NamedNodeMap还通过index声明返回所有属性节点的int getLength()Node item(int index)方法。然后,通过调用诸如getNodeName()这样的方法来获得Node的名称。

除了继承Node的常量和方法,Document还声明了自己的方法。例如,您可以调用DocumentString getXmlEncoding()boolean getXmlStandalone()String getXmlVersion()方法来分别返回 XML 声明的encodingstandaloneversion属性值。

Document声明了三种定位一个或多个元素的方法:

  • Element getElementById(String elementId)返回具有与elementId指定的值相匹配的id属性(如<img id=...>)的元素。
  • NodeList getElementsByTagName(String tagname)返回与指定的tagName匹配的文档元素的节点列表(按文档顺序)。
  • 除了只将那些匹配localNamenamespaceURI值的元素添加到节点列表之外,NodeList getElementsByTagNameNS(String namespaceURI,String localName)等同于第二种方法。传递" * "namespaceURI匹配所有名称空间;传递" * "localName来匹配所有本地名称。

返回的元素节点和列表中的每个元素节点都实现了Element接口。该接口声明了返回树中派生元素的节点列表、与元素相关的属性等的方法。例如,String getAttribute(String name)返回由name标识的属性的值,而Attr getAttributeNode(String name)通过名称返回属性节点。返回的节点是org.w3c.dom.Attr接口的一个实现。

演示 DOM API

现在,您已经有了足够的信息来探索解析和创建 XML 文档的应用程序。清单 3-1 将源代码呈现给基于 DOM 的解析应用程序。

import java.io.IOException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.xml.sax.SAXException;

public class DOMDemo
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
         System.err.println("usage: java DOMDemo xmlfile");
         return;
      }
      try
      {
         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         dbf.setNamespaceAware(true);
         DocumentBuilder db = dbf.newDocumentBuilder();
         Document doc = db.parse(args[0]);
         System.out.printf("Version = %s%n", doc.getXmlVersion());
         System.out.printf("Encoding = %s%n", doc.getXmlEncoding());
         System.out.printf("Standalone = %b%n%n", doc.getXmlStandalone());
         if (doc.hasChildNodes())
         {
            NodeList nl = doc.getChildNodes();
            for (int i = 0; i < nl.getLength(); i++)
            {
               Node node = nl.item(i);
               if (node.getNodeType() == Node.ELEMENT_NODE)
                  dump((Element) node);
            }
         }
      }
      catch (IOException ioe)
      {
         System.err.println("IOE: " + ioe);
      }
      catch (SAXException saxe)
      {
         System.err.println("SAXE: " + saxe);
      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }

      catch (ParserConfigurationException pce)
      {
         System.err.println("PCE: " + pce);
      }
   }

   static void dump(Element e)
   {
      System.out.printf("Element: %s, %s, %s, %s%n", e.getNodeName(),
                        e.getLocalName(), e.getPrefix(),
                        e.getNamespaceURI());
      NamedNodeMap nnm = e.getAttributes();
      if (nnm != null)
         for (int i = 0; i < nnm.getLength(); i++)
         {
            Node node = nnm.item(i);
            Attr attr = e.getAttributeNode(node.getNodeName());
            System.out.printf("  Attribute %s = %s%n", attr.getName(), attr.getValue());
         }
      NodeList nl = e.getChildNodes();
      for (int i = 0; i < nl.getLength(); i++)
      {
         Node node = nl.item(i);
         if (node instanceof Element)
            dump((Element) node);
      }
   }
}

Listing 3-1.

DOMDemo

(Version 1)

DOMDemomain()方法首先验证已经指定了一个命令行参数(XML 文档的名称)。然后,它创建一个文档构建器工厂,通知工厂它需要一个知道名称空间的文档构建器,并让工厂返回这个文档构建器。

继续,main()将文档解析成节点树;输出 XML 声明的版本号、编码和独立属性值。并递归地转储所有元素节点(从根节点开始)及其属性值。

注意清单的一部分使用了getNodeType(),另一部分使用了instanceofgetNodeType()方法调用是不必要的(它只是为了演示才出现的),因为可以用instanceof来代替。然而,在dump()方法调用中从Node类型到Element类型的转换是必要的。

编译清单 3-1 如下:

javac DOMDemo.java

运行生成的应用程序来转储列表 1-3 的文章 XML 内容,如下所示:

java DOMDemo article.xml

您应该观察到以下输出:

Version = 1.0
Encoding = null
Standalone = false

Element: article, article, null, null
  Attribute lang = en
  Attribute title = The Rebirth of JavaFX
Element: abstract, abstract, null, null
Element: code-inline, code-inline, null, null
Element: body, body, null, null

每一个带Element前缀的行表示节点名,后面是本地名,后面是名称空间前缀,再后面是名称空间 URI。节点和本地名称是相同的,因为没有使用名称空间。出于同样的原因,名称空间前缀和名称空间 URI 都是null

继续,执行以下命令转储列表 1-5 的配方内容:

java DOMDemo recipe.xml

这一次,您会看到以下输出,其中包括名称空间信息:

Version = 1.0
Encoding = null
Standalone = false

Element: h:html, html, h, http://www.w3.org/1999/xhtml
  Attribute xmlns:h = http://www.w3.org/1999/xhtml
  Attribute xmlns:r = http://www.javajeff.ca/
Element: h:head, head, h, http://www.w3.org/1999/xhtml
Element: h:title, title, h, http://www.w3.org/1999/xhtml
Element: h:body, body, h, http://www.w3.org/1999/xhtml
Element: r:recipe, recipe, r, http://www.javajeff.ca/
Element: r:title, title, r, http://www.javajeff.ca/
Element: r:ingredients, ingredients, r, http://www.javajeff.ca/
Element: h:ul, ul, h, http://www.w3.org/1999/xhtml
Element: h:li, li, h, http://www.w3.org/1999/xhtml
Element: r:ingredient, ingredient, r, http://www.javajeff.ca/
  Attribute qty = 2
Element: h:li, li, h, http://www.w3.org/1999/xhtml
Element: r:ingredient, ingredient, r, http://www.javajeff.ca/
Element: h:li, li, h, http://www.w3.org/1999/xhtml
Element: r:ingredient, ingredient, r, http://www.javajeff.ca/
  Attribute qty = 2
Element: h:p, p, h, http://www.w3.org/1999/xhtml
Element: r:instructions, instructions, r, http://www.javajeff.ca/

清单 3-2 展示了另一个版本的DOMDemo应用程序,它简要演示了文档树的创建。

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

public class DOMDemo
{

   public static void main(String[] args)
   {
      try
      {
         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         DocumentBuilder db = dbf.newDocumentBuilder();
         Document doc = db.newDocument();
         // Create the root element.
         Element root = doc.createElement("movie");
         doc.appendChild(root);
         // Create name child element and add it to the root.
         Element name = doc.createElement("name");
         root.appendChild(name);
         // Add a text element to the name element.
         Text text = doc.createTextNode("Le Fabuleux Destin d'Amélie " + "Poulain"); name.appendChild(text);
         // Create language child element and add it to the root.
         Element language = doc.createElement("language");
         root.appendChild(language);
         // Add a text element to the language element.
         text = doc.createTextNode("français");
         language.appendChild(text);
         System.out.printf("Version = %s%n", doc.getXmlVersion());
         System.out.printf("Encoding = %s%n", doc.getXmlEncoding());
         System.out.printf("Standalone = %b%n%n", doc.getXmlStandalone());
         NodeList nl = doc.getChildNodes();
         for (int i = 0; i < nl.getLength(); i++)
         {
            Node node = nl.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE)
               dump((Element) node);
         }

      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (ParserConfigurationException pce)
      {
         System.err.println("PCE: " + pce);
      }
   }

   static void dump(Element e)
   {
      System.out.printf("Element: %s, %s, %s, %s%n", e.getNodeName(), e.getLocalName(), e.getPrefix(),  e.getNamespaceURI());
      NodeList nl = e.getChildNodes();
      for (int i = 0; i < nl.getLength(); i++)
      {
         Node node = nl.item(i);
         if (node instanceof Element)
            dump((Element) node);
         else
         if (node instanceof Text)
            System.out.printf("Text: %s%n", ((Text) node).getWholeText());
      }
   }
}

Listing 3-2.
DOMDemo (Version 2)

DOMDemo创建列表 1-2 的电影文档。它使用DocumentcreateElement()方法创建根movie元素和movienamelanguage子元素。它还使用DocumentText createTextNode(String data)方法创建附加到namelanguage节点的文本节点。注意对NodeappendChild()方法的调用,将子节点(如name)追加到父节点(如movie)。

在创建这个树之后,DOMDemo输出树的元素节点和其他信息。该输出如下所示:

Version = 1.0
Encoding = null
Standalone = false

Element: movie, null, null, null
Element: name, null, null, null
Text: Le Fabuleux Destin d'Amélie Poulain
Element: language, null, null, null
Text: français

输出有一个问题:XML 声明的encoding属性没有被设置为ISO-8859-1。您不能通过 DOM API 完成这项任务。相反,您需要使用 XSLT API。在第六章的中探索 XSLT 时,您将学习如何设置encoding属性,还将学习如何将这个树输出到 XML 文档文件中。

Exercises

以下练习旨在测试您对第三章内容的理解。

Define DOM.   True or false: Java 8 supports DOM Levels 1 and 2 only.   Identify the 12 types of DOM nodes.   How do you obtain a document builder?   How do you use a document builder to parse an XML document?   True or false: Document and all other org.w3c.dom interfaces that describe different kinds of nodes are subinterfaces of the Node interface.   How do you use a document builder to create a new XML document?   How would you determine if a node has children?   True or false: When creating a new XML document, you can use the DOM API to specify the XML declaration’s encoding attribute.   Exercise 2-12 asked you to create a DumpUserInfo application that uses SAX to parse the user elements in an example tomcat-users.xml file and, for each user element, dump its username, password, and roles attribute values to standard output in a key = value format. Recreate this application to use DOM.   Create a DOMSearch application that’s the equivalent of Exercise 2-13’s SAXSearch application.   Create a DOMValidate application based on Listing 3-1’s DOMDemo source code (plus one new line that enables validation) to validate Exercise 1-22’s books.xml content against its DTD. Execute java DOMValidate books.xml to perform the validation. You should observe no errors. However, if you attempt to validate books.xml without the DTD, you should observe errors.

摘要

文档对象模型(DOM)是一个 Java API,用于将 XML 文档解析成内存中的节点树,并从节点树创建 XML 文档。在 DOM 解析器创建一棵树后,应用程序使用 DOM API 导航并从树的节点中提取信息集项目。

DOM 将 XML 文档视为由几种节点组成的树:属性、CDATA 节、注释、文档、文档片段、文档类型、元素、实体、实体引用、符号、处理指令和文本。

DOM 解析器也称为文档构建器,因为它在解析和创建 XML 文档方面有双重作用。通过首先实例化DocumentBuilderFactory,您获得了一个文档构建器。然后调用工厂的newDocumentBuilder()方法返回文档构建器。

调用文档构建器的parse()方法之一将 XML 文档解析成节点树。调用以“create”为前缀的各种 document builder 方法(以及一些额外的方法)来创建一个 XML 文档。

第四章介绍了用于解析/创建 XML 文档的 StAX API。

四、用 StAX 解析和创建 XML 文档

Java 还包括用于解析和创建 XML 文档的 StAX API。本章向您介绍 StAX。

StAX 是什么?

Streaming API for XML (StAX)是一个 Java API,用于从开始到结束顺序解析 XML 文档,也用于创建 XML 文档。StAX 是由 Java 6 引入的,作为 SAX 和 DOM 的替代,位于这两个“对立面”的中间。

StAX vs. SAX and DOM

因为 Java 已经支持用于文档解析的 SAX 和 DOM 以及用于文档创建的 DOM,所以您可能想知道为什么还需要另一个 XML API。以下几点证明了 StAX 在 core Java 中的存在:

  • StAX(像 SAX 一样)可以用来解析任意大小的文档。相比之下,DOM 解析的文档的最大大小受到可用内存的限制,这使得 DOM 不适合内存有限的移动设备。
  • StAX(像 DOM 一样)可以用来创建文档。DOM 可以创建最大大小受可用内存限制的文档,与之相反,StAX 可以创建任意大小的文档。SAX 不能用来创建文档。
  • StAX(像 SAX 一样)使得应用程序几乎可以立即使用 infoset 项。相比之下,DOM 在构建完节点树之后才能使用这些项目。
  • StAX(像 DOM 一样)采用 pull 模型,在这种模型中,应用程序告诉解析器何时准备好接收下一个 infoset 项。这个模型基于迭代器设计模式(参见 http://sourcemaking.com/design_patterns/iterator ),这使得应用程序更容易编写和调试。相比之下,SAX 采用推送模型,在这种模型中,解析器通过事件将信息集项目传递给应用程序,而不管应用程序是否准备好接收它们。这个模型基于观察者设计模式(参见 http://sourcemaking.com/design_patterns/observer ),这导致应用程序通常更难编写和调试。

总之,StAX 可以解析或创建任意大小的文档,使应用程序几乎可以立即使用 infoset 项,并使用 pull 模型来管理应用程序。SAX 和 DOM 都没有提供所有这些优势。

探索 StAX

Java 通过存储在javax.xml.streamjavax.xml.stream.eventsjavax.xml.stream.util包中的类型实现 StAX。本节将向您介绍前两个包中的各种类型,同时展示如何使用 StAX 解析和创建 XML 文档。

Stream-Based vs. Event-Based Readers and Writers

StAX 解析器被称为文档阅读器,StAX 文档创建者被称为文档编写器。StAX 将文档阅读器和文档编写器分为基于流的和基于事件的。

基于流的读取器通过游标(信息集项指针)从输入流中提取下一个信息集项。类似地,基于流的编写器将下一个 infoset 项写入光标位置处的输出流。光标一次只能指向一个项目,并且总是向前移动,通常移动一个信息集项目。

在为 Java ME 等内存受限的环境编写代码时,基于流的读取器和写入器是合适的,因为您可以使用它们来创建更小、更高效的代码。它们还为低级别的库提供了更好的性能,在低级别的库中,性能是很重要的。

基于事件的读取器通过获取事件从输入流中提取下一个信息集项。类似地,基于事件的编写器通过向输出流添加事件,将下一个 infoset 项写入流中。与基于流的读取器和编写器相比,基于事件的读取器和编写器没有游标的概念。

基于事件的读取器和编写器适用于创建 XML 处理管道(转换前一个组件的输入并将转换后的输出传递给序列中的下一个组件的组件序列)、修改事件序列等。

解析 XML 文档

文档阅读器是通过调用在javax.xml.stream.XMLInputFactory类中声明的各种create方法获得的。这些创建方法分为两类:创建基于流的读取器的方法和创建基于事件的读取器的方法。

在获得基于流或基于事件的读取器之前,您需要通过调用一个newFactory()类方法来获得工厂的实例,比如XMLInputFactory newFactory():

XMLInputFactory xmlif = XMLInputFactory.newFactory();

Note

您也可以调用XMLInputFactory newInstance()类方法,但是您可能不希望这样做,因为为了保持 API 的一致性,它的同名但参数化的伴随方法已经被弃用,而且newInstance()也可能被弃用。

newFactory()方法遵循一个有序的查找过程来定位XMLInputFactory实现类。这个过程首先检查javax.xml.stream.XMLInputFactory系统属性,最后选择 Java 平台的默认XMLInputFactory实现类的名称。如果这个过程找不到类名,或者如果这个类不能被加载(或实例化),这个方法抛出一个javax.xml.stream.FactoryConfigurationError类的实例。

创建工厂后,调用XMLInputFactoryvoid setProperty(String name, Object value)方法,根据需要设置各种特性和属性。例如,您可以执行xmlif.setProperty(XMLInputFactory.IS_VALIDATING, true); ( true通过自动装箱作为java.lang.Boolean对象传递——参见 http://docs.oracle.com/javase/tutorial/java/data/autoboxing.html )来请求一个验证 DTD 的基于流的阅读器。然而,默认的 StAX 工厂实现抛出了java.lang.IllegalArgumentException,因为它不支持 DTD 验证。类似地,您可以执行xmlif.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true);来请求一个支持名称空间感知的基于事件的读取器。

用基于流的阅读器解析文档

基于流的阅读器是通过调用XMLInputFactorycreateXMLStreamReader()方法之一创建的,比如XMLStreamReader createXMLStreamReader(Reader reader)。当不能创建基于流的读取器时,这些方法抛出javax.xml.stream.XMLStreamException

下面的代码片段创建了一个基于流的阅读器,它的源代码是一个名为recipe.xml的文件:

Reader reader = new FileReader("recipe.xml");
XMLStreamReader xmlsr = xmlif.createXMLStreamReader(reader);

底层的javax.xml.stream.XMLStreamReader接口提供了用 StAX 读取 XML 数据的最有效的方法。当有下一个信息集项目要获取时,该接口的boolean hasNext()方法返回true;否则返回falseint next()方法将光标向前移动一个信息集项目,并返回一个标识该项目的类型的整数代码。

不是将next()的返回值与一个整数值进行比较,而是将这个值与一个javax.xml.stream.XMLStreamConstants信息集常量进行比较,比如START_ELEMENTDTD——XMLStreamReader扩展了XMLStreamConstants接口。

Note

您还可以通过调用XMLStreamReaderint getEventType()方法来获取光标所指向的信息集项的类型。在这个方法的名称中指定“Event”是很不幸的,因为它混淆了基于流的读取器和基于事件的读取器。

下面的代码片段使用hasNext()next()方法来编写一个解析循环,该循环检测每个元素的开始和结束:

while (xmlsr.hasNext())
{
   switch (xmlsr.next())
   {
      case XMLStreamReader.START_ELEMENT: // Do something at element start. break;
      case XMLStreamReader.END_ELEMENT  : // Do something at element end.
   }
}

XMLStreamReader还声明了提取信息集信息的各种方法。例如,next()返回XMLStreamReader.START_ELEMENTXMLStreamReader.END_ELEMENT时,QName getName()返回光标位置元素的限定名(作为javax.xml.namespace.QName实例)。

Note

将限定名描述为名称空间 URI、本地部分和前缀部分的组合。在实例化这个不可变的类(通过一个像QName(String namespaceURI, String localPart, String prefix)这样的构造函数)之后,您可以通过调用QNameString getNamespaceURI()String getLocalPart()String getPrefix()方法来返回这些组件。

清单 4-1 将源代码呈现给一个StAXDemo应用程序,该应用程序通过基于流的阅读器报告 XML 文档的开始和结束元素。

import java.io.FileNotFoundException;
import java.io.FileReader;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

class StAXDemo
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
         System.err.println("usage: java StAXDemo xmlfile");
         return;
      }
      try
      {
         XMLInputFactory xmlif = XMLInputFactory.newFactory();
         XMLStreamReader xmlsr;
         xmlsr = xmlif.createXMLStreamReader(new FileReader(args[0]));
         while (xmlsr.hasNext())
         {

            switch (xmlsr.next())
            {
               case XMLStreamReader.START_ELEMENT:
                  System.out.println("START_ELEMENT");
                  System.out.println("  Qname = " + xmlsr.getName());
                  break;

  case XMLStreamReader.END_ELEMENT:
                  System.out.println("END_ELEMENT");
                  System.out.println("  Qname = " + xmlsr.getName());
            }
         }
      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (FileNotFoundException fnfe)
      {
         System.err.println("FNFE: " + fnfe);
      }
      catch (XMLStreamException xmlse)
      {
         System.err.println("XMLSE: " + xmlse);
      }
   }
}

Listing 4-1.StAXDemo (version 1)

在验证了命令行参数的数量之后,清单 4-1 的main()方法创建一个工厂,使用该工厂创建一个基于流的读取器,该读取器从由单独的命令行参数标识的文件中获取 XML 数据,然后进入一个解析循环。每当next()返回XMLStreamReader.START_ELEMENTXMLStreamReader.END_ELEMENT时,就会调用XMLStreamReadergetName()方法来返回元素的限定名。

编译清单 4-1 如下:

javac StAXDemo.java

运行生成的应用程序来转储列表 1-2 的电影 XML 内容,如下所示:

java StAXDemo movie.xml

您应该观察到以下输出:

START_ELEMENT
  Qname = movie
START_ELEMENT
  Qname = name
END_ELEMENT
  Qname = name
START_ELEMENT
  Qname = language
END_ELEMENT
  Qname = language
END_ELEMENT
  Qname = movie

Note

XMLStreamReader声明了一个void close()方法,如果您的应用程序被设计为长时间运行,您将希望调用该方法来释放与这个基于流的读取器相关联的任何资源。调用此方法不会关闭基础输入源。

用基于事件的阅读器解析文档

基于事件的阅读器是通过调用XMLInputFactorycreateXMLEventReader()方法之一创建的,比如XMLEventReader createXMLEventReader(Reader reader)。当无法创建基于事件的读取器时,这些方法抛出XMLStreamException

下面的代码片段创建了一个基于事件的阅读器,它的源代码是一个名为recipe.xml的文件:

Reader reader = new FileReader("recipe.xml");
XMLEventReader xmler = xmlif.createXMLEventReader(reader);

高级的javax.xml.stream.XMLEventReader接口提供了一种效率稍低但更面向对象的方式来用 StAX 读取 XML 数据。当有事件要获取时,该接口的boolean hasNext()方法返回true;否则返回falseXMLEvent nextEvent()方法将下一个事件作为一个对象返回,该对象的类实现了javax.xml.stream.events.XMLEvent接口的子接口。

Note

XMLEvent是处理标记事件的基本接口。它声明适用于所有子接口的方法;例如,Location getLocation()(返回一个javax.xml.stream.Location对象,其int getCharacterOffset()和其他方法返回事件的位置信息)和int getEventType()(将事件类型作为XMLStreamConstants infoset 常量返回,如START_ELEMENTPROCESSING_INSTRUCTIONXMLEvent扩展XMLStreamConstants)。XMLEvent由其他javax.xml.stream.events接口子类型化,这些接口根据返回 infoset 项目特定信息的方法(例如AttributeQName getName()String getValue()方法)描述不同种类的事件(例如Attribute)。

下面的代码片段使用hasNext()nextEvent()方法编写一个解析循环,该循环检测元素的开始和结束:

while (xmler.hasNext())
{
   switch (xmler.nextEvent().getEventType())
   {
      case XMLEvent.START_ELEMENT: // Do something at element start.
                                   break;
      case XMLEvent.END_ELEMENT  : // Do something at element end.
   }
}

清单 4-2 将源代码呈现给一个StAXDemo应用程序,该应用程序通过基于事件的阅读器报告 XML 文档的开始和结束元素。

import java.io.FileNotFoundException;
import java.io.FileReader;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;

import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

class StAXDemo
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
         System.err.println("usage: java StAXDemo xmlfile");
         return;
      }
      try
      {
         XMLInputFactory xmlif = XMLInputFactory.newFactory();
         XMLEventReader xmler;

xmler = xmlif.createXMLEventReader(new FileReader(args[0]));
         while (xmler.hasNext())
         {
            XMLEvent xmle = xmler.nextEvent();
            switch (xmle.getEventType())
            {
               case XMLEvent.START_ELEMENT:
                  System.out.println("START_ELEMENT");
                  System.out.println("  Qname = " +
                                     ((StartElement) xmle).getName());
                  break;
               case XMLEvent.END_ELEMENT:
                  System.out.println("END_ELEMENT");
                  System.out.println("  Qname = " +
                                     ((EndElement) xmle).getName());
            }
         }
      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (FileNotFoundException fnfe)
      {
         System.err.println("FNFE: " + fnfe);
      }
      catch (XMLStreamException xmlse)
      {
         System.err.println("XMLSE: " + xmlse);
      }
   }
}

Listing 4-2.StAXDemo (version 2)

在验证了命令行参数的数量之后,清单 4-2 的main()方法创建一个工厂,使用该工厂创建一个基于事件的读取器,该读取器从由单独的命令行参数标识的文件中获取 XML 数据,然后进入一个解析循环。每当nextEvent()返回XMLEvent.START_ELEMENTXMLEvent.END_ELEMENT时,就会调用StartElementEndElementgetName()方法来返回元素的限定名。

编译清单 4-2 后,运行生成的应用程序来转储清单 1-3 的文章 XML 内容,如下所示:

java StAXDemo article.xml

您应该观察到以下输出:

START_ELEMENT
  Qname = article
START_ELEMENT
  Qname = abstract
START_ELEMENT
  Qname = code-inline
END_ELEMENT
  Qname = code-inline
END_ELEMENT
  Qname = abstract
START_ELEMENT
  Qname = body
END_ELEMENT
  Qname = body
END_ELEMENT
  Qname = article

Note

您还可以通过调用XMLInputFactorycreateFilteredReader()方法之一,如XMLEventReader createFilteredReader(XMLEventReader reader, EventFilter filter),创建一个基于过滤事件的阅读器来接受或拒绝各种事件。javax.xml.stream.EventFilter接口声明了一个boolean accept(XMLEvent event)方法,当指定的事件是事件序列的一部分时,该方法返回true;否则返回false

创建 XML 文档

文档编写器是通过调用在javax.xml.stream.XMLOutputFactory类中声明的各种create方法获得的。这些创建方法分为两类:创建基于流的编写器的方法和创建基于事件的编写器的方法。

在获得基于流或基于事件的编写器之前,您需要通过调用一个newFactory()类方法来获得工厂的实例,比如XMLOutputFactory newFactory():

XMLOutputFactory xmlof = XMLOutputFactory.newFactory();

Note

您也可以调用XMLOutputFactory newInstance()类方法,但是您可能不希望这样做,因为为了保持 API 的一致性,它的同名但参数化的伴随方法已经被弃用,而且newInstance()也可能被弃用。

newFactory()方法遵循一个有序的查找过程来定位XMLOutputFactory实现类。这个过程首先检查javax.xml.stream.XMLOutputFactory系统属性,最后选择 Java 平台的默认XMLOutputFactory实现类的名称。如果这个过程找不到类名,或者如果这个类不能被加载(或实例化),这个方法抛出一个FactoryConfigurationError类的实例。

创建工厂后,调用XMLOutputFactoryvoid setProperty(String name, Object value)方法,根据需要设置各种特性和属性。目前所有作家支持的唯一财产是XMLOutputFactory.IS_REPAIRING_NAMESPACES。启用时(通过将trueBoolean对象,如Boolean.TRUE传递给value),文档编写器负责所有名称空间绑定和声明,只需应用程序提供最少的帮助。就名称空间而言,输出总是格式良好的。但是,启用该属性会给编写 XML 的工作增加一些开销。

使用基于流的编写器创建文档

基于流的编写器是通过调用XMLOutputFactorycreateXMLStreamWriter()方法之一创建的,比如XMLStreamWriter createXMLStreamWriter(Writer writer)。当无法创建基于流的编写器时,这些方法抛出XMLStreamException

下面的代码片段创建了一个基于流的编写器,它的目标是一个名为recipe.xml的文件:

Writer writer = new FileWriter("recipe.xml");
XMLStreamWriter xmlsw = xmlof.createXMLStreamWriter(writer);

底层的XMLStreamWriter接口声明了几个将 infoset 项写到目的地的方法。下面的列表描述了其中的一些方法:

  • 关闭这个基于流的编写器并释放所有相关的资源。基础编写器未关闭。
  • 将任何缓存的数据写入底层编写器。
  • void setPrefix(String prefix, String uri)标识了uri值绑定到的名称空间prefix。这个prefixwriteStartElement()writeAttribute()writeEmptyElement()方法的变体使用,这些方法接受名称空间参数而不接受前缀。同样,它保持有效,直到对应于最后一次writeStartElement()调用的writeEndElement()调用。这个方法不产生任何输出。
  • void writeAttribute(String localName, String value)将由localName标识并具有指定的value的属性写入底层编写器。不包括名称空间前缀。该方法对&<>"字符进行转义。
  • void writeCharacters(String text)text的字符写入底层编写器。这个方法对&<>字符进行转义。
  • 关闭所有开始标签,并将相应的结束标签写入底层编写器。
  • void endElement()将结束标记写入底层编写器,依靠基于流的编写器的内部状态来确定标记的前缀和本地名称。
  • 将命名空间写入底层编写器。必须调用此方法以确保写入由setPrefix()指定并在此方法调用中复制的名称空间;否则,从名称空间的角度来看,生成的文档将不是格式良好的。
  • 将 XML 声明写入底层编写器。
  • void writeStartElement(String namespaceURI, String localName)用传递给namespaceURIlocalName的参数写一个开始标签给底层编写器。

清单 4-3 将源代码呈现给一个StAXDemo应用程序,该应用程序通过一个基于流的编写器创建一个包含清单 1-5 的信息集项目的recipe.xml文件。

import java.io.FileWriter;
import java.io.IOException;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

class StAXDemo
{
   public static void main(String[] args)
   {
      try
      {
         XMLOutputFactory xmlof = XMLOutputFactory.newFactory();
         XMLStreamWriter xmlsw;
         xmlsw = xmlof.createXMLStreamWriter(new FileWriter("recipe.xml"));
         xmlsw.writeStartDocument();
         xmlsw.setPrefix("h", "http://www.w3.org/1999/xhtml");
         xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "html");
         xmlsw.writeNamespace("h", "http://www.w3.org/1999/xhtml");
         xmlsw.writeNamespace("r", "http://www.javajeff.ca/");
         xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "head");
         xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "title");
         xmlsw.writeCharacters("Recipe");
         xmlsw.writeEndElement();
         xmlsw.writeEndElement();
         xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "body");
         xmlsw.setPrefix("r", "http://www.javajeff.ca/");
         xmlsw.writeStartElement("http://www.javajeff.ca/", "recipe");

         xmlsw.writeStartElement("http://www.javajeff.ca/", "title");
         xmlsw.writeCharacters("Grilled Cheese Sandwich");
         xmlsw.writeEndElement();
         xmlsw.writeStartElement("http://www.javajeff.ca/",
                                 "ingredients");
         xmlsw.setPrefix("h", "http://www.w3.org/1999/xhtml");
         xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "ul");
         xmlsw.writeStartElement("http://www.w3.org/1999/xhtml", "li");
         xmlsw.setPrefix("r", "http://www.javajeff.ca/");
         xmlsw.writeStartElement("http://www.javajeff.ca/", "ingredient");
         xmlsw.writeAttribute("qty", "2");
         xmlsw.writeCharacters("bread slice");
         xmlsw.writeEndElement();
         xmlsw.setPrefix("h", "http://www.w3.org/1999/xhtml");
         xmlsw.writeEndElement();
         xmlsw.writeEndElement();
         xmlsw.setPrefix("r", "http://www.javajeff.ca/");
         xmlsw.writeEndElement();
         xmlsw.writeEndDocument();
         xmlsw.flush();
         xmlsw.close();
      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (IOException ioe)
      {
         System.err.println("IOE: " + ioe);
      }
      catch (XMLStreamException xmlse)
      {

         System.err.println("XMLSE: " + xmlse);
      }
   }
}

Listing 4-3.
StAXDemo (version 3)

尽管清单 4-3 很容易理解,但是您可能会对setPrefix()writeStartElement()方法调用中名称空间 URIs 的重复感到困惑。例如,您可能想知道xmlsw.setPrefix("h", " http://www.w3.org/1999/xhtml ");中的重复 URIs 及其xmlsw.writeStartElement(" http://www.w3.org/1999/xhtml ", "html");继任者。

setPrefix()方法调用创建名称空间前缀(值)和 URI(键)之间的映射,而不生成任何输出。writeStartElement()方法调用指定了 URI 键,该方法使用该键来访问前缀值,然后在将该标签写入底层编写器之前,将该前缀值(用冒号字符)添加到html开始标签的名称之前。

编译清单 4-3 并运行生成的应用程序。您应该在当前目录中发现一个recipe.xml文件。

使用基于事件的编写器创建文档

基于事件的编写器是通过调用XMLOutputFactorycreateXMLEventWriter()方法之一创建的,比如XMLEventWriter createXMLEventWriter(Writer writer)。当无法创建基于事件的编写器时,这些方法抛出XMLStreamException

以下代码片段创建了一个基于事件的编写器,其目标是一个名为recipe.xml的文件:

Writer writer = new FileWriter("recipe.xml");
XMLEventWriter xmlew = xmlof.createXMLEventWriter(writer);

高级的XMLEventWriter接口声明了void add(XMLEvent event)方法,用于将描述信息集项目的事件添加到底层编写器实现的输出流中。传递给event的每个参数都是一个类的实例,该类实现了XMLEvent的子接口(比如AttributeStartElement)。

Tip

XMLEventWriter还声明了一个void add(XMLEventReader reader)方法,您可以用它将一个XMLEventReader实例链接到一个XMLEventWriter实例。

为了省去你实现这些接口的麻烦,StAX 提供了javax.xml.stream.EventFactory。这个实用程序类声明了创建XMLEvent子接口实现的各种工厂方法。例如,Comment createComment(String text)返回一个对象,该对象的类实现了XMLEventjavax.xml.stream.events.Comment子接口。

因为这些工厂方法被声明为abstract,所以您必须首先获得一个EventFactory类的实例。您可以通过调用EventFactoryXMLEventFactory newFactory()类方法轻松完成这项任务,如下所示:

XMLEventFactory xmlef = XMLEventFactory.newFactory();

然后,您可以获得一个XMLEvent子接口实现,如下所示:

XMLEvent comment = xmlef.createComment("ToDo");

清单 4-4 将源代码呈现给一个StAXDemo应用程序,该应用程序通过基于事件的编写器创建一个recipe.xml文件,其中包含清单 1-5 的许多信息集项目。

import java.io.FileWriter;
import java.io.IOException;

import java.util.Iterator;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;

import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Namespace;
import javax.xml.stream.events.XMLEvent;

class StAXDemo
{
   public static void main(String[] args)
   {
      try
      {
         XMLOutputFactory xmlof = XMLOutputFactory.newFactory();
         XMLEventWriter xmlew;
         xmlew = xmlof.createXMLEventWriter(new FileWriter("recipe.xml"));
         final XMLEventFactory xmlef = XMLEventFactory.newFactory();
         XMLEvent event = xmlef.createStartDocument();
         xmlew.add(event);
         Iterator<Namespace> nsIter;
         nsIter = new Iterator<Namespace>()
         {
            int index = 0;
            Namespace[] ns;
            {
               ns = new Namespace[2];
               ns[0] = xmlef.
                          createNamespace("h",
                                          "http://www.w3.org/1999/xhtml");
               ns[1] = xmlef.
                          createNamespace("r",
                                          "http://www.javajeff.ca/");
            }

            @Override
            public boolean hasNext()
            {
               return index != 2;
            }
            @Override
            public Namespace next()
            {
               return ns[index++];
            }
            @Override
            public void remove()
            {
               throw new UnsupportedOperationException();
            }
         };
         event = xmlef.createStartElement("h",
                                          "http://www.w3.org/1999/xhtml",
                                          "html", null, nsIter);
         xmlew.add(event);
         event = xmlef.createStartElement("h",
                                          "http://www.w3.org/1999/xhtml",
                                          "head");
         xmlew.add(event);
         event = xmlef.createStartElement("h",
                                          "http://www.w3.org/1999/xhtml",
                                          "title");
         xmlew.add(event);
         event = xmlef.createCharacters("Recipe");
         xmlew.add(event);
         event = xmlef.createEndElement("h",
                                        "http://www.w3.org/1999/xhtml",
                                        "title");
         xmlew.add(event);
         event = xmlef.createEndElement("h",
                                        "http://www.w3.org/1999/xhtml",
                                        "head");
         xmlew.add(event);
         event = xmlef.createStartElement("h",
                                          "http://www.w3.org/1999/xhtml",
                                          "body");
         xmlew.add(event);
         event = xmlef.createStartElement("r",
                                          "http://www.javajeff.ca/",
                                          "recipe");
         xmlew.add(event);
         event = xmlef.createStartElement("r",
                                          "http://www.javajeff.ca/",
                                          "title");
         xmlew.add(event);
         event = xmlef.createCharacters("Grilled Cheese Sandwich");
         xmlew.add(event);
         event = xmlef.createEndElement("r",
                                        "http://www.javajeff.ca/",

                                        "title");
         xmlew.add(event);
         event = xmlef.createStartElement("r",
                                          "http://www.javajeff.ca/",
                                          "ingredients");
         xmlew.add(event);
         event = xmlef.createStartElement("h",
                                          "http://www.w3.org/1999/xhtml",
                                          "ul");
         xmlew.add(event);
         event = xmlef.createStartElement("h",
                                          "http://www.w3.org/1999/xhtml",
                                          "li");
         xmlew.add(event);
         Iterator<Attribute> attrIter;
         attrIter = new Iterator<Attribute>()
         {

            int index = 0;
            Attribute[] attrs;
            {
               attrs = new Attribute[1];
               attrs[0] = xmlef.createAttribute("qty", "2");
            }
            @Override
            public boolean hasNext()
            {
               return index != 1;
            }
            @Override
            public Attribute next()
            {
               return attrs[index++];
            }
            @Override
            public void remove()
            {
               throw new UnsupportedOperationException();
            }
         };
         event = xmlef.createStartElement("r",
                                          "http://www.javajeff.ca/",
                                          "ingredient", attrIter, null);
         xmlew.add(event);
         event = xmlef.createCharacters("bread slice");
         xmlew.add(event);
         event = xmlef.createEndElement("r",
                                        "http://www.javajeff.ca/",
                                        "ingredient");
         xmlew.add(event);
         event = xmlef.createEndElement("h",
                                        "http://www.w3.org/1999/xhtml

",
                                        "li");
         xmlew.add(event);
         event = xmlef.createEndElement("h",
                                        "http://www.w3.org/1999/xhtml",
                                        "ul");
         xmlew.add(event);
         event = xmlef.createEndElement("r",
                                        "http://www.javajeff.ca/",
                                        "ingredients");
         xmlew.add(event);
         event = xmlef.createEndElement("r",
                                        "http://www.javajeff.ca/",
                                        "recipe");
         xmlew.add(event);
         event = xmlef.createEndElement("h",
                                        "http://www.w3.org/1999/xhtml",
                                        "body");
         xmlew.add(event);
         event = xmlef.createEndElement("h",
                                        "http://www.w3.org/1999/xhtml",
                                        "html");
         xmlew.add(event);
         xmlew.flush();
         xmlew.close();
      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (IOException ioe)
      {
         System.err.println("IOE: " + ioe);
      }
      catch (XMLStreamException xmlse)
      {
         System.err.println("XMLSE: " + xmlse);
      }
   }
}

Listing 4-4.
StAXDemo (version 4)

清单 4-4 应该很容易理解;这是基于事件的清单 4-3 的等价物。注意,这个清单包括从实现这个接口的匿名类创建的java.util.Iterator实例。创建这些迭代器是为了将名称空间或属性传递给XMLEventFactoryStartElement createStartElement(String prefix, String namespaceUri, String localName, Iterator attributes, Iterator namespaces)方法。(当迭代器不适用时,可以将null传递给这个参数;例如,当开始标签没有属性时。)

编译清单 4-4 并运行生成的应用程序。您应该在当前目录中发现一个recipe.xml文件。

Exercises

以下练习旨在测试您对第四章内容的理解。

Define StAX.   What packages make up the StAX API?   True or false: A stream-based reader extracts the next infoset item from an input stream by obtaining an event.   How do you obtain a document reader? How do you obtain a document writer?   What does a document writer do when you call XMLOutputFactory’s void setProperty(String name, Object value) method with XMLOutputFactory.IS_REPAIRING_NAMESPACES as the property name and true as the value?   Create a ParseXMLDoc application that uses a StAX stream-based reader to parse its single command-line argument, an XML document. After creating this reader, the application should verify that a START_DOCUMENT infoset item has been detected, and then enter a loop that reads the next item and uses a switch statement to output a message corresponding to the item that has been read: ATTRIBUTE, CDATA, CHARACTERS, COMMENT, DTD, END_ELEMENT, ENTITY_DECLARATION, ENTITY_REFERENCE, NAMESPACE, NOTATION_DECLARATION, PROCESSING_INSTRUCTION, SPACE, or START_ELEMENT. When START_ELEMENT is detected, output this element’s name and local name, and output the local names and values of all attributes. The loop ends when the END_DOCUMENT infoset item has been detected. Explicitly close the stream reader followed by the file reader upon which it’s based. Test this application with Exercise 1-21’s books.xml file.

摘要

StAX 是一个 Java API,用于从开始到结束顺序解析 XML 文档,也用于创建 XML 文档。Java 通过存储在javax.xml.streamjavax.xml.stream.eventsjavax.xml.stream.util包中的类型实现 StAX。

StAX 解析器被称为文档阅读器,StAX 文档创建者被称为文档编写器。StAX 将文档阅读器和文档编写器分为基于流的和基于事件的。

文档阅读器是通过调用在XMLInputFactory类中声明的各种create方法获得的。文档编写者是通过调用在XMLOutputFactory类中声明的各种create方法获得的。

第五章介绍了 Java 的 XPath API 来简化 DOM 节点访问。

五、使用 XPath 选择节点

Java 包含一个 XPath API,用于简化对 DOM 树节点的访问。本章向您介绍 XPath。

什么是 XPath?

XPath 是一种非 XML 声明性查询语言(由 W3C 定义),用于选择 XML 文档的信息集项目作为一个或多个节点。例如,您可以使用 XPath 来定位清单 1-1 的第三个ingredient元素并返回这个元素节点。

除了简化对 DOM 树节点的访问之外,XPath 通常用于 XSLT 的上下文中(在第六章中讨论),通常用来选择(通过 XPath 表达式)那些要复制到输出文档的输入文档元素。Java 8 支持 XPath 1.0,被分配了包javax.xml.xpath

XPath 语言入门

XPath 将 XML 文档视为从根节点开始的节点树。这种语言识别七种节点:元素、属性、文本、名称空间、处理指令、注释和文档。它不识别 CDATA 节、实体引用或文档类型声明。

Note

DOM 树的根节点(一个org.w3c.dom.Document对象)与文档的根元素不同。DOM 树的根节点包含整个文档,包括根元素、出现在根元素开始标记之前的任何注释或处理指令,以及出现在根元素结束标记之后的任何注释或处理指令。

位置路径表达式

XPath 为选择节点提供了位置路径表达式。位置路径表达式通过从上下文节点(根节点或当前节点的其他文档节点)开始的一系列步骤来定位节点。返回的节点集(称为节点集)可能为空,也可能包含一个或多个节点。

最简单的位置路径表达式选择文档的根节点,由一个正斜杠字符(/)组成。下一个最简单的位置路径表达式是元素的名称,它选择具有该名称的上下文节点的所有子元素。例如,ingredient引用清单 1-1 的菜谱文档中上下文节点的所有ingredient子元素。当上下文节点是ingredients时,这个 XPath 表达式返回一组三个ingredient节点。但是,如果recipeinstructions恰好是上下文节点,ingredient不会返回任何节点(ingredient只是ingredients的子节点)。当表达式以正斜杠(/)开始时,表达式表示从根节点开始的绝对路径。例如,表达式/movie选择清单 1-2 的电影文档中根节点的所有movie子元素。

属性也由位置路径表达式处理。要选择元素的属性,请指定@,后跟属性的名称。例如,@qty选择上下文节点的qty属性节点。

在大多数情况下,您将使用根节点、元素节点和属性节点。但是,您可能还需要使用名称空间节点、文本节点、处理指令节点和注释节点。与通常由 XSLT 处理的名称空间节点不同,您更可能需要处理注释、文本和处理指令。XPath 提供了用于选择注释、文本和处理指令节点的comment()text()processing-instruction()函数。

comment()text()函数不需要参数,因为注释和文本节点没有名称。每个注释都是一个单独的注释节点,每个文本节点指定没有被标签打断的最长文本串。可以用一个标识处理指令目标的参数调用processing-instruction()函数。如果不带参数调用,则选择上下文节点的所有处理指令子节点。

XPath 为选择未知节点提供了三个通配符:

  • *匹配任何元素节点,不考虑节点的类型。它不匹配属性、文本节点、注释或处理指令节点。当您在*前放置一个名称空间前缀时,只有属于该名称空间的元素才被匹配。
  • node()是匹配所有节点的函数。
  • @*匹配所有属性节点。

Note

XPath 允许您使用竖线(|)进行多重选择。例如,author/*|publisher/*选择author的子节点和publisher的子节点,*|@*匹配所有元素和属性,但不匹配文本、注释或处理指令节点。

XPath 允许您通过使用/字符来分隔步骤,从而将它们组合成复合路径。对于以/开头的路径,第一个路径步长是相对于根节点的;否则,第一个路径步骤相对于另一个上下文节点。例如,/movie/name从根节点开始,选择根节点的所有movie元素子节点,选择所选movie节点的所有name子节点。如果您想要返回所选择的name元素的所有文本节点,您可以指定/movie/name/text()

复合路径可以包括//来从上下文节点的所有后代中选择节点(包括上下文节点)。当放置在表达式的开始时,//从整个树中选择节点。例如,//ingredient选择树中的所有ingredient节点。

对于允许您用单句点(.)标识当前目录,用双句点(..)标识其父目录的文件系统,您可以指定单句点来表示当前节点,并用双句点来表示当前节点的父节点。(通常在 XSLT 中使用一个句点来表示您想要访问当前匹配元素的值。)

可能有必要缩小 XPath 表达式返回的节点的选择范围。例如,表达式/recipe/ingredients/ingredient返回所有的ingredient节点,但是也许您只想返回第一个ingredient节点。您可以通过在位置路径中包含谓词来缩小选择范围。

谓词是一个方括号分隔的布尔表达式,针对每个选定的节点进行测试。如果表达式计算结果为true,则该节点包含在 XPath 表达式返回的节点集中;否则,该节点不会包含在集合中。例如,/recipe/ingredients/ingredient[1]选择第一个ingredient元素,它是ingredients元素的子元素。

谓词可以包括预定义的函数(如last()position())、运算符(如-<=)以及其他项。请考虑以下示例:

  • /recipe/ingredients/ingredient[last()]选择最后一个ingredient元素,它是ingredients元素的子元素。
  • /recipe/ingredients/ingredient[last() - 1]选择倒数第二个ingredient元素,它是ingredients元素的子元素。
  • /recipe/ingredients/ingredient[position() < 3]选择前两个ingredient元素,它们是ingredients元素的子元素。
  • //ingredient[@qty]选择所有具有qty属性的ingredient元素(无论它们位于何处)。
  • //ingredient[@qty='1']//ingredient[@qty="1"]选择所有具有qty属性值1ingredient元素(无论它们位于何处)。

Note

XPath 预定义了几个用于节点集的函数:last()返回一个标识最后一个节点的数字,position()返回一个标识节点位置的数字,count()返回其节点集参数中的节点数,id()通过元素的唯一 id 选择元素并返回这些元素的节点集,local-name()返回其节点集参数中第一个节点的限定名的本地部分,namespace-uri()返回其节点集参数中第一个节点的限定名的名称空间部分,name()返回其节点集中第一个节点的限定名

尽管谓词应该是布尔表达式,但谓词可能不会计算为布尔值。例如,它可以计算数字或字符串—XPath 支持布尔值、数字(IEEE 754 双精度浮点值)、字符串表达式类型以及位置路径表达式的节点集类型。如果谓词的计算结果是一个数字,当它等于上下文节点的位置时,XPath 会将这个数字转换为true;否则,XPath 会将这个数字转换成false。如果谓词的结果是一个字符串,当字符串不为空时,XPath 会将该字符串转换为true;否则,XPath 会将该字符串转换为false。最后,如果一个谓词评估为一个节点集,当节点集非空时,XPath 将该节点集转换为true;否则,XPath 会将该节点集转换为false

Note

前面给出的位置路径表达式示例演示了 XPath 的缩写语法。但是,XPath 还支持完整的语法,该语法更好地描述了正在发生的事情,并且基于轴说明符,该说明符指示 XML 文档的树表示中的导航方向。例如,/movie/name使用缩写语法选择根节点的所有movie子元素,然后选择movie元素的所有name子元素,/child::movie/child::name使用扩展语法完成相同的任务。查看维基百科的“XPath”条目( http://en.wikipedia.org/wiki/XPath_1.0 )了解更多信息。

通用表达式

位置路径表达式(返回节点集)是 XPath 表达式的一种。XPath 还支持计算结果为布尔值(比如谓词)、数字或字符串类型的通用表达式;比如position() = 26.8"Hello"。XSLT 中经常使用通用表达式。

XPath 布尔值可以通过关系运算符<<=>>==!=进行比较。布尔表达式可以通过使用操作符andor来组合。此外,XPath 预定义了以下函数:

  • boolean()返回数字、字符串或节点集的布尔值。
  • 当其布尔参数为false时,not()返回true,反之亦然。
  • true()返回true
  • false()返回false
  • lang()根据上下文节点的语言(由xml:lang属性指定)是否与参数字符串指定的语言相同或者是该语言的子语言,返回truefalse

XPath 数值可以通过运算符+-*divmod(余数)进行操作;正斜杠不能用于除法,因为它用于分隔位置步骤。所有五个操作符的行为都像 Java 语言中的操作符一样。XPath 还预定义了以下函数:

  • number()将其参数转换为数字。
  • sum()返回其 nodeset 参数中节点所代表的数值之和。
  • floor()返回不大于其 number 参数的最大(最接近正无穷大)数,这是一个整数。
  • ceiling()返回不小于其 number 参数的最小(最接近负无穷大)数,这是一个整数。
  • round()返回与参数最接近的整数。当有两个这样的数字时,返回最接近正无穷大的一个。

XPath 字符串是用单引号或双引号括起来的有序字符序列。字符串文字不能包含同样用于分隔字符串的引号。例如,包含单引号的字符串不能用单引号分隔。XPath 提供了用于比较字符串的=!=操作符。XPath 还预定义了以下函数:

  • string()将其参数转换为字符串。
  • concat()返回其字符串参数的串联。
  • 当第一个参数字符串以第二个参数字符串开始时,starts-with()返回true(否则返回false)。
  • 当第一个参数字符串包含第二个参数字符串时,contains()返回true(否则返回false)。
  • substring-before()返回第一个参数字符串中第二个参数字符串第一次出现之前的第一个参数字符串的子字符串,或者当第一个参数字符串不包含第二个参数字符串时返回空字符串。
  • substring-after()返回第一个参数字符串中第二个参数字符串第一次出现后的第一个参数字符串的子字符串,或者当第一个参数字符串不包含第二个参数字符串时返回空字符串。
  • substring()返回第一个(字符串)参数的子字符串,从第二个(数字)参数指定的位置开始,长度由第三个(数字)参数指定。
  • string-length()返回其字符串参数中的字符数(或在没有参数的情况下转换为字符串时上下文节点的长度)。
  • normalize-space()返回带有空格的参数字符串,通过去除前导和尾随空格并用单个空格替换空格字符序列(或者在没有参数的情况下转换为字符串时在上下文节点上执行相同的操作)来规范化空格。
  • translate()返回第一个参数字符串,第二个参数字符串中出现的字符被第三个参数字符串中相应位置的字符替换。

XPath 和 DOM

假设你需要有人在你家买一袋糖。你会问这个人“请给我买些糖。”或者,你可以这样说:“请打开前门。走到人行道上。向左转。沿着人行道走三个街区。向右转。沿着人行道走一个街区。进入商店。去 7 号通道。沿着过道走两米。拿起一袋糖。走向收银台。付钱买糖。折回你的家。”大多数人会希望得到较短的指导,如果你养成了提供较长指导的习惯,他们可能会让你去某个机构。

遍历节点的 DOM 树类似于提供更长的指令序列。相比之下,XPath 让您通过简洁的指令遍历这棵树。要亲自了解这种差异,请考虑这样一个场景:您有一个基于 XML 的 contacts 文档,其中列出了您的各种专业联系人。清单 5-1 给出了这样一个文档的简单例子。

<?xml version="1.0"?>
<contacts>
   <contact>
      <name>John Doe</name>
      <city>Chicago</city>
      <city>Denver</city>
   </contact>
   <contact>
      <name>Jane Doe</name>
      <city>New York</city>
   </contact>
   <contact>
      <name>Sandra Smith</name>
      <city>Denver</city>
      <city>Miami</city>
   </contact>
   <contact>
     <name>Bob Jones</name>
     <city>Chicago</city>
   </contact>
</contacts>
Listing 5-1.XML-Based Contacts Database

清单 5-1 揭示了一个简单的 XML 语法,它由一个包含一系列contact元素的contacts根元素组成。每个contact元素包含一个name元素和一个或多个city元素(各种联系人经常出差,在每个城市花费大量时间)。为了保持示例简单,我没有提供 DTD 或模式。

假设您想查找并输出每年至少有一部分时间住在芝加哥的所有联系人的姓名。清单 5-2 将源代码呈现给一个用 DOM API 完成这项任务的DOMSearch应用程序。

import java.io.IOException;

import java.util.ArrayList;
import java.util.List; 

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.xml.sax.SAXException;

public class DOMSearch
{
   public static void main(String[] args)
   {
      try
      {
         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         DocumentBuilder db = dbf.newDocumentBuilder();
         Document doc = db.parse("contacts.xml");
         List<String> contactNames = new ArrayList<String>();
         NodeList contacts = doc.getElementsByTagName("contact");
         for (int i = 0; i < contacts.getLength(); i++)
         {
            Element contact = (Element) contacts.item(i);
            NodeList cities = contact.getElementsByTagName("city");
            boolean chicago = false;
            for (int j = 0; j < cities.getLength(); j++)
            {
               Element city = (Element) cities.item(j);
               NodeList children = city.getChildNodes();
               StringBuilder sb = new StringBuilder();
               for (int k = 0; k < children.getLength(); k++)
               {

                  Node child = children.item(k);
                  if (child.getNodeType() == Node.TEXT_NODE)
                     sb.append(child.getNodeValue());
               }
               if (sb.toString().equals("Chicago"))
               {
                  chicago = true;
                  break;
               }
            }
            if (chicago)
            {
               NodeList names = contact.getElementsByTagName("name");
               contactNames.add(names.item(0).getFirstChild().
                                getNodeValue());
            }
         }

         for (String contactName: contactNames)
            System.out.println(contactName);
      }
      catch (IOException ioe)
      {
         System.err.println("IOE: " + ioe);
      }
      catch (SAXException saxe)
      {
         System.err.println("SAXE: " + saxe);
      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (ParserConfigurationException pce)
      {
         System.err.println("PCE: " + pce);
      }
   }

}

Listing 5-2.Locating Chicago Contacts with the DOM API

在解析contacts.xml并构建 DOM 树之后,main()使用DocumentgetElementsByTagName()方法返回contact元素节点的org.w3c.dom.NodeList。对于这个列表的每个成员,main()提取contact元素节点,并使用这个节点和getElementsByTagName()返回一个contact元素节点的city元素节点的NodeList

对于cities列表中的每个成员,main()提取city元素节点,并使用该节点和getElementsByTagName()返回city元素节点的子节点的NodeList。本例中只有一个子文本节点,但是注释或处理指令的出现会增加子节点的数量。例如,<city>Chicago<!--The windy city--></city>将子节点的数量增加到 2。

如果子节点类型表明它是一个文本节点,则子节点的值(通过getNodeValue()获得)存储在字符串生成器中(在本例中,字符串生成器中只存储一个子节点。)如果构建器的内容表明已经找到了Chicago,则将chicago标志设置为true,并且执行离开cities循环。

如果在cities循环退出时设置了chicago标志,则调用当前contact元素节点的getElementsByTagName()方法来返回contact元素节点的name元素节点的NodeList(其中应该只有一个,我可以通过 DTD 或 schema 来实现)。现在很简单,从这个列表中提取第一个项目,调用这个项目上的getFirstChild()返回文本节点(我假设只有文本出现在<name></name>之间),调用文本节点上的getNodeValue()获得它的值,然后将它添加到contactNames列表中。

编译清单 5-2 如下:

javac DOMSearch.java

运行生成的应用程序,如下所示:

java DOMSearch

您应该观察到以下输出:

John Doe
Bob Jones

遍历 DOM 的节点树在最好的情况下是一项乏味的工作,在最坏的情况下容易出错。幸运的是,XPath 可以大大简化这种情况。

在编写清单 5-2 的 XPath 等价物之前,定义一个位置路径表达式是有帮助的。对于本例,该表达式是//contact[city = "Chicago"]/name/text(),它使用一个谓词选择包含一个Chicago city节点的所有contact节点,然后从这些contact节点中选择所有子name节点,最后从这些name节点中选择所有子文本节点。

清单 5-3 给出了一个XPathSearch应用程序的源代码,该应用程序使用这个 XPath 表达式和 Java 的 XPath API(由javax.xml.xpath包中的各种类型组成)来定位 Chicago 联系人。

import java.io.IOException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathException;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;

import org.xml.sax.SAXException;

public class XPathSearch
{
   public static void main(String[] args)
   {

      try
      {
         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         DocumentBuilder db = dbf.newDocumentBuilder();
         Document doc = db.parse("contacts.xml");
         XPathFactory xpf = XPathFactory.newInstance();
         XPath xp = xpf.newXPath();
         XPathExpression xpe;
         xpe = xp.compile("//contact[city = 'Chicago']/name/text()");
         Object result = xpe.evaluate(doc, XPathConstants.NODESET);
         NodeList nl = (NodeList) result;
         for (int i = 0; i < nl.getLength(); i++)
            System.out.println(nl.item(i).getNodeValue());
      }
      catch (IOException ioe)
      {
         System.err.println("IOE: " + ioe);
      }
      catch (SAXException saxe)
      {
         System.err.println("SAXE: " + saxe);
      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (ParserConfigurationException pce)
      {
         System.err.println("PCE: " + pce);
      }
      catch (XPathException xpe)
      {
         System.err.println("XPE: " + xpe);
      }
   }
}

Listing 5-3.Locating Chicago Contacts with the XPath API

在解析了contacts.xml并构建了 DOM 树之后,main()通过调用它的XPathFactory newInstance()方法实例化了javax.xml.xpath.XPathFactory。产生的XPathFactory实例可以通过调用它的void setFeature(String name, boolean value)方法来设置特性(比如安全处理,安全地处理 XML 文档),通过调用它的XPath newXPath()方法来创建一个javax.xml.xpath.XPath对象,等等。

XPath声明了一个XPathExpression compile(String expression)方法,用于编译指定的expression(一个 XPath 表达式),并将编译后的表达式作为实现javax.xml.xpath.XPathExpression接口的类的实例返回。当表达式不能被编译时,这个方法抛出javax.xml.xpath.XPathExpressionException(一个javax.xml.xpath.XPathException的子类)。

XPath还声明了几个重载的evaluate()方法,用于立即计算表达式并返回结果。因为计算一个表达式需要时间,所以当您计划多次计算这个表达式时,您可能会选择先编译一个复杂的表达式(以提高性能)。

编译完表达式后,main()调用XPathExpressionObject evaluate(Object item, QName returnType)方法对表达式求值。第一个参数是表达式的上下文节点,在本例中恰好是一个Document实例。第二个参数指定了由evaluate()返回的对象的种类,并被设置为javax.xml.xpath.XPathConstants.NODESET,这是 XPath 1.0 节点集类型的限定名,通过 DOM 的NodeList接口实现。

Note

XPath API 将 XPath 的布尔、数字、字符串和节点集类型分别映射到 Java 的java.lang.Booleanjava.lang.Doublejava.lang.StringNodeList类型。当调用evaluate()方法时,通过XPathConstants常量(BOOLEANNUMBERSTRINGNODESET)指定 XPath 类型,该方法负责返回适当类型的对象。XPathConstants还声明了一个NODE常量,它不映射到 Java 类型。相反,它用来告诉evaluate()您只希望结果节点集包含单个节点。

在将Object转换为NodeList之后,main()使用这个接口的getLength()item()方法来遍历节点列表。对于这个列表中的每一项,调用getNodeValue()来返回节点的值,该值随后被输出。

编译清单 5-3 如下:

javac XPathSearch.java

运行生成的应用程序,如下所示:

java XPathSearch

您应该观察到以下输出:

John Doe
Bob Jones

高级 XPath

XPath API 提供了三个高级特性来克服 XPath 1.0 语言的局限性。这些特性是名称空间上下文、扩展函数和函数解析器,以及变量和变量解析器。

命名空间上下文

当 XML 文档的元素属于一个名称空间(包括默认名称空间)时,任何查询文档的 XPath 表达式都必须考虑这个名称空间。对于非默认的名称空间,表达式不需要使用相同的名称空间前缀;它只需要使用相同的 URI。但是,当文档指定默认名称空间时,即使文档不使用前缀,表达式也必须使用前缀。

为了理解这种情况,假设清单 5-1 的<contacts>标记被声明如下,以引入默认的名称空间:<contacts xmlns=" http://www.javajeff.ca/ ">。此外,假设清单 5-3 在实例化DocumentBuilderFactory的行之后包含了dbf.setNamespaceAware(true);。如果您要对修改后的contacts.xml文件运行修改后的XPathSearch应用程序,您将看不到任何输出。

您可以通过实现javax.xml.namespace.NamespaceContext将任意前缀映射到名称空间 URI 来纠正这个问题,然后用XPath实例注册这个名称空间上下文。清单 5-4 给出了NamespaceContext接口的最小实现。

import java.util.Iterator;

import javax.xml.XMLConstants;

import javax.xml.namespace.NamespaceContext;

public class NSContext implements NamespaceContext
{
   @Override
   public String getNamespaceURI(String prefix)
   {
      if (prefix == null)
         throw new IllegalArgumentException("prefix is null");
      else
      if (prefix.equals("tt"))
         return "http://www.javajeff.ca/";
      else
         return null;
   }

   @Override
   public String getPrefix(String uri)
   {
      return null;
   }

   @Override
   public Iterator getPrefixes(String uri)
   {
      return null;
   }
}

Listing 5-4.Minimally Implementing NamespaceContext

getNamespaceURI()方法传递一个必须映射到 URI 的prefix参数。当这个参数为null时,必须抛出一个java.lang.IllegalArgumentException对象(根据 Java 文档)。当参数是所需的前缀值时,将返回命名空间 URI。

在实例化了XPath类之后,通过调用XPathvoid setNamespaceContext(NamespaceContext nsContext)方法,实例化NSContext并向XPath对象注册该对象。例如,您在XPath xp = xpf.newXPath();之后指定xp.setNamespaceContext(new NSContext());来用xp注册NSContext对象。

剩下要做的就是将前缀应用到 XPath 表达式,该表达式现在变成了//tt:contact[tt:city='Chicago']/tt:name/text(),因为contactcityname元素现在是默认名称空间的一部分,其 URI 被映射到NSContext实例的getNamespaceURI()方法中的任意前缀tt

编译并运行修改后的XPathSearch应用程序,你会看到John DoeBob Jones在不同的行上。

扩展函数和函数解析器

XPath API 允许您定义函数(通过 Java 方法),通过提供尚未提供的新特性来扩展 XPath 的预定义函数集。这些 Java 方法不会有副作用,因为 XPath 函数可以按任意顺序计算多次。此外,它们不能覆盖预定义的函数;永远不会执行与预定义函数同名的 Java 方法。

假设您修改了清单 5-1 的 XML 文档,以包含一个birth元素,该元素以 YYYY-MM-DD 格式记录联系人的出生日期信息。清单 5-5 展示了生成的 XML 文件。

<?xml version="1.0"?>
<contacts >
   <contact>
      <name>John Doe</name>
      <birth>1953-01-02</birth>
      <city>Chicago</city>
      <city>Denver</city>
   </contact>
   <contact>
      <name>Jane Doe</name>
      <birth>1965-07-12</birth>
      <city>New York</city>
   </contact>
   <contact>
      <name>Sandra Smith</name>
      <birth>1976-11-22</birth>
      <city>Denver</city>
      <city>Miami</city>
   </contact>
   <contact>
      <name>Bob Jones</name>
      <birth>1958-03-14</birth>
      <city>Chicago</city>
   </contact>
</contacts>

Listing 5-5.XML-Based Contacts Database with Birth Information

现在假设您想根据出生信息选择联系人。例如,您只想选择出生日期大于1960-01-01的联系人。因为 XPath 没有为您提供这个函数,所以您决定声明一个date()扩展函数。你的第一步是声明一个实现了javax.xml.xpath.XPathFunction接口的Date类——参见清单 5-6 。

import java.text.ParsePosition;
import java.text.SimpleDateFormat;

import java.util.List;

import javax.xml.xpath.XPathFunction;
import javax.xml.xpath.XPathFunctionException;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class Date implements XPathFunction
{
   private final static ParsePosition POS = new ParsePosition(0);

   private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd");

   @Override
   public Object evaluate(List args) throws XPathFunctionException
   {
      if (args.size() != 1)
         throw new XPathFunctionException("Invalid number of arguments");
      String value;
      Object o = args.get(0);
      if (o instanceof NodeList)
      {

         NodeList list = (NodeList) o;
         value = list.item(0).getTextContent();
      }
      else
      if (o instanceof String)
         value = (String) o;
      else
         throw new XPathFunctionException("Cannot convert argument type");
      POS.setIndex(0);
      return sdf.parse(value, POS).getTime();
   }
}

Listing 5-6.An Extension Function for Returning a Date as a Milliseconds Value

XPathFunction声明了一个单独的Object evaluate(List args)方法,XPath 在需要执行扩展函数时会调用这个方法。向evaluate()传递一个java.util.List对象,这些对象描述由 XPath 计算器传递给扩展函数的参数。此外,这个方法返回一个适合扩展函数类型的值(date()的长整数返回类型与 XPath 的数字类型兼容)。

date()扩展函数旨在用单个参数调用,该参数可以是 nodeset 类型,也可以是 string 类型。当参数的数量(由列表的大小表示)不等于 1 时,这个扩展函数抛出javax.xml.xpath.XPathFunctionException

当参数类型为NodeList(节点集)时,获取节点集中第一个节点的文本内容;该内容被假定为 YYYY-MM-DD 格式的日期值(为了简洁,我忽略了错误检查)。当参数类型为String时,它被假定为这种格式的日期值。任何其他类型的参数都会导致抛出一个XPathFunctionException对象。

通过将日期转换为毫秒值,简化了日期比较。这个任务是在java.text.SimpleDateFormatjava.text.ParsePosition类的帮助下完成的。重置ParsePosition对象的索引(通过setIndex(0))后,调用SimpleDateFormatDate parse(String text, ParsePosition pos)方法根据SimpleDateFormat实例化时建立的模式解析字符串,从ParsePosition索引标识的解析位置开始。这个索引在parse()方法调用之前被重置,因为parse()更新了这个对象的索引。

parse()方法返回一个java.util.Date对象,该对象的long getTime()方法被调用以返回由解析的日期表示的毫秒数。

在实现扩展函数之后,您需要创建一个函数解析器,它是一个对象,其类实现了javax.xml.xpath.XPathFunctionResolver接口,并告诉 XPath 评估器关于扩展函数(或函数)。清单 5-7 展示了DateResolver类。

import javax.xml.namespace.QName;

import javax.xml.xpath.XPathFunction;
import javax.xml.xpath.XPathFunctionResolver;

public class DateResolver implements XPathFunctionResolver
{
   private static final QName name = new QName("http://www.javajeff.ca/",
                                               "date", "tt");

   @Override
   public XPathFunction resolveFunction(QName name, int arity)
   {
      if (name.equals(this.name) && arity == 1)
         return new Date();
      return null;
   }
}

Listing 5-7.A Function Resolver for the date() Extension Function

XPathFunctionResolver声明了一个单独的XPathFunction resolveFunction(QName functionName, int arity)方法,XPath 调用该方法来识别扩展函数的名称,并获得一个 Java 对象的实例,该对象的evaluate()方法实现了该函数。

functionName参数标识函数的限定名,因为所有的扩展函数必须存在于一个名称空间中,并且必须通过一个前缀来引用(该前缀不必与文档中的前缀匹配)。因此,您还必须通过名称空间上下文将名称空间绑定到前缀(如前所述)。arity参数标识扩展函数接受的参数数量,在重载扩展函数时非常有用。如果functionNamearity值可以接受,扩展函数的 Java 类被实例化并返回;否则,null就返回了。

最后,通过调用XPathvoid setXPathFunctionResolver(XPathFunctionResolver resolver)方法,用 XPath 对象实例化和注册函数解析器类。

以下摘自本章第 3 版的XPathSearch应用程序(在本书的代码档案中)演示了所有这些任务,以便在 XPath 表达式//tt:contact[tt:date(tt:birth) > tt:date('1960-01-01')]/tt:name/text()中使用date(),该表达式只返回出生日期大于 1960-01-01 ( Jane Doe后跟Sandra Smith)的联系人:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse("contacts.xml");
XPathFactory xpf = XPathFactory.newInstance();
XPath xp = xpf.newXPath();
xp.setNamespaceContext(new NSContext());
xp.setXPathFunctionResolver(new DateResolver());
XPathExpression xpe;
String expr; 

expr = "//tt:contact[tt:date(tt:birth) > tt:date('1960-01-01')]" +
       "/tt:name/text()";
xpe = xp.compile(expr);
Object result = xpe.evaluate(doc, XPathConstants.NODESET);
NodeList nl = (NodeList) result;
for (int i = 0; i < nl.getLength(); i++)
   System.out.println(nl.item(i).getNodeValue());

编译并运行修改后的XPathSearch应用程序,你会看到Jane DoeSandra Smith在不同的行上。

变量和变量解析器

之前指定的所有 XPath 表达式都是基于文本的。XPath 还允许您指定变量来参数化这些表达式,其方式类似于在 SQL 预准备语句中使用变量。

变量出现在表达式中的方式是在其名称(可能有也可能没有名称空间前缀)前加上一个$。例如,/a/b[@c = $d]/text()是一个 XPath 表达式,它选择根节点的所有a元素,以及具有包含由变量$d标识的值的c属性的所有ab元素,并返回这些b元素的文本。这个表达式对应于清单 5-8 的 XML 文档。

<?xml version="1.0"?>
<a>

   <b c="x">b1</b>
   <b>b2</b>
   <b c="y">b3</b>
   <b>b4</b>
   <b c="x">b5</b>
</a>
Listing 5-8.A Simple XML Document for Demonstrating an XPath Variable

要指定其值在表达式求值期间获得的变量,您必须用您的XPath对象注册一个变量解析器。变量解析器是一个类的实例,该类根据其Object resolveVariable(QName variableName)方法实现了javax.xml.xpath.XPathVariableResolver接口,并告诉求值器关于变量的信息。

variableName参数包含变量名的限定名。(请记住,变量名可能带有名称空间前缀。)此方法验证限定名是否恰当地命名了变量,然后返回它的值。

创建变量解析器后,通过调用XPathvoid setXPathVariableResolver(XPathVariableResolver resolver)方法,用XPath对象注册它。

以下节选自本章第 4 版的XPathSearch应用程序(在本书的代码档案中)演示了所有这些任务,以便在 XPath 表达式/a/b[@c=$d]/text()中指定$d,该表达式返回b1后跟b5。它假设清单 5-8 存储在名为example.xml的文件中:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse("example.xml");
XPathFactory xpf = XPathFactory.newInstance();
XPath xp = xpf.newXPath();
XPathVariableResolver xpvr;
xpvr = new XPathVariableResolver()
       {
          @Override
          public Object resolveVariable(QName varname)
          {
             if (varname.getLocalPart().equals("d"))
                return "x";
             else
                return null;
          }
       };
xp.setXPathVariableResolver(xpvr);
XPathExpression xpe;
xpe = xp.compile("/a/b[@c = $d]/text()");
Object result = xpe.evaluate(doc, XPathConstants.NODESET);
NodeList nl = (NodeList) result;
for (int i = 0; i < nl.getLength(); i++)
   System.out.println(nl.item(i).getNodeValue());

编译并运行修改后的XPathSearch应用程序,你会看到b1b5在不同的行上。

Caution

当您用名称空间前缀限定变量名时(如在$ns:d中),您还必须注册一个名称空间上下文来解析前缀。

Exercises

以下练习旨在测试你对第五章内容的理解。

Define XPath.   Where is XPath commonly used?   Identify the seven kinds of nodes that XPath recognizes.   True or false: XPath recognizes CDATA sections.   Describe what XPath provides for selecting nodes.   True or false: In a location path expression, you must prefix an attribute name with the @ symbol.   Identify the functions that XPath provides for selecting comment, text, and processing-instruction nodes.   What does XPath provide for selecting unknown nodes?   How do you perform multiple selections?   What is a predicate?   Identify the functions that XPath provides for working with nodesets.   Identify the three advanced features that XPath provides to overcome limitations with the XPath 1.0 language.   True or false: The XPath API maps XPath’s number type to java.lang.Float.   Modify Listing 5-1’s contacts document by changing <name>John Doe</name> to <Name>John Doe</Name>. Because you no longer see John Doe in the output when you run Listing 5-3’s XPathSearch application (you only see Bob Jones), modify this application’s location path expression so that you see John Doe followed by Bob Jones.

摘要

XPath 是一种非 XML 声明性查询语言,用于选择 XML 文档的信息集项目作为一个或多个节点。它简化了对 DOM 树节点的访问,对于 XSLT 也很有用,XSLT 通常用于选择那些要复制到输出文档的输入文档元素(通过 XPath 表达式)。

XPath 将 XML 文档视为从根节点开始的节点树。这种语言识别七种节点:元素、属性、文本、名称空间、处理指令、注释和文档。它不识别 CDATA 节、实体引用或文档类型声明。

XPath 为选择节点提供了位置路径表达式。位置路径表达式通过从上下文节点(根节点或当前节点的其他文档节点)开始的一系列步骤来定位节点。返回的节点集(称为节点集)可能为空,也可能包含一个或多个节点。

位置路径表达式(返回节点集)是 XPath 表达式的一种。XPath 还支持计算结果为布尔值(比如谓词)、数字或字符串类型的通用表达式;比如position() = 26.8"Hello"。XSLT 中经常使用通用表达式。

XPath API 提供了一些高级功能来克服 XPath 1.0 语言的局限性:名称空间上下文(将任意名称空间前缀映射到名称空间 URIs)、扩展函数和函数解析器(用于定义扩展 XPath 预定义函数集的函数),以及变量和变量解析器(用于参数化 XPath 表达式)。

第六章向您介绍用于转换 XML 文档的 XSLT。

六、使用 XSLT 转换 XML 文档

除了 SAX、DOM、StAX 和 XPath,Java 还包括 XSLT API,用于转换 XML 文档。本章向您介绍 XSLT。

XSLT 是什么?

可扩展样式表语言(XSL)是用于转换和格式化 XML 文档的一系列语言。XSL 转换(XSLT)是用于将 XML 文档转换为其他格式的 XSL 语言,例如 HTML(用于通过 web 浏览器呈现 XML 文档的内容)。

XSLT 通过使用 XSLT 处理器和样式表来完成它的工作。XSLT 处理器是一种软件组件,它将 XSLT 样式表(一种由内容和转换指令组成的基于 XML 的模板)应用于输入文档(不修改文档),并将转换后的结果复制到结果树,该结果树可以输出到文件或输出流,甚至可以通过管道传输到另一个 XSLT 处理器进行其他转换。图 6-1 说明了转换过程。

A394211_1_En_6_Fig1_HTML.jpg

图 6-1。

An XSLT processor transforms an XML input document into a result tree

XSLT 的美妙之处在于,您不需要开发定制的软件应用程序来执行转换。相反,您只需创建一个 XSLT 样式表,并将其与需要转换到 XSLT 处理器的 XML 文档一起输入。

探索 XSLT API

Java 通过javax.xml.transformjavax.xml.transform.domjavax.xml.transform.saxjavax.xml.transform.staxjavax.xml.transform.stream包中的类型实现 XSLT。javax.xml.transform包定义了通用 API,用于处理转换指令和执行从源(XSLT 处理器的输入来源)到结果(发送处理器的输出)的转换。其余的包定义了获取不同种类的源和结果的 API。

javax.xml.transform.TransformerFactory类是使用 XSLT 的起点。通过调用它的一个newInstance()方法来实例化TransformerFactory。例如,下面的代码片段使用TransformerFactoryTransformerFactory newInstance()类方法来创建工厂:

TransformerFactory tf = TransformerFactory.newInstance();

在幕后,newInstance()遵循一个有序的查找过程来识别要加载的TransformerFactory实现类。这个过程首先检查javax.xml.transform.TransformerFactory系统属性,最后在找不到其他类时选择 Java 平台的默认TransformerFactory实现类。如果一个实现类不可用(也许由javax.xml.transform.TransformerFactory系统属性标识的类不存在)或者不能被实例化,newInstance()抛出一个javax.xml.transform.TransformerFactoryConfigurationError类的实例。否则,它实例化该类并返回其实例。

获得一个TransformerFactory对象后,可以调用各种配置方法来配置工厂。例如,您可以调用TransformerFactoryvoid setFeature(String name, boolean value)方法来启用一个特性(比如安全处理,安全地转换 XML 文档)。

按照工厂的配置,调用它的一个newTransformer()方法来创建和返回javax.xml.transform.Transformer类的实例。下面的代码片段调用Transformer newTransformer()来完成这个任务:

Transformer t = tf.newTransformer();

noargument newTransformer()方法将源输入复制到目标,不做任何修改。这种转换被称为身份转换。

要更改输入,请指定一个样式表。通过调用工厂的Transformer newTransformer(Source source)方法来完成这项任务,其中的javax.xml.transform.Source接口描述了样式表的来源。以下代码片段完成了这项任务:

Transformer t;
t = tf.newTransformer(new StreamSource(new FileReader("recipe.xsl")));

这段代码创建了一个转换器,它通过一个连接到文件阅读器的javax.xml.transform.stream.StreamSource对象从名为recipe.xsl的文件中获取样式表。习惯上使用.xsl.xslt扩展名来标识 XSLT 样式表文件。

newTransformer()方法不能返回对应于工厂配置的Transformer实例时,它们抛出javax.xml.transform.TransformerConfigurationException

在获得一个Transformer实例之后,您可以调用它的void setOutputProperty(String name, String value)方法来影响一个转换。javax.xml.transform.OutputKeys类声明了常用键的常量。例如,OutputKeys.METHOD是指定输出结果树的方法的键(如 XML、HTML、纯文本或其他)。

Tip

要在一个方法调用中设置多个属性,创建一个java.util.Properties对象并将该对象作为参数传递给Transformervoid setOutputProperties(Properties prop)方法。由setOutputProperty()setOutputProperties()设置的属性覆盖样式表的xsl:output指令设置。

在执行转换之前,您需要获得实现Sourcejavax.xml.transform.Result接口的类的实例。然后将这些实例传递给Transformervoid transform(Source xmlSource, Result outputTarget)方法,当转换过程中出现问题时,该方法抛出一个javax.xml.transform.TransformerException类的实例。

以下代码片段向您展示了如何获取源和结果,以及如何执行转换:

Source source = new DOMSource(doc);
Result result = new StreamResult(System.out);
t.transform(source, result);

第一行实例化了javax.xml.transform.dom.DOMSource类,它作为一个 DOM 树的持有者,这个 DOM 树植根于由doc指定的org.w3c.dom.Document对象。第二行实例化了javax.xml.transform.stream.StreamResult类,它充当标准输出流的容器,转换后的数据项被发送到该输出流。第三行从Source对象读取数据,并将转换后的数据输出到Result对象。

Transformer Factory Feature Detection

尽管 Java 的默认转换器支持位于javax.xml.transform.domjavax.xml.transform.saxjavax.xml.transform.staxjavax.xml.transform.stream包中的各种SourceResult实现类,但非默认转换器(可能通过javax.xml.transform.TransformerFactory系统属性指定)可能更受限制。因此,每个SourceResult实现类都声明了一个FEATURE字符串常量,可以传递给TransformerFactoryboolean getFeature(String name)方法。当支持SourceResult实现类时,该方法返回true。例如,当支持流源时,tf.getFeature(StreamSource.FEATURE)返回true

javax.xml.transform.sax.SAXTransformerFactory类提供了额外的特定于 SAX 的工厂方法,只有当TransformerFactory对象也是该类的实例时才能使用这些方法。为了帮助您做出决定,SAXTransformerFactory还声明了一个FEATURE字符串常量,您可以将它传递给getFeature()。例如,当从tf引用的 transformer factory 是SAXTransformerFactory的实例时,tf.getFeature(SAXTransformerFactory.FEATURE)返回true

大多数 XML API 接口对象和返回它们的工厂都不是线程安全的。这种情况也适用于变压器。尽管您可以在同一线程上多次重用同一个转换器,但是您不能从多个线程访问该转换器。

这个问题可以通过使用实现javax.xml.transform.Templates接口的类的实例来解决。该接口的 Java 文档是这样说的:对于并发运行的多个线程上的给定实例,模板必须是线程安全的,并且可以在给定的会话中多次使用。除了促进线程安全之外,Templates实例还可以提高性能,因为它们代表编译的 XSLT 样式表。

下面的代码片段展示了如何在没有Templates对象的情况下执行转换:

TransformerFactory tf = TransformerFactory.newInstance();
StreamSource ssStyleSheet = new StreamSource(new FileReader("recipe.xsl"));
Transformer t = tf.newTransformer(ssStyleSheet);
t.transform(new DOMSource(doc), new StreamResult(System.out));

您不能从多个线程访问t的转换器。相比之下,下面的代码片段向您展示了如何从一个Templates对象构建一个转换器,以便可以从多个线程访问它:

TransformerFactory tf = TransformerFactory.newInstance();
StreamSource ssStyleSheet = new StreamSource(new FileReader("recipe.xsl"));
Templates te = tf.newTemplates(ssStylesheet);
Transformer t = te.newTransformer();
t.transform(new DOMSource(doc), new StreamResult(System.out));

不同之处在于调用TransformerfactoryTemplates newTemplates(Source source)方法来创建并返回其类实现了Templates接口的对象,以及调用该接口的Transformer newTransformer()方法来获得Transformer对象。

演示 XSLT API

清单 3-2 展示了一个DOMDemo应用程序,它基于清单 1-2 的电影 XML 文档创建了一个 DOM 文档树。不幸的是,您不能使用 DOM API 将ISO-8859-1赋给 XML 声明的encoding属性。此外,您不能使用 DOM 将该树输出到文件或其他目的地。然而,您可以用 XSLT 克服这些问题,如清单 6-1 所示。

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;

import javax.xml.transform.dom.DOMSource;

import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;

public class XSLTDemo
{
   public static void main(String[] args)
   {
      try
      {
         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         DocumentBuilder db = dbf.newDocumentBuilder();
         Document doc = db.newDocument();
         doc.setXmlStandalone(true);
         // Create the root element.
         Element root = doc.createElement("movie");
         doc.appendChild(root);
         // Create name child element and add it to the root.
         Element name = doc.createElement("name");
         root.appendChild(name);
         // Add a text element to the name element.
         Text text =
           doc.createTextNode("Le Fabuleux Destin d'Amélie Poulain");
         name.appendChild(text);
         // Create language child element and add it to the root.
         Element language = doc.createElement("language");
         root.appendChild(language);
         // Add a text element to the language element.
         text = doc.createTextNode("français");
         language.appendChild(text);
         // Use a transformer to output this tree with ISO-8859-1 encoding
         // to the standard output stream.
         TransformerFactory tf = TransformerFactory.newInstance();
         Transformer t = tf.newTransformer();
         t.setOutputProperty(OutputKeys.METHOD, "xml");
         t.setOutputProperty(OutputKeys.ENCODING, "ISO-8859-1");
         t.setOutputProperty(OutputKeys.INDENT, "yes");
         t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "3");
         Source source = new DOMSource(doc);
         Result result = new StreamResult(System.out);
         t.transform(source, result);
      }

      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (ParserConfigurationException pce)
      {
         System.err.println("PCE: " + pce);
      }
      catch (TransformerConfigurationException tce)
      {
         System.err.println("TCE: " + tce);
      }
      catch (TransformerException te)
      {
         System.err.println("TE: " + te);
      }
      catch (TransformerFactoryConfigurationError tfce)
      {
         System.err.println("TFCE: " + tfce);
      }
   }
}

Listing 6-1.Assigning ISO-8859-1 to the XML declaration’s encoding Attribute via XSLT

清单 6-1 首先创建一个 DOM 树。然后,它创建一个转换器工厂,并从这个工厂获得一个转换器。然后在 transformer 上设置四个属性,并获得一个流源和结果。最后,调用transform()方法将源内容转换成结果。

在转换器上设置的四个属性会影响转换。OutputKeys.METHOD指定结果树将被写成 XML,OutputKeys.ENCODING指定ISO-8859-1将是 XML 声明的encoding属性的值,OutputKeys.INDENT指定转换器可以输出额外的空白。

额外的空白用于跨多行输出 XML,而不是在一行中输出。因为指出用于缩进 XML 行的空格数会很好,并且因为该信息不能通过OutputKeys属性来指定,所以使用非标准的"{ http://xml.apache.org/xslt}indent-amount "属性(属性键以大括号分隔的 URIs 开始)来指定适当的值(例如3空格)。在这个应用程序中指定这个属性是可以的,因为 Java 的默认 XSLT 实现是基于 Apache 的 XSLT 实现的。

编译清单 6-1 如下:

javac XSLTDemo.java

运行生成的应用程序,如下所示:

java XSLTDemo

您应该观察到以下输出:

<?xml version="1.0" encoding="ISO-8859-1"?><movie>
   <name>Le Fabuleux Destin d'Amélie Poulain</name>
   <language>français</language>
</movie>

虽然这个例子向您展示了如何输出一个 DOM 树以及如何为结果 XML 文档的 XML 声明指定一个encoding值,但是这个例子并没有真正展示 XSLT 的强大功能,因为(除了设置encoding属性值之外)它执行了一个身份转换。一个更有趣的例子是利用样式表。

考虑这样一个场景,您想要将清单 1-1 的食谱文档转换成 HTML 文档,以便通过 web 浏览器呈现。清单 6-2 展示了一个样式表,转换器可以用它来执行转换。

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/recipe">
<html>
   <head>
      <title>Recipes</title>
   </head>

   <body>
      <h2>

         <xsl:value-of select="normalize-space(title)"/>
      </h2>

      <h3>Ingredients</h3>

      <ul>
      <xsl:for-each select="ingredients/ingredient">
        <li>
           <xsl:value-of select="normalize-space(text())"/>
           <xsl:if test="@qty"> (<xsl:value-of select="@qty"/>)</xsl:if>
        </li>
      </xsl:for-each>
      </ul>

      <h3>Instructions</h3>

      <xsl:value-of select="normalize-space(instructions)"/>
   </body>
</html>
</xsl:template>
</xsl:stylesheet>

Listing 6-2.An XSLT Stylesheet for Converting a Recipe Document to an HTML Document

清单 6-2 揭示了样式表是一个 XML 文档。它的根元素是stylesheet,标识样式表的标准名称空间。习惯上指定xsl作为引用 XSLT 指令元素的名称空间前缀,尽管可以指定任何前缀。

样式表基于控制元素及其内容如何转换的template元素。模板关注通过match属性识别的单个元素。这个属性的值是一个 XPath 位置路径表达式,它匹配根元素节点的所有recipe子节点。关于清单 1-1 ,将只匹配和选择单个recipe根元素。

一个template元素可以包含文字文本和样式表指令。例如,<xsl:value-of select="normalize-space(title)"/>中的value-of指令指定检索title元素的值(它是recipe上下文节点的子节点)并将其复制到输出中。因为该文本被空格和换行符包围,所以在复制标题之前,调用 XPath 的normalize-string()函数来删除这些空格。

XSLT 是一种功能强大的声明性语言,包括控制流指令,如for-eachif。在<xsl:for-each select="ingredients/ingredient">的上下文中,for-each使得ingredients节点的所有ingredient子节点被一次一个地选择和处理。对于每个节点,执行<xsl:value-of select="normalize-space(text())"/>来复制ingredient节点的内容,规范化以删除空白。另外,<xsl:if test="@qty"> (<xsl:value-of select="@qty"/>)中的if指令确定ingredient节点是否有一个qty属性,并且(如果有)将一个空格字符和该属性值(用括号括起来)复制到输出中。

Note

XSLT 还有很多内容无法在这个简短的例子中展示。要了解更多关于 XSLT 的知识,我建议您阅读《从新手到专业人员的 XSLT 2.0 入门》( www.apress.com/9781590593240 ),这是一本由林洋·坦尼森撰写的新书。XSLT 2.0 是 XSLT 1.0 的超集,Java 8 支持 XSLT 1.0。

清单 6-3 将源代码呈现给一个XSLTDemo应用程序,该应用程序向您展示如何编写 Java 代码来通过清单 6-2 的样式表处理清单 1-1 。

import java.io.FileReader;
import java.io.IOException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;

import javax.xml.transform.dom.DOMSource;

import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.w3c.dom.Document;

import org.xml.sax.SAXException;

public class XSLTDemo
{
   public static void main(String[] args)
   {
      try
      {
         DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
         DocumentBuilder db = dbf.newDocumentBuilder();
         Document doc = db.parse("recipe.xml");
         TransformerFactory tf = TransformerFactory.newInstance();
         StreamSource ssStyleSheet;
         ssStyleSheet = new StreamSource(new FileReader("recipe.xsl"));
         Transformer t = tf.newTransformer(ssStyleSheet);
         t.setOutputProperty(OutputKeys.METHOD, "html");
         t.setOutputProperty(OutputKeys.INDENT, "yes");
         Source source = new DOMSource(doc);
         Result result = new StreamResult(System.out);
         t.transform(source, result);
      }

      catch (IOException ioe)
      {
         System.err.println("IOE: " + ioe);
      }
      catch (FactoryConfigurationError fce)
      {
         System.err.println("FCE: " + fce);
      }
      catch (ParserConfigurationException pce)
      {
         System.err.println("PCE: " + pce);
      }
      catch (SAXException saxe)
      {
         System.err.println("SAXE: " + saxe);
      }
      catch (TransformerConfigurationException tce)
      {
         System.err.println("TCE: " + tce);
      }
      catch (TransformerException te)
      {
         System.err.println("TE: " + te);
      }
      catch (TransformerFactoryConfigurationError tfce)
      {
         System.err.println("TFCE: " + tfce);
      }
   }
}

Listing 6-3.Transforming Recipe XML via a Stylesheet

清单 6-3 在结构上与清单 6-1 相似。它揭示了输出方法被设置为html,也揭示了结果 HTML 应该缩进。但是,输出只是部分缩进,如下所示:

<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Recipes</title>
</head>

<body>
<h2>Grilled Cheese Sandwich</h2>
<h3>Ingredients</h3>
<ul>
<li>bread slice (2)</li>
<li>cheese slice</li>
<li>margarine pat (2)</li>
</ul>
<h3>Instructions</h3>Place frying pan on element and select medium heat. For each bread slice, smear one pat of margarine on one side of bread slice. Place cheese slice between bread slices with margarine-smeared sides away from the cheese. Place sandwich in frying pan with one margarine-smeared side in contact with pan. Fry for a couple of minutes and flip. Fry other side for a minute and serve.</body>
</html>

OutputKeys.INDENT和它的"yes"值允许你跨多行输出 HTML,而不是在一行中输出 HTML。但是,XSLT 处理器不执行额外的缩进,并忽略通过代码(如t.setOutputProperty("{ http://xml.apache.org/xslt}indent-amount ", "3");)指定缩进的空格数的尝试。

Note

OutputKeys.METHOD被设置为"html"时,XSLT 处理器输出一个<META>标签。

Exercises

以下练习旨在测试您对第六章内容的理解:

Define XSLT.   How does XSLT accomplish its work?   True or false: Call TransformerFactory’s void transform(Source xmlSource, Result outputTarget) method to transform a source to a result.   Create a books.xsl stylesheet file and a MakeHTML application with a similar structure to the application that processes Listing 6-2’s recipe.xsl stylesheet. MakeHTML uses books.xsl to convert Exercise 1-21’s books.xml content to HTML. When viewed in a web browser, the HTML should result in a web page that’s similar to the page shown in Figure 6-2.

A394211_1_En_6_Fig2_HTML.jpg

图 6-2。

Exercise 1-21’s books.xml content is presented via a web page

摘要

XSL 是用于转换和格式化 XML 文档的一系列语言。XSLT 是用于将 XML 文档转换成其他格式的 XSL 语言,例如 HTML(用于通过 web 浏览器呈现 XML 文档的内容)。

XSLT 通过使用 XSLT 处理器和样式表来完成它的工作。XSLT 处理器将 XSLT 样式表应用于输入文档(不修改文档),并将转换后的结果复制到结果树,结果树可以输出到文件或输出流,甚至可以通过管道传输到另一个 XSLT 处理器进行其他转换。

Java 通过javax.xml.transformjavax.xml.transform.domjavax.xml.transform.saxjavax.xml.transform.staxjavax.xml.transform.stream包中的类型实现 XSLT。javax.xml.transform包定义了通用 API,用于处理转换指令和执行从源(XSLT 处理器的输入来源)到结果(发送处理器的输出)的转换。其余的包定义了获取不同种类的源和结果的 API。

第七章向您介绍 JSON,一种不太冗长的 XML 替代品。

七、JSON 简介

许多应用程序通过交换 JSON 对象而不是 XML 文档来进行通信。本章介绍 JSON,浏览它的语法,在 JavaScript 上下文中演示 JSON,并展示如何在 JSON 模式的上下文中验证 JSON 对象。

JSON 是什么?

JSON (JavaScript Object Notation)是一种独立于语言的数据格式,它将 JSON 对象表示为人类可读的属性列表(名称-值对)。尽管源自 JavaScript 的非严格子集,但将JSON 对象解析成等价的语言相关对象的代码在许多编程语言中都可用。

Note

JSON 允许 Unicode U+2028行分隔符和U+2029段落分隔符在带引号的字符串中不转义。因为 JavaScript 不支持这种能力,所以 JSON 不是 JavaScript 的子集。

JSON 常用于通过 Ajax(https://en.wikipedia.org/wiki/AJAJ)进行的异步浏览器/服务器通信。JSON 也用于 NoSQL 数据库管理系统,如 MongoDb 和 CouchDb 使用 Twitter、脸书、LinkedIn 和 Flickr 等社交媒体网站的应用程序;即使使用流行的谷歌地图 API。

Note

许多开发人员更喜欢 JSON 而不是 XML,因为他们认为 JSON 不那么冗长,更容易阅读。查看“JSON:XML 的无脂肪替代品”( www.json.org/xml.html )了解更多信息。

JSON 语法教程

JSON 数据格式将 JSON 对象表示为用大括号分隔、用逗号分隔的属性列表:

{
   property1 ,
   property2 ,
   ...
   propertyN

}

最后一个属性后没有逗号。

对于每个属性,名称被表示为一个通常用引号括起来的字符串(用一对双引号括起来)。名称字符串后面跟一个冒号字符,后面跟一个特定类型的值(例如,"name": "JSON")。

JSON 支持以下六种类型:

  • Number:一个有符号的十进制数,可以包含小数部分,可以使用指数( E)符号。JSON 不允许非数字(比如 NaN ),也不区分整数和浮点。此外,JSON 不识别八进制和十六进制格式。(尽管 JavaScript 对所有数值都使用了一种双精度浮点格式,但是实现 JSON 的其他语言对数字的编码可能不同。)
  • 字符串:零个或多个 Unicode 字符的序列。字符串用双引号分隔,并支持反斜杠转义语法。
  • 布尔:值truefalse中的任意一个。
  • 数组:零个或多个值的有序列表,每个值可以是任意类型。数组使用方括号符号,元素用逗号分隔。
  • 对象:属性的无序集合,其中的名称(也称为键)是字符串。因为对象旨在表示关联数组,所以建议(尽管不是必需的)每个键在一个对象中是唯一的。对象用大括号分隔,并用逗号分隔每个属性。在每个属性中,冒号字符将键与其值分开。
  • Null :空值,使用关键字null

Note

JSON 模式(稍后讨论)识别第七种类型:整数。这种类型不包括分数或指数,而是数字的子集。

语法元素(值和标点符号)周围或之间允许有空格,但会被忽略。为此,四个特定字符被视为空白:空格、水平制表符、换行符和回车符。还有,JSON 不支持注释。

使用这种数据格式,您可以指定一个 JSON 对象,如下面的匿名对象(节选自维基百科的 JSON 页面上的 https://en.wikipedia.org/wiki/JSON )来描述一个人的名字、姓氏和其他数据项:

{
   "firstName": "John",
   "lastName": "Smith",
   "isAlive": true,
   "age": 25,
   "address":
   {
      "streetAddress": "21 2nd Street",
      "city": "New York",
      "state": "NY",
      "postalCode": "10021-3100"
   },
   "phoneNumbers":
   [
      {
         "type": "home",
         "number": "212 555-1234"
      },
      {
         "type": "office",
         "number": "646 555-4567"
      }
   ],
   "children": [],
   "spouse": null
}

在此示例中,匿名对象由带有以下键的八个属性组成:

  • firstName标识一个人的名字,类型为 string。
  • lastName标识一个人的姓,类型为 string。
  • isAlive标识一个人的存活状态,属于布尔类型。
  • age识别一个人的年龄,属于数字类型。
  • address标识一个人的位置,属于 object 类型。在这个对象中有四个属性(字符串类型):streetAddresscitystatepostalCode
  • 识别一个人的电话号码,并且是数组类型。数组中有两个对象;每个对象由typenumber属性(字符串类型)组成。
  • 标识一个人的孩子(如果有的话),并且是数组类型。
  • spouse标识一个人的伴侣,为空。

前面的示例显示了对象和数组可以嵌套;例如,对象内数组中的对象。

Note

按照惯例,JSON 对象存储在扩展名为.json的文件中。

用 JavaScript 演示 JSON

理想情况下,我会用 Java 的标准 JSON API 来演示 JSON。然而,Java 并不正式支持 JSON。

Note

Oracle 之前推出了一个 Java 增强提案(JEP ),将 JSON API 添加到 Java 9 中。不幸的是,JEP 198:轻量级 JSON API ( http://openjdk.java.net/jeps/198 )被放弃了。

我将通过 JavaScript 演示 JSON,但是是在 Java 环境中通过 Java 的脚本 API。(如果您不熟悉脚本,我将解释这个 API,以便您能够理解代码。)首先,清单 7-1 展示了执行 JavaScript 代码的应用程序的源代码。

import java.io.FileReader;
import java.io.IOException;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class RunScript
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
         System.err.println("usage: java RunScript script");
         return;
      }
      ScriptEngineManager manager = new ScriptEngineManager();
      ScriptEngine engine = manager.getEngineByName("nashorn");
      try
      {
         engine.eval(new FileReader(args[0]));
      }
      catch (ScriptException se)
      {
         System.err.println(se.getMessage());
      }
      catch (IOException ioe)
      {
         System.err.println(ioe.getMessage());
      }      
   }
}

Listing 7-1.Executing JavaScript Code with Assistance from Java

清单 7-1 的main()方法首先验证是否指定了一个命令行参数,该参数命名了一个脚本文件。如果不是这样,它会显示使用信息并终止应用程序。

假设指定了一个命令行参数,javax.script.ScriptEngineManager类被实例化。ScriptEngineManager作为脚本 API 的入口点。

接下来,ScriptEngineManager对象的ScriptEngine getEngineByName(String shortName)方法被调用以获得对应于期望的shortName值的脚本引擎。JavaScript 支持两个脚本引擎:rhinonashorn。我选择获得更现代的nashorn脚本引擎,它作为一个对象返回,该对象的类实现了javax.script.ScriptEngine接口。

ScriptEngine声明了几个用于评估脚本的eval()方法。main()调用Object eval(Reader reader)方法从其java.io.FileReader对象参数中读取脚本,然后(假设java.io.IOException没有被抛出)评估脚本。这个方法返回任何脚本返回值,我忽略了。此外,当脚本中出现错误时,该方法抛出javax.script.ScriptException

编译清单 7-1 如下:

javac RunScript.java

在运行这个应用程序之前,您需要一个合适的脚本文件。清单 7-2 展示了一个声明和访问 JSON 对象的脚本。

var person =
{
   "firstName": "John",
   "lastName": "Smith",
   "isAlive": true,
   "age": 25,
   "address":
   {
      "streetAddress": "21 2nd Street",
      "city": "New York",
      "state": "NY",
      "postalCode": "10021-3100"
   },
   "phoneNumbers":
   [
      {
         "type": "home",
         "number": "212 555-1234"
      },
      {
         "type": "office",
         "number": "646 555-4567"
      }
   ],
   "children": [],
   "spouse": null
};
print(person.firstName);
print(person.lastName);
print(person.address.city);
print(person.phoneNumbers[1].number);
Listing 7-2.Declaring and Accessing a Person Object

假设清单 7-2 存储在person.js中,运行应用程序如下:

java RunScript person.js

您应该观察到以下输出:

John
Smith
New York
646 555-4567

JSON 对象作为独立于语言的文本存在。要将文本转换成语言相关的对象,需要解析文本。JavaScript 为这个任务提供了一个带有parse()方法的JSON对象。将要解析的文本作为参数传递给parse(),并接收生成的基于 JavaScript 的对象作为该方法的返回值。parse()当文本不符合 JSON 格式时抛出SyntaxError

清单 7-3 展示了一个演示parse()的脚本。

var creditCardText =
"{ \"number\": \"1234567890123456\", \"expiry\": \"04/20\", \"type\": " +
"\"visa\" }";
var creditCard = JSON.parse(creditCardText);
print(creditCard.number);
print(creditCard.expiry);
print(creditCard.type);

var creditCardText2 = "{ 'type': 'visa' }";
var creditCard2 = JSON.parse(creditCardText2);

Listing 7-3.Parsing a JSON Object

假设清单 7-3 存储在cc.js中,运行应用程序如下:

java RunScript cc.js

您应该观察到以下输出:

1234567890123456
04/20
visa
SyntaxError: Invalid JSON: <json>:1:2 Expected , or } but found '
{ 'type': 'visa' }
  ^ in <eval> at line number 10

语法错误表明不能用单引号分隔名称。

关于在 JavaScript 环境中使用 JSON,我要说的就是这些。因为这本书是以 Java 为中心的,后续章节将探讨各种第三方 Java APIs,用于将 JSON 对象解析成 Java 相关对象,反之亦然。

验证 JSON 对象

应用程序经常需要验证 JSON 对象,以确保所需的属性存在,并且满足附加的约束条件(比如价格不能低于一美元)。验证通常在 JSON 模式的上下文中执行。

JSON Schema 是一种语法语言,用于定义 JSON 对象的结构、内容和(在某种程度上)语义。它允许您指定关于对象属性含义的元数据(关于数据的数据)以及这些属性的有效值。应用语法语言的结果是一个模式(蓝图),它描述了根据模式有效的 JSON 对象集。

Note

JSON 模式将模式表示为 JSON 对象。

JSON 模式在 JSON 模式网站( http://json-schema.org )上维护。这个网站揭示了 JSON 模式的几个优点:

  • 它描述了您现有的数据格式。
  • 它提供了清晰的、人类可读的和机器可读的文档。
  • 它提供了完整的结构化验证,这对于自动化测试和验证客户提交的数据非常有用。

Note

JSON 模式网站主要关注 JSON 模式规范的草案版本 4。该规范分为三个部分:JSON 模式核心、JSON 模式验证和 JSON 超级模式。

要理解 JSON 模式,请考虑以下 JSON 对象:

{
   "name": "John Doe",
   "age": 35
}

这个对象用一个name和一个age来描述一个人。让我们设置以下约束:两个属性都必须存在,name必须是字符串类型,age必须是数字类型,age的值必须在 18 到 64 之间。

以下模式(基于 JSON 模式的草案版本 4)为该对象提供了必要的约束:

{
   "$schema": "http://json-schema.org/draft-04/schema#",
   "title": "Person",
   "description": "A person",
   "type": "object",
   "properties":
   {
      "name":
      {
         "description": "A person's name",
         "type": "string"
      },
      "age":
      {
         "description": "A person's age",
         "type": "number",
         "minimum": 18,
         "maximum": 64
      }

   },
   "required": ["name", "age"]    
}

从上到下阅读,您会将这个基于 JSON 的模式解释如下:

  • 关键字$schema声明该模式是根据草案版本 4 规范编写的。
  • title关键字标识这个模式正在验证的 JSON 对象。在这种情况下,正在验证一个Person对象。
  • description关键字提供了对Person对象的描述。与title一样,description没有给被验证的数据添加任何约束。
  • type关键字表示包含对象是一个 JSON 对象(通过object值)。此外,它还识别属性类型(例如stringnumber)。
  • 关键字properties引入了一个可以出现在 JSON 对象中的属性数组。这些属性被标识为nameage。每个属性由一个对象进一步描述,该对象提供了一个描述属性的description关键字和一个识别可以分配给属性的值的类型的type关键字。这是一个约束:您必须给name分配一个字符串,给age分配一个数字。对于age属性,minimummaximum关键字被指定来提供额外的约束:分配给age的数字必须在从1864的范围内。
  • required关键字引入了一个数组,该数组标识那些必须存在于 JSON 对象中的属性。在这个例子中,nameage都是必需的属性。

JSON 模式网站提供了不同编程语言的各种验证器实现的链接(参见 http://json-schema.org/implementations.html )。您可以下载一个实现,并将其集成到您的应用程序中,但须符合许可要求。对于这一章,我选择使用一个名为 JSON Schema Lint ( http://jsonschemalint.com/draft4/ )的在线工具来演示验证。

图 7-1 显示了 JSON Schema Lint 在线工具的相应窗口中先前的 JSON 对象和模式。

A394211_1_En_7_Fig1_HTML.jpg

图 7-1。

The schema is valid and the JSON object conforms to this schema

让我们对 JSON 对象进行一些修改,使它不再符合模式,并看看 JSON 模式 Lint 工具如何响应。首先,让我们将65赋值给age,这超过了age属性的maximum约束。图 7-2 显示了结果。

A394211_1_En_7_Fig2_HTML.jpg

图 7-2。

JSON Schema Lint changes its header color to red to signify an error, and also identifies the property and constraint that’s been violated

接下来,让我们将age的值恢复为35,但是用双引号将它括起来,这将类型从数字更改为字符串。结果如图 7-3 所示。

A394211_1_En_7_Fig3_HTML.jpg

图 7-3。

JSON Schema Lint reports that the age property has the wrong type

最后,让我们将age的值恢复为35,但是删除name属性。图 7-4 显示了 JSON 模式 Lint 的响应。

A394211_1_En_7_Fig4_HTML.jpg

图 7-4。

JSON Schema Lint reports that the name property is required Note

查看“JSON 模式:核心定义和术语”( http://json-schema.org/latest/json-schema-core.html )和“JSON 模式:交互式和非交互式验证”( http://json-schema.org/latest/json-schema-validation.html )文档,了解更多关于创建基于 JSON 模式的模式的信息。

Exercises

以下练习旨在测试你对第七章内容的理解。

Define JSON.   True or false: JSON is derived from a strict subset of JavaScript.   How does the JSON data format present a JSON object?   Identify the six types that JSON supports.   True or false: JSON doesn’t support comments.   How would you parse a JSON object into an equivalent JavaScript object?   Define JSON Schema.   When creating a schema, how do you identify those properties that must be present in those JSON objects that the schema validates?   Declare a JSON object for a product in terms of name and price properties. Set the name to "hammer" and the price to 20.   Declare a schema for validating the previous JSON object. The schema should constrain name to be a string, price to be a number, price to be at least 1 dollar, and name and price to be present in the object. Use JSON Schema Lint to verify the schema and JSON object.

摘要

JSON 是一种独立于语言的数据格式,它将 JSON 对象表示为人类可读的属性列表。虽然源自 JavaScript,但是将JSON 对象解析成等价的语言相关对象的代码在许多编程语言中都有。

JSON 数据格式将 JSON 对象表示为用大括号分隔、用逗号分隔的属性列表。对于每个属性,名称都表示为双引号字符串。名称字符串后面跟一个冒号字符,后面跟一个特定 JSON 类型的值。

应用程序经常需要验证 JSON 对象,以确保所需的属性存在,并且满足附加的约束条件(比如价格不能低于一美元)。JSON Schema 是一种让您完成验证的语法语言。

第八章介绍了用于解析和创建 Json 对象的 mJson。

八、使用 mJson 解析和创建 JSON 对象

许多第三方 API 可用于解析和创建 JSON 对象。本章探索这些 API 中最简单的一个:mJson。

mJson 是什么?

mJson 是一个基于 Java 的小型 Json 库,用于将 JSON 对象解析成 Java 对象,反之亦然。mJson 提供以下功能:

  • 单一通用类型(一切都是一个Json对象;没有类型转换)
  • 创建Json对象的方法
  • 了解Json物体的方法
  • 导航Json对象层次的方法
  • 修改Json对象的方法
  • 完全支持 JSON 模式草案 4 验证
  • 用于增强的可插拔工厂Json
  • 实现更紧凑代码的方法链接
  • 快速的手工编码解析
  • 整个库包含在一个 Java 源文件中

与其他 Json 库不同,mJson 专注于在 Java 中操作 JSON 结构,而不是将它们映射到强类型 Java 对象。因此,mJson 减少了冗长,让您在 Java 中像在 JavaScript 中一样自然地使用 Json。

Note

mJson 是由开发者 Borislav Lordanov 创建的。这个库位于 GitHub 的 http://bolerio.github.io/mjson/

获取和使用 mJson

mJson 作为单个 Jar 文件分发;mjson-1.3.jar是编写时最新的 Jar 文件。要获取这个 Jar 文件,请将浏览器指向 http://repo1.maven.org/maven2/org/sharegov/mjson/1.3/mjson-1.3.jar

mjson-1.3.jar包含一个Json类文件和其他描述嵌套在Json类中的包私有类的类文件。此外,这个 Jar 文件显示Json位于mjson包中。

Note

mJson 根据 Apache License 版( www.apache.org/licenses/ )进行许可。

mjson-1.3.jar很容易。编译源代码或运行应用程序时,只需将它包含在类路径中,如下所示:

javac -cp mjson-1.3.jar source file

java -cp mjson-1.3.jar;. main 
classfile

探索 Json 类

Json类描述了一个 JSON 对象或 JSON 对象的一部分。它包含了SchemaFactory接口,50 多个方法和其他成员。本节将探讨这些方法中的大部分以及SchemaFactory

Note

Json类的 API 文档位于 http://bolerio.github.io/mjson/apidocs/index.html

创建 Json 对象

Json声明了几个创建和返回Json对象的static方法。其中三种方法读取并解析外部 JSON 对象:

  • Json read(String s):从传递给s(类型为java.lang.String)的字符串中读取一个 JSON 对象,并解析这个对象。
  • Json read(URL url):从传递给type java.net.URLurl的统一资源定位符(URL)中读取一个 JSON 对象,并解析这个对象。
  • Json read(CharacterIterator ci):从传递给ci(类型java.text.CharacterIterator)的字符迭代器中读取一个 JSON 对象,并解析该对象。

每个方法都返回一个描述被解析的 JSON 对象的Json对象。

清单 8-1 展示了演示read(String)方法的应用程序的源代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"lastName\": \"Smith\"," +
      "\"isAlive\": true," +
      "\"age\": 25," +
      "\"address\":" +
      "{" +
      "\"streetAddress\": \"21 2nd Street\"," +
      "\"city\": \"New York\"," +
      "\"state\": \"NY\"," +
      "\"postalCode\": \"10021-3100\"" +
      "}," +
      "\"phoneNumbers\":" +
      "[" +
      "{" +
      "\"type\": \"home\"," +
      "\"number\": \"212 555-1234\"" +
      "}," +
      "{" +
      "\"type\": \"office\"," +
      "\"number\": \"646 555-4567\"" +
      "}" +
      "]," +
      "\"children\": []," +
      "\"spouse\": null" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println(json);
   }
}

Listing 8-1.Reading and Parsing a String-Based JSON Object

main(String[])方法首先声明一个基于 Java 字符串的 JSON 对象(可以用逗号(,)代替冒号(😃,但是冒号更清楚);然后调用Json.read()读取并解析该对象,并将该对象作为Json对象返回;最后输出Json对象的字符串表示(通过最终调用它的toString()方法将Json对象转换成 Java 字符串)。

编译清单 8-1 如下:

javac -cp mjson-1.3.jar mJsonDemo.java

运行生成的应用程序,如下所示:

java -cp mjson-1.3.jar;. mJsonDemo

您应该观察到以下输出:

{"firstName":"John","lastName":"Smith","isAlive":true,"address":{"streetAddress":"21 2nd Street","city":"New York","postalCode":"10021-3100","state":"NY"},"children":[],"age":25,"phoneNumbers":[{"number":"212 555-1234","type":"home"},{"number":"646 555-4567","type":"office"}],"spouse":null}

read()方法也可以解析更小的 JSON 片段,比如不同类型值的数组。参见清单 8-2 进行演示。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      Json json = Json.read("[4, 5, {}, true, null, \"ABC\", 6]");
      System.out.println(json);
   }
}

Listing 8-2.Reading and Parsing a JSON Fragment

当您运行此应用程序时,您应该观察到以下输出:

[4,5,{},true,null,"ABC",6]

除了读取和解析方法,Json还提供了用于创建Json对象的static方法:

  • Json array():返回一个代表空 JSON 数组的Json对象。
  • Json array(Object... args):返回一个填充了argsJson对象(代表一个 JSON 数组),变量为java.lang.Object s
  • Json make(Object anything):返回一个填充了anything内容的Json对象,为null之一;一个类型为JsonStringjava.util.Collection<?>java.util.Map<?, ?>java.lang.Booleanjava.lang.Number的值;或者这些类型之一的数组。映射、集合和数组被递归复制,使得它们的每个元素都被转换成一个Json对象。映射的键通常是字符串,但是任何具有有意义的toString()实现的对象都可以。当传递给anything的参数的具体类型未知时,该方法抛出java.lang.IllegalArgumentException
  • Json nil():返回一个代表nullJson对象。
  • Json object():返回一个代表空 JSON 对象的Json对象。
  • Json object(Object... args):返回一个Json对象(代表一个 JSON 对象)填充args,一个Object的变量数,这些对象标识属性名和值;对象的数量必须是偶数,偶数索引标识属性名,奇数索引标识属性值。这些名字通常是String类型的,但是也可以是具有适当的toString()方法的任何其他类型。每个值首先通过调用 make(Object )转换成一个Json对象。

清单 8-3 展示了一个应用程序的源代码,该应用程序演示了大多数这些额外的static方法。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      Json jsonAddress =
         Json.object("streetAddress", "21 2nd Street",
                     "city", "New York",
                     "state", "NY",
                     "postalCode", "10021-3100");
      Json jsonPhone1 =
         Json.object("type", "home",
                     "number", "212 555-1234");
      Json jsonPhone2 =
         Json.object("type", "office",
                     "number", "646 555-4567");
      Json jsonPerson =
         Json.object("firstName", "John",

                     "lastName", "Smith",
                     "isAlive", true,
                     "age", 25,
                     "address", jsonAddress,
                     "phoneNumbers", Json.array(jsonPhone1, jsonPhone2),
                     "children", Json.array(),
                     "spouse", Json.nil());
      System.out.println(jsonPerson);
   }
}

Listing 8-3.Creating a Person JSON Object

清单 8-3 描述了一个创建与清单 8-1 中读取和解析的 JSON 对象相同的应用程序。注意,您可以将Json对象传递给array(Object...)object(Object...),这允许您从较小的片段构建完整的 JSON 对象。如果您运行这个应用程序,您会发现与清单 8-1 中描述的应用程序生成的输出相同。

清单 8-4 展示了另一个将make(Object)与 Java 集合和映射结合使用的应用程序的源代码。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      List<String> weekdays = Arrays.asList("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday");
      System.out.println(Json.make(weekdays));

      Map<String, Number> people = new HashMap<>();
      people.put("John", 33);
      people.put("Joan", 27);
      System.out.println(Json.make(people));

      Map<String, String[]> planets = new HashMap<>();
      planets.put("Mercury", null);
      planets.put("Earth", new String[] {"Luna"});
      planets.put("Mars", new String[] {"Phobos", "Deimos"});
      System.out.println(Json.make(planets));
   }
}

Listing 8-4.Making JSON Objects from Java Collections and Maps

main(String[])首先创建一个星期几名称的列表,然后将这个对象传递给make(Object),后者返回的Json对象被输出。接下来,人们的姓名和年龄的地图被创建并随后被传递给make(Object)。输出最终的 JSON 对象。最后,创建了一个行星名称和卫星名称数组的地图。这个 map 被转换成一个更复杂的 JSON 对象,并输出。

如果您编译这个源代码并运行应用程序,您会发现以下输出:

["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]
{"Joan":27,"John":33}

{"Earth":["Luna"],"Mars":["Phobos","Deimos"],"Mercury":null}

了解 Json 对象

Json提供了几种学习由Json对象描述的 JSON 实体的方法。首先,您可以调用Object getValue()方法来返回Json对象的 JSON 值(作为 Java 对象)。返回值将是 Java null或具有 Java BooleanStringNumberMapjava.util.List或数组类型。对于对象和数组,此方法执行所有嵌套元素的深层复制。

要标识 JSON 值的 JSON 类型,请调用以下方法之一:

  • boolean isArray():返回 JSON 数组值的true
  • boolean isBoolean():返回一个 JSON 布尔值true
  • boolean isNull():返回 JSON null值的true
  • boolean isNumber():返回 JSON 数值的true
  • boolean isObject():返回 JSON 对象值的true
  • boolean isPrimitive():返回 JSON 数字、字符串或布尔值的true
  • boolean isString():返回 JSON 字符串值的true

清单 8-5 展示了演示getValue()和这些 JSON 类型识别方法的应用程序的源代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"lastName\": \"Smith\"," +
      "\"isAlive\": true," +
      "\"age\": 25," +
      "\"address\":" +
      "{" +
      "\"streetAddress\": \"21 2nd Street\"," +
      "\"city\": \"New York\"," +
      "\"state\": \"NY\"," +
      "\"postalCode\": \"10021-3100\"" +
      "}," +
      "\"phoneNumbers\":" +
      "[" +
      "{" +
      "\"type\": \"home\"," +
      "\"number\": \"212 555-1234\"" +
      "}," +
      "{" +
      "\"type\": \"office\"," +
      "\"number\": \"646 555-4567\"" +
      "}" +
      "]," +
      "\"children\": []," +
      "\"spouse\": null" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println("Value = " + json.getValue());
      System.out.println();
      classify(json);
   }

   static void classify(Json jsonObject)
   {
      if (jsonObject.isArray())
         System.out.println("Array");
      else
      if (jsonObject.isBoolean())
         System.out.println("Boolean");
      else
      if (jsonObject.isNull())
         System.out.println("Null");
      else
      if (jsonObject.isNumber())
         System.out.println("Number");
      else

      if (jsonObject.isObject())
         System.out.println("Object");
      else
      if (jsonObject.isString())
         System.out.println("String");
      if (jsonObject.isPrimitive())
         System.out.println("Primitive");
   }
}

Listing 8-5.Obtaining a Json Object’s Value and Identifying Its JSON Type

编译这段源代码并运行应用程序,您会发现以下输出:

Value = {firstName=John, lastName=Smith, isAlive=true, address={streetAddress=21 2nd Street, city=New York, postalCode=10021-3100, state=NY}, children=[], age=25, phoneNumbers=[{number=212 555-1234, type=home}, {number=646 555-4567, type=office}], spouse=null}

Object

在验证了一个Json对象代表了预期的 JSON 类型之后,您可以调用Json的一个as方法来获取 JSON 值,作为一个等价 Java 类型的 Java 值:

  • boolean asBoolean():以 Java 布尔值的形式返回 JSON 值。
  • byte asByte():以 Java 字节整数的形式返回 JSON 值。
  • char asChar():返回 JSON 字符串值的第一个字符作为 Java 字符。
  • double asDouble():返回 JSON 值作为 Java 双精度浮点值。
  • float asFloat():以 Java 浮点值的形式返回 JSON 值。
  • int asInteger():返回 Java 整数形式的 JSON 值。
  • List<Json> asJsonList():返回 JSON 数组的底层列表表示。返回的列表是实际的数组表示,所以对它的任何修改都是对Json对象列表的修改。
  • Map<String, Json> asJsonMap():返回 JSON 对象属性的底层映射。返回的贴图是实际的对象表示,所以对它的任何修改都是对Json对象贴图的修改。
  • List<Object> asList():返回描述 JSON 数组的Json对象的元素列表。返回的列表是一个副本,对它的修改不会影响Json对象。
  • long asLong():以 Java 长整型返回 JSON 值。
  • Map<String, Object> asMap():返回描述 JSON 对象的Json对象的属性图。返回的地图是副本,对它的修改不会影响Json对象。
  • short asShort():以 Java 短整型返回 JSON 值。
  • String asString():以 Java 字符串的形式返回 JSON 值。

清单 8-6 展示了一个应用程序的源代码,该应用程序使用asMap()来获取描述 JSON 对象的Json对象属性的映射。

import java.util.Map;

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"lastName\": \"Smith\"," +
      "\"isAlive\": true," +
      "\"age\": 25," +
      "\"address\":" +
      "{" +
      "\"streetAddress\": \"21 2nd Street\"," +
      "\"city\": \"New York\"," +
      "\"state\": \"NY\"," +
      "\"postalCode\": \"10021-3100\"" +
      "}," +
      "\"phoneNumbers\":" +
      "[" +
      "{" +
      "\"type\": \"home\"," +
      "\"number\": \"212 555-1234\"" +
      "}," +
      "{" +
      "\"type\": \"office\"," +
      "\"number\": \"646 555-4567\"" +
      "}" +
      "]," +

      "\"children\": []," +
      "\"spouse\": null" +
      "}";
      Json json = Json.read(jsonStr);
      if (json.isObject())
      {
         Map<String, Object> props = json.asMap();
         for (Map.Entry<String, Object> propEntry: props.entrySet())
            System.out.println(propEntry.getKey() + ": " +  propEntry.getValue());
      }
   }
}

Listing 8-6.Iterating Over a Json Object’s Properties to Learn About a JSON Object

main(String[])声明了与清单 8-1 中相同的 JSON 对象。然后它读取这个对象并解析成一个Json对象。调用isObject()方法来验证Json对象表示一个 JSON 对象。(最好先核实一下。)因为应该是这种情况,所以调用asMap()来返回Json对象属性的映射,然后遍历并输出。

Caution

如果用Json json = Json.make(jsonStr);替换Json json = Json.read(jsonStr);,您将看不到任何输出,因为从make()返回的Json对象标识 JSON 字符串类型,而不是 JSON 对象类型。

研究完源代码后,编译它并运行应用程序。您将发现以下输出:

firstName: John
lastName: Smith
isAlive: true
address: {streetAddress=21 2nd Street, city=New York, postalCode=10021-3100, state=NY}
children: []
age: 25
phoneNumbers: [{number=212 555-1234, type=home}, {number=646 555-4567, type=office}]
spouse: null

您可以通过调用以下at()方法来访问数组和对象的内容,这些方法返回描述数组元素值或对象属性值的Json对象:

  • Json at(int index):返回该Json对象数组中指定index处数组元素的值(作为Json对象)。这个方法只适用于 JSON 数组。当index超出数组界限时,它抛出java.lang.IndexOutOfBoundsException
  • Json at(String propName):返回对象属性的值(作为一个Json对象),该对象属性的名称由该Json对象的地图中的propName标识。当没有这样的属性时返回null。这个方法只适用于 JSON 对象。
  • Json at(String propName, Json defValue):返回对象属性的值(作为一个Json对象),该对象属性的名称由该Json对象的地图中的propName标识。当没有这样的属性时,它创建一个新的属性,其值由defValue指定,并返回defValue。这个方法只适用于 JSON 对象。
  • Json at(String propName, Object defValue):返回对象属性的值(作为一个Json对象),该对象属性的名称由该Json对象的地图中的propName标识。当没有这样的属性时,它创建一个新的属性,其值由defValue指定,并返回defValue。这个方法只适用于 JSON 对象。

清单 8-7 展示了使用前两个at()方法访问 JSON 对象属性值的应用程序的源代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"lastName\": \"Smith\"," +
      "\"isAlive\": true," +
      "\"age\": 25," +
      "\"address\":" +
      "{" +
      "\"streetAddress\": \"21 2nd Street\"," +
      "\"city\": \"New York\"," +
      "\"state\": \"NY\"," +
      "\"postalCode\": \"10021-3100\"" +
      "}," +
      "\"phoneNumbers\":" +
      "[" +
      "{" +
      "\"type\": \"home\"," +
      "\"number\": \"212 555-1234\"" +
      "}," +
      "{" +
      "\"type\": \"office\"," +
      "\"number\": \"646 555-4567\"" +
      "}" +
      "]," +
      "\"children\": []," +
      "\"spouse\": null" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.printf("First name = %s%n", json.at("firstName"));
      System.out.printf("Last name = %s%n", json.at("lastName"));

      System.out.printf("Is alive = %s%n", json.at("isAlive"));
      System.out.printf("Age = %d%n", json.at("age").asInteger());
      System.out.println("Address");
      Json jsonAddr = json.at("address");
      System.out.printf("   Street address = %s%n",  jsonAddr.at("streetAddress"));
      System.out.printf("   City = %s%n", jsonAddr.at("city"));
      System.out.printf("   State = %s%n", jsonAddr.at("state"));
      System.out.printf("   Postal code = %s%n", jsonAddr.at("postalCode"));
      System.out.println("Phone Numbers");
      Json jsonPhone = json.at("phoneNumbers");
      System.out.printf("   Type = %s%n", jsonPhone.at(0).at("type"));
      System.out.printf("   Number = %s%n", jsonPhone.at(0).at("number"));
      System.out.println();
      System.out.printf("   Type = %s%n", jsonPhone.at(1).at("type"));
      System.out.printf("   Number = %s%n", jsonPhone.at(1).at("number"));
      Json jsonChildren = json.at("children");
      System.out.printf("Children = %s%n", jsonChildren);
      System.out.printf("Spouse = %s%n", json.at("spouse"));
   }
}

Listing 8-7.Obtaining and Outputting a JSON Object’s Property Values

表达式json.at("age")返回描述 JSON 编号的Json对象;asInteger()以 32 位 Java 整数的形式返回该值。

编译这个源代码并运行应用程序。您将发现以下输出:

First name = "John"
Last name = "Smith"
Is alive = true
Age = 25
Address
   Street address = "21 2nd Street"
   City = "New York"
   State = "NY"
   Postal code = "10021-3100"
Phone Numbers
   Type = "home"
   Number = "212 555-1234"

   Type = "office"
   Number = "646 555-4567"
Children = []
Spouse = null

您可能想知道如何检测分配给属性名children的空数组。您可以通过调用asList()返回一个List实现对象,然后在这个对象上调用Listsize()方法来完成这个任务,如下所示:

System.out.printf("Array length = %d%n", jsonChildren.asList().size());

这段代码将报告一个长度为零的数组。

最后,Json提供了三种方法来验证属性名是否存在,以及属性名或数组元素是否具有指定的值:

  • boolean has(String propName):当这个Json对象描述一个 JSON 对象,这个 JSON 对象有一个由propName标识的属性时,返回true;否则,返回false
  • boolean is(int index, Object value):当这个Json对象描述一个 JSON 数组在指定的index有指定的value时,返回true;否则,返回false
  • boolean is(String propName, Object value):当这个Json对象描述了一个 JSON 对象,这个 JSON 对象有一个由propName标识的属性,并且这个属性有一个由value标识的值时,返回true;否则,返回false

例如,考虑上市 8-7 。表达式json.has("firstName")返回true,而表达式json.has("middleName")返回false

导航 Json 对象层次结构

当前面讨论的at()方法之一返回描述 JSON 对象或 JSON 数组的Json对象时,您可以通过将另一个at()方法调用链接到表达式来导航到该对象或数组。例如,我在前面的应用程序中使用这种技术来访问一个电话号码:

System.out.printf("   Number = %s%n", jsonPhone.at(0).at("number"));

这里,jsonPhone.at(0)返回一个代表phoneNumbers JSON 数组中第一个数组条目的Json对象。因为数组条目恰好是一个 JSON 对象,所以在这个Json对象上调用at("number")会导致Json返回 JSON 对象的number属性的值(作为一个Json对象)。

每个描述属于一个数组或对象的 JSON 实体的Json对象都包含一个对其封闭的基于数组或对象的Json对象的引用。您可以调用JsonJson up()方法来返回这个封闭的Json对象,如清单 8-8 所示。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"propName\": \"propValue\"," +
      "\"propArray\":" +
      "[" +
      "{" +
      "\"element1\": \"value1\"" +
      "}," +
      "{" +
      "\"element2\": \"value2\"" +
      "}" +
      "]" +
      "}";
      Json json = Json.read(jsonStr);
      Json jsonElement1 = json.at("propArray").at(0);
      System.out.println(jsonElement1);
      System.out.println();
      System.out.println(jsonElement1.up());
      System.out.println();
      System.out.println(jsonElement1.up().up());
      System.out.println();
      System.out.println(jsonElement1.up().up().up());
   }
}

Listing 8-8.Accessing Enclosing Json Objects

编译这段源代码并运行应用程序,您会发现以下输出:

{"element1":"value1"}

[{"element1":"value1"},{"element2":"value2"}]

{"propArray":[{"element1":"value1"},{"element2":"value2"}],"propName":"propValue"}

null

第一个输出行描述了数组中分配给propArray属性的第一个数组元素。这个元素是由单个element1属性组成的对象。

jsonElement1.up()返回一个Json对象,描述包含 JSON 对象的数组,该对象作为数组的第一个元素。jsonElement1.up().up()返回一个Json对象,描述包含数组的 JSON 对象。最后,jsonElement1.up().up().up()返回一个描述null值的Json对象;JSON 对象没有父对象。

修改 Json 对象

您会遇到想要修改现有Json对象的 JSON 值的情况。例如,您可能正在创建和保存几个相似的 JSON 对象,并希望重用现有的Json对象。

Json允许您修改代表 JSON 数组和对象的Json对象。它不允许您修改代表 JSON 布尔值、数字或字符串值的Json对象,因为它们被认为是不可变的。

Json声明了以下用于修改 JSON 数组元素和 JSON 对象属性的set()方法:

  • Json set(int index, Object value):将位于index的 JSON 数组元素的值设置为value
  • Json set(String propName, Json value):将名称由propName指定的 JSON 对象属性的值设置为value
  • Json set(String property, Object value):将名称由propName指定的 JSON 对象属性的值设置为value。该方法调用make(Object)value转换为代表valueJson对象,然后调用set(String, Json)

清单 8-9 展示了使用第一个和第三个set()方法来设置对象属性和数组元素值的应用程序的源代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"name\": null," +
      "\"courses\":" +
      "[null]" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println(json);
      System.out.println();
      json.set("name", "John Doe");
      Json jsonCourses = json.at("courses");
      jsonCourses.set(0, "English");
      System.out.println(json);
   }
}

Listing 8-9.Setting Object Property and Array Element Values

如果您编译这个源代码并运行应用程序,您会发现以下输出:

{"courses":[null],"name":null}

{"courses":["English"],"name":"John Doe"}

如果您试图为一个不存在的属性设置一个值,Json会添加该属性。但是,如果试图为一个不存在的数组元素设置值,Json会抛出IndexOutOfBoundsException。因此,您可能更喜欢调用下面的add()方法:

  • Json add(Json element):将指定的元素追加到由这个Json对象表示的数组中。
  • Json add(Object anything):通过调用make(Object)anything转换成一个Json对象,并将结果附加到由这个Json对象表示的数组中。

清单 8-10 展示了一个应用程序的源代码,该应用程序使用第一个add()方法向空的courses数组添加两个字符串。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"name\": null," +
      "\"courses\":" +
      "[]" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println(json);
      System.out.println();
      json.set("name", "John Doe");
      Json jsonCourses = json.at("courses");
      jsonCourses.add("English");
      jsonCourses.add("French");
      System.out.println(json);
   }
}

Listing 8-10.Appending Strings to an Empty JSON Array

编译这个源代码并运行应用程序。它生成如下所示的输出:

{"courses":[],"name":null}

{"courses":["English","French"],"name":"John Doe"}

Json提供了一对面向数组的remove()方法,它们与它们的add()对应方法采用相同的参数:

  • Json remove(Json element):从这个Json对象表示的数组中删除指定的元素。
  • Json remove(Object anything):通过调用make(Object)并将结果从这个Json对象代表的数组中移除,将anything转换成一个Json对象。

假设您将下面几行添加到清单 8-10 的main(String[])方法中:

jsonCourses.remove("English");
System.out.println(json);

然后,您应该观察到以下附加输出:

{"courses":["French"],"name":"John Doe"}

通过调用以下方法,可以按索引从数组中移除元素,或按名称从对象中移除属性:

  • Json atDel(int index):从这个Json对象的 JSON 数组中删除指定index处的元素,并返回该元素。
  • Json atDel(String propName):从这个Json对象的 JSON 对象中删除由propName标识的属性,并返回属性值(如果属性不存在,则返回null)。
  • Json delAt(int index):从这个Json对象的 JSON 数组中删除指定index处的元素。
  • Json delAt(String propName):从这个Json对象的 JSON 对象中删除由propName标识的属性。

清单 8-11 展示了使用最后两个delAt()方法删除属性和数组元素的应用程序的源代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"lastName\": \"Doe\"," +
      "\"courses\":" +
      "[\"English\", \"French\", \"Spanish\"]" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println(json);
      System.out.println();
      json.delAt("lastName");
      System.out.println(json);
      System.out.println();
      json.at("courses").delAt(1);
      System.out.println(json);
   }
}

Listing 8-11.Removing the Last Name and One of the Courses Being Taken

要查看delAt()方法的结果,编译这个源代码并运行应用程序。其输出如下所示:

{"firstName":"John","lastName":"Doe","courses":["English","French","Spanish"]}

{"firstName":"John","courses":["English","French","Spanish"]}

{"firstName":"John","courses":["English","Spanish"]}

Json提供了另一种修改 JSON 对象的方法:

  • Json with(Json objectorarray):将这个Json对象的 JSON 对象或 JSON 数组与传递给objectorarray的参数相结合。这个Json对象的 JSON 类型和objectorarray的 JSON 类型必须匹配。如果objectorarray标识了一个 JSON 对象,那么它的所有属性都被附加到这个Json对象的对象中。如果objectorarray标识了一个 JSON 数组,那么它的所有元素都被附加到这个Json对象的数组中。

清单 8-12 展示了一个应用程序的源代码,该应用程序使用with(Json)将属性添加到一个对象,将元素添加到一个数组。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      String jsonStr =
      "{" +
      "\"firstName\": \"John\"," +
      "\"courses\":" +
      "[\"English\"]" +
      "}";
      Json json = Json.read(jsonStr);
      System.out.println(json);
      System.out.println();
      Json jsono = Json.read("{\"initial\": \"P\", \"lastName\": \"Doe\"}");
      Json jsona = Json.read("[\"French\", \"Spanish\"]");
      json.with(jsono);
      System.out.println(json);
      System.out.println();
      json.at("courses").with(jsona);
      System.out.println(json);
   }
}

Listing 8-12.Appending Properties to an Object and Elements to an Array

编译清单 8-12 并运行应用程序。下面是应用程序的输出:

{"firstName":"John","courses":["English"]}

{"firstName":"John","courses":["English"],"lastName":"Doe","initial":"P"}

{"firstName":"John","courses":["English","French","Spanish"],"lastName":"Doe","initial":"P"}

确认

Json通过其嵌套的Schema接口和以下static方法支持 JSON 模式草案 4 验证:

  • Json.Schema schema(Json jsonSchema):返回一个Json.Schema对象,根据jsonSchema描述的模式验证 JSON 文档。
  • Json.Schema schema(Json jsonSchema, URI uri):返回一个Json.Schema对象,根据jsonSchema描述的模式验证 JSON 文档,也位于传递给uri的统一资源标识符(URI),类型为java.net.URI
  • Json.Schema schema(URI uri):返回一个Json.Schema对象,根据位于uri的模式验证 JSON 文档。

通过调用SchemaJson validate(Json document)方法来执行验证,该方法试图根据这个Schema对象来验证一个 JSON document。即使检测到验证错误,验证也会尝试继续进行。返回值总是一个Json对象,其 JSON 对象包含名为ok的布尔属性。当oktrue时,没有其他属性。当它是false时,JSON 对象还包含一个名为errors的属性,这是所有检测到的模式违规的错误消息数组。

我已经创建了两个演示验证的示例应用程序。清单 8-13 基于 mJson 网站上的示例代码。

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args)
   {
      // A simple schema that accepts only JSON objects with a
         // mandatory property 'id'.
      Json.Schema schema = Json.schema(Json.object("type", "object", "required", Json.array("id")));
      System.out.println(schema.validate(Json.object("id", 666, "name", "Britlan")));

      System.out.println(schema.validate(Json.object("ID", 666, "name", "Britlan")));
   }
}

Listing 8-13.Validating JSON Objects That Include the id Property

如果您编译这个源代码并运行应用程序,您会发现以下输出:

{"ok":true}
{"ok":false,"errors":["Required property id missing from object {\"name\":\"Britlan\",\"ID\":666}"]}

在第七章中,我展示了以下 JSON 对象:

{
   "name": "John Doe",
   "age": 35
}

我还以 JSON 对象的形式展示了以下模式:

{
   "$schema": "http://json-schema.org/draft-04/schema#",
   "title": "Person",
   "description": "A person",
   "type": "object",
   "properties":
   {
      "name":
      {
         "description": "A person's name",
         "type": "string"
      },
      "age":
      {
         "description": "A person's age",
         "type": "number",
         "minimum": 18,
         "maximum": 64
      }
   },
   "required": ["name", "age"]    
}

假设我将这个模式复制到一个schema.json文件中,并将其存储在我的网站上 http://javajeff.ca/schema.json 。清单 8-14 展示了一个应用程序的源代码,该应用程序使用Json.Schema schema(URI)来获取这个模式,以验证之前的 JSON 对象。

import java.net.URI;
import java.net.URISyntaxException;

import mjson.Json;

public class mJsonDemo
{
   public static void main(String[] args) throws URISyntaxException
   {
      Json.Schema schema =
         Json.schema(new URI("http://javajeff.ca/schema.json"));
      Json json = Json.read("{\"name\": \"John Doe\", \"age\": 35}");
      System.out.println(schema.validate(json));
      json = Json.read("{\"name\": \"John Doe\", \"age\": 65}");
      System.out.println(schema.validate(json));
      json = Json.read("{\"name\": \"John Doe\", \"age\": \"35\"}");
      System.out.println(schema.validate(json));
      json = Json.read("{\"age\": 35}");
      System.out.println(schema.validate(json));
   }
}

Listing 8-14.Validating JSON Objects via an External Schema

编译这个源代码并运行应用程序。您将发现以下输出:

{"ok":true}
{"ok":false,"errors":["Number 65 is above allowed maximum 64.0"]}
{"ok":false,"errors":["Type mistmatch for \"35\", allowed types: [\"number\"]"]}
{"ok":false,"errors":["Required property name missing from object {\"age\":35}"]}

通过工厂定制

JsonJson对象的创建委托给工厂,工厂是实现Json.Factory接口方法的类的实例:

  • Json array()
  • Json bool(boolean value)
  • Json make(Object anything)
  • Json nil()
  • Json number(Number value)
  • Json object()
  • Json string(String value)

Json.DefaultFactory类提供了这些方法的默认实现,但是您可以在必要时提供自定义实现。为了避免实现所有这些方法,您可以扩展 DefaultFactory ,只覆盖那些感兴趣的方法。

创建自定义的Factory类后,实例化它,然后通过调用以下static Json方法之一来安装对象:

  • T0
  • void attachFactory(Json.Factory factory)

第一种方法将指定的factory安装为一个全局工厂,它由所有没有特定线程本地工厂的线程使用。第二种方法仅将指定的factory附加到调用线程,这允许您在同一个类加载器中使用不同的线程工厂。您可以通过调用void dettachFactory()方法移除线程本地工厂,并恢复到线程的全局工厂。

mJson 文档中提到的定制之一是不区分大小写的字符串比较。通过调用基于字符串的Json对象上的equals()和另一个基于字符串的Json对象作为参数,比较两个字符串是否相等:

Json json1 = Json.read("\"abc\"");
Json json2 = Json.read("\"abc\"");
Json json3 = Json.read("\"Abc\"");
System.out.println(json1.equals(json2)); // Output: true
System.out.println(json1.equals(json3));

因为equals()默认区分大小写,json1.equals(json3)返回false

Note

被调用的equals()方法不在Json类中。相反,它位于一个嵌套的包私有类中,比如StringJson

通过首先创建下面的Factory类,可以使基于字符串的Json对象的equals()不区分大小写:

class MyFactory extends Json.DefaultFactory
{
   @Override
   public Json string(String x)
   {

      // Obtain the StringJson instance.
      final Json json = super.string(x);

      class StringIJson extends Json
      {
         private static final long serialVersionUID = 1L;

         String val;

         StringIJson(String val)
         {
            this.val = val;
         }

         @Override
         public byte asByte()
         {
            return json.asByte();
         }

         @Override
         public char asChar()
         {
            return json.asChar();
         }

         @Override
         public double asDouble()
         {
            return json.asDouble();
         }

         @Override
         public float asFloat()
         {
            return json.asFloat();
         }

         @Override
         public int asInteger()
         {
            return json.asInteger();
         }

         @Override
         public List<Object> asList()
         {
            return json.asList();

         }

         @Override
         public long asLong()
         {
            return json.asLong();
         }

         @Override
         public short asShort()
         {
            return json.asShort();
         }

         @Override
         public String asString()
         {
            return json.asString();
         }

         @Override
         public Json dup()
         {
            return json.dup();
         }

         @Override
         public boolean equals(Object x)
         {
            return x instanceof StringIJson &&
                   ((StringIJson) x).val.equalsIgnoreCase(val);
         }

         @Override
         public Object getValue()
         {
            return json.getValue();
         }

         @Override
         public int hashCode()
         {
            return json.hashCode();
         }

         @Override

         public boolean isString()
         {
            return json.isString();
         }

         @Override
         public String toString()
         {
            return json.toString();
         }
      }
      return new StringIJson(x);
   }
}

MyFactory覆盖了string(String)方法,该方法负责创建代表 JSON 字符串的Json对象。在Json.java源代码中(可以从 mJson 网站获得),string(String)执行return new StringJson(x, null);

StringJson是嵌套的包私有static类的名称。因为不能从mjson包外部访问,MyFactory的覆盖string(String)方法声明了一个等价的StringIJson类(I不区分大小写)。

我选择使用适配器/包装器设计模式( https://en.wikipedia.org/wiki/Adapter_pattern ),而不是将所有代码从StringJson复制到StringIJson,这是浪费的重复,而且无论如何也不会工作,因为一些代码依赖于其他包私有类型。

适配器模式背后的想法是让StringIJson复制StringJson方法的头部,并编码主体将几乎所有的方法调用转发给StringJson的对等物。这可以通过让MyFactorystring(String)方法首先调用DefaultFactorystring(String)方法来实现,后者返回StringJson对象。然后,将调用转移到这个对象就很简单了。

例外是equals()方法。StringIJson将该方法整理成与它的StringJson对应方法几乎相同。主要区别是对StringequalsIgnoreCase()方法的调用,而不是对其equals()方法的调用。结果是一个不区分大小写的equals()方法。

在执行任何相等性测试之前,MyFactory需要被实例化并向Json注册,这由下面的方法调用完成:

Json.setGlobalFactory(new MyFactory());

这次,json1.equals(json3)返回true

Exercises

以下练习旨在测试你对第八章内容的理解。

Define mJson.   Describe the Json class.   Identify Json’s methods for reading and parsing external JSON objects.   True or false: The read() methods can also parse smaller JSON fragments, such as an array of different-typed values.   Identify the methods that Json provides for creating JSON objects.   What does Json’s boolean isPrimitive() method accomplish?   How do you return a Json object’s JSON array?   True or false: Json’s Map<String, Json> asJsonMap() method returns a map of the properties of a Json object that describes a JSON object. The returned map is a copy and modifications to it don’t affect the Json object.   Which Json methods let you access the contents of arrays and objects?   What does Json’s boolean is(int index, Object value) method accomplish?   What does Json do when you attempt to set the value for a nonexistent array element?   What is the difference between Json’s atDel() and delAt() methods?   What does Json’s Json with(Json objectorarray) method accomplish?   Identify Json’s methods for obtaining a Json.Schema object.   How do you validate a JSON document against a schema?   What is the difference between Json’s setGlobalFactory() and attachFactory() methods?   Two Json methods that were not discussed in this chapter are Json dup() and String pad(String callback). What do they do?   Write an mJsonDemo application that demonstrates dup() and pad().

摘要

mJson 是一个基于 Java 的小型 Json 库,用于将 JSON 对象解析成 Java 对象,反之亦然。它由一个描述 JSON 对象或 JSON 对象的一部分的Json类组成。Json包含SchemaFactory接口,50 多个方法,以及其他成员。

获得 mJson 库之后,您学习了如何使用这个库来创建Json对象,了解了Json对象,导航了Json对象层次结构,修改了Json对象,根据模式验证了 Json 文档,并通过安装非默认工厂定制了Json

第九章介绍了用于解析和创建 JSON 对象的 Gson。

九、使用 Gson 解析和创建 JSON 对象

Gson 是用于解析和创建 JSON 对象的另一个 API。本章探索了这个开源谷歌产品的最新版本。

Gson 是什么?

Gson(也称为 Google Gson)是一个基于 Java 的小型库,用于解析和创建 JSON 对象。谷歌为自己的项目开发了 Gson,但后来从 1.0 版本开始,Gson 公开可用。根据维基百科,最新版本(在撰写本文时)是 2.6.1。

Gson 的开发目标如下:

  • 提供简单的toJson()fromJson()方法将 Java 对象转换成 JSON 对象,反之亦然。
  • 允许预先存在的不可修改对象与 JSON 相互转换。
  • 为 Java 泛型提供广泛的支持。
  • 允许对象的自定义表示。
  • 支持任意复杂的对象(具有深度继承层次和广泛使用泛型类型)。

Gson 通过将 JSON 对象反序列化为 Java 对象来解析 JSON 对象。类似地,它通过将 Java 对象序列化为 JSON 对象来创建 JSON 对象。Gson 依靠 Java 的反射 API 来协助序列化和反序列化。

获取和使用 Gson

Gson 作为单个 Jar 文件分发;gson-2.6.1.jar是编写时最新的 Jar 文件。要获取这个 Jar 文件,将浏览器指向 http://search.maven.org/#artifactdetails|com.google.code.gson|gson|2.6.1|jar ,从页面底部附近的列表中选择gson-2.6.1.jar,并下载。另外,您可能想下载gson-2.6.1-javadoc.jar,它包含这个 API 的 Javadoc。

Note

Gson 根据 Apache 许可证版本 2.0 ( www.apache.org/licenses/ )进行许可。

使用gson-2.6.1.jar很容易。编译源代码或运行应用程序时,只需将它包含在类路径中,如下所示:

javac -cp gson-2.6.1.jar source file

java -cp gson-2.6.1.jar;. main classfile

探索 GSon

Gson 由 30 多个类和接口组成,分布在四个包中:

  • com.google.gson:这个包提供了对Gson的访问,它是使用 Gson 的主类。
  • com.google.gson.annotations:这个包提供了与 Gson 一起使用的注释类型。
  • 这个包提供了一个从泛型类型中获取类型信息的工具类。
  • com.google.gson.stream:这个包提供了用于读写 JSON 编码值的实用程序类。

在本节中,我首先向您介绍Gson类。然后,我将重点放在Gson反序列化(解析 JSON 对象)和Gson序列化(创建 JSON 对象)上。最后,我简要讨论了 Gson 的其他特性,比如注释和类型适配器。

Gson 类简介

Gson类处理 JSON 和 Java 对象之间的转换。您可以通过使用Gson()构造函数实例化这个类,或者通过使用com.google.gson.GsonBuilder类获得一个Gson实例。以下代码片段演示了这两种方法:

Gson gson1 = new Gson();
Gson gson2 = new GsonBuilder()
     .registerTypeAdapter(Id.class, new IdTypeAdapter())
     .serializeNulls()
     .setDateFormat(DateFormat.LONG)
     .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
     .setPrettyPrinting()
     .setVersion(1.0)
     .create();

当您想要使用默认配置时调用Gson(),当您想要覆盖默认配置时使用GsonBuilder。配置方法调用被链接在一起,最后调用GsonBuilderGson create()方法以返回结果Gson对象。

Gson支持以下默认配置(列表不完整;查看GsonGsonBuilder文档了解更多信息):

  • Gsonjava.lang.Enumjava.util.MapURLjava.net.URI、、java.util.Localejava.util.Datejava.math.BigDecimaljava.math.BigInteger实例提供了默认的序列化和反序列化。您可以通过GsonBuilder.registerTypeAdapter(Type, Object)注册一个类型适配器(稍后讨论)来改变默认表示。
  • 生成的 JSON 文本省略了所有的null字段。然而,它在数组中保留了null s,因为数组是一个有序列表。同样,如果一个字段不是null,但是它生成的 JSON 文本是空的,那么这个字段将被保留。您可以通过调用GsonBuilder.serializeNulls()配置Gson来序列化null值。
  • 默认Date格式与java.text. DateFormat.DEFAULT 相同。这种格式在序列化过程中忽略日期的毫秒部分。您可以通过调用GsonBuilder.setDateFormat(int)GsonBuilder.setDateFormat(String)来更改默认格式。
  • 输出 JSON 文本的默认字段命名策略与 Java 中的相同。例如,一个名为versionNumber的 Java 类字段在 JSON 中将被输出为"versionNumber"。同样的规则也适用于将传入的 JSON 映射到 Java 类。您可以通过调用GsonBuilder.setFieldNamingPolicy(FieldNamingPolicy )来更改此策略。
  • toJson()方法生成的 JSON 文本被简洁地表示出来:所有不需要的空白都被删除。您可以通过调用GsonBuilder.setPrettyPrinting()来改变这种行为。
  • 默认情况下,Gson忽略@ Since@Until标注。您可以通过调用GsonBuilder.setVersion(double)来启用Gson使用这些注释。
  • 默认情况下,Gson忽略@ Expose标注。您可以通过调用GsonBuilder.excludeFieldsWithoutExposeAnnotation()使Gson只序列化/反序列化那些用这个注释标记的字段。
  • 默认情况下,Gsontransientstatic字段排除在序列化和反序列化考虑之外。您可以通过调用GsonBuilder.excludeFieldsWithModifiers(int...)来改变这种行为。

一旦有了一个Gson对象,就可以调用各种fromJson()toJson()方法在 JSON 和 Java 对象之间进行转换。例如,清单 9-1 给出了一个简单的应用程序,它获得了一个Gson对象,并根据 JSON 原语演示了 JSON-Java 对象转换。

 import com.google.gson.Gson;

public class GsonDemo
{
   public static void main(String[] args)
   {
      Gson gson = new Gson();
      String name = gson.fromJson("\"John Doe\"", String.class);
      System.out.println(name);
      gson.toJson(256, System.out);
   }

}

Listing 9-1.Converting Between JSON and Java Primitives

清单 9-1 的main()方法首先实例化Gson,保持其默认配置。然后,它调用Gson<T> T fromJson(String json, Class<T> classOfT)泛型方法将指定的基于java.lang.String的 JSON 文本(在json中)反序列化为指定类的对象(classOfT),这个对象恰好是String

JSON 字符串"John Doe"(双引号是必需的),被表示为 Java String对象,被转换(去掉双引号)为 Java String对象。对该对象的引用被分配给name

在输出返回的名字后,main()调用Gsonvoid toJson(Object src, Appendable writer)方法将自动装箱的整数256(由编译器存储在java.lang.Integer对象中)转换成 JSON 整数,并将结果输出到标准输出流。

编译清单 9-1 如下:

javac -cp gson-2.6.1.jar GsonDemo.java

运行生成的应用程序,如下所示:

java -cp gson-2.6.1.jar;. GsonDemo

您应该观察到以下输出:

John Doe

256

产量并不可观,但这是一个开始。在接下来的两节中,您将看到更多有用的反序列化和序列化示例。

通过反序列化解析 JSON 对象

除了将 JSON 原语(比如数字或字符串)解析成它们的 Java 等价物之外,Gson还允许您将 JSON 对象解析成 Java 对象。例如,假设您有下面的 JSON 对象,它描述了一个人:

{ "name": "John Doe", "age": 45 }

此外,假设您有以下 Java 类:

class Person
{
   String name;
   int age;
}

您可以使用前面的fromJson()方法将 JSON 对象解析成Person类的实例,如清单 9-2 所示。

import com.google.gson.Gson;

public class GsonDemo
{
   static class Person
   {
      String name;
      int age;

      Person(String name, int age)
      {
         this.name = name;
         this.age = age;
      }

      @Override
      public String toString()
      {
         return name + ": " + age;
      }
   }

   public static void main(String[] args)
   {
      Gson gson = new Gson();
      String json = "{ name: \"John Doe\", age: 45 }";
      Person person = gson.fromJson(json, Person.class);
      System.out.println(person);
   }
}

Listing 9-2.Parsing a JSON Object into a Java Object

清单 9-2 声明了一个GsonDemo类和一个嵌套的Person类,后者根据姓名和年龄描述了一个人。

GsonDemomain()方法首先实例化Gson,保持其默认配置。然后,它构造一个基于String的 JSON 对象来表示一个人,并将该对象与Person.class一起传递给fromJson(String json, Class<T> classOfT). fromJson(),解析存储在传递给json的字符串中的姓名和年龄,并使用Person.class和反射 API 来创建一个Person对象,并用姓名和年龄填充它。对Person对象的引用被返回并存储在person变量中,随后被传递给System.out.println()。这个方法最终调用PersontoString()方法来返回Person对象的字符串表示,然后将这个字符串写入标准输出流。

编译清单 9-2 并运行生成的应用程序。您应该观察到以下输出:

John Doe: 45

定制的 JSON 对象解析

前面的gson.fromJson(json, Person.class)方法调用依赖于Gson的默认反序列化机制来解析 JSON 对象。您经常会遇到需要将复杂的 JSON 对象解析成 Java 对象的情况,这些 Java 对象的类与要解析的 JSON 对象的结构不同。您可以使用定制的反序列化器来执行这种解析,反序列化器控制 JSON 对象如何映射到 Java 对象。

com.google.gson.JsonDeserializer<T>接口描述了一个定制的反序列化器。传递给T的参数标识了反序列化器所使用的类型。例如,当需要解析结构稍有不同的 JSON 对象时,可以将Person传递给T

JsonDeserializer声明一个处理反序列化的方法(JSON 对象解析):

T deserialize(JsonElement json,Type typeOfT,
              JsonDeserializationContext context)

deserialize()Gson在反序列化过程中调用的回调方法。使用以下参数调用此方法:

  • json标识被反序列化的 JSON 元素。
  • typeOfT标识要反序列化的 Java 对象的类型json
  • context标识执行反序列化的上下文。(我将在后面详细介绍上下文。)

当传递给json的 JSON 元素与传递给typeOfT的类型不兼容时deserialize()抛出com.google.gson.JsonParseException。因为JsonParseException扩展了 java.lang.RuntimeException ,所以不用追加throws子句。

About Jsonelement

com.google.gson.JsonElement类表示一个 JSON 元素(比如一个数字、一个布尔值或者一个数组)。它提供了多种获取元素值的方法,如double getAsDouble()boolean getAsBoolean()JsonArray getAsJsonArray()

JsonElement是一个抽象类,作为以下 JSON 元素类的超类(在com.google.gson包中):

  • JsonArray:表示 JSON 的数组类型的具体类。一个数组是一组JsonElement的列表,每一个都可以是不同的类型。这是一个有序列表,意味着元素添加的顺序是保留的。
  • JsonNull:表示 JSON null值的具体类。
  • JsonObject:表示 JSON 对象类型的具体类。一个对象由名称-值对组成,其中名称是字符串,值是任何其他类型的JsonElement,这导致了一个JsonElement的树。
  • JsonPrimitive:表示 JSON 的数字、字符串或布尔类型之一的具体类。

除了JsonNull,这些子类都提供了各种获取元素值的方法。

创建一个JsonDeserializer对象后,需要用Gson注册它。通过调用下面的GsonBuilder方法来完成这个任务:

GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) 

传递给type的对象标识了解串器的类型,传递给typeAdapter的对象标识了解串器。因为registerTypeAdapter(Type, Object)返回一个GsonBuilder对象,所以只能在GsonBuilder上下文中使用这个方法。

为了演示定制的 JSON 对象解析,考虑前面的 JSON 对象的扩展版本:

{ "first-name": "John", "last-name": "Doe", "age": 45, "address": "Box 1 " }

这个 JSON 对象与之前的 JSON 对象有很大的不同,之前的 JSON 对象由nameage字段组成:

  • 字段name已经被重构为first-namelast-name字段。注意,连字符(-)不是 Java 标识符的合法字符。
  • 添加了一个address字段。

如果您通过用这个新对象替换分配给json的对象来修改清单 9-2 ,您应该不会对下面的输出感到惊讶:

null: 45

解析完全混乱了。但是,您可以通过引入以下自定义反序列化程序来解决此问题:

class PersonDeserializer implements JsonDeserializer<Person>
{
   @Override
   public Person deserialize(JsonElement json, Type typeOfT,  JsonDeserializationContext context)
   {
      JsonObject jsonObject = json.getAsJsonObject();
      String firstName = jsonObject.get("first-name").getAsString();
      String lastName = jsonObject.get("last-name").getAsString();
      int age = jsonObject.getAsJsonPrimitive("age").getAsInt();
      String address = 

jsonObject.get("address").getAsString();
      return new Person(firstName + " " + lastName, 45);
   }
}

当定制的反序列化器与之前的 JSON 对象一起使用时,deserialize()只被调用一次,并且类型为JsonObject的对象被传递给json。您可以将这个值转换成一个JsonObject,就像在JsonObject jsonObject = (JsonObject) json;中一样。或者,您可以调用JsonElementJsonObject getAsJsonObject()方法来获取JsonObject引用,这是deserialize()首先要完成的。

获得JsonObject引用后,deserialize()调用其JsonElement get( String memberName)方法返回一个JsonElement作为所需的memberName值。第一个调用从first-name传递到get();您希望获得这个 JSON 字段的值。因为返回一个JsonPrimitive来代替JsonElement,所以对JsonPrimitiveString getAsString()方法的调用被链接到JsonPrimitive引用,并获得first-name的值。按照这种模式获取last-nameaddress字段的值。

为了多样化,我决定在age领域做一些不同的事情。我调用JsonObjectJsonPrimitive getAsJsonPrimitive( String memberName)方法返回一个JsonPrimitive引用对应age。然后,我调用JsonPrimitiveint getAsInt()方法返回整数值。

获得所有字段值后,创建并返回一个Person对象。因为我重用了清单 9-2 中所示的Person类,并且因为这个类中没有address字段,所以我丢弃了address的值。您可能想要修改Person来包含这个字段。

下面的代码片段展示了如何实例化PersonDeserializer并用GsonBuilder实例注册它,该实例也用于获取Gson实例,以便调用fromJson(),通过 person 反序列化器解析之前的 JSON 对象:

GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Person.class, new PersonDeserializer());
Gson gson = gsonBuilder.create();

我已经将这些代码片段组合成一个工作应用程序。清单 9-3 展示了应用程序的源代码。

import java.lang.reflect.Type;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;

public class GsonDemo
{
   static class Person
   {
      String name;
      int age;

      Person(String name, int age)
      {
         this.name = name;
         this.age = age;
      }

      @Override
      public String toString()
      {
         return name + ": " + age;
      }
   }

   public static void main(String[] args)
   {
      class PersonDeserializer implements JsonDeserializer<Person>
      {
         @Override
         public Person deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
         {
            JsonObject jsonObject = json.getAsJsonObject();
            String firstName = jsonObject.get("first-name").getAsString();
            String lastName = jsonObject.get("last-name").getAsString();
            int age = jsonObject.getAsJsonPrimitive("age").getAsInt();
            String address = jsonObject.get("address").getAsString();
            return new Person(firstName + " " + lastName, 45);
         }
      }
      GsonBuilder gsonBuilder = new GsonBuilder();
      gsonBuilder.registerTypeAdapter(Person.class, new PersonDeserializer());
      Gson gson = gsonBuilder.create();
      String json = "{ first-name: \"John\", last-name: \"Doe\", " + "age: 45, address: \"Box 1\" }";
      Person person = gson.fromJson(json, Person.class);
      System.out.println(person);
   }
}

Listing 9-3.Parsing a JSON Object into a Java Object via a Custom Deserializer

编译清单 9-3 并运行生成的应用程序。您应该观察到以下输出:

John Doe: 45

通过序列化创建 JSON 对象

Gson让您通过调用GsontoJson()方法之一,从 Java 对象创建 JSON 对象。清单 9-4 提供了一个简单的演示。

import com.google.gson.Gson;

public class GsonDemo
{
   static class Person
   {
      String name;
      int age;

      Person(String name, int age)
      {
         this.name = name;
         this.age = age;
      }
   }

   public static void main(String[] args)
   {
      Person p = new Person("Jane Doe", 59);
      Gson gson = new Gson();
      String json = gson.toJson(p);
      System.out.println(json);
   }
}

Listing 9-4.Creating a JSON Object from a Java Object

清单 9-4 的main()方法首先从嵌套的Person类创建一个Person对象。然后它创建一个Gson对象,并调用该对象的StringtoJson(Objectsrc)方法将Person对象序列化为其等效的 JSON 字符串表示,由toJson(Object)返回。

编译清单 9-4 并运行生成的应用程序。您应该观察到以下输出:

{"name":"Jane Doe","age":59}

如果您更喜欢将 JSON 对象写到文件、字符串缓冲区或其他一些java.lang.Appendable中,您可以调用void toJson(Object src, Appendable writer)来完成这项任务。这个toJson()变体将其输出发送到指定的writer,如清单 9-5 所示。

import java.io.FileWriter;
import java.io.IOException;

import com.google.gson.Gson;

public class GsonDemo
{
   static class Student
   {
      String name;
      int id;
      int[] grades;

      Student(String name, int id, int... grades)
      {
         this.name = name;
         this.id = id;
         this.grades = grades;
      }

   }

   public static void main(String[] args) throws IOException
   {
      Student s = new Student("John Doe", 820787, 89, 78, 97, 65);
      Gson gson = new Gson();
      FileWriter fw = new FileWriter("student.json");
      gson.toJson(s, fw);
      fw.close();
   }
}

Listing 9-5.Creating a JSON Object from a Java Object and Writing the JSON Object to a File

清单 9-5 的main()方法首先从嵌套的Student类中创建一个Student对象。然后创建Gsonjava.io.FileWriter对象,并调用Gson对象的toJson( Object , Appendable)方法将Student对象序列化为其等效的 JSON 字符串表示,并将结果写入student.json。然后关闭文件写入器,以便将缓冲的内容写入文件(您可以改为指定fw.flush();)。

如果您运行这个应用程序,您将不会观察到任何输出。但是,您应该观察到一个包含以下内容的student.json文件:

{"name":"John Doe","id":820787,"grades":[89,78,97,65]}

Note

当出现 I/O 错误时,void toJson(Object src, Appendable writer)抛出未检查的com.google.gson.JsonIOException

定制的 JSON 对象创建

前面的gson.toJson(p)gson. toJson (s, fw)方法调用依靠Gson的默认序列化机制来创建 JSON 对象。您经常会遇到需要从 Java 对象创建 JSON 对象的情况,这些 Java 对象的类与要创建的 JSON 对象的结构不同。您可以使用定制的序列化程序来执行这种创建,它控制 Java 对象如何映射到 JSON 对象。

com.google.gson.JsonSerializer<T>接口描述了一个定制的串行化器。传递给T的参数标识了正在使用的序列化程序的类型。例如,当需要创建结构稍有不同的 JSON 对象时,可以将Person传递给T

JsonSerializer声明一个处理序列化(JSON 对象创建)的方法:

JsonElement serialize(T src, Type typeOfSrc,
                      JsonSerializationContext context)

serialize()Gson在序列化过程中调用的回调方法。使用以下参数调用此方法:

  • src标识需要序列化的 Java 对象。
  • typeOfSrc标识由src指定的要序列化的 Java 对象的实际类型。
  • context标识执行序列化的上下文。(我将在后面详细介绍上下文。)

创建一个JsonSerializer对象后,需要用Gson注册它。通过调用下面的GsonBuilder方法来完成这个任务:

GsonBuilder registerTypeAdapter(Type type, Object typeAdapter)

传递给type的对象标识串行器的类型,传递给typeAdapter的对象标识串行器。因为registerTypeAdapter(Type, Object)返回一个GsonBuilder对象,所以只能在GsonBuilder上下文中使用这个方法。

为了演示定制的 JSON 对象创建,考虑清单 9-6 中的Book类。

public class Book
{
   private String title;
   private String[] authors;
   private String isbn10;
   private String isbn13;

   public Book(String title, String[] authors, String isbn10, String isbn13)
   {
      this.title = title;
      this.authors = authors;
      this.isbn10 = isbn10;
      this.isbn13 = isbn13;
   }

   public String getTitle()
   {
      return title;
   }

   public String[] getAuthors()
   {
      return authors;
   }

   public String getIsbn10()
   {
      return isbn10;
   }

   public String getIsbn13()
   {
      return isbn13;
   }
}

Listing 9-6.Describing a Book as a Title, List of Authors, and ISBN Numbers

继续,假设Book对象将被序列化为具有以下格式的 JSON 对象:

{
   "title": title

   "lead-author": author0

   "other-authors": [ author1, author2, ... ]
   "isbn-10": isbn10

   "isbn-13": isbn13

}

您不能使用默认序列化,因为Book类没有声明lead-authorother-authorsisbn-10isbn-13字段。在任何情况下,缺省序列化都会创建与 Java 类的字段名相匹配的 JSON 属性名(对于 Java 标识符,连字符是非法的)。为了证明您无法使用默认序列化获得所需的 JSON 对象,假设您尝试执行以下代码片段:

Book book = new Book("PHP and MySQL Web Development, Second Edition", new String[] { "Luke Welling", "Laura Thomson" }, "067232525X", "075-2063325254");
Gson gson = new Gson();
System.out.println(gson.toJson(book));

此代码片段生成以下输出:

{"title":"PHP and MySQL Web Development, Second Edition","authors":["Luke Welling","Laura Thomson"],"isbn10":"067232525X","isbn13":"075-2063325254"}

输出与预期的 JSON 对象不匹配。但是,您可以通过引入以下自定义序列化程序来解决此问题:

class BookSerializer implements JsonSerializer<Book>
{
   @Override
   public JsonElement serialize(Book src, Type typeOfSrc,                                 JsonSerializationContext context)
   {
      JsonObject jsonObject = new JsonObject();
      jsonObject.addProperty("title", src.getTitle());
      jsonObject.addProperty("lead-author", src.getAuthors()[0]);
      JsonArray jsonOtherAuthors = new JsonArray();
      for (int i = 1; i < src.getAuthors().length; i++)
      {

         JsonPrimitive jsonAuthor =
            new JsonPrimitive(src.getAuthors()[i]);
         jsonOtherAuthors.add(jsonAuthor);
      }
      jsonObject.add("other-authors", jsonOtherAuthors);
      jsonObject.addProperty("isbn-10", src.getIsbn10());
      jsonObject.addProperty("isbn-13", src.getIsbn13());
      return jsonObject;
   }
}

当自定义序列化程序与之前的 Java Book对象一起使用时,serialize()只被调用一次,而Book对象被传递给src。因为这个方法需要一个 JSON 对象,serialize()首先创建一个JsonObject实例。

JsonObject声明了几个addProperty()方法,用于向一个JsonObject实例表示的 JSON 对象添加属性。serialize()调用void addProperty(Stringproperty,Stringvalue)方法添加titlelead-authorisbn-10isbn-13属性。

other-authors属性的处理方式不同。首先,serialize()创建一个JsonArray实例,并用除第一作者之外的所有作者填充它。然后,调用JsonObjectvoid add( String property, JsonElement value)方法将JsonArray对象添加到JsonObject中。

序列化完成后,serialize()返回创建并填充的JsonObject

下面的代码片段展示了如何实例化BookSerializer并用一个GsonBuilder实例注册它,该实例也用于获得一个Gson实例以便调用toJson(),通过图书序列化程序创建所需的 JSON 对象:

GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Book.class, new BookSerializer());

Gson gson = gsonBuilder.create();

我已经将这些代码片段组合成一个工作应用程序。清单 9-7 展示了应用程序的源代码。

import java.lang.reflect.Type;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class GsonDemo
{
   public static void main(String[] args)
   {
      class BookSerializer implements JsonSerializer<Book>
      {
         @Override
         public JsonElement serialize(Book src, Type typeOfSrc, JsonSerializationContext context)
         {
            JsonObject jsonObject = new JsonObject();
            jsonObject.addProperty("title", src.getTitle());
            jsonObject.addProperty("lead-author", src.getAuthors()[0]);
            JsonArray jsonOtherAuthors = new JsonArray();
            for (int i = 1; i < src.getAuthors().length; i++)
            {
               JsonPrimitive jsonAuthor =
                  new JsonPrimitive(src.getAuthors()[i]);
               jsonOtherAuthors.add(jsonAuthor);
            }
            jsonObject.add("other-authors", jsonOtherAuthors); 

            jsonObject.addProperty("isbn-10", src.getIsbn10());
            jsonObject.addProperty("isbn-13", src.getIsbn13());
            return jsonObject;
         }
      }
      GsonBuilder gsonBuilder = new GsonBuilder();
      gsonBuilder.registerTypeAdapter(Book.class, new BookSerializer());
      Gson gson = gsonBuilder.setPrettyPrinting().create();
      Book book = new Book("PHP and MySQL Web Development, Second Edition", new String[] { "Luke Welling", "Laura Thomson" }, "067232525X", "075-2063325254");
      System.out.println(gson.toJson(book));
   }
}

Listing 9-7.Creating a JSON Object from a Java Object via a Custom Serializer

编译清单 9-7 并运行生成的应用程序。您应该观察到下面的输出,它已经被漂亮地打印出来(通过对GsonBuilder对象的setPrettyPrinting()方法调用)以使输出更加清晰:

{
  "title": "PHP and MySQL Web Development, Second Edition",
  "lead-author": "Luke Welling",
  "other-authors": [
    "Laura Thomson"
  ],
  "isbn-10": "067232525X",
  "isbn-13": "075-2063325254"
}

了解更多关于 Gson 的信息

现在您已经对 Gson 库的基础有了相当好的理解,您可能想了解这个库提供的其他特性。在这一节中,我将向您介绍注释、上下文、Gson 对泛型的支持以及类型适配器。

Note

我对 Gson 其他特性的介绍并不详尽。查看“Gson 用户指南”( https://github.com/google/gson/blob/master/UserGuide.md )来了解我没有涉及的主题,比如实例创建者。

释文

Gson 提供了几种注释类型(在com.google.gson.annotations包中)来简化序列化和反序列化:

  • Expose:向 Gson 的序列化和/或反序列化机制暴露或隐藏带注释的字段。
  • JsonAdapter:标识与类或字段一起使用的类型适配器。(我将在以后关注类型适配器时讨论这种注释类型。)
  • SerializedName:表示应该将带注释的字段或方法序列化为 JSON,并将提供的名称值作为其名称。
  • Since:标识序列化字段或类型的起始版本号。如果创建的Gson对象的版本号小于@Since注释中的值,那么带注释的字段/类型将不会被序列化。
  • Until:标识序列化字段或类型的结束版本号。如果创建的Gson对象的版本号等于或大于@Until注释中的值,那么带注释的字段/类型将不会被序列化。

Note

根据 Gson 文档,SinceUntil对于在 web 服务上下文中管理 JSON 类的版本非常有用。

显示和隐藏字段

默认情况下,Gson不会序列化和反序列化标记为transient(或static)的字段。你可以调用GsonBuilderGsonBuilder excludeFieldsWithModifiers(int... modifiers)方法来改变这种行为。另外,Gson允许您通过使用Expose注释类型的实例来注释这些字段,从而有选择地确定要序列化和/或反序列化哪些非transient字段。

Expose提供了以下元素,用于确定字段是否可以序列化以及是否可以反序列化:

  • serialize:当true时,标有此@Expose注释的字段被序列化为 JSON 文本;否则,该字段不会被序列化。默认值为true
  • deserialize:当true时,标有此@Expose注释的字段从 JSON 文本反序列化;否则,该字段不会被反序列化。默认值为true

以下代码片段显示了如何使用Expose和这些元素,以便名为someField的字段将被序列化而不是反序列化:

@Expose(serialize = true, deserialize = false)
int someField;

默认情况下,Gson忽略Expose。您必须配置Gson,通过调用下面的GsonBuilder方法来公开/隐藏用@Expose注释的字段:

GsonBuilder excludeFieldsWithoutExposeAnnotation()

创建一个GsonBuilder对象,然后调用GsonBuilderexcludeFieldsWithoutExposeAnnotation()方法,然后调用该对象的Gson create()方法,以返回一个已配置的Gson对象:

GsonBuilder gsonb = new GsonBuilder();
gsonb.excludeFieldsWithoutExposeAnnotation();
Gson gson = gsonb.create();

清单 9-8 描述了一个演示Expose注释类型的应用程序。

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import com.google.gson.annotations.Expose;

public class GsonDemo
{
   static class SomeClass
   {
      transient int id;
      @Expose(serialize = true, deserialize = true)
      transient String password;
      @Expose(serialize = false, deserialize = false)
      int field1;
      @Expose(serialize = false, deserialize = true)
      int field2;
      @Expose(serialize = true, deserialize = false)
      int field3;
      @Expose(serialize = true, deserialize = true)
      int field4;
      @Expose(serialize = true, deserialize = true)
      static int field5;
      static int field6;
   }

   public static void main(String[] args)
   {
      SomeClass sc = new SomeClass();
      sc.id = 1;
      sc.password = "abc";
      sc.field1 = 2;
      sc.field2 = 3;
      sc.field3 = 4;
      sc.field4 = 5;
      sc.field5 = 6;
      sc.field6 = 7;
      GsonBuilder gsonb = new GsonBuilder();
      gsonb.excludeFieldsWithoutExposeAnnotation();
      Gson gson = gsonb.create();
      String json = gson.toJson(sc);
      System.out.println(json);
      SomeClass sc2 = gson.fromJson(json, SomeClass.class);
      System.out.printf("id = %d%n", sc2.id);
      System.out.printf("password = %s%n", sc2.password);
      System.out.printf("field1 = %d%n", sc2.field1);
      System.out.printf("field2 = %d%n", sc2.field2);
      System.out.printf("field3 = %d%n", sc2.field3);
      System.out.printf("field4 = %d%n", sc2.field4);
      System.out.printf("field5 = %d%n", sc2.field5);
      System.out.printf("field6 = %d%n", sc2.field6);
   }
}

Listing 9-8.Exposing and Hiding Fields to and from Serialization and Deserialization

清单 9-8 展示了带有transient实例字段的Expose以及非transient实例字段和static字段。

编译清单 9-8 并运行生成的应用程序。您应该观察到以下输出:

{"field3":4,"field4":5}
id = 0
password = null
field1 = 0
field2 = 0
field3 = 0

field4 = 5
field5 = 6
field6 = 7

第一个输出行显示只有field3field4被序列化。其他字段不序列化。

第二行和第三行显示transient idpassword字段接收默认值。transient字段未被序列化/反序列化。

第四、第五和第六行显示默认的0值被分配给field1field2field3。对于field1field3deserialize被分配给false,因此只能将默认值分配给这些字段。因为field2没有序列化,所以唯一可以赋给它的值是0

第七行显示5被分配给field4。这是有意义的,因为serializedeserialize元素被赋予了true

因为static字段没有被序列化或反序列化,所以它们保持初始值,如第八和第九行所示(对于field5field6)。

Note

即使Gson序列化了static字段,field6也不会被序列化,因为它没有用@Expose注释,也因为gsonb.excludeFieldsWithoutExposeAnnotation()方法调用,导致Gson绕过没有用@ Expose注释的字段。

更改字段名称

当您只想在序列化和反序列化期间更改字段和/或方法名时,您不必使用JsonSerializer<T>JsonDeserializer<T>;比如把isbn10改成isbn-10,把isbn13改成isbn-13。您可以使用SerializedName来代替,如下所示:

@SerializedName("isbn-10")
String isbn10;
@SerializedName("isbn-13")
String isbn13;

JSON 对象表示isbn-10isbn-13属性名,而 Java 类表示isbn10isbn13字段名称。

清单 9-9 描述了一个演示SerializedName注释类型的应用程序。

import com.google.gson.Gson;

import com.google.gson.annotations.SerializedName;

public class GsonDemo
{
   static class Book
   {
      String title;
      @SerializedName("isbn-10")
      String isbn10;
      @SerializedName("isbn-13")
      String isbn13;
   }

   public static void main(String[] args)
   {
      Book book = new Book();
      book.title = "PHP and MySQL Web Development, Second Edition";
      book.isbn10 = "067232525X";
      book.isbn13 = "075-2063325254";
      Gson gson = new Gson();
      String json = gson.toJson(book);
      System.out.println(json);
      Book book2 = gson.fromJson(json, Book.class);
      System.out.printf("title = %s%n", book2.title);
      System.out.printf("isbn10 = %s%n", book2.isbn10);
      System.out.printf("isbn13 = %s%n", book2.isbn13);
   }
}

Listing 9-9.Changing Names

编译清单 9-9 并运行生成的应用程序。您应该观察到以下输出:

{"title":"PHP and MySQL Web Development, Second Edition","isbn-10":"067232525X","isbn-13":"075-2063325254"}
title = PHP and MySQL Web Development, Second Edition
isbn10 = 067232525X
isbn13 = 075-2063325254

版本控制

SinceUntil对你的类版本化很有用。使用这些注释类型,您可以确定哪些字段和/或类型被序列化为 JSON 对象。

每个@Since@Until注释都接收一个双精度浮点值作为其参数。该值指定版本号,如下所示:

@Since(1.0) private String userID;
@Since(1.0) private String password;
@Until(1.1) private String emailAddress;

@Since(1.0)表示它注释的字段将被序列化为大于或等于1.0的所有版本。类似地,@Until(1.1)表示它注释的字段将被序列化为小于1.1的所有版本。

@Since@Until版本参数相比较的版本号由下面的GsonBuilder方法指定:

GsonBuilder setVersion(double ignoreVersionsAfter)

Expose一样,您首先创建一个GsonBuilder对象,然后在该对象上用期望的版本号调用该方法,最后在GsonBuilder对象上调用create()以返回一个新创建的Gson对象:

GsonBuilder gsonb = new GsonBuilder();
gsonb.setVersion(2.0);
Gson gson = gsonb.create();

清单 9-10 描述了一个演示SinceUntil注释类型的应用程序。

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import com.google.gson.annotations.Since;
import com.google.gson.annotations.Until;

public class GsonDemo
{
   @Since(1.0)
   @Until(2.5)
   static class SomeClass
   {
      @Since(1.1)
      @Until(1.5)
      int field;
   }

   public static void main(String[] args)
   {
      SomeClass sc = new SomeClass();
      sc.field = 1;
      GsonBuilder gsonb = new GsonBuilder();
      gsonb.setVersion(0.9);
      Gson gson = gsonb.create();
      System.out.printf("%s%n%n", gson.toJson(sc));
      gsonb.setVersion(1.0);
      gson = gsonb.create();
      System.out.printf("%s%n%n", gson.toJson(sc));
      gsonb.setVersion(1.1);
      gson = gsonb.create();
      System.out.printf("%s%n%n", gson.toJson(sc));
      gsonb.setVersion(1.5);
      gson = gsonb.create();
      System.out.printf("%s%n%n", gson.toJson(sc));
      gsonb.setVersion(2.5);
      gson = gsonb.create();
      System.out.printf("%s%n", gson.toJson(sc));
   }
}

Listing 9-10.Versioning a Class and Its Fields

清单 9-10 呈现了一个嵌套的SomeClass,只要传递给setVersion()的版本号范围从1.0到几乎2.5,它就会被序列化。这个类提供了一个名为field的字段,只要传递给setVersion()的版本号范围从1.1到几乎1.5,这个字段就会被序列化。

编译清单 9-10 并运行生成的应用程序。您应该观察到以下输出:

null

{}

{"field":1}

{}

Null

内容

JsonSerializerJsonDeserializer接口声明的serialize()deserialize()方法分别用com.google.gson.JsonSerializationContextcom.google.gson.JsonDeserializationContext对象调用,作为它们的最终参数。这些对象提供了对特定 Java 对象执行默认序列化和默认反序列化的serialize()deserialize()方法。在处理不需要特殊处理的嵌套 Java 对象时,您会发现它们非常方便。

假设您有下面的DateEmployee类:

class Date
{
   int year;
   int month;
   int day;

   Date(int year, int month, int day)
   {
      this.year = year;
      this.month = month;
      this.day = day;
   }
}

class Employee
{
   String name;
   Date hireDate;
}

现在,假设您决定创建一个定制的序列化程序,将emp-namehire-date属性(而不是namehireDate属性)添加到生成的 JSON 对象中。因为在序列化过程中没有改变Date字段的名称或顺序,所以可以利用传递给JsonSerializerserialize()方法的上下文来处理序列化的这一部分。

以下代码片段显示了序列化Employee对象及其嵌套的Date对象的序列化程序:

class EmployeeSerializer implements JsonSerializer<Employee>
{
   @Override
   public JsonElement serialize(Employee emp, Type typeOfSrc,                                 JsonSerializationContext context)
   {
      JsonObject jo = new JsonObject();
      jo.addProperty("emp-name", emp.name);
      jo.add("hire-date", context.serialize(emp.hireDate));
      return jo;
   }
}

serialize()首先创建一个JsonObject来描述序列化的 JSON 对象。然后,它将一个以雇员姓名为值的emp-name属性添加到这个JsonObject中。因为默认序列化可以序列化hireDate字段,serialize()调用context.serialize(emp.hireDate)生成属性值。这个值和hire-date属性名被添加到从方法返回的JsonObject中。

清单 9-11 展示了演示这个serialize()方法的应用程序的源代码。

import java.lang.reflect.Type;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class GsonDemo
{

   static class Date
   {
      int year;
      int month;
      int day;

      Date(int year, int month, int day)
      {
         this.year = year;
         this.month = month;
         this.day = day;
      }
   }

   static class Employee
   {
      String name;
      Date hireDate;
   }

   public static void main(String[] args)
   {
      Employee e = new Employee();
      e.name = "John Doe";
      e.hireDate = new Date(1982, 10, 12);
      GsonBuilder gb = new GsonBuilder();
      class EmployeeSerializer implements JsonSerializer<Employee>
      {
         @Override
         public JsonElement serialize(Employee emp, Type typeOfSrc, JsonSerializationContext context)
         {
            JsonObject jo = new JsonObject();
            jo.addProperty("emp-name", emp.name);
            jo.add("hire-date", context.serialize(emp.hireDate));
            return jo;
         }
      }

      gb.registerTypeAdapter(Employee.class, new EmployeeSerializer());
      Gson gson = gb.create();
      System.out.printf("%s%n%n", gson.toJson(e));
   }
}

Listing 9-11.Leveraging a Context to Serialize a Date

编译清单 9-11 并运行生成的应用程序。您应该观察到以下输出:

{"emp-name":"John Doe","hire-date":{"year":1982,"month":10,"day":12}}

泛型支持

当您调用StringtoJson(Objectsrc)void toJson(Object src, Appendable writer)时,Gson调用src.getClass()来获取srcjava.lang.Class对象,以便它可以反射性地了解要序列化的字段。同样,当您调用一个反序列化方法,如<T> T fromJson(Stringjson,Class<T> classOfT)Gson使用传递给classOfTClass对象来帮助它反射性地构建一个结果 Java 对象。对于从非泛型类型实例化的对象,这些操作可以正常工作。但是,当从泛型类型创建对象时,可能会出现问题,因为泛型类型信息会因类型擦除而丢失。考虑下面的代码片段:

List<String> weekdays = Arrays.asList("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
String json = gson.toJson(weekdays);
System.out.printf("%s%n%n", json);
System.out.printf("%s%n%n",
                  gson.fromJson(json, weekdays.getClass()));

变量weekdays是通用类型java.util.List<String>的对象。toJson()方法调用weekdays.getClass(),并发现List作为类型。但是,它仍然成功地将weekdays序列化为下面的 JSON 对象:

["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]

反序列化不成功。当调用gson.fromJson(json, weekdays.getClass())时,这个方法抛出一个java.lang.ClassCastException类的实例。在内部,它试图将java.util.ArrayList转换为java.util.Arrays$ArrayList,但这不起作用。

这个问题的解决方案是指定正确的List<String>参数化类型(泛型类型实例),而不是从weekdays.getClass()返回的原始List类型。为此,您使用了com.google.gson.reflect.TypeToken<T>类。

TypeToken<T>表示一个通用类型T,并在运行时启用类型信息的检索,这是Gson所需要的。使用如下表达式实例化TypeToken:

Type listType = new TypeToken<List<String>>() {}.getType();

这个习惯用法定义了一个匿名的本地内部类,其继承的getType()方法将完全参数化的类型作为一个java.lang.reflect.Type对象返回。在这段代码中,返回了以下类型:

java.util.List<java.lang.String>

将得到的Type对象传递给<T> T fromJson(Stringjson,TypetypeOfT)方法,如下:

gson.fromJson(json, listType)

这个方法调用解析 JSON 对象并将其作为List<String>返回。

您可能希望使用如下表达式输出结果:

System.out.printf("%s%n%n", gson.fromJson(json, listType));

但是,您会收到一个抛出的ClassCastException,声明您不能将ArrayList强制转换为java.lang.Object[],而是观察输出。问题的解决方案是向List引入一个强制转换,如下所示:

System.out.printf("%s%n%n", (List) gson.fromJson(json, listType));

进行此更改后,您将观察到以下输出:

[Sun, Mon, Tue, Wed, Thu, Fri, Sat]

清单 9-12 展示了一个应用程序的源代码,演示了这个问题以及其他面向泛型的序列化/反序列化问题,以及如何解决它们。

import java.lang.reflect.Type;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

import com.google.gson.Gson;

import com.google.gson.reflect.TypeToken;

import java.util.Arrays;
import java.util.List;

public class GsonDemo
{
   static
   class Vehicle<T>
   {
      T vehicle;

      T get()
      {
         return vehicle;
      }

      void set(T vehicle)
      {
         this.vehicle = vehicle;
      }

      @Override
      public String toString()
      {
         System.out.printf("Class of vehicle: %s%n", vehicle.getClass());
         return "Vehicle: " + vehicle.toString();
      }
   }

   static
   class Truck
   {
      String make;
      String model;

      Truck(String make, String model)
      {
         this.make = make;
         this.model = model;
      }

      @Override
      public String toString()
      {
         return "Make: " + make + " Model: " + model;
      }
   }

   public static void main(String[] args)
   {
      Gson gson = new Gson();

      // ...

      System.out.printf("PART 1%n");
      System.out.printf("------%n%n");

      List<String> weekdays = Arrays.asList("Sun", "Mon", "Tue", "Wed", "Thu

", "Fri", "Sat");
      String json = gson.toJson(weekdays);
      System.out.printf("%s%n%n", json);
      try
      {
         System.out.printf("%s%n%n", gson.fromJson(json, weekdays.getClass()));
      }
      catch (ClassCastException cce)
      {
         cce.printStackTrace();
         System.out.println();
      }
      Type listType = new TypeToken<List<String>>() {}.getType(); System.out.printf("Type = %s%n%n", listType);
      try
      {
         System.out.printf("%s%n%n", gson.fromJson(json, listType));
      }
      catch (ClassCastException cce)
      {
         cce.printStackTrace();
         System.out.println();
      }
      System.out.printf("%s%n%n", (List) gson.fromJson(json, listType));

      // ...

      System.out.printf("PART 2%n");
      System.out.printf("------%n%n");

      Truck truck = new Truck("Ford", "F150");
      Vehicle<Truck> vehicle = new Vehicle<>();
      vehicle.set(truck);

      json = gson.toJson(vehicle);
      System.out.printf("%s%n%n", json);
      System.out.printf("%s%n%n", gson.fromJson(json, vehicle.getClass()));

      // ...

      System.out.printf("PART 3%n");
      System.out.printf("------%n%n");

      Map<String, String> map = new HashMap<String, String>()
      {
         {
            put("key", "value");
         }
      };
      System.out.printf("Map = %s%n%n", map);
      System.out.printf("%s%n%n", gson.toJson(map));
      System.out.printf("%s%n%n", gson.fromJson(gson.toJson(map),
                                                map.getClass()));

      // ...

      System.out.printf("PART 4%n");
      System.out.printf("------%n%n");

      Type vehicleType = new TypeToken<Vehicle<Truck>>() {}.getType();
      json = gson.toJson(vehicle, vehicleType);
      System.out.printf("%s%n%n", json);
      System.out.printf("%s%n%n", (Vehicle) gson.fromJson(json, vehicleType));

      Type mapType = new TypeToken<Map<String,String>>() {}.getType();
      System.out.printf("%s%n%n", gson.toJson(map, mapType));
      System.out.printf("%s%n%n", (Map) gson.fromJson(gson.toJson(map, mapType), mapType));
   }
}

Listing 9-12.Serializing and Deserializing Objects Based on Generic Types

清单 9-12 的GsonDemo类被组织成嵌套的VehicleTruck static类,后跟main()入口点方法。这种方法分为四个部分,演示问题和解决方案。下面是输出,我将在讨论main()时引用它:

PART 1
------

["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]

java.lang.ClassCastException: Cannot cast java.util.ArrayList to java.util.Arrays$ArrayList
        at java.lang.Class.cast(Class.java:3369)
        at com.google.gson.Gson.fromJson(Gson.java:766)
        at GsonDemo.main(GsonDemo.java:75)

Type = java.util.List<java.lang.String>

java.lang.ClassCastException: java.util.ArrayList cannot be cast to [Ljava.lang.Object;
        at GsonDemo.main(GsonDemo.java:86)

[Sun, Mon, Tue, Wed, Thu, Fri, Sat]

PART 2
------

{"vehicle":{"make":"Ford","model":"F150"}}

Class of vehicle: class com.google.gson.internal.LinkedTreeMap
Vehicle: {make=Ford, model=F150}

PART 3
------

Map = {key=value}

null

null

PART 4
------

{"vehicle":{"make":"Ford","model":"F150"}}

Class of vehicle: class GsonDemo$Truck
Vehicle: Make: Ford Model: F150

{"key":"value"}

{key=value}

第一部分主要关注前面讨论的List<String>例子。输出显示通过toJson()成功序列化,接着是通过gson.fromJson(json, weekdays.getClass()不成功反序列化,接着是存储在第一个创建的TypeToken实例中的类型,接着是有强制转换问题的成功反序列化,接着是没有强制转换问题的成功反序列化。

第二部分关注名为vehicleVehicle<Truck>对象的序列化和反序列化。这个通用对象通过一个gson.toJson(vehicle)调用被成功序列化。虽然您经常可以成功地将通用对象传递给toJson(Object src),但是这个方法偶尔会失败,正如我将要展示的。对gson.fromJson(json, vehicle.getClass())的后续调用试图反序列化输出,但是有一个问题:您观察到的是Vehicle: {make=Ford, model=F150}而不是Vehicle: Make: Ford Model: F150。因为指定了Vehicle而不是完整的Vehicle<Truck>通用类型,所以Vehicle类中的vehicle字段被指定为com.google.gson.internal.LinkedTreeMap而不是Truck作为其类型。

第三部分试图基于一个匿名子类java.util.HashMap序列化和反序列化一个 map。第一个null值显示toJson()没有成功:toJson()的内部map.getClass()调用返回一个GsonDemo$2引用,它没有提供对要序列化的对象的洞察。第二个null值是通过fromJson(Stringjson,Class<T> classOfT)中的nulljson得到的。

第四部分展示了如何修复第二部分和第三部分中的问题。这个部分创建了TypeToken<Vehicle<Truck>>TypeToken<Map<String,String>>对象来存储Vehicle<Truck>Map<String, String>参数化类型。这些对象然后被传递给String toJson(Object src, Type typeOfSrc)<T> T fromJson(Stringjson,TypetypeOfT)方法的type参数。(虽然gson.toJson(vehicle, vehicleType)不是必需的,因为序列化与gson.toJson(vehicle)一起工作,但是为了安全起见,您应该养成基于TypeToken实例传递一个Type对象作为第二个参数的习惯。)

Note

当指定对象(src和从classOfT派生的对象)的任何字段基于通用类型时,toJson(Object src)<T> T fromJson(String json,<T> classOfT)和类似的方法都能正常工作。唯一的规定是指定的对象不能是类属的。

类型适配器

在本章的前面,我展示了如何使用JsonSerializerJsonDeserializer(分别)将 Java 对象序列化为 JSON 字符串,反之亦然。这些接口简化了 Java 对象和 JSON 字符串之间的转换,但是增加了一个处理中间层。

中间层由将 Java 对象和 JSON 字符串转换为JsonElement的代码组成。这种转换降低了解析或创建无效 JSON 字符串的风险,但它确实需要时间来执行,这会影响性能。通过使用com.google.gson.TypeAdapter<T>类,您可以避开中间层并创建更高效的代码,其中T标识 Java 类序列化源和反序列化目标。

Note

你应该更喜欢效率更高的TypeAdapter而不是效率更低的JsonSerializerJsonDeserializer。事实上,Gson使用内部的TypeAdapter实现来处理 Java 对象和 JSON 字符串之间的转换。

TypeAdapter是一个抽象类,它声明了几个具体的方法以及下面的一对抽象方法:

  • T read( JsonReader in):读取一个 JSON 值(数组、对象、字符串、数字、布尔或null),转换成 Java 对象,返回。返回值可能是null
  • void write( JsonWriter out, T value):写一个 JSON 值(数组、对象、字符串、数字、布尔或null),传递给value

当一个 I/O 问题发生时,每个方法抛出java.io.IOException

read()write()方法分别读取 JSON 标记序列和写入 JSON 标记序列。对于read(),这些令牌的来源是具体的com.google.gson.stream.JsonReader类的一个实例。对于write(),这些令牌的目的地是具体的com.google.gson.stream.JsonWriter类。令牌由com.google.gson.stream.JsonToken枚举描述(例如BEGIN_ARRAY表示左方括号)。通过调用JsonReaderJsonWriter方法来读写它们,如下所示:

  • void beginObject():这个JsonReader方法使用 JSON 流中的下一个令牌,并断言它是一个新对象的开始。一个伴随的void endObject()方法消耗来自 JSON 流的下一个令牌,并断言它是当前对象的结尾。当出现 I/O 问题时,这两种方法都会抛出IOException
  • JsonWriter name(String name):这个JsonWriter方法对属性名进行编码,不能是发生 I/O 问题时抛出的null. IOException

创建了一个TypeAdapter子类后,通过调用我之前介绍的GsonBuilder registerTypeAdapter(Type type, Object typeAdapter)方法,实例化它并向Gson注册实例。传递给type的对象表示其对象被序列化或反序列化的类。传递给typeAdapter的对象是类型适配器实例。

清单 9-13 展示了演示类型适配器的应用程序的源代码。

import java.io.IOException;

import java.util.ArrayList;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

public class GsonDemo
{

   static
   class Country
   {
      String name;
      int population;
      String[] cities;

      Country() {}

      Country(String name, int population, String... cities)
      {
         this.name = name;
         this.population = population;
         this.cities = cities;
      }
   }

   public static void main(String[] args)
   {
      class CountryAdapter extends TypeAdapter<Country>
      {
         @Override
         public Country read(JsonReader in) throws IOException
         {
            Country c = new Country();
            List<String> cities = new ArrayList<>();
            in.beginObject();
            while (in.hasNext())
               switch (in.nextName())
               {
                  case "name":
                     c.name = in.nextString();
                              break;

                  case "population":
                     c.population = in.nextInt();
                     break;

                  case "cities":
                     in.beginArray();
                     while (in.hasNext())
                        cities.add(in.nextString());
                     in.endArray();
                     c.cities = cities.toArray(new String[0]); 

               }
            in.endObject();
            return c;
         }

         @Override
         public void write(JsonWriter out, Country c) throws IOException
         {
            out.beginObject();
            out.name("name").value(c.name);
            out.name("population").value(c.population);
            out.name("cities");
            out.beginArray();
            for (int i = 0; i < c.cities.length; i++)
               out.value(c.cities[i]);
            out.endArray();
            out.endObject();
         }
      }
      Gson gson = new GsonBuilder().
                     registerTypeAdapter(Country.class,                                         new CountryAdapter()).
                     create();

      Country c = new Country("England", 53012456 /* 2011 census */, "London", "Birmingham", "Cambridge");
      String json = gson.toJson(c);
      System.out.println(json);
      c = gson.fromJson(json, c.getClass());
      System.out.printf("Name = %s%n", c.name);
      System.out.printf("Population = %d%n", c.population);
      System.out.print("Cities = ");
      for (String city: c.cities)
         System.out.print(city + " ");
      System.out.println();
   }
}

Listing 9-13.Serializing and Deserializing a Country Object via a Type Adapter

清单 9-13 的GsonDemo类嵌套了一个Country类(它将一个国家描述为一个名称、一个人口数和一组城市名),并且还提供了一个main()入口点方法。

main()方法首先声明一个本地CountryAdapter类,该类扩展了TypeAdapter<Country>. CountryAdapter并覆盖了read()write()方法来处理序列化和反序列化任务。

read()方法首先创建一个新的Country对象,它将存储从被反序列化的 JSON 对象中读取的值(并从JsonReader参数中访问)。

在创建一个列表来存储将要读取的城市名数组之后,read()调用beginObject()来断言从令牌流中读取的下一个令牌是 JSON 对象的开始。

此时,read()进入一个while循环。当JsonReaderboolean hasNext()方法返回true时,这个循环继续:有另一个对象元素。

每个while循环迭代执行一个switch语句,调用JsonReaderString nextName()方法返回下一个令牌,这是 JSON 对象中的一个属性名。然后,它将令牌与三种可能性(namepopulationcities)进行比较,并执行相关代码来检索属性值,并将该值分配给之前创建的Country对象中的适当字段。

如果属性是name,调用JsonReaderString nextString()方法返回下一个令牌的字符串值。如果属性是 population,JsonReaderint nextInt()方法被调用来返回令牌的int值。

处理cities属性更加复杂,因为它的值是一个数组:

JsonReader’s void beginArray() method is called to signify that a new array has been detected and to consume the open square bracket token.   A while loop is entered to repeatedly obtain the next array string value and add it to the previously created cities list.   JsonReader’s void endArray() method is called to signify the end of the current array and to consume the close square bracket token.   The cities list is converted to a Java array, which is assigned to the Country object’s cities member.

外层while循环结束后,read()调用endObject()断言从令牌流中读取的下一个令牌是当前 JSON 对象的结尾,然后返回Country对象。

write()方法有点类似于read()。它调用JsonWriterJsonWriter name(String name)方法将name指定的属性名编码成一个 JSON 属性名。同样,它调用JsonWriterJsonWriter value(long value)JsonWriter value(String value)方法将value编码为一个 JSON 数字或一个 JSON 字符串。

main()方法继续从一个GsonBuilder对象创建一个Gson对象,该对象执行registerTypeAdapter(Country.class, new CountryAdapter())来实例化CountryAdapter并用将要返回的Gson对象进行注册。Country.class表示Country对象将被序列化和反序列化。

最后,创建一个Country对象,将其序列化为一个字符串,然后反序列化为一个新的Country对象。

编译清单 9-13 并运行生成的应用程序。您应该观察到以下输出:

{"name":"England","population":53012456,"cities":["London","Birmingham","Cambridge"]}
Name = England
Population = 53012456
Cities = London Birmingham Cambridge

方便地将类型适配器与类和字段相关联

JsonAdapter注释类型与TypeAdapter Class对象参数一起使用,将TypeAdapter实例与类或字段相关联。这样做之后,您就不需要向Gson注册TypeAdapter,这样会减少一点编码。

清单 9-14 重构清单 9-13 来演示JsonAdapter

import java.io.IOException;

import java.util.ArrayList;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;

import com.google.gson.annotations.JsonAdapter;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

public class GsonDemo
{
   @JsonAdapter(CountryAdapter.class)

   static
   class Country
   {
      String name;
      int population;
      String[] cities;

      Country() {}

      Country(String name, int population, String... cities)
      {
         this.name = name;
         this.population = population;
         this.cities = cities;
      }
   }

   static
   class CountryAdapter extends TypeAdapter<Country>
   {
      @Override
      public Country read(JsonReader in) throws IOException
      {
         System.out.println("read() called");
         Country c = new Country();
         List<String> cities = new ArrayList<>();
         in.beginObject();
         while (in.hasNext())
            switch (in.nextName())
            {
               case "name":
                  c.name = in.nextString();
                           break;

               case "population":
                  c.population = in.nextInt();
                  break;

               case "cities":
                  in.beginArray();
                  while (in.hasNext())
                     cities.add(in.nextString());
                  in.endArray();
                  c.cities = cities.toArray(new String[0]);
            }
         in.endObject();
         return c;
      }

      @Override
      public void write(JsonWriter out, Country c) throws IOException
      {

         System.out.println("write() called");
         out.beginObject();
         out.name("name").value(c.name);
         out.name("population").value(c.population);
         out.name("cities");
         out.beginArray();
         for (int i = 0; i < c.cities.length; i++)
            out.value(c.cities[i]);
         out.endArray();
         out.endObject();
      }
   }

   public static void main(String[] args)
   {
      Gson gson = new Gson();

      Country c = new Country("England", 53012456 /* 2011 census */, "London", "Birmingham", "Cambridge");
      String json = gson.toJson(c);
      System.out.println(json);
      c = gson.fromJson(json, c.getClass());
      System.out.printf("Name = %s%n", c.name);
      System.out.printf("Population = %d%n", c.population);
      System.out.print("Cities = ");
      for (String city: c.cities)
         System.out.print(city + " ");
      System.out.println();
   }
}

Listing 9-14.Serializing and Deserializing a Country Object Annotated with a Type Adapter

在清单 9-14 中,我加粗了与清单 9-13 的两个本质区别:Country类型适配器类被标注为@JsonAdapter(CountryAdapter.class),并且指定了Gson gson = new Gson();而不是使用GsonBuilder对象及其create()方法。

编译清单 9-14 并运行生成的应用程序。您应该观察到以下输出:

write() called
{"name":"England","population":53012456,"cities":["London","Birmingham","Cambridge"]}
read() ca

lled
Name = England
Population = 53012456
Cities = London Birmingham Cambridge

read() calledwrite called()输出行证明Gson使用自定义类型适配器而不是其内部类型适配器。

Exercises

以下练习旨在测试你对第九章内容的理解。

Define Gson.   Identify and describe Gson’s packages.   What are the two ways to obtain a Gson object?   Identify the types for which Gson provides default serialization and deserialization.   How would you enable pretty-printing?   True or false: By default, Gson excludes transient or static fields from consideration for serialization and deserialization.   Once you have a Gson object, what methods can you call to convert between JSON and Java objects?   How do you use Gson to customize JSON object parsing?   Describe the JsonElement class.   Identify the JsonElement subclasses.   What GsonBuilder method do you call to register a serializer or deserializer with a Gson object?   What method does JsonSerializer provide to serialize a Java object to a JSON object?   What annotation types does Gson provide to simplify serialization and deserialization?   True or false: To use Expose, it’s enough to annotate a field, as in @Expose(serialize = true, deserialize = false).   What do JsonSerializationContext and JsonDeserializationContext provide?   True or false: You can call <T> T fromJson( String json, Class <T> classOfT) to deserialize any kind of object.   Why should you prefer TypeAdapter to JsonSerializer and JsonDeserializer?   Modify Listing 9-8 so that the static field named field5 is also serialized and deserialized.

摘要

Gson 是一个小型的基于 Java 的库,用于解析和创建 JSON 对象。谷歌为自己的项目开发了 Gson,但后来从 1.0 版本开始,Gson 公开可用。

Gson 通过将 JSON 对象反序列化为 Java 对象来解析 JSON 对象。类似地,它通过将 Java 对象序列化为 JSON 对象来创建 JSON 对象。Gson 依靠 Java 的反射 API 来协助完成这些任务。

Gson 由 30 多个类和接口组成,分布在四个包中:com.google.gson(提供对主类Gson的访问);com.google.gson.annotations(提供用于 Gson 的注释类型);com.google.gson.reflect(提供用于从泛型类型获取类型信息的实用程序类);com.google.gson.stream(提供用于读写 JSON 编码值的实用程序类)。

Gson类处理 JSON 和 Java 对象之间的转换。您可以通过使用Gson()构造函数实例化这个类,或者通过使用GsonBuilder类获得一个Gson实例。

一旦有了一个Gson对象,就可以调用各种fromJson()toJson()方法在 JSON 和 Java 对象之间进行转换。因为这些方法分别依赖于 Gson 的默认反序列化和序列化机制,所以您可以通过使用JsonDeserializer<T>JsonSerializer<T>接口来自定义反序列化和序列化。

Gson 提供了额外的有用特性,包括用于简化序列化和反序列化的注释、用于自动化嵌套对象和数组序列化的上下文、对泛型的支持以及类型适配器。

第十章介绍了用于提取 JSON 值的 JsonPath。

十、用 JsonPath 提取 JSON 值

XPath 用于从 XML 文档中提取值。JsonPath 为 JSON 文档执行这项任务。本章向您介绍 JsonPath。

Note

如果您不熟悉 XPath,我建议您在阅读本章之前先阅读第五章。JsonPath 派生自 XPath。

JsonPath 是什么?

JsonPath 是一种声明式查询语言(也称为路径表达式语法),用于选择和提取 JSON 文档的属性值。例如,可以使用 JsonPath 在{"firstName": "John"}中定位"John"并返回这个值。JsonPath 基于 XPath 1.0。

JsonPath 是由 Stefan Goessner ( http://goessner.net )创建的。Goessner 还创建了 JsonPath 的基于 JavaScript 和基于 PHP 的实现。要获得完整的文档,请查看高斯纳的网站( http://goessner.net/articles/JsonPath/index.html )。

瑞典软件公司 Jayway ( www.jayway.com )随后将 JsonPath 改编为 Java。他们的 JSON path Java 版本是本章的重点。你会在 https://github.com/jayway/JsonPath 找到 Jayway 实现 JsonPath 的完整文档。

学习 JsonPath 语言

JsonPath 是一种简单的语言,具有与 XPath 相似的各种特性。这种语言用于构造路径表达式。

JsonPath 表达式以美元符号($)字符开始,它表示查询的根元素。美元符号后面是一系列子元素,通过点(.)符号或方括号([])符号分隔。例如,考虑以下 JSON 对象:

{
   "firstName": "John",
   "lastName": "Smith",
   "age": 25,
   "address":
   {
      "streetAddress": "21 2nd Street",
      "city": "New York",
      "state": "NY",
      "postalCode": "10021-3100"
   },
   "phoneNumbers":
   [
      {
         "type": "home",
         "number": "212 555-1234"
      },
      {
         "type": "office",
         "number": "646 555-4567"
      }
   ]
}

以下基于点符号的 JsonPath 表达式从前面的匿名 JSON 对象中提取电话号码(212 555-1234),该电话号码分配给匿名 JSON 对象中的number字段,该字段分配给phoneNumbers数组中的第一个元素:

$.phoneNumbers[0].number

$字符代表匿名的根 JSON 对象。最左边的点字符将对象根与phoneNumbers属性名分开,因为分配给phoneNumbers的值是一个数组。[0]语法标识分配给phoneNumbers的数组中的第一个元素。

第一个数组元素存储由"type": "home""number": "212 555-1234"属性组成的匿名对象。最右边的点字符访问这个对象的number子属性名,它被赋值为212 555-1234。该值从表达式中返回。

或者,我可以指定以下方括号符号来提取相同的电话号码:

$['phoneNumbers'][0]['number']

Jayway 文档将$标识为一个操作符,还标识了其他几个基本操作符。表 10-1 描述了这些操作符。

表 10-1。

JsonPath Basic Operators

| 操作员 | 描述 | | --- | --- | | `| 操作员 | 描述 | | --- | --- | | 要查询的根元素。该运算符启动所有路径表达式。相当于 XPath 的`/`符号。 | | `@` | 过滤器谓词正在处理的当前节点。相当于 XPath 的`.`符号。 | | `*` | 通配符。适用于任何需要名称或数值的地方。 | | `..` | 深度扫描(也称为递归下降)。适用于任何需要名称的地方。相当于 XPath 的`//`符号。 | | `.`姓名 | 带点号的孩子。点相当于 XPath 的`/`符号。 | | `['`姓名`' (, '`姓名`')]` | 带括号的一个或多个孩子。 | | `[`号`(,`号`)]` | 一个或多个数组索引。 | | `[`开始`:`结束`]` | 数组切片运算符。 | | `[?(`表情`)]` | 过滤运算符。表达式的计算结果必须是布尔值。换句话说,它是一个谓语。 |

Jayway 文档还确定了几个可以在路径末端调用的函数——函数的输入是路径表达式的输出;函数输出由函数本身决定。表 10-2 描述了这些功能。

表 10-2。

JsonPath Functions

| 功能 | 描述 | | --- | --- | | `min()` | 返回一个数字数组中的最小值(作为一个`double`)。 | | `max()` | 返回数字数组中的最大值(作为`double`)。 | | `avg()` | 返回一个数字数组的平均值(作为一个`double`)。 | | `stddev()` | 返回数字数组的标准偏差值(作为`double`)。 | | `length()` | 返回数组的长度(作为一个`int`)。 |

最后,Jayway 文档确定了过滤器的各种操作符,这些操作符使用谓词(布尔表达式)来限制返回的项目列表。谓词可以使用表 10-3 中的过滤操作符来确定等式、匹配正则表达式以及测试包含性。

表 10-3。

JsonPath Filter Operators

| 操作员 | 描述 | | --- | --- | | `==` | 当左操作数等于右操作数时,返回`true`。注意`1`不等于`'1'`(也就是数字`1`和字符串`1`是两回事)。 | | `!=` | 当左操作数不等于右操作数时,返回`true`。 | | `<` | 当左操作数小于右操作数时,返回`true`。 | | `<=` | 当左操作数小于或等于右操作数时,返回`true`。 | | `>` | 当左操作数大于右操作数时,返回`true`。 | | `>=` | 当左操作数大于或等于右操作数时,返回`true`。 | | `=∼` | 当左操作数匹配右操作数指定的正则表达式时,返回`true`;比如`[?(@.name =∼ /foo.*?/i)]`。 | | `In` | 当左操作数存在于右操作数中时,返回`true`;比如`[?(@.grade in ['A', 'B'])]`。 | | `Nin` | 当左操作数不在右操作数中时,返回`true`。 |

这个表显示了作为简单谓词的@.name =∼ /foo.*?/i@.grade in ['A', 'B']。您可以通过使用逻辑 AND 运算符(&&)和逻辑 or 运算符(||)来创建更复杂的谓词。此外,在谓词中,必须用单引号或双引号将任何字符串括起来。

获取和使用 JsonPath 库

与第章 8 的 mJson 和第章 9 的 Gson 一样,可以从中央 Maven 资源库( http://search.maven.org/ )获取 JsonPath。

Note

如果你对 Maven 不熟悉,可以把它当成 Java 项目的构建工具,尽管 Maven 开发者认为 Maven 不仅仅是一个构建工具——参见 http://maven.apache.org/background/philosophy-of-maven.html

如果您熟悉 Maven,将下面的 XML 片段添加到依赖于 JsonPath 的 Maven 项目的项目对象模型(POM)文件中,您就可以开始了!(要了解 POM,请查看 https://maven.apache.org/pom.html#What_is_the_POM )。)

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.2.0</version>
</dependency>

这个 XML 片段显示了我在本章中使用的 Jayway JsonPath 的版本 2.2.0。

Note

Maven 项目依赖于其他项目是很常见的。比如我在第八章讨论的 mJson 项目,就依赖于 TestNG ( https://en.wikipedia.org/wiki/TestNG )。我在那一章中没有提到或讨论下载 TestNG,因为这个库不是正常使用所必需的。还有,我在第九章中讨论的 Gson 项目依赖于 JUnit ( https://en.wikipedia.org/wiki/JUnit )。我在那一章中没有提到或讨论下载 JUnit,因为这个库不是正常使用所必需的。

因为我目前没有使用 Maven,所以我下载了 JsonPath Jar 文件和 JsonPath 依赖的所有 Jar 文件,然后将所有这些 Jar 文件添加到我的类路径中。我完成下载任务最简单的方法是将浏览器指向 https://github.com/jayway/JsonPath/releases and download json-path-2.2.0-SNAPSHOT-with-dependencies.zip

解压 Zip 文件后,我发现了json-path-2.2.0-SNAPSHOT-with-dependencies主目录的以下子目录:

  • api:包含 JsonPath 的基于 Javadoc 的 API 文档。
  • lib:包含要添加到类路径中的 Jar 文件,以便使用 JSON path——并非在所有情况下都需要所有的 Jar 文件,但是最好将它们都包含在类路径中。
  • lib-optional:用于配置 JsonPath 的可选 Jar 文件。
  • source:包含 JsonPath API 的 Java 源代码的 Jar 文件。

Note

Jayway JsonPath 是根据 Apache 许可证版本 2.0 ( www.apache.org/licenses/ )进行许可的。

对于编译访问 JsonPath 的 Java 源代码,我发现只有json-path-2.2.0-SNAPSHOT.jar需要包含在类路径中:

javac -cp json-path-2.2.0-SNAPSHOT.jar source file

为了运行访问 JsonPath 的应用程序,我使用下面的命令行:

java -cp accessors-smart-1.1.jar;asm-5.0.3.jar;json-path-2.2.0-SNAPSHOT.jar;json-smart-2.2.1.jar;slf4j-api-1.7.16.jar;tapestry-json-5.4.0.jar;. main classfile

Tip

为了便于使用这些命令行,请将它们放在 Windows 平台上的一对批处理文件中,或者放在其他平台上的对应文件中。

探索 JsonPath 库

JsonPath 库被组织成几个包。您通常会与com.jayway.jsonpath包及其类型进行交互。在这一节中,我将专门关注这个包,同时向您展示如何从 JSON 对象中提取值并使用谓词来过滤项目。

从 JSON 对象中提取值

com.jayway. jsonpath包提供了JsonPath类作为使用 JsonPath 库的入口点。清单 10-1 介绍了这个类。

import java.util.HashMap;
import java.util.List;

import com.jayway.jsonpath.JsonPath;

public class JsonPathDemo
{
   public static void main(String[] args)
   {
      String json =
      "{" +
      "   \"store\":" +
      "   {" +
      "      \"book\":" +
      "      [" +
      "         {" +
      "            \"category\": \"reference\"," +
      "            \"author\": \"Nigel Rees\"," +
      "            \"title\": \"Sayings of the Century\"," +
      "            \"price\": 8.95" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"Evelyn Waugh\"," +
      "            \"title\": \"Sword of Honour\"," +
      "            \"price\": 12.99" +
      "         }" +
      "      ]," +
      "      \"bicycle\":" +
      "      {" +
      "         \"color\": \"red\"," +
      "         \"price\": 19.95" +
      "      }" +
      "   }" +
      "}";

      JsonPath path = JsonPath.compile("$.store.book[1]");
      HashMap books = path.read(json);
      System.out.println(books);
      List<Object> authors = JsonPath.read(json, "$.store.book[*].author");
      System.out.println(authors);
      String author = JsonPath.read(json, "$.store.book[1].author");
      System.out.println(author);
   }
}

Listing 10-1.A First Taste of JsonPath

清单 10-1 提供了一个JsonPathDemo类,其main()方法使用JsonPath类从 JSON 对象中提取值。main()首先声明一个基于字符串的 JSON 对象,并将其引用赋给变量json。然后,它调用下面的static JsonPath方法来编译 JsonPath 表达式(以提高性能),并将编译后的结果作为JsonPath对象返回:

JsonPath compile(String jsonPath, Predicate... filters)

Predicate varargs 列表允许您指定一个过滤器谓词数组,以响应jsonPath字符串中的过滤器谓词占位符(标识为?字符)。我将在本章后面演示Predicate和相关类型。

编译完$.store.book[1] JsonPath 表达式后,main()将该表达式传递给下面的JsonPath方法,该表达式标识了分配给store属性的匿名对象的book属性数组的第二个元素中的匿名对象:

<T> T read(String json) 

这个通用方法在先前编译的JsonPath实例上被调用。它接收基于字符串的 JSON 对象(分配给json)作为其参数,并将编译后的JsonPath实例中的 JsonPath 表达式应用于该参数。结果是由$.store.book[1]标识的 JSON 对象。

read()方法是通用的,因为它可以返回几种类型中的一种。在这个例子中,它返回了一个用于存储 JSON 对象属性名及其值的java.util.LinkedHashMap类的实例(java.util.Hashmap的子类)。

当您打算重用 JsonPath 表达式时,最好编译它们,这样可以提高性能。因为我不重用$.store.book[1],我可以使用JsonPathstatic read()方法之一来代替。例如,main()接下来演示了下面的read()方法:

<T> T read(String json, String jsonPath, Predicate... filters)

该方法为jsonPath参数创建一个新的JsonPath对象,并将其应用于json字符串。我忽略例子中的filters

传递给jsonPath的 JsonPath 表达式是$.store.book[*].author。这个表达式包含了用于匹配book数组中所有元素的*通配符。它返回这个数组中每个元素的author属性的值。

read()将该值作为net.minidev.json.JSONArray类的实例返回,它存储在必须包含在类路径中的json-smart-2.2.1.jar文件中。因为JSONArray扩展了java.util.ArrayList<Object>,所以将返回的对象强制转换为List<Object>是合法的。

为了进一步演示read()main()最后用 JsonPath 表达式$.store.book[1].author调用这个方法,它返回存储在book数组的第二个元素中的匿名对象的author属性的值。这次,read()返回一个java.lang.String对象。

Note

对于一般的read()方法,JsonPath自动尝试将结果转换为方法调用程序所期望的类型,比如 JSON 对象的 hashmap、JSON 数组的对象列表和 JSON 字符串的 string。

编译清单 10-1 如下:

javac -cp json-path-2.2.0-SNAPSHOT.jar JsonPathDemo.java

运行生成的应用程序,如下所示:

java -cp accessors-smart-1.1.jar;asm-5.0.3.jar;json-path-2.2.0-SNAPSHOT.jar;json-smart-2.2.1.jar;slf4j-api-1.7.16.jar;tapestry-json-5.4.0.jar;. JsonPathDemo

您应该观察到以下输出:

{category=fiction, author=Evelyn Waugh, title=Sword of Honour, price=12.99}
["Nigel Rees","Evelyn Waugh"]
Evelyn Waugh

您可能还会看到一些关于 SLF4J(Java 的简单日志外观)无法加载StaticLoggerBinder类并默认为无操作日志实现的消息。您可以安全地忽略这些消息。

使用谓词过滤项目

JsonPath 支持过滤器,用于将从 JSON 文档中提取的节点限制为与谓词(布尔表达式)指定的标准相匹配的节点。您可以使用内联谓词、过滤谓词或自定义谓词。

行内谓词

内联谓词是基于字符串的谓词。清单 10-2 向应用程序展示了源代码,展示了几个内联谓词。

import java.util.List;

import com.jayway.jsonpath.JsonPath;

public class JsonPathDemo
{
   public static void main(String[] args)
   {
      String json =
      "{" +
      "   \"store\":" +
      "   {" +
      "      \"book\":" +
      "      [" +
      "         {" +
      "            \"category\": \"reference\"," +
      "            \"author\": \"Nigel Rees\"," +
      "            \"title\": \"Sayings of the Century\"," +
      "            \"price\": 8.95" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"Evelyn Waugh\"," +
      "            \"title\": \"Sword of Honour\"," +
      "            \"price\": 12.99" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"J. R. R. Tolkien\"," +
      "            \"title\": \"The Lord of the Rings\"," +
      "            \"isbn\": \"0-395-19395-8\"," +
      "            \"price\": 22.99" +
      "         }" +
      "      ]," +
      "      \"bicycle\":" +
      "      {" +
      "         \"color\": \"red\"," +
      "         \"price\": 19.95" +
      "      }" +
      "   }" +

      "}";

      String expr = "$.store.book[?(@.isbn)].title";
      List<Object> titles = JsonPath.read(json, expr);
      System.out.println(titles);
      expr = "$.store.book[?(@.category == 'fiction')].title";
      titles = JsonPath.read(json, expr);
      System.out.println(titles);
      expr = "$..book[?(@.author =∼ /.*REES/i)].title";
      titles = JsonPath.read(json, expr);
      System.out.println(titles);
      expr = "$..book[?(@.price > 10 && @.price < 20)].title";
      titles = JsonPath.read(json, expr);
      System.out.println(titles);
      expr = "$..book[?(@.author in ['Nigel Rees'])].title";
      titles = JsonPath.read(json, expr);
      System.out.println(titles);
      expr = "$..book[?(@.author nin ['Nigel Rees'])].title";
      titles = JsonPath.read(json, expr);
      System.out.println(titles);
   }
}

Listing 10-2.Demonstrating Inline Predicates

清单 10-2 的main()方法使用以下 JsonPath 表达式来缩小返回的书名字符串列表:

  • $.store.book[?(@.isbn)].title返回包含isbn属性的所有book元素的title值。
  • $.store.book[?(@.category == 'fiction')].title返回所有book元素的title值,这些元素的category属性被赋予字符串值fiction.
  • $..book[?(@.author =∼ /.*REES/i)].title返回其author属性值以rees结尾的所有book元素的title值(大小写无关紧要)。
  • $..book[?(@.price >= 10 && @.price <= 20)].title返回所有book元素的title值,这些元素的price属性值介于1020.之间
  • $..book[?(@.author in ['Nigel Rees'])].title返回所有book元素的title值,这些元素的author属性值与Nigel Rees.匹配
  • $..book[?(@.author nin ['Nigel Rees'])].title返回所有book元素的title值,这些元素的author属性值与Nigel Rees.不匹配

编译清单 10-2 并运行生成的应用程序。您应该会发现以下输出:

["The Lord of the Rings"]
["Sword of Honour","The Lord of the Rings"]
["Sayings of the Century"]
["Sword of Honour"]
["Sayings of the Century"]
["Sword of Honour","The Lord of the Rings"]

过滤谓词

过滤器谓词是一个被表达为抽象Filter类的实例的谓词,它实现了Predicate接口。

为了创建一个过滤谓词,您通常将位于Criteria类中的各种 fluent 方法( https://en.wikipedia.org/wiki/Fluent_interface )的调用链接在一起,该类还实现了Predicate,并将结果传递给FilterFilter filter( Predicate predicate)方法。

Filter filter = Filter.filter(Criteria.where("price").lt(20.00));

CriteriaCriteria where(String key) static方法返回一个Criteria对象,存储提供的key,在本例中为price。它的Criteria lt(Object o) static方法为<操作符返回一个Criteria对象,该对象标识与key的值进行比较的值。

要使用过滤器谓词,首先在路径中插入一个过滤器谓词的?占位符:

String expr = "$['store']['book'][?].title";

Note

当提供多个过滤谓词时,它们以占位符从左到右的顺序应用,其中占位符的数量必须与提供的过滤谓词的数量相匹配。您可以在一个筛选操作[?, ?]中指定多个谓词占位符;两个谓词必须匹配。

接下来,因为Filter实现了Predicate,所以您将过滤谓词传递给一个带Predicate参数的read()方法:

List<Object> titles = JsonPath.read(json, expr, filter);

对于每个book元素,read()方法在检测到 JsonPath 表达式中的?占位符时执行过滤谓词。

清单 10-3 展示了一个应用程序的源代码,演示了前面的过滤器谓词代码片段。

import java.util.List; 

import com.jayway.jsonpath.Criteria;
import com.jayway.jsonpath.Filter;
import com.jayway.jsonpath.JsonPath;

public class JsonPathDemo
{
   public static void main(String[] args)
   {
      String json =
      "{" +
      "   \"store\":" +
      "   {" +
      "      \"book\":" +
      "      [" +
      "         {" +
      "            \"category\": \"reference\"," +
      "            \"author\": \"Nigel Rees\"," +
      "            \"title\": \"Sayings of the Century\"," +
      "            \"price\": 8.95" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"Evelyn Waugh\"," +
      "            \"title\": \"Sword of Honour\"," +
      "            \"price\": 12.99" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"J. R. R. Tolkien\"," +
      "            \"title\": \"The Lord of the Rings\"," +
      "            \"isbn\": \"0-395-19395-8\"," +
      "            \"price\": 22.99" +
      "         }" +
      "      ]," +
      "      \"bicycle\":" +
      "      {" +
      "         \"color\": \"red\"," +
      "         \"price\": 19.95" +
      "      }" +
      "   }" +
      "}";

      Filter filter = Filter.filter(Criteria.where("price").lt(20.00));
      String expr = "$['store']['book'][?].title";
      List<Object> titles = JsonPath.read(json, expr, filter);
      System.out.println(titles);
   }
}

Listing 10-3.Demonstrating Filter Predicates

编译清单 10-3 并运行生成的应用程序。您应该会发现以下输出(两本书的价格都低于 20 美元):

["Sayings of the Century","Sword of Honour"]

自定义谓词

自定义谓词是从实现Predicate接口的类创建的谓词。

要创建自定义谓词,请实例化一个实现Predicate并覆盖以下方法的类:

boolean apply(Predicate.PredicateContext ctx)

PredicateContext是一个嵌套接口,其方法提供了关于调用apply()的上下文的信息。例如,Object root()返回一个对整个 JSON 文档的引用,而Object item()返回这个谓词正在评估的当前项。

apply()返回谓词值:true(项目被接受)或false(项目被拒绝)。

以下代码片段创建了一个自定义谓词,用于返回包含值超过20美元的price属性的Book元素:

Predicate expensiveBooks =
   new Predicate()
   {
      @Override
      public boolean apply(PredicateContext ctx)
      {

         String value = ctx.item(Map.class).get("price").toString();
         return Float.valueOf(value) > 20.00;
      }
   };

PredicateContext<T> T item(java.lang.Class<T> class)泛型方法将Book元素中的 JSON 对象映射到一个java.util.Map

要使用定制谓词,首先在路径中插入一个定制谓词的?占位符:

String expr = "$.store.book[?]";

接下来,将自定义谓词传递给一个采用Predicate参数的read()方法:

List<Map<String, Object>> titles = JsonPath.read(json, expr,
                                                 expensiveBooks);

对于每个book元素,read()执行与?相关联的定制谓词,并返回一个映射列表(每个接受的项目一个映射)。

清单 10-4 展示了一个应用程序的源代码,演示了前面的定制谓词代码片段。

import java.util.List;
import java.util.Map;

import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Predicate;

public class JsonPathDemo
{
   public static void main(String[] args)
   {

      String json =
      "{" +
      "   \"store\":" +
      "   {" +
      "      \"book\":" +
      "      [" +
      "         {" +
      "            \"category\": \"reference\"," +
      "            \"author\": \"Nigel Rees\"," +
      "            \"title\": \"Sayings of the Century\"," +
      "            \"price\": 8.95" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"Evelyn Waugh\"," +
      "            \"title\": \"Sword of Honour\"," +
      "            \"price\": 12.99" +
      "         }," +
      "         {" +
      "            \"category\": \"fiction\"," +
      "            \"author\": \"J. R. R. Tolkien\"," +
      "            \"title\": \"The Lord of the Rings\"," +
      "            \"isbn\": \"0-395-19395-8\"," +
      "            \"price\": 22.99" +
      "         }" +
      "      ]," +
      "      \"bicycle\":" +
      "      {" +
      "         \"color\": \"red\"," +
      "         \"price\": 19.95" +
      "      }" +
      "   }" +
      "}";

      Predicate expensiveBooks =
         new Predicate()
         {
            @Override
            public boolean apply(PredicateContext ctx)
            {
               String value = ctx.item(Map.class).get("price").toString();
               return Float.valueOf(value) > 20.00;
            }
         };
      String expr = "$.store.book[?]";
      List<Map<String, Object>> titles = JsonPath.read(json, expr,
                                                       expensiveBooks); 

      System.out.println(titles);
   }
}

Listing 10-4.Demonstrating Custom Predicates

编译清单 10-4 并运行生成的应用程序。您应该会发现以下输出(一本书的价格超过 20 美元):

[{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}]

Exercises

以下练习旨在测试你对第十章内容的理解。

Define JsonPath.   True or false: JsonPath is based on XPath 2.0.   Identify the operator that represents the root JSON object.   In what notations can you specify JsonPath expressions?   What operator represents the current node being processed by a filter predicate?   True or false: JsonPath’s deep scan operator (..) is equivalent to XPath’s / symbol.   What does JsonPath’s JsonPath compile(String jsonPath, Predicate... filters) static method accomplish?   What is the return type of the <T> T read(String json) generic method that returns JSON object property names and their values?   Identify the three predicate categories.   Given JSON object { "number": [10, 20, 25, 30] }, write a JsonPathDemo application that extracts and outputs the maximum (30), minimum (10), and average (21.25) values.

摘要

JsonPath 是一种声明式查询语言(也称为路径表达式语法),用于选择和提取 JSON 文档的属性值。

JsonPath 是一种简单的语言,具有与 XPath 相似的各种特性。这种语言用于构造路径表达式。每个表达式都以$操作符开始,它标识查询的根元素,并且对应于 XPath /符号。

与第章第 8 的 mJson 和第章第 9 的 Gson 一样,您可以从中央 Maven 资源库获得 JsonPath。或者,如果您没有使用 Maven,可以下载 JsonPath Jar 文件和 JsonPath 依赖的所有 Jar 文件,然后将所有这些 Jar 文件添加到您的类路径中。

JsonPath 库被组织成几个包。您通常会与com.jayway.jsonpath包及其类型进行交互。在本章中,您专门关注了这个包,同时学习了如何从 JSON 对象中提取值并使用谓词来过滤项目。

附录 A 给出了每章练习的答案。

posted @ 2024-08-06 16:36  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报