SAX (Simple API for XML) 是 Java 原生提供的流式、事件驅動 XML 解析器。與 DOM 全量載入內存不同,SAX 逐行掃描、邊讀邊拋事件,極適合大文件、只讀、內存受限場景。本文深度解析 SAX 核心回調機制,提供生產級 Handler 實現模板,並演示如何通過事件流完美還原 XML 文檔。
解析器讀取 XML 時,遇到特定節點會回調 ContentHandler 接口方法,核心五大事件:
| 事件方法 | 觸發時機 | 核心參數 | 典型用途 |
|---|---|---|---|
| startDocument() | 文檔開始 | 無 | 初始化上下文、重置狀態 |
| endDocument() | 文檔結束 | 無 | 資源清理、最終結果輸出 |
| startElement(uri, localName, qName, attrs) | 元素開始標簽 | 命名空間、標簽名、屬性集合 | 創建對象、解析屬性、入棧 |
| endElement(uri, localName, qName) | 元素結束標簽 | 命名空間、標簽名 | 對象入庫、出棧、文本歸屬 |
| characters(ch[], start, length) | 元素文本內容 | 字符數組、起始、長度 | 提取文本、CDATA、實體引用 |
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
public class SaxDebugHandler 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 attrs)
throws SAXException {
System.out.print("【開始元素】" + qName);
if (attrs != null && attrs.getLength() > 0) {
System.out.print(" 屬性: ");
for (int i = 0; i < attrs.getLength(); i++) {
System.out.print(attrs.getQName(i) + "=\"" + attrs.getValue(i) + "\" ");
}
}
System.out.println();
}
@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);
}
}
}
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
public class SaxReconstructHandler extends DefaultHandler {
private final StringBuilder output = new StringBuilder();
private int depth = 0;
@Override
public void startDocument() throws SAXException {
output.append("\n");
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attrs)
throws SAXException {
indent();
output.append('<').append(qName);
if (attrs != null) {
for (int i = 0; i < attrs.getLength(); i++) {
output.append(' ')
.append(attrs.getQName(i))
.append("=\"")
.append(escapeXml(attrs.getValue(i)))
.append('"');
}
}
output.append('>').append('\n');
depth++;
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
depth--;
indent();
output.append("").append(qName).append(">\n");
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String text = new String(ch, start, length);
if (!text.trim().isEmpty()) {
indent();
output.append(escapeXml(text)).append('\n');
}
}
private void indent() {
for (int i = 0; i < depth; i++) output.append(" ");
}
private String escapeXml(String s) {
return s.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
public String getReconstructedXml() {
return output.toString();
}
}
Harry Potter
J K. Rowling
Learning XML
Erik T. Ray
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
public class SaxParserDemo {
public static void main(String[] args) throws Exception {
// 1. 獲取解析器工廠
SAXParserFactory factory = SAXParserFactory.newInstance();
// 可選:啟用命名空間感知
// factory.setNamespaceAware(true);
// 2. 創建解析器
SAXParser parser = factory.newSAXParser();
// 3. 解析文件
File xmlFile = new File("books.xml");
// 使用調試 Handler
SaxDebugHandler debugHandler = new SaxDebugHandler();
parser.parse(xmlFile, debugHandler);
System.out.println("\n--- 文檔還原演示 ---");
// 使用還原 Handler
SaxReconstructHandler reconHandler = new SaxReconstructHandler();
parser.parse(xmlFile, reconHandler);
System.out.println(reconHandler.getReconstructedXml());
}
}
現象:長文本、CDATA、實體引用會導致 characters 分片回調。
解決:使用 StringBuilder 累積,在 endElement 統一處理:
private StringBuilder textBuffer = new StringBuilder();
@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) {
String completeText = textBuffer.toString().trim();
textBuffer.setLength(0); // 重置
// 處理 completeText...
}
uri:命名空間 URIlocalName:不帶前綴的本地名qName:帶前綴的限定名(如 ns:book)factory.setNamespaceAware(true) 後,優先使用 uri + localName 判斷元素身份。XMLStreamReader) 或分塊處理。| 維度 | SAX | DOM | StAX (Pull) |
|---|---|---|---|
| 解析模式 | Push (推) | Tree (樹) | Pull (拉) |
| 內存佔用 | 極低 (流式) | 高 (全量樹) | 低 (游標) |
| 隨機訪問 | 不支持 | 完全支持 | 不支持 (單向) |
| 修改文檔 | 困難 | 容易 | 困難 |
| 適用場景 | 大文件、只讀、流式轉換 | 小文件、需修改、XPath查詢 | 大文件、需控制流程、高性能 |
SAX 以事件驅動、流式解析著稱,是處理大型 XML 的利器。掌握 DefaultHandler 五大核心回調、characters 分片累積技巧、命名空間識別,並能構建「文檔還原器」驗證解析邏輯,即掌握了 SAX 的精髓。現代項目中,StAX (XMLStreamReader) 提供更靈活的 Pull 模型,可作為 SAX 的進階替代選擇。