
用 Java dom4j 解析 XML:从创建文档到查询节点
XML 常见于配置文件、数据交换、旧系统接口和各类行业标准。Java 自带的 XML API 能处理这些数据,但直接操作有时比较繁琐;dom4j 则提供了相对简洁的文档树模型,可用于创建 XML、解析文档、修改节点和执行 XPath 查询。
本文从 Maven 配置开始,依次介绍文档创建、文件和字符串解析、节点与属性读取、XPath 查询、命名空间处理,以及外部实体等安全问题。示例使用 dom4j 2.x API,适合 Java 项目中的一般 XML 读写任务。需要注意,XML 解析并不只是“把文本转成对象”:输入来源、文档大小、命名空间和解析器配置都会影响程序的正确性与安全性。
一、dom4j 适合做什么
dom4j 将 XML 表示为文档树:文档由根元素、子元素、属性、文本和注释等节点组成。它适合以下场景:
- 读取结构不太复杂的 XML 配置或业务报文,并按节点关系提取数据。
- 动态创建、修改 XML 文档,再保存或发送给其他系统。
- 使用 XPath 按路径、属性或条件定位节点。
- 处理已有 Java 项目中的 XML,且项目已经使用 dom4j 相关 API。
如果只需绑定固定结构、将 XML 映射成 Java 对象,可以考虑 JAXB 等数据绑定方案;如果处理超大 XML,且不能把整份文档放进内存,可以考虑 StAX 或 SAX 这类流式解析方式。dom4j 的树模型通常会在内存中保留整棵文档树,文件越大,内存占用通常也越高。它不是所有 XML 任务的默认答案。
二、添加依赖
下面示例使用 Maven。XPath 支持与 Jaxen 有关,显式声明依赖可以让项目配置更清楚,也避免因依赖传递配置不同而缺少 XPath 实现。
<dependencies>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
版本号不是永远固定的。实际项目应根据团队的依赖管理策略选择经过验证的版本,并检查其传递依赖和 Java 版本要求。以下代码使用常见的 dom4j 2.x API。
三、创建 XML 文档
创建文档时,通常从 DocumentHelper.createDocument() 开始,再创建根元素和子元素。元素内容可通过 setText 设置,属性可通过 addAttribute 添加。不要把整段 XML 当作普通字符串拼接,否则容易遗漏转义规则;使用树 API 时,特殊字符由 XML 序列化器处理。
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import java.io.StringWriter;
public class CreateXmlExample {
public static void main(String[] args) throws Exception {
Document document = DocumentHelper.createDocument();
Element root = document.addElement("catalog");
root.addAttribute("source", "demo");
Element book = root.addElement("book");
book.addAttribute("id", "b-101");
book.addElement("title").setText("XML & Java");
book.addElement("price").setText("39.90");
OutputFormat format = OutputFormat.createPrettyPrint();
format.setEncoding("UTF-8");
StringWriter out = new StringWriter();
XMLWriter writer = new XMLWriter(out, format);
writer.write(document);
writer.close();
System.out.println(out);
}
}
这里的 setText("XML & Java") 在 Java 源码中实际传入的是包含 & 字符的文本;序列化时,XML 输出会将其转义为合法的实体表示。本文代码区为 HTML 页面做了转义,复制代码时应注意区分 HTML 转义和 Java 字符串内容。更直观地说,在普通 Java 源码中写 setText("XML & Java") 时,若希望文本是“XML & Java”,字符串中的 & 就是普通字符;XMLWriter 会负责输出 XML 所需的转义。
若要保存为文件,可将 StringWriter 换成字符或字节输出流,并在使用完毕后关闭流。指定 XML 声明中的编码时,应让实际输出流的编码与格式声明一致。对文件写入还要考虑覆盖策略、临时文件替换和异常时的清理,不能只依赖示例中的控制台输出。
四、解析字符串、文件与输入流
dom4j 的 SAXReader 可以读取字符串、文件或输入流。解析字符串时,调用 DocumentHelper.parseText 比较直接;解析文件或流时,可使用 SAXReader.read。输入必须是完整且格式正确的 XML,格式错误通常会导致解析异常,而不是自动修复。
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
public class ParseStringExample {
public static void main(String[] args) throws Exception {
String xml =
"<catalog>" +
" <book id='b-101'>" +
" <title>Java XML</title>" +
" <price>39.90</price>" +
" </book>" +
"</catalog>";
Document document = DocumentHelper.parseText(xml);
Element root = document.getRootElement();
System.out.println(root.getName());
System.out.println(root.element("book").elementText("title"));
}
}
上例中的 XML 字符串使用单引号包围属性值,以便少写 Java 字符串转义。若 XML 来自外部输入,解析前不要用字符串替换的方式“修复”特殊字符;应按数据格式生成合法 XML,或者由发送方修正输入。
读取文件可以这样写:
import org.dom4j.Document;
import org.dom4j.io.SAXReader;
import java.io.File;
public class ParseFileExample {
public static void main(String[] args) throws Exception {
SAXReader reader = new SAXReader();
Document document = reader.read(new File("data/catalog.xml"));
System.out.println(document.getRootElement().getName());
}
}
这段基础示例适用于可信文件,但不应直接照搬为解析不可信 XML 的安全配置。文件解析还涉及文件路径权限:如果路径来自用户输入,必须限制可访问目录,防止路径穿越或读取应用权限范围内的其他文件。
五、读取元素、属性和文本
最常用的读取方式是从根元素开始逐层访问。element("name") 返回指定名称的第一个直接子元素,找不到时返回 null;elements("name") 返回匹配的直接子元素列表。elementText("name") 读取对应子元素的文本,目标元素不存在时同样可能得到 null。
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.DocumentHelper;
import java.util.List;
public class ReadNodesExample {
public static void main(String[] args) throws Exception {
String xml =
"<catalog>" +
" <book id='b-101'>" +
" <title>Java XML</title>" +
" <price>39.90</price>" +
" </book>" +
" <book id='b-102'>" +
" <title>DOM Basics</title>" +
" <price>25.00</price>" +
" </book>" +
"</catalog>";
Document document = DocumentHelper.parseText(xml);
Element root = document.getRootElement();
List<Element> books = root.elements("book");
for (Element book : books) {
String id = book.attributeValue("id");
String title = book.elementText("title");
String price = book.elementText("price");
System.out.println(id + " | " + title + " | " + price);
}
}
}
读取外部 XML 时,不要假设节点一定存在或内容一定可转换。例如价格字段可能缺失、为空或不是合法数字。先检查空值,再按业务规则解析,并对异常数据给出明确错误。attributeValue 在属性不存在时也会返回 null。
getText() 与 getTextTrim() 都面向元素的文本内容,但它们不会替代对 XML 结构的理解。若元素包含多个子元素或混合内容,简单取文本可能不符合预期;应根据文档结构决定是读取直接文本、子元素文本,还是完整内容。对混合内容应特别谨慎,例如:
<p>请阅读 <em>重要</em> 说明。</p>
此时“元素文本”并不是一个独立的、与子元素无关的字段。遇到复杂内容时,先检查节点结构,再决定使用文本访问器还是遍历子节点。
六、用 XPath 查询节点
当文档层级较深,或者需要按属性、条件定位时,XPath 可以减少逐层遍历代码。dom4j 可通过 selectNodes 查询多个节点,通过 selectSingleNode 取一个节点。以下表达式选择所有图书标题,以及价格大于 30 的图书。
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Node;
import java.util.List;
public class XPathExample {
public static void main(String[] args) throws Exception {
String xml =
"<catalog>" +
" <book id='b-101'><title>Java XML</title><price>39.90</price></book>" +
" <book id='b-102'><title>DOM Basics</title><price>25.00</price>" +
"</catalog>";
Document document = DocumentHelper.parseText(xml);
List<Node> titles = document.selectNodes("/catalog/book/title");
for (Node node : titles) {
System.out.println(node.getText());
}
List<Node> expensiveBooks =
document.selectNodes("/catalog/book[number(price) > 30]");
for (Node node : expensiveBooks) {
System.out.println(node.asXML());
}
}
}
XPath 表达式中的 / 从文档根部开始,// 表示在当前范围内查找后代节点;谓词方括号可按位置或条件筛选。XPath 1.0 的数值和字符串转换有自己的规则,不应把它当成 Java 表达式。特别是空文本、非数字文本和多节点转换,可能产生与直觉不同的结果。对关键业务值,通常更稳妥的做法是先选出节点,再在 Java 中执行严格校验。
若只需一个结果,仍应考虑“可能查不到”的情况:
import org.dom4j.Node;
Node titleNode = document.selectSingleNode("/catalog/book[@id='b-101']/title");
if (titleNode != null) {
System.out.println(titleNode.getText());
} else {
System.out.println("未找到指定标题");
}
不要把用户直接提供的字符串拼接进 XPath 表达式。例如将用户输入直接拼入属性谓词,可能造成表达式被改变,产生错误结果或越权访问其他节点。对动态条件,应优先使用 dom4j 提供的变量上下文等受支持机制,并根据所用版本验证具体 API;如果没有使用变量绑定,应严格限定输入、正确构造表达式,并避免把 XPath 当作安全边界。不同查询实现和版本对扩展能力的支持也可能不同。
七、处理 XML 命名空间
带命名空间的 XML 是常见的查询错误来源。下面两个元素即使本地名称都是 book,只要命名空间 URI 不同,就不是同一个 XML 名称:
<catalog xmlns="urn:books">
<book><title>Java XML</title></book>
</catalog>
默认命名空间不会让 XPath 中未带前缀的名称自动匹配该命名空间。因此,表达式 /catalog/book 往往匹配不到上例中的元素。应为 XPath 注册一个前缀,并让前缀映射到 XML 中实际的命名空间 URI。前缀本身可以不同,URI 必须一致。
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Namespace;
import org.dom4j.XPath;
import java.util.HashMap;
import java.util.Map;
public class NamespaceXPathExample {
public static void main(String[] args) throws Exception {
String xml =
"<catalog xmlns='urn:books'>" +
" <book><title>Java XML</title></book>" +
"</catalog>";
Document document = DocumentHelper.parseText(xml);
Map<String, String> namespaces = new HashMap<>();
namespaces.put("b", "urn:books");
XPath xpath = DocumentHelper.createXPath("/b:catalog/b:book/b:title");
xpath.setNamespaceURIs(namespaces);
System.out.println(xpath.selectSingleNode(document).getText());
}
}
具体项目中也可以按使用的 dom4j 版本选择相应的命名空间上下文 API。关键原则不变:XPath 表达式中的前缀要在查询上下文中绑定到正确 URI,不能只看 XML 文本里使用了什么前缀。两个 XML 文档使用不同前缀但相同 URI,仍可能代表相同命名空间。
八、解析不可信 XML:安全边界
XML 外部实体可能让解析器读取本地文件或访问网络资源,形成 XXE 等风险。风险取决于解析器实现、功能开关、运行环境和输入内容,不能因为“使用的是 dom4j”就推断已经安全。对不可信来源的 XML,应明确禁止 DTD 和外部实体,并验证所使用的 SAX 解析器是否支持这些设置。
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.xml.sax.SAXException;
import java.io.InputStream;
public class SecureXmlReader {
public static Document readUntrusted(InputStream input)
throws DocumentException, SAXException {
SAXReader reader = new SAXReader();
// 禁止文档类型声明;这是拒绝含 DTD 输入的严格策略。
reader.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", true);
// 禁止外部一般实体、外部参数实体以及外部 DTD 加载。
reader.setFeature(
"http://xml.org/sax/features/external-general-entities", false);
reader.setFeature(
"http://xml.org/sax/features/external-parameter-entities", false);
reader.setFeature(
"http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
return reader.read(input);
}
}
上述功能标识是常见 SAX 实现支持的配置项,但并非所有 SAX 驱动都保证支持。若某项设置不被识别或不被支持,解析器可能抛出异常。安全代码应将这种情况视为配置失败并拒绝继续处理,而不是忽略异常后照常解析。部署时要针对实际使用的 JDK、解析器提供者和依赖组合做测试。对于允许 DTD 的业务,还需另行设计受控的实体解析策略,不能简单放开外部访问。
即使关闭外部实体,也要防范资源消耗问题。对来自网络、用户上传或消息队列的 XML,应设置输入大小限制、处理时间限制和合理的并发控制;对超大文档,优先考虑流式解析。还应限制应用进程可访问的文件和网络资源,因为解析器配置不是系统权限的替代品。
示例方法没有关闭传入的 InputStream,因为流通常由调用方拥有。调用方应使用 try-with-resources 管理它:
try (InputStream input = openInput()) {
Document document = SecureXmlReader.readUntrusted(input);
// 使用 document
}
实际使用时应根据 readUntrusted 的异常签名处理解析失败和安全特性配置失败。若应用要求对任何不兼容解析器都拒绝启动,可在初始化阶段设置并验证解析器能力,而不是等到处理用户输入时才发现环境不支持。
九、修改节点与序列化
dom4j 也可以在解析后修改文档树,例如更新文本、添加属性或追加节点:
Element book = document.getRootElement().element("book");
if (book != null) {
book.addAttribute("status", "active");
Element price = book.element("price");
if (price != null) {
price.setText("42.00");
}
book.addElement("note").setText("已更新");
}
修改完成后可通过 XMLWriter 输出到文件或字符串。输出时应明确字符编码,并使用结构化 API 写节点,避免将未经处理的输入拼接成 XML 片段。若需要保留原文件的格式、注释、空白或特定声明形式,应先验证解析再序列化后的结果;树模型的读写过程不保证逐字节保留原始排版。
十、常见错误与排查方法
- 节点查不到:确认节点是直接子节点还是更深层后代;检查 XML 大小写;检查默认命名空间和 XPath 命名空间映射。XML 名称区分大小写。
- 空指针异常:
element、elementText和selectSingleNode都可能遇到目标不存在的情况。读取前检查返回值,不要假设文档始终符合预期。 - 中文乱码:检查 XML 声明、字节流实际编码和输出编码是否一致。尽量把原始字节流交给 XML 解析器,由 XML 声明和解析规则识别编码;不要先用错误字符集解码成字符串。
- 特殊字符导致解析失败:文本中的
<、&等字符必须符合 XML 规则。创建文档时使用元素 API,读取时不要用字符串拼接生成未转义片段。 - XPath 执行时报错或找不到实现:检查 Jaxen 等 XPath 相关依赖是否存在、依赖版本是否冲突,并确认表达式符合所使用实现支持的 XPath 语法。
- 解析大文件时内存不足:dom4j 树模型会构造文档结构。减少无用副本并不能消除整树驻留的成本;需要处理超大输入时,评估 SAX 或 StAX。
- 启用安全选项后解析失败:检查是否有 DTD、外部实体或解析器不支持的功能开关。安全选项拒绝输入时,不要为了“先跑起来”就无条件关闭防护。
十一、实践建议
在一般业务代码中,可以把 XML 处理拆成几个清晰步骤:先确认输入来源和大小限制,再用配置明确的解析器读取;随后检查根元素、命名空间和必需字段;将文本转换为业务类型时做格式校验;最后再执行查询或修改。对于固定结构的业务报文,节点缺失和类型错误应有明确处理策略,而不是依靠空指针异常暴露问题。
XPath 适合表达清晰的节点定位逻辑,不适合替代完整的业务校验。表达式越复杂,越应拆分并配合测试,尤其要覆盖空文档、缺失节点、重复节点、默认命名空间和异常文本等边界情况。解析外部 XML 时,安全设置、输入限制和最小权限应一起考虑。
掌握这些边界后,dom4j 的常用流程并不复杂:用 DocumentHelper 创建文档或由 SAXReader 解析输入,使用元素 API 遍历常规结构,在需要时用带正确命名空间映射的 XPath 查询,最后通过 XMLWriter 输出。选择合适的解析方式,并对输入和解析器行为做验证,才是让 XML 处理代码可靠的关键。




