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
累积。elementValue
在startElement
中初始化,在endElement
使用后不会立即清空,依赖下一次startElement
覆盖。确保只在TITLE
和CONTENT
这类文本节点中使用。- 该 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