Spring源码学习7:番外-SAX

什么是 SAX ?

SAX(Simple API for XML)是一种用于解析XML文档的事件驱动API。与传统的DOM(Document Object Model)解析方式不同,SAX解析不会将整个文档加载到内存中,而是按顺序逐行读取文档,并在遇到XML元素时触发相应的事件

SAX 特点

  • 事件驱动
    SAX解析器在遇到XML文档中的元素、属性、文本节点等时,会触发相应的事件。程序通过实现这些事件处理器来处理解析过程中的数据。(也可以自己下钩子,遇见了特定的XML元素就会触发特定的事件)
  • 顺序读取
    SAX解析器按照文档的顺序依次读取并处理XML数据,不会将整个文档保存在内存中。因此,它的内存消耗较低,适用于处理大型XML文件。
  • 只读解析
    SAX是只读的,它只能用来解析XML文档,无法修改文档结构。
  • 不保留文档结构
    由于SAX是基于事件的,因此它不会保留整个文档结构。开发者需要自己在事件处理器中管理上下文信息(如当前正在解析的元素和层级关系)。

SAX 的主要类和接口

SAX 解析器相关的类和接口
SAXParser
SAXParserFactory
XMLReader
事件处理相关的接口
ContentHandler
ErrorHandler
EntityResolver
DTDHandler
DeclHandler
LexicalHandler
异常类
SAXException
SAXParseException
辅助类
Attributes
InputSource
Locator

SAX 简单案例

步骤一:创建解析器

① SAXParserFactory > ② SAXParser > ③ XMLReader

步骤二:设置事件处理器

实现 ContentHandler、ErrorHandler 等接口,并注册到 XMLReader 中。

步骤三:解析 XML

调用 XMLReader.parse() 方法,传入 InputSource 或文件路径,开始解析 XML 文档。

步骤四:处理事件

在解析过程中,根据 XML 结构触发不同的事件,由注册的处理器来处理。

public class SAXParserExample {

