SAX 解析
本章定位 :掌握 SAX(Simple API for XML)的事件驱动解析模型——ContentHandler 回调序列、startElement/endElement/characters,以及内存优势。
定义与作用
SAX (Simple API for XML)是最早的 XML 流式解析 API。它采用"推式"模型:解析器从头到尾扫描 XML 文档,每遇到一个结构(开始标签、文本、结束标签等),就向应用"推送"一个事件回调。
SAX 的最大优势是 内存占用极低——它不在内存中构建树结构,只是顺序读取。解析 500MB 的 XML 文件,DOM 需要 2-3GB 内存,SAX 只需几 MB。
代价是:只能向前读,不能回退;不能修改文档;需要应用自己维护状态(如"我现在在哪个元素内部")。
核心原理:SAX 回调时序
图解释 :SAX 解析器按文档顺序触发事件回调。每个 XML 结构对应一个回调——startElement 在遇到 <tag> 时触发,characters 在遇到文本时触发,endElement 在遇到 </tag> 时触发。应用程序必须自己维护当前所处的上下文。
语法/结构要点
ContentHandler 核心回调
| 回调方法 | 触发时机 | 参数 |
|---|---|---|
startDocument() | 文档开始 | — |
endDocument() | 文档结束 | — |
startElement(uri, localName, qName, atts) | 遇到开始标签 | 命名空间、标签名、属性列表 |
endElement(uri, localName, qName) | 遇到结束标签 | 命名空间、标签名 |
characters(ch, start, length) | 遇到文本内容 | 字符数组、起始位置、长度 |
重要 :
characters()可能被 多次调用——一段文本可能被分割成多次回调。必须将多次回调的文本拼接起来。
完整示例:大翔用 SAX 解析超大日志
场景说明
飞翔科技的运维 大翔 有一个 500MB 的服务器日志 XML,需要提取所有 level="ERROR" 的日志条目的 msg 文本。DOM 会导致内存溢出,他用 SAX 来解决。
SAX Handler 实现
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
public class LogErrorExtractor extends DefaultHandler {
private boolean inLog = false;
private boolean isError = false;
private boolean inMsg = false;
private StringBuilder textBuffer = new StringBuilder();
private int errorCount = 0;
@Override
public void startElement(String uri, String localName,
String qName, Attributes attributes) {
textBuffer.setLength(0); // 每个元素开始清空缓存
if ("log".equals(qName)) {
inLog = true;
String level = attributes.getValue("level");
isError = "ERROR".equals(level);
}
if ("msg".equals(qName) && isError) {
inMsg = true;
}
}
@Override
public void characters(char[] ch, int start, int length) {
textBuffer.append(ch, start, length);
}
@Override
public void endElement(String uri, String localName, String qName) {
if ("msg".equals(qName) && inMsg) {
System.out.println("[" + (++errorCount) + "] "
+ textBuffer.toString().trim());
inMsg = false;
}
if ("log".equals(qName)) {
inLog = false;
isError = false;
}
}
public static void main(String[] args) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse("server_log.xml", new LogErrorExtractor());
}
}
操作结果
[1] 数据库连接超时:jdbc:mysql://db01/order timeout=30000ms
[2] NullPointerException at OrderService.process(OrderService.java:156)
[3] 磁盘空间不足:/data/logs/ 占用 98%
...
大翔的 SAX 解析器在解析 500MB 日志文件时只占用了约 8MB 内存。如果用 DOM 加载整个文件,需要约 2GB 内存。
易错场景
错误一:characters 被多次调用导致数据截断
@Override
public void characters(char[] ch, int start, int length) {
// ❌ 直接赋值会覆盖之前的内容
this.currentText = new String(ch, start, length);
}
// ✅ 在 startElement 清空,在 characters 追加
StringBuilder buffer = new StringBuilder();
public void startElement(...) { buffer.setLength(0); }
public void characters(...) { buffer.append(ch, start, length); }
错误二:条件判断依赖元素的出现顺序
SAX 中元素的处理是线性的。如果逻辑需要"先知道 B 的值再决定如何处理 A",SAX 无法做到(因为不能回退)。这种情况考虑用 StAX 或分两次解析。
面试考点
| 考点 | 参考答案要点 |
|---|---|
| SAX 的事件驱动模型如何工作? | 解析器扫描 XML 文档,遇到开始标签/文本/结束标签时触发 ContentHandler 回调方法。应用在回调中处理数据,不构建完整树 |
| SAX 相比 DOM 的优缺点? | 优点:内存极低、速度快。缺点:只能向前读(单向),不能修改,需要应用自行维护复杂的状态机 |
| characters 为什么可能被多次调用? | SAX 规范允许解析器将一段文本分多次回调。必须用 StringBuilder 在 startElement 清空、characters 追加、endElement 使用完整文本 |