Java-XML-和-JSON-教程-全-
Java XML 和 JSON 教程(全)
一、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 文档树结构。
图 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
没有父元素。还有,recipe
和ingredients
有子元素:recipe
的子元素是title
、ingredients
和instructions
;而ingredients
子代是ingredient
的三个实例。title
、instructions
和ingredient
元素没有子元素。
元素可以包含子元素、内容或混合内容(子元素和内容的组合)。清单 1-2 揭示了movie
元素包含name
和language
子元素,还揭示了这些子元素中的每一个都包含内容(例如,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
,它包含了abstract
和body
子元素。abstract
元素将内容与包含内容的code-inline
元素混合在一起。相反,body
元素是空的。
Note
与清单 1-1 和 1-2 一样,清单 1-3 也包含空格(不可见的字符,比如空格、制表符、回车符和换行符)。XML 规范允许在文档中添加空白。内容中出现的空白(如单词之间的空格)被视为内容的一部分。相反,解析器通常会忽略结束标记和下一个开始标记之间出现的空白。这样的空白不被认为是内容的一部分。
XML 元素的开始标记可以包含一个或多个属性。例如,清单 1-1 的<ingredient>
标签有一个qty
(数量)属性,清单 1-3 的<article>
标签有title
和lang
属性。属性提供了关于元素的附加细节。例如,qty
表示可以添加的成分的量,title
表示文章的标题,lang
表示文章使用的语言(en
表示英语)。属性可以是可选的。例如,当未指定qty
时,假定默认值为1
。
Note
元素和属性名称可以包含英语或其他语言的任何字母数字字符,还可以包含下划线(_
)、连字符(-
)、句点(。),以及冒号(:)标点字符。冒号应该只用于名称空间(将在本章后面讨论),并且名称不能包含空格。
字符引用和 CDATA 节
某些字符不能出现在开始标记和结束标记之间的内容中,也不能出现在属性值中。例如,不能在开始标记和结束标记之间放置文字字符<
,因为这样做会使 XML 解析器误以为遇到了另一个标记。
这个问题的一个解决方案是用字符引用替换原义字符,字符引用是代表字符的代码。字符引用分为数字字符引用或字符实体引用:
- 数字字符引用通过其 Unicode 码位来引用字符,并遵循格式
&#
nnnn;
(不限于四位)或&#x
hhhh;
(不限于四位),其中 nnnn 提供码位的十进制表示,hhhh 提供十六进制表示。例如,Σ
和Σ
代表希腊文大写字母 sigma。虽然 XML 要求&#x
hhhh;
中的x
是小写的,但是它很灵活,前导零在两种格式中都是可选的,并且允许您为每个 h 指定大写或小写字母。因此,Σ
、Σ
和Σ
也是希腊大写字母 sigma 的有效表示。 - 字符实体引用通过实体名称(别名数据)引用字符,该实体将所需字符指定为其替换文本。字符实体引用由 XML 预定义,格式为
&
name;
,其中 name 是实体的名称。XML 预定义了五个字符实体引用:<
(<
)>
(>
)&
(&
)'
(’
)和"
("
)。
考虑一下<expression>6 < 4</expression>
。你可以用数字参考<
代替<
,产生<expression>6 < 4</expression>
,或者更好的是用<
,产生<expression>6 < 4</expression>
。第二种选择更清晰,更容易记忆。
假设您想在一个元素中嵌入一个 HTML 或 XML 文档。为了使嵌入的文档能够被 XML 解析器接受,您需要用它的<
和&
预定义的字符实体引用来替换每个文字<
(标签的开始)和&
(实体的开始)字符,这是一项繁琐且可能容易出错的工作——您可能会忘记替换其中的一个字符。为了避免繁琐和潜在的错误,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
部分,避免了用<
预定义的字符实体引用替换所有<
字符的需要。
命名空间
创建结合不同 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 )。
图 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 文档验证类似于编译器分析源代码,以确保代码在机器上下文中有意义。例如,int
、count
、=
、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
是根元素。此外,这个元素必须包含title
、ingredients
和instructions
子元素中的一个,并且按照这个顺序。子元素必须指定为逗号分隔的列表。此外,列表总是用括号括起来。
第二个元素声明声明title
元素包含解析的字符数据(非标记文本)。第三个元素声明声明至少一个ingredient
元素必须出现在ingredients
中。+
字符是表示一个或多个的正则表达式的一个例子。其他可能使用的表达式有*
(零或更多)和?
(一次或根本不使用)。第四个和第五个元素声明与第二个类似,声明ingredient
和instructions
元素包含解析的字符数据。
Note
元素声明支持其他三种内容说明符。您可以指定<!ELEMENT
名称ANY>
来允许任何类型的元素内容,或者指定<!ELEMENT
名称EMPTY>
来禁止任何元素内容。要声明一个元素包含混合内容,您可以指定#PCDATA
和一个元素名称列表,用竖线(|
)分隔。例如,<!ELEMENT ingredient (#PCDATA | measure | note)*>
表示ingredient
元素可以包含已解析的字符数据、零个或多个measure
元素以及零个或多个note
元素。它没有指定被解析的字符数据和这些元素出现的顺序。但是,#PCDATA
必须是列表中指定的第一项。在此上下文中使用正则表达式时,它必须出现在右括号的右侧。
清单 1-7 的 DTD 最后声明了菜谱语言的属性,其中只有一个:qty
。属性声明的形式是<!ATTLIST
ename aname type default-value>
,其中 ename 是属性所属元素的名称,aname 是属性的名称,type 是属性的类型,default-value 是属性的默认值。
属性声明将qty
标识为ingredient
的属性。它还说明了qty
的类型是CDATA
(任何不包括&符号、小于或大于符号或双引号的字符串都可能出现;这些字符可以分别通过&
、<
、>
或"
来表示,并且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
声明中指定属性列表。例如,<!ATTLIST
ename aname1 type 1 default-value 1 aname2 type 2 default-value 2>
声明了标识为 aname 1 和 aname 2 的两个属性。
基于 DTD 的验证 XML 解析器在验证文档之前,要求文档包含一个标识 DTD 的文档类型声明,该 DTD 指定了文档的语法。
Note
文档类型定义和文档类型声明是两回事。DTD 首字母缩略词标识文档类型定义,从不标识文档类型声明。
文档类型声明紧跟在 XML 声明之后,并以下列方式之一指定:
<!DOCTYPE
root-element-nameSYSTEM
uri>
通过 uri 引用一个外部但私有的 DTD。引用的 DTD 不可用于公共审查。例如,我可能将我的配方语言的 DTD 文件(recipe.dtd
)存储在我的www.javajeff.ca
网站上的私有dtds
目录中,并使用<!DOCTYPE recipe SYSTEM "
http://www.javajeff.ca/dtds/recipe.dtd
">
通过系统标识符http://www.javajeff.ca/dtds/recipe.dtd
来标识该 DTD 的位置。<!DOCTYPE
root-element-namePUBLIC
fpi uri>
通过 FPI、正式的公共标识符(参见http://en.wikipedia.org/wiki/Formal_Public_Identifier
)和 uri 引用外部但公共的 DTD。如果验证 XML 解析器不能通过公共标识符 fpi 定位 DTD,它可以使用系统标识符 uri 来定位 DTD。比如<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "
http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd
">
首先通过公共标识符-//W3C//DTD XHTML 1.0 Transitional//EN
引用 XHTML 1.0 DTD,其次通过系统标识符http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd
引用。<!DOCTYPE
根元素[
dtd]>
引用了一个内部 dtd,一个嵌入在 XML 文档中的 DTD。内部 DTD 必须出现在方括号中。
清单 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;。例如预定义的lt
、gt
、amp
、apos
和quot
角色实体,其<
、>
、&
、'
和"
角色实体引用分别是角色<
、>
、&
、’
和"
的别名。
一般实体分为内部实体和外部实体。内部一般实体是其值存储在 DTD 中的一般实体,其形式为<!ENTITY
name value >
,其中 name 标识实体,value 指定其值。例如,<!ENTITY copyright "Copyright © 2016 Jeff Friesen. All rights reserved.">
声明了一个名为copyright
的内部通用实体。这个实体的值可能包括另一个声明的实体,比如©
(版权符号的 HTML 实体),并且可以通过指定©right;
从 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
添加到salesperson
、lawyer
和accountant
中,而是像在<!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 种原语类型,通过以下标识符来表示:anyURI
、base64Binary
、boolean
、date
、dateTime
、decimal
、double
、duration
、float
、hexBinary
、gDay
、gMonth
、gMonthDay
、gYear
、gYearMonth
、NOTATION
、QName
、string
和time
。
XML Schema 提供了限制(通过约束减少允许值的集合)、列表(允许值的序列)和联合(允许从几种类型中选择值)派生方法,用于从这些原始类型创建新的简单类型。比如 XML Schema 通过限制从decimal
派生出 13 个整数类型;这些类型通过以下标识符表示:byte
、int
、integer
、long
、negativeInteger
、nonNegativeInteger
、nonPositiveInteger
、positiveInteger
、short
、unsignedByte
、unsignedInt
、unsignedLong
和unsignedShort
。它还支持从简单类型创建复杂类型。
熟悉 XML 模式的一个好方法是通过一个例子,比如为清单 1-1 的菜谱语言文档创建一个模式。创建这个配方语言模式的第一步是识别它的所有元素和属性。要素有recipe
、title
、ingredients
、instructions
、ingredient
;qty
是孤属性。
下一步是根据 XML Schema 的内容模型对元素进行分类,该模型指定了元素中可以包含的子元素和文本节点的类型(参见 [http://en.wikipedia.org/wiki/Node_(computer_science
](http://en.wikipedia.org/wiki/Node_(computer_science) )
)。当元素没有子元素或文本节点时,该元素被认为是空的;当只接受文本节点时,该元素被认为是简单的;当只接受子元素时,该元素被认为是复杂的;当接受子元素和文本节点时,该元素被认为是混合的。清单 1-1 的元素都没有空的或混合的内容模型。然而,title
、ingredient
和instructions
元素具有简单的内容模型;并且recipe
和ingredients
元素具有复杂的内容模型。
对于具有简单内容模型的元素,我们可以区分有属性的元素和没有属性的元素。XML Schema 将具有简单内容模型并且没有属性的元素分类为简单类型。此外,它将具有简单内容模型和属性的元素或者来自其他内容模型的元素分类为复杂类型。此外,XML Schema 将属性分类为简单类型,因为它们只包含文本值——属性没有子元素。清单 1-1 的title
和instructions
元素及其qty
属性是简单类型。它的recipe
、ingredients
和ingredient
元素是复杂类型。
此时,您可以开始声明模式了。以下代码片段展示了介绍性的schema
元素:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
元素介绍了语法。它还将常用的xs
名称空间前缀分配给标准 XML 模式名称空间;xs:
随后被添加到 XML 模式元素名称的前面。
接下来,使用element
元素声明title
和instructions
简单类型元素,如下所示:
<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
由它的element
的ref
属性引用。
下一个要声明的复杂类型是ingredients
。下面的代码片段提供了它的声明:
<xs:element name="ingredients">
<xs:complexType>
<xs:sequence>
<xs:element ref="ingredient" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>
这个声明声明ingredients
是一个复杂类型,由一个或多个ingredient
元素组成。“或更多”是通过包含element
的maxOccurs
属性并将该属性的值设置为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:xsi
和xsi: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
包的抽象SAXParser
和SAXParserFactory
类实现 SAX 1,通过org.xml.sax
包的XMLReader
接口和org.xml.sax.helpers
包的XMLReaderFactory
类实现 SAX 2。org.xml.sax
、org.xml.sax.ext
和org.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.SAXNotSupportedException
。SAXNotRecognizedException
和SAXNotSupportedException
是SAXException
的子类。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
解析器必须支持namespaces
和namespace-prefixes
特性。namespaces
决定是否将 URIs 和本地名字传递给ContentHandler
的startElement()
和endElement()
方法。默认为true
—这些名字都是经过的。当false. namespace-prefixes
决定名称空间声明的xmlns
和xmlns:prefix
属性是否包含在传递给startElement()
的org.xml.sax.Attributes
列表中时,解析器可以传递空字符串,并且还决定限定名是否作为方法的第三个参数传递——限定名是一个前缀加一个本地名。它默认为false
,意味着不包括xmlns
和xmlns:prefix
,也意味着解析器不必传递限定名。没有强制的属性。JDK 文档的org.xml.sax
包页面列出了标准的 SAX 2 特性和属性。
setProperty()
调用将实现org.xml.sax.ext.LexicalHandler
接口的类的实例分配给lexical-handler
属性,这样就可以调用接口方法来报告注释、CDATA 部分等等。物业名称以 http://xml.org/sax/properties/
为前缀。
Note
与ContentHandler
、DTDHandler
、EntityResolver
和ErrorHandler
不同,LexicalHandler
是一个扩展(它不是核心 SAX API 的一部分),这就是为什么XMLReader
没有声明void setLexicalHandler(LexicalHandler handler)
方法。如果你想安装一个词法处理程序,你必须使用XMLReader
的setProperty()
方法来安装这个处理程序作为 http://xml.org/sax/properties/lexical-handler
属性的值。
要素和属性可以是只读的,也可以是读写的。(在极少数情况下,功能或属性可能是只写的。)设置或读取特性或属性时,可能会抛出SAXNotSupportedException
或SAXNotRecognizedException
。例如,如果你试图修改一个只读的特征/属性,就会抛出一个SAXNotSupportedException
类的实例。此外,如果在解析过程中调用setFeature()
或setProperty()
,可能会抛出这个异常。试图为不执行验证的解析器设置验证特性是一种抛出SAXNotRecognizedException
类实例的情况。
浏览处理程序和解析器接口
由setContentHandler()
、setDTDHandler()
、setErrorHandler()
安装的基于接口的处理程序;setEntityResolver()
安装的实体解析器;并且lexical-handler
属性描述的处理程序提供了各种回调方法。您需要理解这些方法,然后才能对它们进行编码,以便有效地响应解析事件。
旅游内容处理程序
ContentHandler
声明了以下面向内容的信息回调方法:
void characters(char[] ch, int start, int length)
通过ch
数组报告一个元素的字符数据。传递给start
和length
的参数标识数组中与该方法调用相关的部分。字符通过一个char[]
数组传递,而不是通过一个String
对象传递,以优化性能。解析器通常将大量文档存储在一个数组中,并反复将对该数组的引用以及更新后的start
和length
值传递给characters()
。void endDocument()
报告已到达文档的结尾。应用程序可能会使用此方法来关闭输出文件或执行一些其他清理。void
endElement
(String uri, String localName, String qName)
报告已经到达一个元素的末尾。uri
标识元素的名称空间 URI,或者当没有名称空间 URI 或名称空间处理尚未启用时为空。localName
标识元素的本地名称,即没有前缀的名称(例如html
或h:html
中的html
)。qName
引用限定名,例如,当没有前缀时,h:html
或html
。当检测到结束标签时,调用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
数组报告注释。传递给start
和length
的参数标识了数组中与该方法调用相关的部分。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
实现ContentHandler
、DTDHandler
、EntityResolver
、ErrorHandler
。SAX 也提供了org.xml.sax.ext.DefaultHandler2
,它继承了DefaultHandler
,也实现了LexicalHandler
。
演示 SAX API
清单 2-1 向SAXDemo
展示了源代码,这是一个演示 SAX API 的应用程序。该应用程序由一个SAXDemo
入口点类和一个DefaultHandler2
的Handler
子类组成。
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
SAXDemo
的main()
方法首先验证是否指定了一个或两个命令行参数(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-examples
的startElement()
调用之后和对example
的endElement()
调用之后的输出实例就有些奇怪了。<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-examples
的startElement()
之后和example
的endElement()
之后调用了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-resolver2
的setFeature()
调用代替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
包的抽象SAXParser
和SAXParserFactory
类实现 SAX 1,通过org.xml.sax
包的XMLReader
接口和org.xml.sax.helpers
包的XMLReaderFactory
类实现 SAX 2。org.xml.sax
、org.xml.sax.ext
和org.xml.sax.helpers
包提供了各种类型来增强这两种 Java 实现。
XMLReader
提供了几种配置解析器和解析文档内容的方法。其中一些方法获取并设置内容处理程序、DTD 处理程序、实体解析器和错误处理程序,这些由ContentHandler
、DTDHandler
、EntityResolver
和ErrorHandler
接口描述。在了解了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-section
、comment
、document
、document-fragment
或text
中的一个。节点也有本地名称(没有前缀的名称)、前缀和名称空间 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 声明的实体的引用。每个实体引用节点都有一个名称,当解析器没有用它们的值替换实体引用时,它就会包含在树中。解析器从不包含字符引用的实体引用节点(如
&
或Σ
),因为它们被各自的字符替换并包含在文本节点中。 - 符号节点:DTD 声明的符号。读取 DTD 的解析器将符号节点的映射(由符号名索引)附加到文档类型节点。每个符号节点都有一个名称和一个公共标识符或系统标识符,无论哪个标识符用于在 DTD 中声明符号。符号节点没有子节点。
- 处理指令节点:出现在单据中的处理指令。它有一个名称(指令的目标)、一个字符串值(指令的数据)和一个父节点(它的包含节点)。
- 文本节点:文档内容。它的名字是
#text
,当必须创建一个中间节点(比如注释)时,它代表元素内容的一部分。通过字符引用在文档中表示的字符,如<
和&
,将被它们所表示的文字字符替换。当这些节点被写入文档时,这些字符必须被转义。
尽管这些节点类型存储了关于 XML 文档的大量信息,但是也有一些限制,比如不能在根元素之外暴露空白。此外,大多数 DTD 或模式信息,比如元素类型(<!ELEMENT...>)
和属性类型(<xs:attribute...>
,都不能通过 DOM 访问。
DOM Level 3 解决了 DOM 的各种限制。例如,尽管 DOM 没有为 XML 声明提供节点类型,但是 DOM Level 3 使得通过文档节点的属性访问 XML 声明的version
、encoding
和standalone
属性值成为可能。
Note
非根节点从不孤立存在。例如,元素节点永远不会不属于文档或文档片段。即使当这些节点与主树断开连接时,它们仍然知道它们所属的文档或文档片段。
探索 DOM API
Java 通过javax.xml.parsers
包的抽象DocumentBuilder
和DocumentBuilderFactory
类以及非抽象FactoryConfigurationError
和ParserConfigurationException
类实现 DOM。org.w3c.dom
、org.w3c.dom.bootstrap
、org.w3c.dom.events
、org.w3c.dom.ls
和org.w3c.dom.views
包提供了各种类型来增强这种实现。
获取 DOM 解析器/文档构建器
DOM 解析器也称为文档构建器,因为它在解析和创建 XML 文档方面有双重作用。通过首先实例化DocumentBuilderFactory
,调用它的一个newInstance()
类方法,可以获得一个 DOM 解析器/文档构建器。例如,下面的代码片段调用了DocumentBuilderFactory
的DocumentBuilderFactory newInstance()
类方法:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
在幕后,newInstance()
遵循一个有序的查找过程来识别要加载的DocumentBuilderFactory
实现类。这个过程首先检查javax.xml.parsers.DocumentBuilderFactory
系统属性,最后在找不到其他类时选择 Java 平台的默认DocumentBuilderFactory
实现类。如果一个实现类不可用(也许由javax.xml.parsers.DocumentBuilderFactory
系统属性标识的类不存在)或者不能被实例化,newInstance()
抛出一个FactoryConfigurationError
类的实例。否则,它实例化该类并返回其实例。
获得一个DocumentBuilderFactory
实例后,可以调用各种配置方法来配置工厂。例如,您可以用一个true
参数调用DocumentBuilderFactory
的void 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
包相关联,该包还包括SchemaFactory
、SchemaFactoryLoader
、TypeInfoProvider
、Validator
,而ValidatorHandler. Schema
是中心类,代表一个语法的不可变内存表示。
DOM 通过DocumentBuilderFactory
的void setSchema(Schema schema)
和Schema getSchema()
方法支持验证 API。类似地,SAX 1.0 支持通过 j avax.xml.parsers.SAXParserFactory
的void 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
。最后一种异常类型表明DocumentBuilder
的parse()
方法依赖 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
除了DocumentBuilder
、DocumentBuilderFactory
和其他几个类,DOM 是基于接口的,其中Document
和DocumentType
就是例子。在幕后,DOM 方法(如parse()
方法)返回其类实现这些接口的对象。
Document
和所有其他描述不同类型节点的org.w3c.dom
接口都是org.w3c.dom.Node
接口的子接口。因此,它们继承了Node
的常量和方法。
Node
声明 12 个常数,表示各种节点;ATTRIBUTE_NODE
和ELEMENT_NODE
就是例子。要识别给定的Node
对象所代表的节点类型,调用Node
的short
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()
抛出DOMException
。DOMException
声明了一系列常量(如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
要获得元素节点的属性,首先调用Node
的NamedNodeMap
getAttributes()
方法。当节点表示一个元素时,该方法返回一个org.w3c.dom.NamedNodeMap
实现;否则,它返回null
。除了通过名字声明访问这些节点的方法(比如Node getNamedItem (String name)
)之外,NamedNodeMap
还通过index
声明返回所有属性节点的int getLength()
和Node item(int index)
方法。然后,通过调用诸如getNodeName()
这样的方法来获得Node
的名称。
除了继承Node
的常量和方法,Document
还声明了自己的方法。例如,您可以调用Document
的String getXmlEncoding()
、boolean getXmlStandalone()
和String getXmlVersion()
方法来分别返回 XML 声明的encoding
、standalone
和version
属性值。
Document
声明了三种定位一个或多个元素的方法:
Element getElementById(String elementId)
返回具有与elementId
指定的值相匹配的id
属性(如<img id=...>
)的元素。NodeList getElementsByTagName(String tagname)
返回与指定的tagName
匹配的文档元素的节点列表(按文档顺序)。- 除了只将那些匹配
localName
和namespaceURI
值的元素添加到节点列表之外,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)
DOMDemo
的main()
方法首先验证已经指定了一个命令行参数(XML 文档的名称)。然后,它创建一个文档构建器工厂,通知工厂它需要一个知道名称空间的文档构建器,并让工厂返回这个文档构建器。
继续,main()
将文档解析成节点树;输出 XML 声明的版本号、编码和独立属性值。并递归地转储所有元素节点(从根节点开始)及其属性值。
注意清单的一部分使用了getNodeType()
,另一部分使用了instanceof
。getNodeType()
方法调用是不必要的(它只是为了演示才出现的),因为可以用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 的电影文档。它使用Document
的createElement()
方法创建根movie
元素和movie
的name
和language
子元素。它还使用Document
的Text createTextNode(String data)
方法创建附加到name
和language
节点的文本节点。注意对Node
的appendChild()
方法的调用,将子节点(如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.stream
、javax.xml.stream.events
和javax.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
类的实例。
创建工厂后,调用XMLInputFactory
的void 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);
来请求一个支持名称空间感知的基于事件的读取器。
用基于流的阅读器解析文档
基于流的阅读器是通过调用XMLInputFactory
的createXMLStreamReader()
方法之一创建的,比如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
;否则返回false
。int next()
方法将光标向前移动一个信息集项目,并返回一个标识该项目的类型的整数代码。
不是将next()
的返回值与一个整数值进行比较,而是将这个值与一个javax.xml.stream.XMLStreamConstants
信息集常量进行比较,比如START_ELEMENT
或DTD
——XMLStreamReader
扩展了XMLStreamConstants
接口。
Note
您还可以通过调用XMLStreamReader
的int
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_ELEMENT
或XMLStreamReader.END_ELEMENT
时,QName getName()
返回光标位置元素的限定名(作为javax.xml.namespace.QName
实例)。
Note
将限定名描述为名称空间 URI、本地部分和前缀部分的组合。在实例化这个不可变的类(通过一个像QName(String namespaceURI, String localPart, String prefix)
这样的构造函数)之后,您可以通过调用QName
的String 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_ELEMENT
或XMLStreamReader.END_ELEMENT
时,就会调用XMLStreamReader
的getName()
方法来返回元素的限定名。
编译清单 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()
方法,如果您的应用程序被设计为长时间运行,您将希望调用该方法来释放与这个基于流的读取器相关联的任何资源。调用此方法不会关闭基础输入源。
用基于事件的阅读器解析文档
基于事件的阅读器是通过调用XMLInputFactory
的createXMLEventReader()
方法之一创建的,比如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
;否则返回false
。XMLEvent nextEvent()
方法将下一个事件作为一个对象返回,该对象的类实现了javax.xml.stream.events.XMLEvent
接口的子接口。
Note
XMLEvent
是处理标记事件的基本接口。它声明适用于所有子接口的方法;例如,Location getLocation()
(返回一个javax.xml.stream.Location
对象,其int getCharacterOffset()
和其他方法返回事件的位置信息)和int getEventType()
(将事件类型作为XMLStreamConstants
infoset 常量返回,如START_ELEMENT
和PROCESSING_INSTRUCTION
— XMLEvent
扩展XMLStreamConstants
)。XMLEvent
由其他javax.xml.stream.events
接口子类型化,这些接口根据返回 infoset 项目特定信息的方法(例如Attribute
的QName 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_ELEMENT
或XMLEvent.END_ELEMENT
时,就会调用StartElement
或EndElement
的getName()
方法来返回元素的限定名。
编译清单 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
您还可以通过调用XMLInputFactory
的createFilteredReader()
方法之一,如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
类的实例。
创建工厂后,调用XMLOutputFactory
的void setProperty(String name, Object value)
方法,根据需要设置各种特性和属性。目前所有作家支持的唯一财产是XMLOutputFactory.IS_REPAIRING_NAMESPACES
。启用时(通过将true
或Boolean
对象,如Boolean.TRUE
传递给value
),文档编写器负责所有名称空间绑定和声明,只需应用程序提供最少的帮助。就名称空间而言,输出总是格式良好的。但是,启用该属性会给编写 XML 的工作增加一些开销。
使用基于流的编写器创建文档
基于流的编写器是通过调用XMLOutputFactory
的createXMLStreamWriter()
方法之一创建的,比如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
。这个prefix
由writeStartElement()
、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)
用传递给namespaceURI
和localName
的参数写一个开始标签给底层编写器。
清单 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
文件。
使用基于事件的编写器创建文档
基于事件的编写器是通过调用XMLOutputFactory
的createXMLEventWriter()
方法之一创建的,比如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
的子接口(比如Attribute
和StartElement
)。
Tip
XMLEventWriter
还声明了一个void add(XMLEventReader reader)
方法,您可以用它将一个XMLEventReader
实例链接到一个XMLEventWriter
实例。
为了省去你实现这些接口的麻烦,StAX 提供了javax.xml.stream.EventFactory
。这个实用程序类声明了创建XMLEvent
子接口实现的各种工厂方法。例如,Comment createComment(String text)
返回一个对象,该对象的类实现了XMLEvent
的javax.xml.stream.events.Comment
子接口。
因为这些工厂方法被声明为abstract
,所以您必须首先获得一个EventFactory
类的实例。您可以通过调用EventFactory
的XMLEventFactory 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
实例。创建这些迭代器是为了将名称空间或属性传递给XMLEventFactory
的StartElement 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.stream
、javax.xml.stream.events
和javax.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
节点。但是,如果recipe
或instructions
恰好是上下文节点,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
属性值1
的ingredient
元素(无论它们位于何处)。
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() = 2
、6.8
、"Hello"
。XSLT 中经常使用通用表达式。
XPath 布尔值可以通过关系运算符<
、<=
、>
、>=
、=
和!=
进行比较。布尔表达式可以通过使用操作符and
和or
来组合。此外,XPath 预定义了以下函数:
boolean()
返回数字、字符串或节点集的布尔值。- 当其布尔参数为
false
时,not()
返回true
,反之亦然。 true()
返回true
。false()
返回false
。lang()
根据上下文节点的语言(由xml:lang
属性指定)是否与参数字符串指定的语言相同或者是该语言的子语言,返回true
或false
。
XPath 数值可以通过运算符+
、-
、*
、div
、mod
(余数)进行操作;正斜杠不能用于除法,因为它用于分隔位置步骤。所有五个操作符的行为都像 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()
使用Document
的getElementsByTagName()
方法返回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()
调用XPathExpression
的Object evaluate(Object item, QName returnType)
方法对表达式求值。第一个参数是表达式的上下文节点,在本例中恰好是一个Document
实例。第二个参数指定了由evaluate()
返回的对象的种类,并被设置为javax.xml.xpath.XPathConstants.NODESET
,这是 XPath 1.0 节点集类型的限定名,通过 DOM 的NodeList
接口实现。
Note
XPath API 将 XPath 的布尔、数字、字符串和节点集类型分别映射到 Java 的java.lang.Boolean
、java.lang.Double
、java.lang.String
和NodeList
类型。当调用evaluate()
方法时,通过XPathConstants
常量(BOOLEAN
、NUMBER
、STRING
和NODESET
)指定 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
类之后,通过调用XPath
的void setNamespaceContext(NamespaceContext nsContext)
方法,实例化NSContext
并向XPath
对象注册该对象。例如,您在XPath xp = xpf.newXPath();
之后指定xp.setNamespaceContext(new NSContext());
来用xp
注册NSContext
对象。
剩下要做的就是将前缀应用到 XPath 表达式,该表达式现在变成了//tt:contact[tt:city='Chicago']/tt:name/text()
,因为contact
、city
和name
元素现在是默认名称空间的一部分,其 URI 被映射到NSContext
实例的getNamespaceURI()
方法中的任意前缀tt
。
编译并运行修改后的XPathSearch
应用程序,你会看到John Doe
和Bob 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.SimpleDateFormat
和java.text.ParsePosition
类的帮助下完成的。重置ParsePosition
对象的索引(通过setIndex(0)
)后,调用SimpleDateFormat
的Date 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
参数标识扩展函数接受的参数数量,在重载扩展函数时非常有用。如果functionName
和arity
值可以接受,扩展函数的 Java 类被实例化并返回;否则,null
就返回了。
最后,通过调用XPath
的void 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 Doe
和Sandra Smith
在不同的行上。
变量和变量解析器
之前指定的所有 XPath 表达式都是基于文本的。XPath 还允许您指定变量来参数化这些表达式,其方式类似于在 SQL 预准备语句中使用变量。
变量出现在表达式中的方式是在其名称(可能有也可能没有名称空间前缀)前加上一个$
。例如,/a/b[@c = $d]/text()
是一个 XPath 表达式,它选择根节点的所有a
元素,以及具有包含由变量$d
标识的值的c
属性的所有a
的b
元素,并返回这些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
参数包含变量名的限定名。(请记住,变量名可能带有名称空间前缀。)此方法验证限定名是否恰当地命名了变量,然后返回它的值。
创建变量解析器后,通过调用XPath
的void 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
应用程序,你会看到b1
和b5
在不同的行上。
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() = 2
、6.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 说明了转换过程。
图 6-1。
An XSLT processor transforms an XML input document into a result tree
XSLT 的美妙之处在于,您不需要开发定制的软件应用程序来执行转换。相反,您只需创建一个 XSLT 样式表,并将其与需要转换到 XSLT 处理器的 XML 文档一起输入。
探索 XSLT API
Java 通过javax.xml.transform
、javax.xml.transform.dom
、javax.xml.transform.sax
、javax.xml.transform.stax
和javax.xml.transform.stream
包中的类型实现 XSLT。javax.xml.transform
包定义了通用 API,用于处理转换指令和执行从源(XSLT 处理器的输入来源)到结果(发送处理器的输出)的转换。其余的包定义了获取不同种类的源和结果的 API。
javax.xml.transform.TransformerFactory
类是使用 XSLT 的起点。通过调用它的一个newInstance()
方法来实例化TransformerFactory
。例如,下面的代码片段使用TransformerFactory
的TransformerFactory newInstance()
类方法来创建工厂:
TransformerFactory tf = TransformerFactory.newInstance();
在幕后,newInstance()
遵循一个有序的查找过程来识别要加载的TransformerFactory
实现类。这个过程首先检查javax.xml.transform.TransformerFactory
系统属性,最后在找不到其他类时选择 Java 平台的默认TransformerFactory
实现类。如果一个实现类不可用(也许由javax.xml.transform.TransformerFactory
系统属性标识的类不存在)或者不能被实例化,newInstance()
抛出一个javax.xml.transform.TransformerFactoryConfigurationError
类的实例。否则,它实例化该类并返回其实例。
获得一个TransformerFactory
对象后,可以调用各种配置方法来配置工厂。例如,您可以调用TransformerFactory
的void 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
对象并将该对象作为参数传递给Transformer
的void setOutputProperties(Properties prop)
方法。由setOutputProperty()
和setOutputProperties()
设置的属性覆盖样式表的xsl:output
指令设置。
在执行转换之前,您需要获得实现Source
和javax.xml.transform.Result
接口的类的实例。然后将这些实例传递给Transformer
的void 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.dom
、javax.xml.transform.sax
、javax.xml.transform.stax
和javax.xml.transform.stream
包中的各种Source
和Result
实现类,但非默认转换器(可能通过javax.xml.transform.TransformerFactory
系统属性指定)可能更受限制。因此,每个Source
和Result
实现类都声明了一个FEATURE
字符串常量,可以传递给TransformerFactory
的boolean getFeature(String name)
方法。当支持Source
或Result
实现类时,该方法返回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));
不同之处在于调用Transformerfactory
的Templates 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-each
和if
。在<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.
图 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.transform
、javax.xml.transform.dom
、javax.xml.transform.sax
、javax.xml.transform.stax
和javax.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 字符的序列。字符串用双引号分隔,并支持反斜杠转义语法。
- 布尔:值
true
或false
中的任意一个。 - 数组:零个或多个值的有序列表,每个值可以是任意类型。数组使用方括号符号,元素用逗号分隔。
- 对象:属性的无序集合,其中的名称(也称为键)是字符串。因为对象旨在表示关联数组,所以建议(尽管不是必需的)每个键在一个对象中是唯一的。对象用大括号分隔,并用逗号分隔每个属性。在每个属性中,冒号字符将键与其值分开。
- 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 类型。在这个对象中有四个属性(字符串类型):streetAddress
、city
、state
和postalCode
。- 识别一个人的电话号码,并且是数组类型。数组中有两个对象;每个对象由
type
和number
属性(字符串类型)组成。 - 标识一个人的孩子(如果有的话),并且是数组类型。
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 支持两个脚本引擎:rhino
和nashorn
。我选择获得更现代的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
值)。此外,它还识别属性类型(例如string
和number
)。- 关键字
properties
引入了一个可以出现在 JSON 对象中的属性数组。这些属性被标识为name
和age
。每个属性由一个对象进一步描述,该对象提供了一个描述属性的description
关键字和一个识别可以分配给属性的值的类型的type
关键字。这是一个约束:您必须给name
分配一个字符串,给age
分配一个数字。对于age
属性,minimum
和maximum
关键字被指定来提供额外的约束:分配给age
的数字必须在从18
到64
的范围内。 required
关键字引入了一个数组,该数组标识那些必须存在于 JSON 对象中的属性。在这个例子中,name
和age
都是必需的属性。
JSON 模式网站提供了不同编程语言的各种验证器实现的链接(参见 http://json-schema.org/implementations.html
)。您可以下载一个实现,并将其集成到您的应用程序中,但须符合许可要求。对于这一章,我选择使用一个名为 JSON Schema Lint ( http://jsonschemalint.com/draft4/
)的在线工具来演示验证。
图 7-1 显示了 JSON Schema Lint 在线工具的相应窗口中先前的 JSON 对象和模式。
图 7-1。
The schema is valid and the JSON object conforms to this schema
让我们对 JSON 对象进行一些修改,使它不再符合模式,并看看 JSON 模式 Lint 工具如何响应。首先,让我们将65
赋值给age
,这超过了age
属性的maximum
约束。图 7-2 显示了结果。
图 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 所示。
图 7-3。
JSON Schema Lint reports that the age
property has the wrong type
最后,让我们将age
的值恢复为35
,但是删除name
属性。图 7-4 显示了 JSON 模式 Lint 的响应。
图 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 对象的一部分。它包含了Schema
和Factory
接口,50 多个方法和其他成员。本节将探讨这些方法中的大部分以及Schema
和Factory
。
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.URL
的url
的统一资源定位符(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)
:返回一个填充了args
的Json
对象(代表一个 JSON 数组),变量为java.lang.Object
sJson make(Object anything)
:返回一个填充了anything
内容的Json
对象,为null
之一;一个类型为Json
、String
、java.util.Collection<?>
、java.util.Map<?, ?>
、java.lang.Boolean
、java.lang.Number
的值;或者这些类型之一的数组。映射、集合和数组被递归复制,使得它们的每个元素都被转换成一个Json
对象。映射的键通常是字符串,但是任何具有有意义的toString()
实现的对象都可以。当传递给anything
的参数的具体类型未知时,该方法抛出java.lang.IllegalArgumentException
。Json nil()
:返回一个代表null
的Json
对象。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 Boolean
、String
、Number
、Map
、java.util.List
或数组类型。对于对象和数组,此方法执行所有嵌套元素的深层复制。
要标识 JSON 值的 JSON 类型,请调用以下方法之一:
boolean isArray()
:返回 JSON 数组值的true
。boolean isBoolean()
:返回一个 JSON 布尔值true
。boolean isNull()
:返回 JSONnull
值的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
实现对象,然后在这个对象上调用List
的size()
方法来完成这个任务,如下所示:
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
对象的引用。您可以调用Json
的Json 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
转换为代表value
的Json
对象,然后调用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 文档。
通过调用Schema
的Json validate(Json document)
方法来执行验证,该方法试图根据这个Schema
对象来验证一个 JSON document
。即使检测到验证错误,验证也会尝试继续进行。返回值总是一个Json
对象,其 JSON 对象包含名为ok
的布尔属性。当ok
为true
时,没有其他属性。当它是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}"]}
通过工厂定制
Json
将Json
对象的创建委托给工厂,工厂是实现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
的对等物。这可以通过让MyFactory
的string(String)
方法首先调用DefaultFactory
的string(String)
方法来实现,后者返回StringJson
对象。然后,将调用转移到这个对象就很简单了。
例外是equals()
方法。StringIJson
将该方法整理成与它的StringJson
对应方法几乎相同。主要区别是对String
的equalsIgnoreCase()
方法的调用,而不是对其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
包含Schema
和Factory
接口,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
。配置方法调用被链接在一起,最后调用GsonBuilder
的Gson
create()
方法以返回结果Gson
对象。
Gson
支持以下默认配置(列表不完整;查看Gson
和GsonBuilder
文档了解更多信息):
Gson
为java.lang.Enum
、java.util.
、、Map
、、URL
、、java.net.
、URI
、、java.util.
、、Locale
、、java.util.
、、Date
、java.math.
、、BigDecimal
、java.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
只序列化/反序列化那些用这个注释标记的字段。 - 默认情况下,
Gson
将transient
或static
字段排除在序列化和反序列化考虑之外。您可以通过调用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()
调用Gson
的void 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
类,后者根据姓名和年龄描述了一个人。
GsonDemo
的main()
方法首先实例化Gson
,保持其默认配置。然后,它构造一个基于String
的 JSON 对象来表示一个人,并将该对象与Person.class
一起传递给fromJson(String json, Class<T> classOfT). fromJson()
,解析存储在传递给json
的字符串中的姓名和年龄,并使用Person.class
和反射 API 来创建一个Person
对象,并用姓名和年龄填充它。对Person
对象的引用被返回并存储在person
变量中,随后被传递给System.out.println()
。这个方法最终调用Person
的toString()
方法来返回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
:表示 JSONnull
值的具体类。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 对象由name
和age
字段组成:
- 字段
name
已经被重构为first-name
和last-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;
中一样。或者,您可以调用JsonElement
的JsonObject
getAsJsonObject()
方法来获取JsonObject
引用,这是deserialize()
首先要完成的。
获得JsonObject
引用后,deserialize()
调用其JsonElement
get(
String
memberName)
方法返回一个JsonElement
作为所需的memberName
值。第一个调用从first-name
传递到get()
;您希望获得这个 JSON 字段的值。因为返回一个JsonPrimitive
来代替JsonElement
,所以对JsonPrimitive
的 String
getAsString()
方法的调用被链接到JsonPrimitive
引用,并获得first-name
的值。按照这种模式获取last-name
和address
字段的值。
为了多样化,我决定在age
领域做一些不同的事情。我调用JsonObject
的JsonPrimitive
getAsJsonPrimitive(
String
memberName)
方法返回一个JsonPrimitive
引用对应age
。然后,我调用JsonPrimitive
的int 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
让您通过调用Gson
的toJson()
方法之一,从 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
对象,并调用该对象的String
toJson(
Object
src)
方法将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
对象。然后创建Gson
和java.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-author
、other-authors
、isbn-10
和isbn-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(
String
property,
String
value)
方法添加title
、lead-author
、isbn-10
和isbn-13
属性。
other-authors
属性的处理方式不同。首先,serialize()
创建一个JsonArray
实例,并用除第一作者之外的所有作者填充它。然后,调用JsonObject
的void 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 文档,Since
和Until
对于在 web 服务上下文中管理 JSON 类的版本非常有用。
显示和隐藏字段
默认情况下,Gson
不会序列化和反序列化标记为transient
(或static
)的字段。你可以调用GsonBuilder
的GsonBuilder 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
对象,然后调用GsonBuilder
的excludeFieldsWithoutExposeAnnotation()
方法,然后调用该对象的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
第一个输出行显示只有field3
和field4
被序列化。其他字段不序列化。
第二行和第三行显示transient id
和password
字段接收默认值。transient
字段未被序列化/反序列化。
第四、第五和第六行显示默认的0
值被分配给field1
、field2
和field3
。对于field1
和field3
,deserialize
被分配给false
,因此只能将默认值分配给这些字段。因为field2
没有序列化,所以唯一可以赋给它的值是0
。
第七行显示5
被分配给field4
。这是有意义的,因为serialize
和deserialize
元素被赋予了true
。
因为static
字段没有被序列化或反序列化,所以它们保持初始值,如第八和第九行所示(对于field5
和field6
)。
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-10
和isbn-13
属性名,而 Java 类表示isbn10
和isbn13
字段名称。
清单 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
版本控制
Since
和Until
对你的类版本化很有用。使用这些注释类型,您可以确定哪些字段和/或类型被序列化为 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 描述了一个演示Since
和Until
注释类型的应用程序。
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
内容
由JsonSerializer
和JsonDeserializer
接口声明的serialize()
和deserialize()
方法分别用com.google.gson.JsonSerializationContext
和com.google.gson.JsonDeserializationContext
对象调用,作为它们的最终参数。这些对象提供了对特定 Java 对象执行默认序列化和默认反序列化的serialize()
和deserialize()
方法。在处理不需要特殊处理的嵌套 Java 对象时,您会发现它们非常方便。
假设您有下面的Date
和Employee
类:
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-name
和hire-date
属性(而不是name
和hireDate
属性)添加到生成的 JSON 对象中。因为在序列化过程中没有改变Date
字段的名称或顺序,所以可以利用传递给JsonSerializer
的serialize()
方法的上下文来处理序列化的这一部分。
以下代码片段显示了序列化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}}
泛型支持
当您调用String
toJson(
Object
src)
或void toJson(Object src, Appendable writer)
时,Gson
调用src.getClass()
来获取src
的java.lang.Class
对象,以便它可以反射性地了解要序列化的字段。同样,当您调用一个反序列化方法,如<T> T fromJson(
String
json,
Class
<T> classOfT)
,Gson
使用传递给classOfT
的Class
对象来帮助它反射性地构建一个结果 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(
String
json,
Type
typeOfT)
方法,如下:
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
类被组织成嵌套的Vehicle
和Truck 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
实例中的类型,接着是有强制转换问题的成功反序列化,接着是没有强制转换问题的成功反序列化。
第二部分关注名为vehicle
的Vehicle<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(
String
json,
Class
<T> classOfT)
中的null
到json
得到的。
第四部分展示了如何修复第二部分和第三部分中的问题。这个部分创建了TypeToken<Vehicle<Truck>>
和TypeToken<Map<String,String>>
对象来存储Vehicle<Truck>
和Map<String, String>
参数化类型。这些对象然后被传递给String toJson(Object src,
Type
typeOfSrc)
和<T> T fromJson(
String
json,
Type
typeOfT)
方法的type
参数。(虽然gson.toJson(vehicle, vehicleType)
不是必需的,因为序列化与gson.toJson(vehicle)
一起工作,但是为了安全起见,您应该养成基于TypeToken
实例传递一个Type
对象作为第二个参数的习惯。)
Note
当指定对象(src
和从classOfT
派生的对象)的任何字段基于通用类型时,toJson(Object src)
、<T> T fromJson(String json,
、、、<T> classOfT)
和类似的方法都能正常工作。唯一的规定是指定的对象不能是类属的。
类型适配器
在本章的前面,我展示了如何使用JsonSerializer
和JsonDeserializer
(分别)将 Java 对象序列化为 JSON 字符串,反之亦然。这些接口简化了 Java 对象和 JSON 字符串之间的转换,但是增加了一个处理中间层。
中间层由将 Java 对象和 JSON 字符串转换为JsonElement
的代码组成。这种转换降低了解析或创建无效 JSON 字符串的风险,但它确实需要时间来执行,这会影响性能。通过使用com.google.gson.TypeAdapter<T>
类,您可以避开中间层并创建更高效的代码,其中T
标识 Java 类序列化源和反序列化目标。
Note
你应该更喜欢效率更高的TypeAdapter
而不是效率更低的JsonSerializer
和JsonDeserializer
。事实上,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
表示左方括号)。通过调用JsonReader
和JsonWriter
方法来读写它们,如下所示:
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
循环。当JsonReader
的boolean hasNext()
方法返回true
时,这个循环继续:有另一个对象元素。
每个while
循环迭代执行一个switch
语句,调用JsonReader
的String nextName()
方法返回下一个令牌,这是 JSON 对象中的一个属性名。然后,它将令牌与三种可能性(name
、population
或cities
)进行比较,并执行相关代码来检索属性值,并将该值分配给之前创建的Country
对象中的适当字段。
如果属性是name
,调用JsonReader
的String nextString()
方法返回下一个令牌的字符串值。如果属性是 population,JsonReader
的int 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()
。它调用JsonWriter
的JsonWriter name(String name)
方法将name
指定的属性名编码成一个 JSON 属性名。同样,它调用JsonWriter
的JsonWriter 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() called
和write 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]
,我可以使用JsonPath
的static 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
属性值介于10
和20.
之间$..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
,并将结果传递给Filter
的Filter
filter(
Predicate
predicate)
方法。
Filter filter = Filter.filter(Criteria.where("price").lt(20.00));
Criteria
的Criteria
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 给出了每章练习的答案。