    public static void main(String[] args) {
        try {
            ** 步骤一:创建解析器
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser saxParser = factory.newSAXParser();
            XMLReader xmlReader = saxParser.getXMLReader();

            ** 步骤二:设置事件处理器
            MyHandler handler = new MyHandler();
            xmlReader.setContentHandler(handler);
            xmlReader.setErrorHandler(handler);

            /** 步骤三:解析 XML
            ** File xmlFile = new File("example.xml");
            ** xmlReader.parse(xmlFile.getAbsolutePath());
			**/
			
			** 步骤三:解析 XML
			InputStream inputStream = new FileInputStream("example.xml");
            InputSource inputSource = new InputSource(inputStream);
            inputSource.setEncoding("UTF-8"); // 可选:设置字符编码

            xmlReader.parse(inputSource);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 步骤二:实现事件处理器
class MyHandler extends DefaultHandler {
    
    @Override
    public void startDocument() throws SAXException {
        System.out.println("开始解析文档...");
    }

    @Override
    public void endDocument() throws SAXException {
        System.out.println("文档解析结束.");
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        System.out.println("开始元素: " + qName);
        if (attributes.getLength() > 0) {
            for (int i = 0; i < attributes.getLength(); i++) {
                System.out.println("属性: " + attributes.getQName(i) + " = " + attributes.getValue(i));
            }
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        System.out.println("结束元素: " + qName);
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        String content = new String(ch, start, length).trim();
        if (!content.isEmpty()) {
            System.out.println("元素内容: " + content);
        }
    }

    @Override
    public void error(SAXParseException e) throws SAXException {
        System.out.println("错误: " + e.getMessage());
    }
}
<bookstore>
    <book category="cooking">
        <title lang="en">Everyday Italian</title>
        <author>Giada De Laurentiis</author>
        <year>2005</year>
        <price>30.00</price>
    </book>
</bookstore>
开始解析文档...
开始元素: bookstore
开始元素: book
属性: category = cooking
开始元素: title
属性: lang = en
元素内容: Everyday Italian
结束元素: title
开始元素: author
元素内容: Giada De Laurentiis
结束元素: author
开始元素: year
元素内容: 2005
结束元素: year
开始元素: price
元素内容: 30.00
结束元素: price
结束元素: book
结束元素: bookstore
文档解析结束.

为什么 XMLReader.parse() 方法要使用InputSource,而不是直接使用InputStream ?

XMLReader.parse() 方法使用 InputSource 而不是直接使用 InputStream 是因为 InputSource 是一个更高级别的抽象,它可以封装多种输入源,而不仅仅是 InputStream。使用 InputSource 可以更灵活地处理不同的输入类型。

  1. 封装了多种输入类型:InputSource 可以封装 InputStream(字节流)、Reader(字符流)以及系统标识符(通常是 URI 或 URL),使其成为一个更通用的输入源。
  2. 字符编码处理:通过 InputSource,你可以明确指定字符流的编码方式。如果直接使用 InputStream,解析器需要自己猜测或依赖 XML 声明中的编码声明。
  3. 灵活性:InputSource 可以处理更加复杂的输入场景,例如通过 Reader 读取部分内容,然后切换到 InputStream 读取其他内容,这在某些高级应用中可能会有帮助。

InputSource 和 InputStream 有什么区别?

InputStream 是处理字节数据的低级别工具,而 InputSource 是一个高级别的 SAX 特定工具,它能为 XML 解析器提供更多上下文信息和更高的灵活性。这使得 InputSource 成为 XML 解析中首选的输入源。


核心类:SAXParser

public class SAXParser {
    private final XMLReader xmlReader;
 	...

    public void parse(InputSource source, DefaultHandler handler) throws SAXException, IOException {
        xmlReader.setContentHandler(handler);
        xmlReader.setErrorHandler(handler);
        xmlReader.parse(source);
    }
    ...
}

SAXParser 是用于解析 XML 文档的类,基于 SAX 事件驱动模型。它封装了 XMLReader,提供了更高级的 API 来处理 XML 数据。

核心类:XMLReader

package org.xml.sax;

public interface XMLReader {

    **
     * 设置内容处理器,用于处理 XML 内容事件。
     *
     * @param handler 处理 XML 内容事件的对象。
     * @throws NullPointerException 如果处理器是 null*/
    void setContentHandler(ContentHandler handler);

    **
     * 获取当前的内容处理器。
     *
     * @return 当前的内容处理器。
     */
    ContentHandler getContentHandler();

    **
     * 设置实体解析器,用于解析实体引用。
     *
     * @param resolver 实体解析器。
     * @throws NullPointerException 如果解析器是 null*/
    void setEntityResolver(EntityResolver resolver);

    **
     * 获取当前的实体解析器。
     *
     * @return 当前的实体解析器。
     */
    EntityResolver getEntityResolver();

    **
     * 设置错误处理器,用于处理 XML 解析中的错误。
     *
     * @param handler 错误处理器。
     * @throws NullPointerException 如果处理器是 null*/
    void setErrorHandler(ErrorHandler handler);

    **
     * 获取当前的错误处理器。
     *
     * @return 当前的错误处理器。
     */
    ErrorHandler getErrorHandler();

    **
     * 设置解析器的特性。
     *
     * @param name 特性的名称。
     * @param value 特性的值。
     * @throws SAXNotRecognizedException 如果解析器不识别该特性。
     * @throws SAXNotSupportedException 如果解析器不支持该特性。
     */
    void setFeature(String name, boolean value)
        throws SAXNotRecognizedException, SAXNotSupportedException;

    **
     * 获取解析器的特性值。
     *
     * @param name 特性的名称。
     * @return 特性的值。
     * @throws SAXNotRecognizedException 如果解析器不识别该特性。
     * @throws SAXNotSupportedException 如果解析器不支持该特性。
     */
    boolean getFeature(String name)
        throws SAXNotRecognizedException, SAXNotSupportedException;

    **
     * 设置解析器的属性。
     *
     * @param name 属性的名称。
     * @param value 属性的值。
     * @throws SAXNotRecognizedException 如果解析器不识别该属性。
     * @throws SAXNotSupportedException 如果解析器不支持该属性。
     */
    void setProperty(String name, Object value)
        throws SAXNotRecognizedException, SAXNotSupportedException;

    **
     * 获取解析器的属性值。
     *
     * @param name 属性的名称。
     * @return 属性的值。
     * @throws SAXNotRecognizedException 如果解析器不识别该属性。
     * @throws SAXNotSupportedException 如果解析器不支持该属性。
     */
    Object getProperty(String name)
        throws SAXNotRecognizedException, SAXNotSupportedException;

    **
     * 解析给定的输入源。
     *
     * @param inputSource 输入源。
     * @throws IOException 如果发生 I/O 错误。
     * @throws SAXException 如果解析错误发生。
     */
    void parse(InputSource inputSource)
        throws SAXException, IOException;
}

方法1:setFeature 设置特性

setFeature 方法在 XMLReader 接口中用于设置解析器的特性。这些特性控制解析器的行为,例如启用或禁用特定的功能。这使得 SAX 解析器具有高度的可配置性。

void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException;

name: 特性的名称,表示要设置的特性。例如,`http://xml.org/sax/features/namespaces`。
value: 要设置的特性值, `true` 启用验证,`false` 禁用验证。

主要特性示例

  1. http://xml.org/sax/features/namespaces
    功能: 启用或禁用对 XML 命名空间的支持。
  2. http://xml.org/sax/features/namespace-prefixes
    功能: 启用或禁用命名空间前缀的支持。
  3. http://xml.org/sax/features/validation
    功能: 启用或禁用 DTD 验证。
  4. http://xml.org/sax/features/validation/schema
    功能: 启用或禁用 XML Schema 验证(如果解析器支持 Schema 验证)。

XML 特性是什么?

特性 (Features) 在 XML 解析中,特性是指解析器支持的功能或行为。例如,解析器是否支持命名空间、是否处理空白字符、是否支持外部实体等。

  1. 特性1:命名空间
  2. 特性2:命名空间前缀

命名空间是什么?命名空间前缀是什么?

1:解决名称冲突

在 XML 文档中,不同的元素或属性可能会使用相同的名称,特别是在合并来自不同来源的 XML 数据时。这种情况很容易导致混淆或冲突。命名空间通过为每个元素和属性附加一个独特的标识(通常是一个 URI)来解决这个问题。

示例:
假设你有两个 XML 文档,一个是关于书籍的,另一个是关于客户的。它们都可能有一个名为 的元素。如果你把这两个文档合并在一起,不使用命名空间的话,XML 解析器就无法区分出哪个 元素是书名,哪个是客户名。

	<!-- 无命名空间,可能产生冲突 -->
	<book>
	    <name>XML Basics</name>
	</book>
	
	<customer>
	    <name>John Doe</name>
	</customer>

通过使用命名空间(命名空间前缀通过xmlns定义:格式:xmlns:自定义前缀),你可以明确区分这些元素:

	<!-- 使用命名空间避免冲突 -->
	<book xmlns:b="http://example.com/book">
	    <b:name>XML Basics</b:name>
	</book>
	
	<customer xmlns:c="http://example.com/customer">
	    <c:name>John Doe</c:name>
	</customer>

2:提供上下文和组织数据

命名空间不仅仅是为了避免冲突,它还提供了上下文,使得 XML 文档的结构更清晰,更易于理解和管理。通过命名空间,可以将相关元素和属性组织在一起,使文档更具可读性和逻辑性。

示例:

	<document xmlns:content="http://example.com/content"
	          xmlns:metadata="http://example.com/metadata">
	    <content:title>Example Document</content:title>
	    <metadata:author>John Doe</metadata:author>
	    <metadata:date>2023-09-01</metadata:date>
	</document>

在这个例子中,命名空间帮助将内容信息(如 title)和元数据(如 author 和 date)明确区分开来。这样,文档的结构就更为清晰。

3:标识和引用特定的 XML 词汇表或标准

既然是个URI,那肯定要用起来啊,URI指向的资源里面便是该命名空间有那些元素,那些属性,以及元素所需要遵守的规则!

示例:
一个常见的例子是 XHTML,它是 HTML 的 XML 版本。XHTML 使用了 http://www.w3.org/1999/xhtml 作为其命名空间 URI,这表明文档中的元素和属性应该遵循 XHTML

	<html xmlns="http://www.w3.org/1999/xhtml">
	    <head>
	        <title>XHTML Example</title>
	    </head>
	    <body>
	        <p>Hello, world!</p>
	    </body>
	</html>

在这个例子中,http://www.w3.org/1999/xhtml URI 告诉解析器文档使用了 XHTML 词汇表,这样解析器就可以正确地解析和处理文档。

4:与 DTD 和 XML Schema 配合使用

DTD 比较古老,就不说明了,重点学习 XML Schema

4.1:定义.xsd文件

示例1:XML Schema (XSD) 定义文件 example.xsd

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <!-- xs:simpleType :定义一个简单的类型,类似于内部实体 -->
    <xs:simpleType name="CompanyName">
        <xs:restriction base="xs:string">
            <xs:enumeration value="OpenAI"/>
        </xs:restriction>
    </xs:simpleType>

	<!-- xs:complexType:定义一个复杂的类型 -->
    <xs:element name="note">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="to" type="xs:string"/>
                <xs:element name="from" type="CompanyName"/> <!-- 使用自定义的简单类型 -->
                <xs:element name="heading" type="xs:string"/>
                <xs:element name="body" type="xs:string"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>

对应的 XML 文件 example.xml

<note xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="example.xsd">
    <to>Turing</to>
    <from>OpenAI</from> <!-- 使用定义好的类型,类似于内部实体 -->
    <heading>Reminder</heading>
    <body>Don't forget the meeting at 10 AM.</body>
</note>
  1. <xs:schema> 元素
    <xs:schema> 是定义 XML Schema 的根元素。xmlns:xs="http://www.w3.org/2001/XMLSchema" 声明了 XML Schema 命名空间,使得在此模式定义中使用的所有 xs: 前缀元素都与 XML Schema 规范相关联。

  2. <xs:simpleType> 元素

    • <xs:simpleType> 定义了一个简单类型,简单类型是没有子元素的 XML 元素。这里定义了一个名为 CompanyName 的简单类型。
    • <xs:restriction> 元素用于约束这个简单类型的取值范围。它的 base 属性指定了基本类型,这里是 xs:string,表示它的值是字符串类型。
    • <xs:enumeration> 元素指定了这个字符串类型的取值范围。在这个例子中,CompanyName 只能是 "OpenAI"
  3. <xs:element> 元素

    • <xs:element> 定义了一个元素的结构。这里定义了一个名为 note 的元素。
    • name="note" 表示元素的名称是 note
    • <xs:element name="from" type="CompanyName"/> 这一行代码定义了一个名为 from 的 XML 元素,并且它的类型是 CompanyName。 CompanyName 是之前通过 xs:simpleType 定义的一个简单类型,它只能取 “xm” 作为值。
  4. <xs:complexType> 元素

    • <xs:complexType> 定义了一个复杂类型,复杂类型的 XML 元素可以包含子元素或者属性。这里的 note 元素就是一个复杂类型。
    • <xs:sequence> 元素定义了 note 元素的子元素顺序。在这个例子中,note 元素必须包含 tofromheadingbody 四个子元素,并且它们必须按照定义的顺序出现。
  5. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance
    是一个标准的命名空间 URI,用于标识和引用 W3C 的 XML Schema 相关规范。它并不是用来存放你自己的 XSD 文件。说白了自定义的XSD文件都可以用xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  6. xsi:noNamespaceSchemaLocation
    使用 xsi:noNamespaceSchemaLocation 或 xsi:schemaLocation 属性在 XML 文档中引用你的 XSD 文件。如果你的 XSD 文件在同一目录下,文件名是 note.xsd,你可以如案例这样引用。

4.2:① 引用本地自定义XSD文件
	<?xml version="1.0" encoding="UTF-8"?>
	<note xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	      xsi:noNamespaceSchemaLocation="example.xsd">
	  <to>Tove</to>
	  <from>Jani</from>
	  <heading>Reminder</heading>
	  <body>Don't forget me this weekend!</body>
	</note>
4.2:② 引用远程服务器自定义XSD文件
	<note xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	      xsi:noNamespaceSchemaLocation="http://example.com/schemas/example.xsd">
4.3:Java 项目使用 JAXP 来进行验证

JAXP(Java API for XML Processing)

	import java.io.File;
	import javax.xml.XMLConstants;
	import javax.xml.validation.SchemaFactory;
	import javax.xml.validation.Validator;
	import org.xml.sax.SAXException;
	import java.io.IOException;
	import javax.xml.transform.stream.StreamSource;
	
	public class XMLValidator {
	    public static void main(String[] args) {
	        try {
	            // 创建 SchemaFactory 并加载 .xsd 文件
	            SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
	            File schemaFile = new File("example.xsd");
	            javax.xml.validation.Schema schema = factory.newSchema(schemaFile);
	
	            // 创建 Validator 并验证 XML 文件
	            Validator validator = schema.newValidator();
	            validator.validate(new StreamSource(new File("example.xml")));
	            System.out.println("XML is valid!");
	        } catch (SAXException | IOException e) {
	            System.out.println("XML is invalid: " + e.getMessage());
	        }
	    }
	}

验证通过了,就可以使用我们的XmlBeanDefinitionReader来加载XML文件中的bean定义了!!!


方法2:setEntityResolver(EntityResolver resolver)

当 XML 解析器遇到外部实体(比如通过 <!DOCTYPE> 声明的实体)时,它会调用实现了 EntityResolver 接口的 resolveEntity 方法。开发者可以在这个方法中自定义如何处理这些外部实体,比如从本地缓存中读取,或者直接返回空值来忽略外部实体。

什么是实体?

在 XML 解析过程中,实体(Entity) 是一个可以被引用的文本片段,通常用于在文档中插入外部内容或者定义一组重复使用的文本。

1. 预定义实体
  • &lt; 表示 <
  • &gt; 表示 >
  • &amp; 表示 &
  • &apos; 表示 '
  • &quot; 表示 "
2. 内部实体

在文档内部定义的文本片段。例如:

  <!DOCTYPE note [
    <!ENTITY company "xm">
  ]>
  <note>
    <to>&company;</to>
    <from>&company;</from>
  </note>

在上例中,&company; 实体会在解析时被替换为 xm

3.外部实体
  <!DOCTYPE note [
    <!ENTITY logo SYSTEM "logo.png">
  ]>
  <note>
    <logo>&logo;</logo>
  </note>

在上例中,&logo; 引用外部的 logo.png 文件。


方法3:XMLReader.parse(InputSource inputSource)

问题:事件是怎么触发的?方法与方法之间怎么调用的?总有一个掌控全局的角色存在吧?
答案:XMLReader.parse(InputSource inputSource) 方法就是 全局掌控者 ,该方法读取元素,触发事件。

com.sun.org.apache.xerces.internal.parsers.XMLReaderImpl

源码

package com.sun.org.apache.xerces.internal.parsers;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.IOException;

** XMLReaderImplXerces 实现的 XMLReader 接口的具体类
public class XMLReaderImpl implements XMLReader {

    ** 配置对象,用于存储解析过程中的各种配置
    protected XMLParserConfiguration fConfiguration;

    ** 解析 XML 文档的入口方法
    @Override
    public void parse(InputSource inputSource) throws SAXException, IOException {
        if (inputSource == null) {
            throw new IllegalArgumentException("InputSource cannot be null");
        }

        **InputSource 转换为内部的 XMLInputSource 对象
        XMLInputSource xmlInputSource = new XMLInputSource(inputSource.getPublicId(),
                                                           inputSource.getSystemId(),
                                                           null);

        try {
            ** 调用配置对象的解析方法,开始解析 XML 文档
            fConfiguration.parse(xmlInputSource);
        } catch (XMLParseException e) {
            throw new SAXException(e.getMessage(), e);
        } catch (XNIException e) {
            throw new SAXException(e.getMessage(), e);
        }
    }

    // 其他方法省略...
}
XMLParserConfiguration 接口概述

XMLParserConfiguration 是一个接口,它定义了 XML 解析器配置的基本行为。实际的解析逻辑由具体的实现类完成。Xerces 提供了多个 XMLParserConfiguration 的实现类,其中一个常见的实现类是 StandardParserConfiguration

StandardParserConfiguration.parse(XMLInputSource inputSource) 源码
public class StandardParserConfiguration extends BasicParserConfiguration {

    // XML 文档处理器
    protected XMLDocumentHandler fDocumentHandler;

    // 核心的解析方法
    public void parse(XMLInputSource inputSource) throws XNIException, IOException {
        if (fDocumentHandler != null) {
            // 设置文档定位器
            XMLLocator locator = inputSource.getLocator();
            fDocumentHandler.setDocumentLocator(locator);

            // 开始文档的处理
            fDocumentHandler.startDocument(locator, null, null, null);

            try (Reader reader = inputSource.getCharacterStream()) {
                if (reader == null) {
                    throw new IOException("InputSource does not contain a character stream.");
                }

                int c;
                StringBuilder elementName = new StringBuilder();
                boolean insideElement = false;

                // 实际读取和解析 XML 数据
                while ((c = reader.read()) != -1) {  // 逐字符读取数据
                    char ch = (char) c;

                    if (ch == '<') {
                        insideElement = true;
                        elementName.setLength(0);  // 开始读取元素名
                    } else if (ch == '>') {
                        insideElement = false;
                        // 读取到了完整的元素名,触发 startElement 事件
                        fDocumentHandler.startElement(new QName(elementName.toString()), null, null, null);
                    } else if (insideElement) {
                        elementName.append(ch);  // 构建元素名
                    }
                }

                // 继续解析其他部分,如属性、文本节点等...

            } catch (XNIException e) {
                // 捕获并处理解析过程中发生的异常
                throw new XMLParseException(e.getLocator(), e.getMessage());
            } finally {
                // 结束文档的处理
                fDocumentHandler.endDocument(null);
            }
        }
    }

    // 其他方法和配置省略...
}

定位器XMLLocator)用于提供有关当前 XML 文档的位置信息。它包含文档的起始位置、当前处理的位置、行号、列号等信息。这些信息对于错误报告和调试非常有用。定位器通常用于以下目的:

  • 错误定位:当解析器遇到错误时,可以使用定位器提供的位置信息来帮助确定错误发生的位置。
  • 处理进度:在处理过程中,定位器可以跟踪当前处理的位置,以便能够准确地报告进度或其他文档相关的信息。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值