此Java教程针对JDK 8编写。本页面中描述的示例和实践不利用后续版本引入的改进,并可能使用不再可用的技术。
有关Java SE 9及后续版本中更新的语言特性的摘要,请参阅Java语言更改。
有关所有JDK版本的新功能、增强功能和已删除或弃用选项的信息,请参阅JDK版本说明。
在实际应用中,您将希望使用SAX解析器来处理XML数据并对其进行有用的操作。本节将介绍一个名为SAXLocalNameCount的JAXP示例程序,该程序使用元素的localName组件来计算元素的数量,忽略命名空间名称以简化处理。此示例还演示了如何使用SAX的ErrorHandler。
SAXLocalNameCount程序保存在一个名为SAXLocalNameCount.java的文件中。
public class SAXLocalNameCount { static public void main(String[] args) { // ... } }
由于您将独立运行该程序,因此需要一个main()方法。同时需要命令行参数,以便告诉应用程序要处理哪个文件。在SAXLocalNameCount.java
文件中可以找到示例的完整代码。
应用程序将使用的类的导入语句如下所示。
package sax; import javax.xml.parsers.*; import org.xml.sax.*; import org.xml.sax.helpers.*; import java.util.*; import java.io.*; public class SAXLocalNameCount { // ... }
javax.xml.parsers包包含SAXParserFactory类,用于创建使用的解析器实例。如果无法生成与指定的选项配置匹配的解析器,则会抛出ParserConfigurationException异常(稍后将详细了解配置选项)。javax.xml.parsers包还包含SAXParser类,这是工厂用于解析的返回值。org.xml.sax包定义了SAX解析器使用的所有接口。org.xml.sax.helpers包包含DefaultHandler,它定义了将处理解析器生成的SAX事件的类。java.util和java.io中的类用于提供哈希表和输出功能。
首要任务是处理命令行参数,目前只需获取要处理的文件的名称。在main方法中,以下代码告诉应用程序您希望SAXLocalNameCount处理哪个文件。
static public void main(String[] args) throws Exception { String filename = null; for (int i = 0; i < args.length; i++) { filename = args[i]; if (i != args.length - 1) { usage(); } } if (filename == null) { usage(); } }
这段代码设置了main方法在遇到问题时抛出Exception,并定义了命令行选项,用于告诉应用程序要处理的XML文件的名称。在本课程的后面部分,我们将介绍代码中的其他命令行参数,当我们开始查看验证时。
运行应用程序时,您提供的filename字符串将通过一个内部方法convertToFileURL()转换为java.io.File URL。这是在SAXLocalNameCount中以下代码完成的。
public class SAXLocalNameCount { private static String convertToFileURL(String filename) { String path = new File(filename).getAbsolutePath(); if (File.separatorChar != '/') { path = path.replace(File.separatorChar, '/'); } if (!path.startsWith("/")) { path = "/" + path; } return "file:" + path; } // ... }
如果在运行程序时指定了不正确的命令行参数,则会调用SAXLocalNameCount应用程序的usage()方法,在屏幕上打印出正确的选项。
private static void usage() { System.err.println("Usage: SAXLocalNameCount <file.xml>"); System.err.println(" -usage or -help = this message"); System.exit(1); }
更多usage()选项将在本课程的后面部分进行讨论,当处理验证时。
SAXLocalNameCount中最重要的接口是ContentHandler。该接口要求在各种解析事件发生时,由SAX解析器调用的一些方法。其中主要的事件处理方法是:startDocument,endDocument,startElement和endElement。
实现该接口的最简单方法是扩展org.xml.sax.helpers包中定义的DefaultHandler类。该类为所有ContentHandler事件提供了空方法。示例程序扩展了该类。
public class SAXLocalNameCount extends DefaultHandler { // ... }
注意 - DefaultHandler还为DTDHandler,EntityResolver和ErrorHandler接口中定义的其他主要事件提供了空方法。您将在本课程的后面部分了解有关这些方法的更多信息。
接口要求这些方法中的每一个都抛出SAXException。在这里抛出的异常将被发送回解析器,解析器将其发送给调用解析器的代码。
本节显示了处理ContentHandler事件的代码。
当遇到开始标签或结束标签时,标签的名称以字符串形式传递给startElement或endElement方法。当遇到开始标签时,它定义的任何属性也会以Attributes列表的形式传递。在元素内找到的字符会与字符数组一起传递,同时传递字符的数量(长度)和指向第一个字符的偏移量。
以下代码处理了开始文档和结束文档事件:
public class SAXLocalNameCount extends DefaultHandler { private Hashtable tags; public void startDocument() throws SAXException { tags = new Hashtable(); } public void endDocument() throws SAXException { Enumeration e = tags.keys(); while (e.hasMoreElements()) { String tag = (String)e.nextElement(); int count = ((Integer)tags.get(tag)).intValue(); System.out.println("本地名称 \"" + tag + "\" 出现 " + count + " 次"); } } private static String convertToFileURL(String filename) { // ... } // ... }
此代码定义了解析器遇到正在解析的文档的起始点和结束点时应用程序的操作。ContentHandler接口的startDocument()方法创建了一个java.util.Hashtable实例,在元素事件中,这个实例将会被解析器用找到的XML元素填充。当解析器到达文档的结尾时,将调用endDocument()方法来获取散列表中包含的元素的名称和计数,并在屏幕上打印一条消息告诉用户找到了每个元素的多少个实例。
这两个ContentHandler方法都抛出SAXException异常。您将在设置错误处理中了解更多关于SAX异常的信息。
如文档事件所述,由startDocument方法创建的散列表需要被找到的文档中的各个元素填充。以下代码处理了开始元素事件:
public void startDocument() throws SAXException { tags = new Hashtable(); } public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { String key = localName; Object value = tags.get(key); if (value == null) { tags.put(key, new Integer(1)); } else { int count = ((Integer)value).intValue(); count++; tags.put(key, new Integer(count)); } } public void endDocument() throws SAXException { // ... }
此代码处理元素标签,包括在开始标签中定义的任何属性,以获取该元素的命名空间统一资源标识符(URI),本地名称和限定名称。然后,startElement()方法使用startDocument()创建的哈希映射填充本地名称及其计数,对于每种类型的元素。请注意,当调用startElement()方法时,如果未启用命名空间处理,则元素和属性的本地名称可能为空字符串。该代码通过在简单名称为空字符串时使用限定名称来处理此情况。
JAXP SAX API还允许您使用ContentHandler.characters()方法处理解析器传递给您的应用程序的字符。
注意 - 字符事件在SAXLocalNameCount示例中没有演示,但是为了完整起见,在本节中包含了一个简要说明。
解析器不需要一次返回任何特定数量的字符。解析器可以一次返回从一个字符到几千个字符,仍然是符合标准的实现。因此,如果您的应用程序需要处理所看到的字符,则最好使characters()方法在一个java.lang.StringBuffer中累积字符,并且只有在确定找到所有字符时才对它们进行操作。
当元素结束时,您完成文本解析,因此通常在那一点上执行字符处理。但是,您可能还想在元素开始时处理文本。这对于文档样式数据是必要的,其中可以包含与文本混合的XML元素。例如,考虑此文档片段:
<para>这个段落包含<bold>重要</bold>的想法。</para>
初始文本这个段落包含在<bold>元素的开始标签结束时终止。文本重要在结束标签</bold>处终止,最后的文本的想法。在结束标签</para>处终止。
严格来说,字符处理程序应该扫描并替换和号字符(&)和左尖括号字符(<),并分别替换为字符串&或<。这在下一节中解释。
在XML中,实体是具有名称的XML结构(或纯文本)。通过引用实体名称,将其插入到文档中以替换实体引用。要创建实体引用,您需要用和号和分号将实体名称括起来:
&entityName;
当处理包含许多特殊字符的大块XML或HTML时,可以使用CDATA部分。 CDATA部分的工作原理类似于HTML中的<code>...</code>,只是更加强大:CDATA部分中的所有空格都是有意义的,并且其中的字符不被解释为XML。 CDATA部分以<![[CDATA[开始,以]]>结束。
下面是CDATA部分的一个示例。
<p><termdef id="dt-cdsection" term="CDATA Section"<<term>CDATA sections</term> may occur anywhere character data may occur; they are used to escape blocks of text containing characters which would otherwise be recognized as markup. CDATA sections begin with the string "<code><![CDATA[</code>" and end with the string "<code>]]></code>"
解析后,此文本将显示如下:
CDATA部分可能出现在任何可能出现字符数据的地方;它们用于转义包含否则将被识别为标记的字符的文本块。 CDATA部分以字符串"<![CDATA["开始,以字符串"]]>"结束。
CDATA的存在使得正确回显XML有点棘手。如果要输出的文本不在CDATA部分中,则文本中的任何尖括号、和和其他特殊字符应替换为相应的实体引用。(替换左尖括号和和最重要,其他字符将被正确解释而不会误导解析器。)但是,如果输出文本在CDATA部分中,则不应进行替换,从而导致文本与前面示例中的类似。在像我们的SAXLocalNameCount应用程序这样的简单程序中,这并不特别严重。但是,许多XML过滤应用程序将希望跟踪文本是否出现在CDATA部分中,以便它们可以正确处理特殊字符。
以下代码设置解析器并启动它:
static public void main(String[] args) throws Exception { // 用于解析命令行参数的代码(如上所示) // ... SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setNamespaceAware(true); SAXParser saxParser = spf.newSAXParser(); }
这些代码行创建了一个SAXParserFactory实例,由javax.xml.parsers.SAXParserFactory系统属性的设置确定。通过将setNamespaceAware设置为true,设置要创建的工厂支持XML命名空间,然后通过调用其newSAXParser()方法从工厂中获取一个SAXParser实例。
注意:javax.xml.parsers.SAXParser类是一个定义了许多便利方法的包装器。它包装了(稍微不太友好的)org.xml.sax.Parser对象。如果需要,可以使用SAXParser类的getParser()方法获取该解析器。
现在您需要实现所有解析器必须实现的XMLReader。应用程序使用XMLReader告诉SAX解析器对所讨论的文档执行什么处理。在main方法中的以下代码中实现了XMLReader。
// ... SAXParser saxParser = spf.newSAXParser(); XMLReader xmlReader = saxParser.getXMLReader(); xmlReader.setContentHandler(new SAXLocalNameCount()); xmlReader.parse(convertToFileURL(filename));
在这里,您通过调用SAXParser实例的getXMLReader()方法获取解析器的XMLReader实例。然后,XMLReader将SAXLocalNameCount类注册为其内容处理程序,以便解析器执行的操作是处理内容事件中显示的startDocument()、startElement()和endDocument()方法。最后,XMLReader通过将XML文件的位置以File URL的形式传递给解析器来告诉解析器解析哪个文档,这是由设置I/O中定义的convertToFileURL()方法生成的。
您现在可以开始使用解析器,但最好实现一些错误处理。解析器可以生成三种类型的错误:致命错误、错误和警告。当发生致命错误时,解析器无法继续。所以如果应用程序没有生成异常,那么默认的错误事件处理程序会生成一个异常。但对于非致命错误和警告,默认的错误处理程序不会生成异常,也不会显示任何消息。
如文档事件所示,应用程序的事件处理方法会抛出SAXException。例如,ContentHandler接口中startDocument()方法的签名被定义为返回SAXException。
public void startDocument() throws SAXException { /* ... */ }
可以使用消息、另一个异常或两者来构造SAXException。
由于默认解析器只为致命错误生成异常,并且由默认解析器提供的有关错误的信息有些有限,因此SAXLocalNameCount程序通过MyErrorHandler类定义了自己的错误处理。
xmlReader.setErrorHandler(new MyErrorHandler(System.err)); // ... private static class MyErrorHandler implements ErrorHandler { private PrintStream out; MyErrorHandler(PrintStream out) { this.out = out; } private String getParseExceptionInfo(SAXParseException spe) { String systemId = spe.getSystemId(); if (systemId == null) { systemId = "null"; } String info = "URI=" + systemId + " Line=" + spe.getLineNumber() + ": " + spe.getMessage(); return info; } public void warning(SAXParseException spe) throws SAXException { out.println("Warning: " + getParseExceptionInfo(spe)); } public void error(SAXParseException spe) throws SAXException { String message = "Error: " + getParseExceptionInfo(spe); throw new SAXException(message); } public void fatalError(SAXParseException spe) throws SAXException { String message = "Fatal Error: " + getParseExceptionInfo(spe); throw new SAXException(message); } }
与“设置解析器”中的方法类似,该方法展示了将XMLReader指向正确的内容处理器,这里通过调用setErrorHandler()方法将XMLReader指向新的错误处理器。
MyErrorHandler类实现了标准的org.xml.sax.ErrorHandler接口,并定义了一个方法来获取解析器生成的任何SAXParseException实例提供的异常信息。这个方法getParseExceptionInfo()简单地通过调用标准的SAXParseException方法getLineNumber()和getSystemId()来获取XML文档中错误发生的行号和运行该文档的系统的标识符。然后,将这个异常信息传递给基本的SAX错误处理方法error()、warning()和fatalError()的实现,这些方法将更新相应的消息,提供有关文档中错误的性质和位置的信息。
当XML文档不满足有效性约束时,会发生非致命错误。如果解析器发现文档无效,则会生成一个错误事件。当给定文档类型定义(DTD)或模式的验证解析器遇到无效的标签、在不允许的位置找到标签或(在模式的情况下)元素包含无效数据时,会生成此类错误。
了解非致命错误的最重要原则是,默认情况下会忽略它们。但是,如果文档中发生验证错误,您可能不希望继续处理它。您可能希望将这些错误视为致命错误。
要接管错误处理,您需要覆盖DefaultHandler中处理致命错误、非致命错误和警告的方法,作为ErrorHandler接口的一部分。正如在上一节的代码示例中所示,SAX解析器将SAXParseException传递给这些方法中的每一个,因此当发生错误时,抛出异常就是如此简单。
注意:检查org.xml.sax.helpers.DefaultHandler中定义的错误处理方法可能是有益的。您会发现error()和warning()方法什么也不做,而fatalError()方法会抛出一个异常。当然,您始终可以覆盖fatalError()方法以抛出不同的异常。但是,如果您的代码在发生致命错误时没有抛出异常,那么SAX解析器将抛出异常。这是XML规范的要求。
默认情况下,警告也会被忽略。警告是信息性的,只有在存在DTD或模式的情况下才会生成。例如,如果在DTD中定义了两次一个元素,就会生成一个警告。这是不合法的,但它不会引起问题,但您可能希望知道这一点,因为它可能不是有意为之。将XML文档与DTD进行验证将在本节中介绍。
以下步骤说明如何运行不带验证的SAX解析器示例。
SAXLocalNameCount.java
文件保存在名为sax
的目录中。javac sax/SAXLocalNameCount.java
rich_iii.xml
和two_gent.xml
保存在data
目录中。选择data目录中的一个XML文件,并在其上运行SAXLocalNameCount程序。这里,我们选择在文件rich_iii.xml上运行该程序。
java sax/SAXLocalNameCount data/rich_iii.xml
XML文件rich_iii.xml包含了威廉·莎士比亚的戏剧《理查三世》的XML版本。当您在其上运行SAXLocalNameCount时,应该会看到以下输出。
本地名称“STAGEDIR”出现230次 本地名称“PERSONA”出现39次 本地名称“SPEECH”出现1089次 本地名称“SCENE”出现25次 本地名称“ACT”出现5次 本地名称“PGROUP”出现4次 本地名称“PLAY”出现1次 本地名称“PLAYSUBT”出现1次 本地名称“FM”出现1次 本地名称“SPEAKER”出现1091次 本地名称“TITLE”出现32次 本地名称“GRPDESCR”出现4次 本地名称“P”出现4次 本地名称“SCNDESCR”出现1次 本地名称“PERSONAE”出现1次 本地名称“LINE”出现3696次
SAXLocalNameCount程序解析了XML文件,并提供了每个类型的XML标签的实例数量计数。
为了检查错误处理是否有效,请删除XML文件中一个条目的结束标签,例如第21行的结束标签</PERSONA>,如下所示。
21 <PERSONA>EDWARD, Prince of Wales, afterwards King Edward V.</PERSONA>
这次,您应该会看到以下致命错误消息。
Exception in thread "main" org.xml.sax.SAXException: Fatal Error: URI=file:data/rich_iii.xml Line=21: The element type "PERSONA" must be terminated by the matching end-tag "</PERSONA>".
如您所见,当遇到错误时,解析器会生成一个SAXParseException,它是SAXException的子类,用于标识发生错误的文件和位置。