尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
dom4j实战指南:Java中XML增删改查与常见坑全解析
最近接手一个老项目发现配置文件解析到处都在操作 XML用的正是标题里写的这个“domj4”——严格说它的大名是 dom4j圈内人都习惯这么叫。我花了两天把整套增删改查保存流程重新撸了一遍顺手把踩过的坑全记下来了。这篇就专注讲清楚用 dom4j 做 XML 开发时文档对象模型怎么组织、增删改查每一步怎么写、保存文件时为什么老是乱码或没有缩进、以及 XPath 查询取不到值的真实原因。适合刚接触 Java XML 操作的开发者也适合写过几轮 dom4j 但一直被编码和格式化问题折腾的人。1. 整体思路为什么 XML 操作绕不开 dom4j1.1 先搞懂 dom4j 在 Java XML 解析里的位置Java 里操作 XML 的方式其实非常多官方有 JAXP 自带的 DOM、SAX、StAX社区又有 JDOM、dom4j、XStream 这一票库。dom4j 属于“第三方 DOM 风格解析器”它把整个 XML 文档直接读进内存构建成一棵对象树之后你可以随便遍历、修改、删除、新增最后再把整棵树写回文件。这个“读进来变成树改完再吐出去”的方式最大的好处就是直观。你看到的 XML 长什么样代码里的对象结构就长什么样。比如config database host127.0.0.1/host /database /config在 dom4j 里就是一个 Document 对象下面挂着一个名为 config 的根 Element再往下挂 database、host。修改时不需要关心文件偏移量不需要手动拼字符串只需要拿到对应的 Element调用 setText 换掉内容就行。dom4j 在 2000 年左右开始流行到现在仍然有大量遗留系统和中小型项目的配置模块在用。你去看很多开源项目的 XML 读写工具类底层都是 dom4j。所以不管新项目选型有没有它掌握这套 API 依然是 Java 开发的基本功之一。1.2 核心对象模型Document、Element、Attribute 到底怎么配合我第一次用 dom4j 时最懵的是不知道拿到手的是什么对象。后来我把它的对象模型归纳成三层Document代表整个 XML 文档可以理解为一个“文件容器”。它本身不存业务数据主要用来获取根元素、设置编码、写文件。Element代表一个 XML 节点也就是尖括号里的那一对标签。节点可以有子节点、可以有属性、可以有文本内容。Attribute代表节点上的属性也就是user id1里的id1。属性不是子节点它挂在 Element 上。另外还有 Text、Comment、CDATA、ProcessingInstruction 这些更细的节点类型。日常增删改查中95% 时间都在操作 Element 和 AttributeDocument 只在开头和结尾出现。有个细节容易忽略Element 的getText()方法返回的是该节点直接包含的文本而不是所有后代文本拼接。比如ahellobworld/b/a对 a 节点调用 getText() 得到的是hello而不是helloworld。如果想把整个子树里的文本都取出来要递归遍历或者用getStringValue()。1.3 选型对比dom4j、JDOM、JAXP 原生 DOM 怎么选很多教程喜欢罗列一堆对比但落到实际项目里选择逻辑其实很简单如果你只需要读一次配置文件、不修改、不写回直接用 Java 自带的 JAXP DOM 就够了零依赖。如果你的 XML 文件特别大比如几十 MB 上百 MB不要用 dom4j用 SAX 或者 StAX 流式解析否则内存会炸。如果你要频繁增删改查、动态生成 XML、还要用 XPath 精确定位dom4j 是最顺手的。dom4j 相对 JDOM 的优势是 XPath 支持更完整API 更紧凑。dom4j 里一行document.selectSingleNode(//user[id1])就能查到节点JDOM 早期版本做同样的事要绕不少弯。另外 dom4j 对 CDATA、命名空间、格式化输出的控制也更细。所以我的结论是中小型 XML 操作首选 dom4j简单粗暴别想太多。2. 环境准备与核心 API动手前必须搞清的几件事2.1 Maven 依赖与版本选择dom4j 的坐标很稳定直接用 Maven 引入即可。我个人现在用的是 2.1.4因为 1.x 系列太老部分 API 已经被标记废弃而且 2.x 对 Java 8 和模块化支持更好。dependency groupIdorg.dom4j/groupId artifactIddom4j/artifactId version2.1.4/version /dependency如果你还在用 1.6.1 这类老版本建议升级。2.x 改动的核心点是把原本依赖的 JAXP 行为统一了并且在 XPath 处理上更规范。升级时多数代码不用动但要注意DocumentFactory这类扩展点有变化。另外如果你要用 XPath必须额外引入 jaxen否则运行时会报ClassNotFoundException: org.jaxen.JaxenException。这个坑特别经典很多人只加了 dom4j一跑selectNodes就挂。dependency groupIdjaxen/groupId artifactIdjaxen/artifactId version1.2.0/version /dependency2.2 读懂 Node 接口和分支类型Element、Text、Comment、CDATAdom4j 里所有节点都实现org.dom4j.Node接口但这个接口的方法很长初学者容易一头扎进去出不来。我的建议是不要背接口记住两个分支Branch拥有子节点的节点包括 Document 和 Element。Leaf没有子节点的节点比如 Text、Comment、Attribute、CDATA。Branch类有addElement、addComment、addCDATA、content()这些方法。Element 本身既是 Branch又可以有属性和文本。所以你会发现 Element 的方法特别多比如addAttribute来自 ElementaddElement来自 Branch。实际操作时你只需要记住Element 是唯一需要大量操作的节点Document 只做容器。获取节点集合时elements()返回所有子 Elementcontent()返回所有子节点包括文本节点和注释。如果你要判断一个子节点是什么类型用instanceof就行。2.3 输出格式与编码XMLWriter 和 OutputFormat 的配合XML 保存的核心是XMLWriter而格式控制在OutputFormat。很多人只写writer.write(document)结果保存出来的 XML 全挤在一行或者中文乱码就是因为没设置 OutputFormat。我常用的格式设置是这样的OutputFormat format new OutputFormat(); format.setEncoding(UTF-8); format.setIndentSize(4); format.setNewlines(true); format.setTrimText(true); format.setPadText(true);这几个参数含义setEncoding重要必须和文件写入流的编码一致否则乱码。setIndentSize缩进空格数美观用的。setNewlines在节点之间换行否则全是一行。setTrimText把文本节点的首尾空白去掉避免解析时因为格式化而混入多余空格。setPadText在atext/a这种内联文本两边补空格默认看情况。还有一个setExpandEmptyElements默认 false意思是空节点输出为node/设为 true 后输出为node/node。如果下游系统是严格的老派解析器可能需要调这个。2.4 从路径到修改XPath 在这里的真正用途dom4j 的 XPath 支持分两类绝对路径/config/database/host和相对路径//host。前者从根找后者在整个文档里模糊搜索。最常用的是//因为大多数配置文件层级不深直接按节点名找最省事。例如Element host (Element) document.selectSingleNode(//host);查到的是第一个匹配的节点。如果要用属性条件定位Element user (Element) document.selectSingleNode(//user[id1]);这里id是属性过滤。还有按文本内容定位的Element node (Element) document.selectSingleNode(//item[text()abc]);XPath 在增删改查里最大的价值不是查询而是“定位”。你想改某个深层节点的文本不需要写一堆递归遍历一行 XPath 拿到 Element 后直接 setText 即可。3. 实操从零实现 XML 的增删改查保存3.1 构建根文档两种常见方式实际操作中要么从已有 XML 文件加载要么直接内存新建。这两个场景都绕不开 Document 的构建。从文件加载推荐用 SAXReaderimport org.dom4j.Document; import org.dom4j.io.SAXReader; import java.io.File; public class XmlDemo { public static Document load(File file) throws Exception { SAXReader reader new SAXReader(); return reader.read(file); } }read方法也支持 InputStream、Reader、URL。需要注意如果传入的是 InputStream解析完流会被关闭再次操作同一个流会报错。所以比较稳妥的做法是直接传 File或者把流先读成字节数组再包装。从内存新建更简单import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; Document document DocumentHelper.createDocument(); Element root document.addElement(config);这样根节点 config 就建好了之后所有子节点都挂在 root 下面。这里有个容易被忽略的点DocumentHelper.createDocument()创建的 Document 根节点是空的必须自己addElement而通过 SAXReader 读进来的 Document 已经带根节点再次addElement就会变成第二个根节点保存时直接报错“Document already has a root element”。所以拿到 Document 后先判断document.getRootElement()是否为 null 再决定要不要 add。3.2 新增节点setText、addAttribute、addElement 的层级关系新增节点的套路非常固定。比如要生成下面这段 XMLusers user id1 statusactive name张三/name age28/age /user /users代码如下Element root document.addElement(users); Element user root.addElement(user); user.addAttribute(id, 1); user.addAttribute(status, active); Element name user.addElement(name); name.setText(张三); Element age user.addElement(age); age.setText(28);你会发现 dom4j 的 API 是链式的addElement返回新创建的 Element然后继续往下挂子节点就行。addAttribute返回当前 Element所以也可以写成user.addAttribute(id, 1).addAttribute(status, active);我实际编码时更喜欢分开写因为链式太长容易看不清层级关系调试时也要拆开。新增时有个细节setText和addElement不能混用在一个节点上否则文本节点和元素节点会共存保存后变成name张三extranext/extra/name这种结构。大多数情况下这不是你想要的所以要么用文本节点要么用元素子节点二选一。如果想新增注释root.addComment(这个文件由程序自动生成);保存后会出现在 users 节点之前。新增 CDATA 段Element script root.addElement(script); script.addCDATA(if (a b) { alert(no); });CDATA 的作用是让里面包含、等特殊字符时不转义比较适合存代码片段和 JSON 字符串。3.3 查询节点迭代器、XPath、条件筛选场景查询是增删改查里最有意思的部分因为它决定了你后续怎么定位。最简单的遍历方式ListElement users document.getRootElement().elements(user); for (Element user : users) { String id user.attributeValue(id); String name user.elementText(name); System.out.println(id - name); }elements(user)只返回直接子节点中名为 user 的节点不递归。如果想递归拿到子树里所有某个名字的节点XPath 最香ListNode nodes document.selectNodes(//user); for (Node node : nodes) { Element element (Element) node; // ... }如果筛选条件比较复杂比如“status 为 active 并且年龄大于 25”XPath 虽然也能写但可读性差我一般直接内存过滤ListElement users document.selectNodes(//user); ListElement result new ArrayList(); for (Element user : users) { String status user.attributeValue(status); Element ageElement user.element(age); if (active.equals(status) ageElement ! null Integer.parseInt(ageElement.getTextTrim()) 25) { result.add(user); } }这种做法的好处是逻辑直观而且避开了 XPath 对命名空间的复杂处理。如果你处理的 XML 没有命名空间那 XPath 随便用一旦有xmlns前缀情况会复杂不少后面我会单独说。另一个查询细节是elementText和element(name).getText()的区别elementText(name)如果子节点存在直接返回其文本不存在返回 null不会抛异常。element(name).getText()如果子节点不存在element 返回 null再调 getText 会空指针。所以老手都用elementText取子节点文本少写很多判空。3.4 修改节点改文本、改属性、改节点名与替换节点修改节点要注意“对象是引用”这个特性。你通过 XPath 或者遍历拿到的 Element本身就是文档树里的节点直接 set 什么保存时就变成什么。修改文本Element name user.element(name); name.setText(李四);修改属性值user.addAttribute(status, inactive);注意这里不是“新增”addAttribute遇到同名属性会直接覆盖旧值。想删除属性用user.remove(attribute)或user.removeAttribute(attribute)。修改节点名稍微麻烦一点dom4j 没有直接的setName方法但 Element 有setName吗实际是有的Element接口继承了setName(String)所以可以Element node (Element) document.selectSingleNode(//oldNode); node.setName(newNode);这个操作只改标签名子节点和属性都保留。替换整个节点更常用的是借助 ListElement oldNode (Element) document.selectSingleNode(//user[id1]); Element parent oldNode.getParent(); int index parent.indexOf(oldNode); ListElement children parent.elements(); children.set(index, newElement);不过这种操作容易出问题因为elements()返回的是树内部 List直接 set 不一定同步。我更推荐先删除再插入新节点Element parent oldNode.getParent(); parent.remove(oldNode); parent.add(newElement);这样新节点会排到最后不会保持原来的位置。如果必须保持位置要先记录 index再使用parent.elements().add(index, newElement)并且先 remove 再 add一般能保持位置。3.5 删除节点remove 和 detach 的区别删除节点的第一种方式是调用父节点的 removeElement parent user.getParent(); parent.remove(user);第二种是直接调用节点自己的detach()user.detach();两者最终都能把节点从树上去掉。区别在于remove是父节点调用删除指定子节点detach是节点自己调用自动从父节点脱离。底层实现差不多但语义上detach更适合在不确定父节点是谁的时候用。需要特别注意的是remove之后这个节点对象还是可用的只是不在树里了。你把它重新挂到其他节点下也没问题。而如果把这个节点对象引用还留在某个 List 里继续遍历可能触发并发修改异常。删除带子树的节点整个子树会一起没了没有回收站。建议在删除前先确认目标节点选对了别把根节点删了。3.6 保存回文件write 与缩进换行细节保存这一步是重灾区。很多人前面都好好的最后保存出来要么乱码要么没有换行。正确写法我建议封装成工具方法public static void writeDocument(Document document, File target) throws Exception { OutputFormat format new OutputFormat(); format.setEncoding(UTF-8); format.setIndentSize(4); format.setNewlines(true); format.setTrimText(true); try (FileOutputStream out new FileOutputStream(target)) { XMLWriter writer new XMLWriter(out, format); writer.write(document); writer.flush(); } }注意这里用的是 FileOutputStream 而不是 FileWriter。原因很简单FileWriter 默认使用平台编码Windows 上可能输出 GBK而 XML 声明里写的是 UTF-8两边不一致就直接乱码。FileOutputStream 是字节流配合 OutputFormat 里设置的 UTF-8才能保证编码统一。另外有些场景要求 XML 第一行有?xml version1.0 encodingUTF-8?默认 XMLWriter 会输出。如果你不想输出声明可以用format.setSuppressDeclaration(true);需要控制 XML 编码为 UTF-8 时如果声明缺失很多解析器会默认按 UTF-8 读但在 Windows 记事本环境下容易读成乱码。建议保留声明。4. 常见问题与坑位实录4.1 中文乱码和编码声明的坑乱码的原因很简单读写两端的编码不一致。但排查时经常被误导。我遇到过的情况是代码里明明设置了 UTF-8保存出来还是乱码最后发现是 Windows 环境变量里file.encoding被改成了 GBK而代码里用FileWriter直接写字符串绕过了 OutputFormat 的编码设置。所以第一原则写 XML 永远不要用 FileWriter直接用 FileOutputStream 或 OutputStreamWriter并显式指定编码。读 XML 时也有类似的坑。SAXReader 默认按 XML 声明里的编码读取但如果 XML 文件没有声明编码而且你直接用 File 读取可能按平台默认编码解析。稳妥做法是SAXReader reader new SAXReader(); reader.setEncoding(UTF-8);这样的话即使文件声明缺失也会把内容按 UTF-8 读进来。不过要小心如果文件声明是 GBK你强制 UTF-8 反而会乱码。所以这个设置要结合实际情况处理。4.2 格式化输出不生效的坑很多人在代码里设置了setIndentSize和setNewlines但保存出来的 XML 依然是一行原因是 setTrimText 没开。文本节点里的换行和缩进会被当成文本内容带进树里导致格式化逻辑被干扰。打开setTrimText(true)后会把纯空白文本清掉才能让 XMLWriter 按自己的格式重新排。还有一个隐蔽点如果你用OutputFormat.createPrettyPrint()默认缩进是\t不是空格。有些下游系统解析时会把 tab 和空格混用看起来没问题但 diff 或者文本比对时会出问题。我习惯自己 new OutputFormat 再设 setIndentSize(4)始终用 4 个空格保持跟项目里其他代码风格一致。4.3 特殊字符转义与 CDATAXML 里、、、、这五个字符在文本节点里会被转义。比如你想存a b保存后会是a lt; b读回来时又变成a b这是正常的。但如果内容里混合了 HTML 标签或者有一段 JS 代码转义后看起来会很痛苦这时候就该用 CDATA。用 CDATA 保存Element script root.addElement(script); script.addCDATA(if (a b c d) { run(); });保存后script![CDATA[if (a b c d) { run(); }]]/script读 CDATA 内容时dom4j 会把它作为 CDATA 节点但getText()依然能拿到原文。所以读取端不用区分是普通文本还是 CDATA只要在写端按需选择即可。我踩过最难受的坑是用 CDATA 包了字符串]]保存后直接破坏结构。遇到内容里有]]要拆成两个 CDATA 拼接或者改用普通文本转义。4.4 XPath 取不到值命名空间问题如果你处理的 XML 带默认命名空间比如config xmlnshttp://example.com/config database host127.0.0.1/host /database /config直接document.selectSingleNode(//host)是查不到结果的。因为 XPath 的//host匹配的是无命名空间的 host而文档里的 host 实际属于http://example.com/config这个命名空间。解决方式有两种。第一种是注册命名空间并在 XPath 里带前缀MapString, String nsMap new HashMap(); nsMap.put(c, http://example.com/config); Document document reader.read(file); XPath xpath document.createXPath(//c:host); xpath.setNamespaceURIs(nsMap); Node node xpath.selectSingleNode(document);第二种是干脆不用命名空间感知很多配置文件其实不需要严格区分命名空间。但 SAXReader 默认是启用了命名空间处理的XPath 查找时就会受限。我的习惯是能改 XML 源头就去掉默认命名空间不能改就用第一种方式注册。千万别硬记前缀名因为 XML 前缀只是个映射实际匹配靠 URI。4.5 只读集合 ConcurrentModificationException 问题dom4j 里elements()和content()返回的 List 是树内部的实时视图。一边遍历一边 remove 会出现ConcurrentModificationException。我最初就犯过这种错for (Element user : document.selectNodes(//user)) { if (condition) { user.getParent().remove(user); // 触发并发修改 } }解决办法是先把要删除的节点收集到一个临时 List遍历完成后再统一删除ListElement needRemove new ArrayList(); for (Element user : document.selectNodes(//user)) { if (condition) { needRemove.add(user); } } for (Element user : needRemove) { user.detach(); }同理添加节点时也不要在遍历 elements() 时直接 add先收集再操作更安全。4.6 实战速查表为了大家日常查阅方便我把最常用的操作整理成一张表操作推荐写法备注加载 XML 文件new SAXReader().read(file)文件较大时勿用会吃内存创建空文档DocumentHelper.createDocument()需手动 addElement 根节点添加子节点parent.addElement(node)返回新 Element可链式设置文本element.setText(text)覆盖原有文本获取子节点文本element.elementText(child)子节点缺失时返回 null添加属性element.addAttribute(key,value)同名属性会覆盖获取属性值element.attributeValue(key)属性不存在返回 nullXPath 查询单节点document.selectSingleNode(//node)无匹配返回 null删除节点node.detach()比 parent.remove 更简洁保存文件XMLWriter FileOutputStream注意编码和 OutputFormat输出格式化setIndentSize setNewlines配合 setTrimText 使用这张表基本覆盖了日常 90% 的 XML 操作场景。剩下的边角知识碰到具体问题时再查官方文档就行。个人在实际项目中体会最深的是dom4j 本身并不复杂真正折磨人的永远是编码、格式、命名空间这三个周边问题。所以每次新写 XML 工具类我会先把工具方法固定下来里面直接封装 UTF-8 编码 4 空格缩进 删除前收集 XPath 命名空间注册。后面再写业务代码只管增删改查不碰这些底层细节项目的稳定性会高很多。如果你也经常被这些坑折腾建议照这个思路先做一个小工具类后面能省一大半调试时间。
RELATED

相关推荐

PHP反序列化调试实战:用Xdebug精准定位类型丢失问题

PHP反序列化调试实战:用Xdebug精准定位类型丢失问题

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/24 3:10:50
卷积神经网络(CNN)核心原理与图像特征提取实战解析

卷积神经网络(CNN)核心原理与图像特征提取实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/24 3:10:50
基于PCA9422与PIC32MX675F256L的嵌入式电源管理设计

基于PCA9422与PIC32MX675F256L的嵌入式电源管理设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/24 3:10:50
MORE NEWS

更多资讯

📰

从盲测A/B到taste层:motionmaxxing四代迭代史,复盘它如何一步步杀死AI味

【免费下载链接】motionmaxxing Looksmaxxing for motion graphics. An agent skill that turns AI slop videos into films that look made by a motion designer. 项目地址: https://gitcode.com/gh_mirrors/mo/motionmaxxing 点击查看 免费下载 motionmaxxing 是…

📰

从预览版到开源权重:M3.1 Flash 一个月内走完了两段路

从预览版到开源权重:M3.1 Flash 一个月内走完了两段路 【免费下载链接】MiniMax-M3.1-Flash MiniMax-M3.1-Flash 是一款原生多模态混合专家(MoE)模型,面向编程、智能体工作流、计算机使用和网络安全等场景。它支持文本、图像和视频…

📰

Java开发者就业突围:技术升级与全栈能力构建

1. Java开发者面临的就业现状分析2023年Stack Overflow开发者调查报告显示,Java在全球编程语言使用率中仍保持前五位置,但企业招聘需求确实出现了结构性变化。我接触过的数十位Java开发者普遍反映:初级岗位竞争激烈,3年经验以下的…

📰

LX Music:免费多音源聚合音乐播放器,一个入口搜全五大平台

LX Music:免费多音源聚合音乐播放器,一个入口搜全五大平台 【免费下载链接】lx-music-desktop 一个基于 Electron 的音乐软件 项目地址: https://gitcode.com/GitHub_Trending/lx/lx-music-desktop LX Music 是基于 Electron 的免费开源桌面音乐播…

📰

无没有了没有了

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

TgtoDrive进阶技巧:多网盘联动与资源聚合搜索配置

TgtoDrive进阶技巧:多网盘联动与资源聚合搜索配置 TgtoDrive是一款强大的网盘媒体自动化平台,它通过巧妙的多网盘联动机制和资源聚合搜索配置,让用户能够高效管理115、123、光鸭云盘等多个网盘资源。本文将为您揭示如何配置这些高级功能&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