1. 概述

SAX(Simple API for XML)是一种用于解析 XML 文档的轻量级 API。

本文将带你深入理解 SAX 的核心机制,探讨它的适用场景,并通过实际代码演示如何高效使用 SAX 解析器。对于需要处理大体积 XML 文件、关注内存占用的场景,SAX 是一个非常值得考虑的选择。

2. SAX:基于事件的 XML 解析

SAX 的本质是一个事件驱动的解析器。它不会一次性把整个 XML 文件加载进内存,而是边读边解析,遇到标签开始、标签结束、文本内容等节点时,就触发对应的事件。

我们通过继承 DefaultHandler 并重写其回调方法来捕获这些事件。整个过程就像“流式”处理,数据过一遍就丢,✅ 内存占用极低,非常适合处理大型 XML 文件。

⚠️ 与 DOM 不同,SAX 不保留文档结构,解析完就没了,所以你无法像操作树那样来回跳转。

3. SAX vs DOM:内存与灵活性的权衡

对比项 SAX DOM
解析方式 事件驱动(流式) 树形结构(加载整个文档)
内存占用 ✅ 极低,适合大文件 ❌ 高,整树驻留内存
随机访问 ❌ 不支持,只能顺序处理 ✅ 支持 XPath,可任意跳转
使用复杂度 ✅ 简单粗暴,适合简单提取 ✅ 复杂操作更方便

总结

  • 如果你只是想“捞”出几个关键字段,XML 文件又很大,选 SAX 准没错。
  • 如果你需要频繁查询、修改、遍历整个文档结构,DOM 更合适。

4. SAX vs StAX:推 vs 拉的哲学

StAX(Streaming API for XML)是比 SAX 更新的流式解析方案,核心区别在于:

  • SAX 是“推”模式:解析器主动调用你的回调方法(startElement, endElement),你只能被动接收。
  • StAX 是“拉”模式:你主动调用 next() 去“拉”下一个事件,控制权在你手里。

StAX 的优势

  • 更容易编写清晰的逻辑,避免回调地狱。
  • 可以随时停止解析,比如只读前几条记录。
  • 代码更易测试和调试。

⚠️ 但 SAX 的优势是支持 XML Schema 验证,而 StAX 原生不支持。

5. 使用自定义 Handler 解析 XML

我们以下面这个表示 Baeldung 网站文章列表的 XML 为例:

<baeldung>
    <articles>
        <article>
            <title>Parsing an XML File Using SAX Parser</title>
            <content>SAX Parser's Lorem ipsum...</content>
        </article>
        <article>
            <title>Parsing an XML File Using DOM Parser</title>
            <content>DOM Parser's Lorem ipsum...</content>
        </article>
        <article>
            <title>Parsing an XML File Using StAX Parser</title>
            <content>StAX's Lorem ipsum...</content>
        </article>
    </articles>
</baeldung>

5.1 定义数据模型

先创建对应的 POJO:

public class Baeldung {
    private List<BaeldungArticle> articleList;
    
    // getter and setter
    public List<BaeldungArticle> getArticleList() {
        return articleList;
    }

    public void setArticleList(List<BaeldungArticle> articleList) {
        this.articleList = articleList;
    }
}
public class BaeldungArticle {
    private String title;
    private String content;

    // getters and setters
    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

5.2 实现自定义 Handler

核心是继承 DefaultHandler,重写关键方法:

public class BaeldungHandler extends DefaultHandler {
    private static final String ARTICLES = "articles";
    private static final String ARTICLE = "article";
    private static final String TITLE = "title";
    private static final String CONTENT = "content";

    private Baeldung website;
    private StringBuilder elementValue;

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (elementValue == null) {
            elementValue = new StringBuilder();
        } else {
            elementValue.append(ch, start, length);
        }
    }

    @Override
    public void startDocument() throws SAXException {
        website = new Baeldung();
    }

    @Override
    public void startElement(String uri, String lName, String qName, Attributes attr) throws SAXException {
        switch (qName) {
            case ARTICLES:
                website.articleList = new ArrayList<>();
                break;
            case ARTICLE:
                website.articleList.add(new BaeldungArticle());
                break;
            case TITLE:
                elementValue = new StringBuilder();
                break;
            case CONTENT:
                elementValue = new StringBuilder();
                break;
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        switch (qName) {
            case TITLE:
                latestArticle().setTitle(elementValue.toString());
                break;
            case CONTENT:
                latestArticle().setContent(elementValue.toString());
                break;
        }
        // 注意:这里没有清空 elementValue,下次 startElement 会重新初始化
    }

    private BaeldungArticle latestArticle() {
        List<BaeldungArticle> articleList = website.articleList;
        int latestArticleIndex = articleList.size() - 1;
        return articleList.get(latestArticleIndex);
    }

    public Baeldung getWebsite() {
        return website;
    }
}

⚠️ 踩坑提醒

  • characters() 方法可能被多次调用(比如文本被分段读取),所以要用 StringBuilder 累积。
  • elementValuestartElement 中初始化,在 endElement 使用后不会立即清空,依赖下一次 startElement 覆盖。确保只在 TITLECONTENT 这类文本节点中使用。
  • 该 Handler 不是线程安全的,因为内部状态(website, elementValue)在多次回调间共享。

6. 测试解析器

// 初始化 SAX 工厂和解析器
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
BaeldungHandler handler = new BaeldungHandler();

// 执行解析
saxParser.parse("src/test/resources/sax/baeldung.xml", handler);

// 获取结果并验证
Baeldung result = handler.getWebsite();

assertNotNull(result);
List<BaeldungArticle> articles = result.getArticleList();

assertNotNull(articles);
assertEquals(3, articles.size());

BaeldungArticle articleOne = articles.get(0);
assertEquals("Parsing an XML File Using SAX Parser", articleOne.getTitle());
assertEquals("SAX Parser's Lorem ipsum...", articleOne.getContent());

BaeldungArticle articleTwo = articles.get(1);
assertEquals("Parsing an XML File Using DOM Parser", articleTwo.getTitle());
assertEquals("DOM Parser's Lorem ipsum...", articleTwo.getContent());

BaeldungArticle articleThree = articles.get(2);
assertEquals("Parsing an XML File Using StAX Parser", articleThree.getTitle());
assertEquals("StAX Parser's Lorem ipsum...", articleThree.getContent());

测试通过,说明 XML 被正确解析,所有文章数据都已提取。

7. 总结

SAX 是处理 XML 的“瑞士军刀”之一,尤其适合:

  • 大文件解析(内存友好)
  • 只需提取部分数据的场景
  • 对性能要求较高的后端服务

虽然它的事件驱动模型需要适应,但一旦掌握,写起解析逻辑来非常高效。在实际项目中,如果遇到 XML 性能瓶颈,不妨试试 SAX,说不定能带来意想不到的提升。

示例代码已托管至 GitHub:https://github.com/baeldung/tutorials/tree/master/xml


原始标题:Parsing an XML File Using SAX Parser | Baeldung