这些Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
有关Java SE 9及其后续版本中更新的语言特性摘要,请参阅Java语言变更。
有关所有JDK版本的新功能、增强功能以及已删除或弃用选项的信息,请参阅JDK发布说明。
示例程序SAXLocalNameCount默认使用非验证解析器,但也可以激活验证。激活验证允许应用程序判断XML文档是否包含正确的标签以及这些标签是否按照正确的顺序出现。换句话说,它可以告诉您文档是否有效。然而,如果没有激活验证,它只能告诉您文档是否格式正确,就像在前面的部分中当您删除XML元素的闭合标签时所示。要进行验证,XML文档需要与DTD或XML模式相关联。这两个选项在SAXLocalNameCount程序中都是可能的。
如果没有指定其他工厂类,则使用默认的SAXParserFactory类。要使用来自不同制造商的解析器,可以更改指向该解析器的环境变量的值。可以通过命令行完成此操作:
java -Djavax.xml.parsers.SAXParserFactory=yourFactoryHere [...]
您指定的工厂名称必须是一个完全限定的类名(包括所有包前缀)。有关更多信息,请参阅SAXParserFactory类的newInstance()方法中的文档。
到目前为止,本课程集中在非验证解析器上。本节将检查验证解析器,以了解在使用它解析示例程序时会发生什么。
关于验证解析器,必须了解两件事:
需要模式或DTD。
由于存在模式或DTD,每当可能时,将调用ContentHandler的ignorableWhitespace()方法。
当存在DTD时,解析器将不再调用characters()方法处理其知道是无关的空白。从只对XML数据感兴趣的应用程序的角度来看,这是一件好事,因为应用程序不会被纯粹为了使XML文件可读而存在的空白干扰。
另一方面,如果您编写的应用程序过滤XML数据文件,并且希望输出一个同样可读的版本,则该空白将不再是无关的:它是必需的。要获取这些字符,您需要将ignorableWhitespace方法添加到应用程序中。为了处理解析器看到的任何(通常)可忽略的空白,您需要添加类似以下代码以实现ignorableWhitespace事件处理程序。
public void ignorableWhitespace (char buf[], int start, int length) throws SAXException { emit("IGNORABLE"); }
这段代码只是生成一个消息,告诉你看到了可忽略的空白字符。然而,并不是所有的解析器都是相同的。SAX规范并不要求调用这个方法。Java XML实现在DTD使其可能时会调用该方法。
SAXParserFactory需要进行设置,以使用验证解析器而不是默认的非验证解析器。以下来自SAXLocalNameCount示例的main()方法的代码展示了如何配置工厂以实现验证解析器。
static public void main(String[] args) throws Exception { String filename = null; boolean dtdValidate = false; boolean xsdValidate = false; String schemaSource = null; for (int i = 0; i < args.length; i++) { if (args[i].equals("-dtd")) { dtdValidate = true; } else if (args[i].equals("-xsd")) { xsdValidate = true; } else if (args[i].equals("-xsdss")) { if (i == args.length - 1) { usage(); } xsdValidate = true; schemaSource = args[++i]; } else if (args[i].equals("-usage")) { usage(); } else if (args[i].equals("-help")) { usage(); } else { filename = args[i]; if (i != args.length - 1) { usage(); } } } if (filename == null) { usage(); } SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setNamespaceAware(true); spf.setValidating(dtdValidate || xsdValidate); SAXParser saxParser = spf.newSAXParser(); // ... }
这里,SAXLocalNameCount程序被配置为在启动时接受额外的参数,这些参数告诉它实现无验证、DTD验证、XML Schema定义(XSD)验证或针对特定模式源文件的XSD验证。(这些选项的描述-dtd、-xsd和-xsdss也添加到usage()方法中,但在此处未显示。)然后,工厂被配置为在调用newSAXParser时生成适当的验证解析器。如在设置解析器中所见,还可以使用setNamespaceAware(true)来配置工厂以返回一个命名空间感知的解析器。Oracle的实现支持任何组合的配置选项。(如果某个特定实现不支持某个组合,则必须生成工厂配置错误)。
虽然对XML Schema的完整处理超出了本教程的范围,但本节向您展示了使用XML Schema语言中的现有模式对XML文档进行验证的步骤。要了解有关XML Schema的更多信息,可以在http://www.w3.org/TR/xmlschema-0/上查看在线教程XML Schema Part 0: Primer。
注意 - 存在多种模式定义语言,包括RELAX NG,Schematron和W3C的“XML Schema”标准。(即使DTD也符合“模式”的定义,尽管它是唯一一个不使用XML语法来描述模式约束的。)然而,“XML Schema”给我们带来了一个术语上的挑战。虽然短语“XML Schema schema”会很准确,但我们将使用短语“XML Schema定义”以避免冗余的外观。
要在XML文档中收到验证错误的通知,解析器工厂必须配置为创建一个验证解析器,如前一节所示。此外,以下条件必须满足:
从设置属性时定义常量开始会很有帮助。例如,SAXLocalNameCount示例设置了以下常量。
public class SAXLocalNameCount extends DefaultHandler { static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage"; static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema"; static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource"; }
注意 - 解析器工厂必须配置为生成一个具有命名空间感知能力和验证能力的解析器。这在配置工厂中已经展示过。有关命名空间的更多信息,请参见文档对象模型,但现在只需了解模式验证是一个基于命名空间的过程。因为符合JAXP的解析器默认情况下不具备命名空间感知能力,所以必须设置模式验证的属性。
然后,您必须配置解析器以告诉它要使用哪种模式语言。在SAXLocalNameCount中,可以针对DTD或XML Schema执行验证。以下代码使用上面定义的常量来指定如果在程序启动时指定了-xsd选项,则使用W3C的XML Schema语言。
// ... if (xsdValidate) { saxParser.setProperty(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA); // ... }
除了设置错误处理中描述的错误处理外,还有一种错误可能发生在为基于模式的验证配置解析器时。如果解析器不符合JAXP规范,因此不支持XML Schema,则可能会抛出SAXNotRecognizedException。为了处理这种情况,setProperty()语句被包裹在一个try/catch块中,如下面的代码所示。
// ... if (xsdValidate) { try { saxParser.setProperty(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA); } catch (SAXNotRecognizedException x){ System.err.println("错误:JAXP SAXParser属性未识别:" + JAXP_SCHEMA_LANGUAGE); System.err.println("检查解析器是否符合JAXP规范。"); System.exit(1); } } // ...
要使用XML Schema定义验证数据,需要确保XML文档与其关联。有两种方法可以实现。
注意 - 当应用程序指定要使用的模式时,将覆盖文档中的任何模式声明。
要在文档中指定模式定义,可以创建如下的XML:
<documentRoot xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation='YourSchemaDefinition.xsd'>
第一个属性定义了XML命名空间(xmlns)前缀xsi,它代表XML Schema实例。第二行指定了要用于不带命名空间前缀的文档元素的模式,通常是任何简单、简洁的XML文档中定义的元素。
注意 - 有关命名空间的更多信息,请参阅文档对象模型中的使用XML Schema验证。现在,将这些属性视为验证不使用命名空间的简单XML文件的"魔术咒语"。在了解更多关于命名空间的知识之后,您将看到如何使用XML Schema来验证使用命名空间的复杂文档。这些概念在文档对象模型中的使用多个命名空间进行验证中进行了讨论。
您也可以在应用程序中指定模式文件,就像SAXLocalNameCount中所做的那样。
// ... if (schemaSource != null) { saxParser.setProperty(JAXP_SCHEMA_SOURCE, new File(schemaSource)); } // ...
在上面的代码中,变量schemaSource与一个模式源文件相关联,您可以通过使用-xsdss选项启动SAXLocalNameCount应用程序,并提供要使用的模式源文件的名称。
需要注意的是,当文件未通过验证时,抛出异常的唯一原因是由设置错误处理中的错误处理代码引起的。这段代码在这里再次重复作为提醒:
// ... public void warning(SAXParseException spe) throws SAXException { out.println("警告: " + getParseExceptionInfo(spe)); } public void error(SAXParseException spe) throws SAXException { String message = "错误: " + getParseExceptionInfo(spe); throw new SAXException(message); } public void fatalError(SAXParseException spe) throws SAXException { String message = "严重错误: " + getParseExceptionInfo(spe); throw new SAXException(message); } // ...
如果这些异常没有被抛出,验证错误将被简单地忽略。通常情况下,SAX解析错误是一种验证错误,尽管如果文件指定了解析器无法处理的XML版本,也可能会生成此错误。请记住,除非提供一个错误处理程序,否则您的应用程序不会生成验证异常,就像这里提供的错误处理程序一样。
如前所述,只有在SAX解析器处理DTD时才会生成警告。只有验证解析器才会生成一些警告。非验证解析器的主要目标是尽可能快地运行,但它也会生成一些警告。
XML规范建议根据以下情况生成警告:
为实体、属性或符号提供额外的声明。(这些声明将被忽略。只使用第一个声明。此外,请注意,当验证时,元素的重复定义总是会产生严重错误,就像您之前看到的那样。)
引用未声明的元素类型。(只有在实际使用未声明类型的XML文档中才会发生有效性错误。在DTD中引用未声明的元素时会产生警告。)
为未声明的元素类型声明属性。
Java XML SAX解析器还在其他情况下发出警告:
在验证时没有 <!DOCTYPE ...>。
在非验证时引用未定义的参数实体。(在验证时会产生错误。尽管非验证解析器不需要读取参数实体,但Java XML解析器会这样做。因为这不是必需的,所以Java XML解析器生成警告,而不是错误。)
某些字符编码声明看起来不正确的情况。
在本节中,将再次使用之前使用过的SAXLocalNameCount示例程序,但这次将使用XML Schema或DTD进行验证。展示不同类型的验证的最佳方式是修改被解析的XML文件的代码,以及相关的模式和DTD,以打破处理并使应用程序生成异常。
如上所述,这些示例重复使用了SAXLocalNameCount程序。你可以在运行SAX解析器示例不进行验证中找到示例及其相关文件的位置。
SAXLocalNameCount.java
文件保存在名为sax
的目录中。在文本编辑器中打开该文件并进行上述更改。javac sax/SAXLocalNameCount.java
rich_iii.xml
和two_gent.xml
保存在data
目录中。要做到这一点,运行程序时必须指定-dtd选项。
java sax/SAXLocalNameCount -dtd data/rich_iii.xml
你将看到类似于以下的结果:
Exception in thread "main" org.xml.sax.SAXException: Error: URI=file:data/rich_iii.xml Line=4: Document is invalid: no grammar found.
该消息表示文档rich_iii.xml无法通过验证,因为找不到可以对其进行验证的语法。换句话说,该消息表示你试图验证文档,但是没有声明DTD,因为没有DOCTYPE声明。现在你知道了DTD是一个有效文档的要求。这是有道理的。
play.dtd
保存在data
目录中。在文本编辑器中打开文件data/rich_iii.xml。在data/rich_iii.xml的开头插入以下DOCTYPE声明。(该声明将验证解析器指向名为play.dtd的DTD文件。如果启用了DTD验证,将检查被解析的XML文件的结构与play.dtd中提供的结构是否一致。)
<!DOCTYPE PLAY SYSTEM "play.dtd">不要忘记保存修改,但是保持文件打开,因为稍后还需要用到它。
将起始标签和结束标签从<PERSONA>和</PERSONA>更改为<PERSON>和</PERSON>。第18行现在应该是这样的:
18:<PERSON>KING EDWARD The Fourth</PERSON>
再次,不要忘记保存修改,并保持文件打开。
这次运行程序时,你会看到一个不同的错误:
java sax/SAXLocalNameCount -dtd data/rich_iii.xml Exception in thread "main" org.xml.sax.SAXException: 错误: URI=file:data/rich_iii.xml 行=26: 元素类型 "PERSON" 必须声明。
在这里你可以看到解析器反对的是一个没有包含在DTD data/play.dtd中的元素。
将起始标签和结束标签恢复为它们的原始版本,<PERSONA>和</PERSONA>。
再次,不要忘记保存修改。
与之前一样,你会看到另一个验证错误:
java sax/SAXLocalNameCount -dtd data/rich_iii.xml Exception in thread "main" org.xml.sax.SAXException: 错误: URI=file:data/rich_iii.xml 行=77: 元素类型 "PERSONAE" 的内容必须匹配 "(TITLE,(PERSONA|PGROUP)+)"。
通过从第16行删除<TITLE>元素,<PERSONAE>元素变得无效,因为它不包含DTD对<PERSONAE>元素期望的子元素。请注意,错误消息指出错误在data/rich_iii.xml的第77行,尽管你从第16行删除了<TITLE>元素。这是因为<PERSONAE>元素的结束标签位于第77行,解析器只在解析到元素的结束时才抛出异常。
在DTD文件中,你可以看到<PERSONAE>元素的声明,以及符合戏剧DTD的XML文档中可以使用的所有其他元素。 <PERSONAE>的声明如下。
<!ELEMENT PERSONAE (TITLE, (PERSONA | PGROUP)+)>
如你所见,<PERSONAE>元素需要一个<TITLE>子元素。管道符号(|)表示<PERSONA>或<PGROUP>子元素都可以包含在<PERSONAE>元素中,加号(+)表示(PERSONA | PGROUP)组合中必须包含至少一个或多个这些子元素。
在DTD中为子元素的声明添加问号,表示该子元素的存在是可选的。
<!ELEMENT PERSONAE (TITLE?, (PERSONA | PGROUP)+)>
如果在元素之后添加一个星号(*),可以包含零个或多个该子元素的实例。但是,在这种情况下,在文档的某个部分中只有一个标题是没有意义的。
不要忘记保存对data/play.dtd所做的修改。
java sax/SAXLocalNameCount -dtd data/rich_iii.xml
这次,你应该看到SAXLocalNameCount的正确输出,没有错误。
前面的练习演示了如何使用SAXLocalNameCount对一个XML文件进行DTD验证。在这个练习中,您将使用SAXLocalNameCount来验证另一个XML文件,同时还会验证标准的XML模式定义和自定义的模式源文件。同样,通过修改XML文件和模式,破坏解析过程来演示这种类型的验证,以便解析器抛出错误。
如上所述,这些示例重用了SAXLocalNameCount程序。您可以在运行无验证的SAX解析器示例中找到示例及其相关文件的位置。
SAXLocalNameCount.java
文件保存在名为sax
的目录中。在文本编辑器中打开文件并进行上述更改。javac sax/SAXLocalNameCount.java
personal-schema.xml
保存在data
目录中,然后在文本编辑器中打开它。
这是一个简单的XML文件,提供了一个小公司员工的姓名和联系方式。在这个XML文件中,可以看到它与一个模式定义文件personal.xsd
关联起来。
<personnel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation='personal.xsd'>
personal.xsd
保存在data
目录中,然后在文本编辑器中打开它。
此模式定义了关于每个员工所需的信息种类,以便将与模式关联的XML文档视为有效。例如,通过检查模式定义,可以看到每个person
元素需要一个name
,而且每个人的名字必须由一个family
名和一个given
名组成。员工还可以选择性地拥有电子邮件地址和URL。
data/personal.xsd
中,将person
元素所需的电子邮件地址的最小数量从0
更改为1
。
现在email
元素的声明如下。
<xs:element ref="email" minOccurs='1' maxOccurs='unbounded'/>
data/personal-schema.xml
中,从person
元素one.worker
中删除email
元素。
现在Worker One的代码如下:
<person id="one.worker"> <name><family>Worker</family> <given>One</given></name> <link manager="Big.Boss"/> </person>
SAXLocalNameCount
对personal-schema.xml
进行处理。
java sax/SAXLocalNameCount data/personal-schema.xml
SAXLocalNameCount
会告诉你每个元素在personal-schema.xml
中出现的次数。
Local Name "email" 出现了5次 Local Name "name" 出现了6次 Local Name "person" 出现了6次 Local Name "family" 出现了6次 Local Name "link" 出现了6次 Local Name "personnel" 出现了1次 Local Name "given" 出现了6次
你会发现email
只出现了五次,而personal-schema.xml
中有六个person
元素。因此,因为我们将email
元素的最小出现次数设置为每个person
元素的1次,我们知道该文档是无效的。然而,因为SAXLocalNameCount
没有被告知要根据模式进行验证,所以不会报告错误。
如你在上面的使用XML Schema进行验证中所见,SAXLocalNameCount有一个选项可以启用模式验证。使用以下命令运行SAXLocalNameCount。
java sax/SAXLocalNameCount -xsd data/personal-schema.xml
这次,你会看到以下错误信息。
Exception in thread "main" org.xml.sax.SAXException: Error: URI=file:data/personal-schema.xml Line=14: cvc-complex-type.2.4.a: 从元素 'link' 开始找到了无效的内容。 预期其中之一为'{email}'。
java sax/SAXLocalNameCount -xsd data/personal-schema.xml
这次,你将看到正确的输出,没有错误。
从personnel元素中删除斜体代码。
<personnel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation='personal.xsd'/>
java sax/SAXLocalNameCount -xsd data/personal-schema.xml
显然,这样是行不通的,因为没有声明要验证XML文件的模式定义。你会看到以下错误。
Exception in thread "main" org.xml.sax.SAXException: Error: URI=file:data/personal-schema.xml Line=2: cvc-elt.1: 找不到元素 'personnel' 的声明。
java sax/SAXLocalNameCount -xsdss data/personal.xsd data/personal-schema.xml
这次你使用了允许你指定不在应用程序中硬编码的模式定义的SAXLocalNameCount选项。你应该看到正确的输出。