在Java项目中处理配置文件、第三方接口报文或历史数据交换格式时,XML依然是非常常见的载体。不同于用字符串切割的野路子做法,Java标准库提供了多套成熟API,可以从字节流层面正确还原文档结构与字符编码。理解这些解析方式的底层机制,才能避免内存溢出和乱码问题。

一、DOM解析:把文档变成内存树
DOM(Document Object Model)解析的核心思想是,解析器一次性把整个XML文件读入内存,构建一个由节点组成的树状对象模型。开发者可以像操作普通Java对象一样,随意遍历、修改、增删节点。这种方式对小型配置文件非常友好,代码写起来直观,也方便使用XPath做复杂查询。
不过DOM的代价是内存占用与文件大小成正比。如果一个XML有几百兆,直接DOM解析很容易撑爆堆内存。因此在选择前,要先评估文件体积和部署环境的内存限制。
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
public class DomDemo {
public static void main(String[] args) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 关闭命名空间感知可简化演示,实际项目按需求开启
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
// 从类路径或文件加载,这里以本地文件为例
Document doc = builder.parse("config.xml");
Element root = doc.getDocumentElement();
System.out.println("根节点名: " + root.getNodeName());
NodeList nodes = root.getElementsByTagName("item");
for (int i = 0; i < nodes.getLength(); i++) {
Element item = (Element) nodes.item(i);
String value = item.getTextContent();
System.out.println("item内容: " + value);
}
}
}
上面代码演示了如何用DOM读取根节点下所有名为item的子元素。注意DocumentBuilderFactory默认不开启命名空间感知,若XML带命名空间而代码未设置,会导致getElementsByTagName匹配不到带前缀的节点。
另外,DOM解析在parse方法里如果传入的XML含有外部实体引用,且未做安全限制,可能存在XXE注入风险。生产环境应通过factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)等方式禁用不安全特性。
二、SAX解析:基于事件的流式读取
SAX(Simple API for XML)采用事件驱动模型。解析器从头到尾扫描字节流,遇到开始标签、结束标签、文本内容时,回调开发者编写的Handler方法。由于不构建完整树,SAX的内存占用极低,适合处理超大日志或批量数据文件。
但SAX是单向只读的,不能回头访问已经处理过的节点,也不方便做复杂的结构修改。如果业务逻辑需要在不同层级节点间来回参照,SAX会让代码变得繁琐。
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
public class SaxDemo {
public static void main(String[] args) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
DefaultHandler handler = new DefaultHandler() {
public void startElement(String uri, String localName, String qName, Attributes attrs) {
System.out.println("开始标签: " + qName);
}
public void characters(char[] ch, int start, int length) {
String text = new String(ch, start, length).trim();
if (!text.isEmpty()) {
System.out.println("文本: " + text);
}
}
public void endElement(String uri, String localName, String qName) {
System.out.println("结束标签: " + qName);
}
};
parser.parse("data.xml", handler);
}
}
在characters方法中,需要注意XML解析器可能把一个文本节点拆成多次回调,所以生产代码往往要用StringBuilder缓存,直到endElement再处理完整文本。
SAX同样面临XXE风险,使用时应通过factory.setFeature("http://xml.org/sax/features/external-general-entities", false)关闭外部实体加载。
三、StAX解析:拉取式游标控制
StAX(Streaming API for XML)兼顾了SAX的低内存与DOM的易用性。它提供游标模式,开发者主动调用next()方法推进解析器,拿到当前事件类型后再决定如何处理。这种拉取模型让代码逻辑更线性,也更容易在循环中做条件跳过。
StAX从Java 6起就是标准库的一部分,位于javax.xml.stream包下。对于需要边读边写或者只抽取部分字段的场景,StAX通常比SAX更顺手。
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import java.io.FileInputStream;
public class StaxDemo {
public static void main(String[] args) throws Exception {
XMLInputFactory factory = XMLInputFactory.newInstance();
// 禁用外部实体防止XXE
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
XMLStreamReader reader = factory.createXMLStreamReader(new FileInputStream("data.xml"));
while (reader.hasNext()) {
int event = reader.next();
if (event == XMLStreamReader.START_ELEMENT) {
String name = reader.getLocalName();
if ("item".equals(name)) {
System.out.println("发现item节点");
}
} else if (event == XMLStreamReader.CHARACTERS) {
String txt = reader.getText().trim();
if (!txt.isEmpty()) {
System.out.println("字符内容: " + txt);
}
}
}
reader.close();
}
}
上面示例通过getLocalName获取不带前缀的节点名,避免命名空间前缀变化带来的匹配问题。StAX的XMLStreamReader还支持getAttributeValue直接取属性,比SAX在回调里操作Attributes更直观。
在实际业务里,如果只关心某几个字段,用StAX可以在匹配到目标节点后跳过子树,显著提升大文件处理效率。
四、编码与转义的常见坑
XML文件头部通常声明了<?xml version="1.0" encoding="UTF-8"?>。Java解析器会读取该声明决定解码方式,但如果文件实际保存编码与声明不符,就会出现乱码。最稳妥的做法是用InputStream传给解析器,而不是先用FileReader按平台默认编码读成字符串再解析。
另外XML文本中的小于号、大于号、与号必须写成<、>、&。如果业务数据里包含这些字符,生成XML时应使用StringEscapeUtils或Jackson等库的转义功能,解析时解析器会自动还原,无需手动替换。
| 解析方式 | 内存占用 | 随机访问 | 适用场景 |
|---|---|---|---|
| DOM | 高 | 支持 | 小配置文件、需修改结构 |
| SAX | 低 | 不支持 | 超大文件、单向提取 |
| StAX | 低到中 | 有限 | 大文件局部读取、流式处理 |
综合来看,配置类小文件优先用DOM配合XPath;数据交换大文件用StAX或SAX;对安全性有要求的系统务必在创建解析工厂时禁用DOCTYPE与外部实体。这样既能保证功能正确,也能规避常见漏洞与性能陷阱。