说实话Java后端做到一定阶段几乎绕不开XML解析这件事。哪怕现在JSON已经是数据交换的主流XML依然稳如老狗地待在配置系统、报文协议、旧版框架适配这些场景里。而这门课选的Dom4j又是Java生态里开发效率最高的那一档解析库——我第一次用的时候觉得它比JDK自带的DOM那堆API优雅太多了。这篇内容不是教科书式地罗列API而是按我在项目里实际使用Dom4j的路径来拆先搞懂为什么需要它再动手读取、创建、修改XML最后配合XPath定位节点顺带把那些不踩一遍根本不知道的坑全部挑明。1. XML解析这件事为什么值得专门讲一课很多新人对XML有误解觉得它是一种过时的数据格式。但你去看现在的Java技术栈pom.xml是XMLweb.xml是XMLmybatis的全局配置是XMLSpring的早期配置也是XML。更不用说在银行、电信这些行业里系统间接口报文几乎全是XML。所以“会写Java”和“能处理XML”是两码事后者更像一种基础设施能力。XML本身是一种“自描述”的层级结构。一个标签可以带属性也可以嵌套子标签数据天然形成一棵树。比如book idB001 categoryJava titleJava编程基础/title author张三/author price59.9/price /book这里book是父节点title、author、price是子节点id和category是属性。程序要使用这些数据就必须把这种纯文本结构转换成内存里的对象——这就是“解析”的本质把XML字符流变成Java对象树或者变成一个个可处理的事件。我习惯用一个类比XML文件就像一份带目录的纸质档案解析器做的就是把档案的内容逐项录入电子表格。DOM方式是一键全量导入SAX方式是逐行读完马上登记而Dom4j是导入的同时还给你一套特别好用的检索和编辑工具。你不需要一次性把所有API背下来只需要抓住三条主线创建、读取、修改。这三条主线解决90%以上的业务需求。2. 解析方式横向对比Dom4j赢在哪里Java生态里XML解析方案不少JDK自带的最典型有DOM、SAX后来JDK 6加入StAX再加上第三方库Dom4j、JDOM等。很多人第一次接触会被绕晕我用表格直接对比常用方案每行都是我实际用过的体感方案模型优点缺点典型场景DOM树模型符合思维习惯可随意读写一次性加载全部内容内存吃紧小文件解析、需要频繁修改SAX事件模型流式处理内存占用极低只能顺序读不能随意跳转和修改超大文件解析上百MBStAX拉式流模型性能好读写双向API较底层编码繁琐高性能中间件Dom4j树模型增强接口友好链式调用支持XPath需要引入第三方依赖日常业务开发大部分场景从这个表能看出来DOM是全量树SAX是推送事件StAX是主动拉取流。Dom4j本质上吸收了DOM的易用性又在接口设计上做了大量优化把“读写XML”这件事的代码量压缩到最低。举个极端的例子用原生DOM创建一个带属性的节点要写四五行的模板代码Dom4j一行链式调用就搞定了。选Dom4j还有一个很现实的原因——它在国内的生态惯性太大了。早期的很多Java框架和项目底层XML处理用的就是Dom4j。你看一些讲Web框架和中间件源码的文章经常能看到它的影子。如果你只打算学一种XML解析方式Dom4j的性价比是最高的。另外Dom4j从2.x版本开始解决了JDK 9模块系统的兼容问题所以我的建议很明确不要再用网上老教程里翻出来的1.6.1直接上2.1.4省得后面遇到一堆ClassNotFound异常。3. 动手前的准备依赖引入和核心API图谱用Maven的话一个依赖就够了。这里我强烈建议把XPath的依赖也一并带上后面你会感谢这个决定dependency groupIdorg.dom4j/groupId artifactIddom4j/artifactId version2.1.4/version /dependency dependency groupIdjaxen/groupId artifactIdjaxen/artifactId version1.2.0/version /dependencyGradle用户对应加上这两条即可。在写代码之前先把Dom4j里的核心对象对应到XML本身会更容易理解。XML里有标签、属性、文本Dom4j就提供对应的Element元素、Attribute属性、Text文本模型。它们组合构成一棵Document树Document就是整棵树的根容器。简单说Document对应一个XML文件Element对应一个标签节点Attribute对应标签上的属性Text对应标签里的文字内容。从操作上讲主要记住这几个类类/接口作用关键行为SAXReader读入XML文件read()方法返回DocumentDocument文档对象getRootElement()、selectNodes()Element元素节点addElement()、element()、elements()、elementText()Attribute属性节点通过element.attributeValue()读取DocumentHelper工厂类创建Document、创建Element、创建XPathOutputFormat输出格式控制缩进、编码XMLWriter写出XML把Document写到文件或Writer有个新手经常忽略的细节SAXReader的read()方法不只接受File还接受InputStream、Reader甚至是URL。所以在实际项目里你完全可以直接从一个输入流构建Document不需要先落盘成临时文件。关于编码问题我多说一句。很多人在读XML时遇到乱码第一反应是给SAXReader设置setEncoding但根本原因往往是文件本身的编码和XML声明不一致。比如文件实际是UTF-8但第一行声明了GBK。所以要么保证两种编码一致要么在读取前指定和文件一致的编码。4. 第一个实战读取并遍历XML理论说再多不如直接写代码。我准备了一个典型的XML样本模拟某系统中的书籍列表。这里只保留两本书实际项目里可能是上千本?xml version1.0 encodingUTF-8? books book idB001 categoryJava titleJava编程基础/title author张三/author price59.9/price /book book idB002 categoryWeb title深入浅出Spring/title author李四/author price79.0/price /book /books读取它并遍历所有book节点import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.File; import java.util.Iterator; public class XmlReadDemo { public static void main(String[] args) throws Exception { SAXReader reader new SAXReader(); Document document reader.read(new File(books.xml)); Element root document.getRootElement(); System.out.println(根节点: root.getName()); // 方式一迭代器遍历适合大数据量场景 for (IteratorElement it root.elementIterator(book); it.hasNext();) { Element book it.next(); String id book.attributeValue(id); String title book.elementText(title); String author book.elementText(author); String price book.elementText(price); System.out.println(id id , title title , author author , price price); } } }运行结果根节点: books idB001, titleJava编程基础, author张三, price59.9 idB002, title深入浅出Spring, author李四, price79.0这里的elementText()是个高频API它直接返回子标签内的文本内部帮你处理了嵌套查找比我早期手动getText()再trim好太多。如果文本里可能有空白和换行推荐用elementTextTrim()它会自动去掉首尾空白。我不建议所有场景都无脑遍历。如果你确定每本书的tag只有一个title那用elementText(title)很干净。如果同名子节点有多个那就需要elements(子节点名)拿到列表再分别处理。还有一个遍历时删除节点的经典坑。比较典型的错误写法是在迭代器循环里直接调用element.getParent().remove(element)。虽然Dom4j允许这么做但有时候你会遇到ConcurrentModificationException或者漏删的情况。最稳妥的方式是先收集要删除的节点循环结束后再统一删除ListElement removeList new ArrayList(); for (IteratorElement it root.elementIterator(book); it.hasNext();) { Element book it.next(); if (B002.equals(book.attributeValue(id))) { removeList.add(book); } } for (Element e : removeList) { e.detach(); }detach()是Dom4j的Node接口提供的“摘除”方法比通过parent.remove()更直白后面删节点的时候还会见到它。5. 第二个实战创建、修改、删除XML很多教程把重点放在读取上但实际项目里动态生成XML同样常见。比如你要给接口生成请求报文或者导出一批配置数据到文件。Dom4j最爽的地方就在这里——创建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.FileOutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; public class XmlCreateDemo { public static void main(String[] args) throws Exception { Document document DocumentHelper.createDocument(); // 根元素 Element config document.addElement(config); // 子元素链式添加属性 Element database config.addElement(database) .addAttribute(type, mysql); Element host database.addElement(host).addText(127.0.0.1); database.addElement(port).addText(3306); database.addElement(name).addText(shop); // 格式化输出 OutputFormat format OutputFormat.createPrettyPrint(); format.setEncoding(StandardCharsets.UTF_8.name()); try (FileOutputStream fos new FileOutputStream(config.xml)) { OutputStreamWriter writer new OutputStreamWriter(fos, StandardCharsets.UTF_8); XMLWriter xmlWriter new XMLWriter(writer, format); xmlWriter.write(document); xmlWriter.flush(); } } }生成的config.xml?xml version1.0 encodingUTF-8? config database typemysql host127.0.0.1/host port3306/port nameshop/name /database /config这里有两个我踩过多次的关键细节必须单独拿出来讲。第一写入文件别用FileWriter要用OutputStreamWriter并指定UTF-8。JDK的FileWriter默认依赖平台编码在Windows机器上会写出GBK编码的文件。如果XML声明里写着encodingUTF-8结果实际字节是GBK那文件到Linux服务器就产生了乱码。这个属于项目里最难排查的隐性Bug因为它本地跑起来一切正常。第二OutputFormat.createPrettyPrint()和createCompactFormat()要区分用。PrettyPrint带缩进和换行人类看着舒服适合配置文件CompactFormat把节点压成一行体积小适合报文传输。如果报文串明文传输用CompactFormat能省不少带宽。再来说修改。修改的本质是“找到节点改掉内容”。假设你想要把上面config.xml里的port改成3307并将database的type属性改成postgresqlDocument document new SAXReader().read(new File(config.xml)); Element database document.getRootElement().element(database); database.setAttributeValue(type, postgresql); database.element(port).setText(3307);setAttributeValue这个API很聪明如果属性存在就更新值如果属性不存在就自动创建。所以没必要先去判断属性是否存在。setText同理它无缝覆盖文本内容如果节点还带着一堆子节点这些子节点会被清掉所以只对叶子节点用setText。删除操作也很直接Element port database.element(port); port.detach();detach()执行后的瞬间这个节点就从父节点里摘除了内存树里那份引用也切断了父子关系。如果只是把节点引用置为null那起不到任何删除效果节点还在原来的位置。6. 进阶XPath快速定位节点遍历是“无差别扫荡”XPath则是“精准狙击”。我把XPath加入这课有一个很重要的理由当你面对一个复杂到让人头秃的XML结构时XPath可以在几行代码内跳过层层嵌套直达目标节点。Dom4j的selectNodes()和selectSingleNode()方法让我们可以把XPath表达式直接当成查询语句用。还是拿之前的books.xml来说public class XPathDemo { public static void main(String[] args) throws Exception { Document document new SAXReader().read(new File(books.xml)); // 选取所有book节点 ListNode allBooks document.selectNodes(//book); // 选取category为Java的book Node javaBook document.selectSingleNode(//book[categoryJava]); System.out.println(((Element) javaBook).elementText(title)); // 选取price大于60的book ListNode expensiveBooks document.selectNodes(//book[price 60]); for (Node node : expensiveBooks) { Element book (Element) node; System.out.println(book.attributeValue(id)); } } }XPath表达式本身不复杂但有几个要点需要刻意记忆表达式含义/books/book根路径下的book子节点//book任意层级下的book节点/books/book[1]第一个book节点XPath索引从1开始//book[id]带有id属性的book节点//book[idB001]id属性值为B001的book节点//book[price 60]price子元素值大于60的book节点//book/title/text()取所有title的文本内容XPath里的索引从1开始这一点和Java的习惯不一样我每次从0开始取结果都会一脸懵所以大家记牢。还有一个高发问题带命名空间的XML用XPath查不到节点。典型场景是SOAP协议报文和Spring配置文件ns:book xmlns:nshttp://example.com/ns ns:titleJava编程基础/ns:title /ns:book这种情况下直接写//ns:book如果没注册前缀selectNodes返回的就是空列表。解决方式有二一是创建一个Map给XPath设置NamespaceContext二是绕过命名空间用local-name()来匹配document.selectNodes(//*[local-name()book]);这个方法在很多兼容场景下最省事缺点是表达式可读性变差看你自己权衡。XPath好用但也不是银弹。如果文档结构非常深非常复杂selectNodes每执行一次都会在树上做一轮匹配计算。对于几千到几万节点的规模完全不是问题但如果XML超大且查询极其频繁建议还是提前把数据映射成Java对象后续从内存对象里操作。7. 真实项目中的应用场景与排查手册到了这里Dom4j的读写你已经会了。但一个新工具真正融入项目还需要知道它被用在哪些地方以及出了问题往哪个方向查。先说场景都是我亲历过的。第一个场景是配置文件解析。一些老系统里的XML配置需要程序启动时读取并转换成配置对象。这种场景不需要什么高级技巧就是SAXReader读进来然后遍历定义好的标签。第二个场景是接口报文处理。某个系统对外提供接口请求和响应都走XML报文里面嵌套三四层是家常便饭。报文里有消息头、消息体、扩展字段每层都要解析还要回填校验信息。这种场景用Dom4j定位节点和生成报文是非常舒服的。第三个场景是底层框架阅读。很多框架的源码里插件配置通过XML描述底层就是Dom4j解析。你学会了Dom4j再去读这些框架的源码会发现很多地方豁然开朗。第四个场景是数据交换和导出。比如你有一个列表数据需要按约定格式导出成XML发给下游或者从上游批量导入XML。这时候创建文档、写入文件的流程就直接套用第5章的代码。现在把我在报错现场积累的问题整理成一张速查表基本覆盖90%的常见报错问题现象解决方案乱码中文变成问号或火星文读写统一指定UTF-8写入用OutputStreamWriter不要用FileWriter缺少jaxen使用XPath时NoClassDefFoundError引入jaxen依赖或用local-name()绕开非法字符文本包含或时解析报错源数据先清洗转义或用CDATA包裹属性值不存在attributeValue返回null不要直接调用先判空或使用contains方法elementText返回null子节点不存在先调用element(xxx)判空节点删不掉引用置空但文件不变必须调用detach()遍历时删除ConcurrentModificationException先收集后删除或用Iterator.remove()命名空间查不到XPath返回空节点用local-name()或注册NamespaceContext工具类虽然看着简单但在工程落地时还是建议封装一下。我一般会在项目里写一个XmlUtil对外只暴露parseXml、createDoc等方法。目的有两个一是把SAXReader、OutputFormat等配置集中管理二是一旦将来解析库有变化调用方不用跟着改。这也是老手和新手的一个重要区别不把第三方依赖直接撒在业务代码里。8. 踩过几次坑之后的个人心得我写代码这些年XML解析工具换过不少但Dom4j始终是我最常用的一个。原因特别简单它把最常见、最繁琐的操作都简化到了能直接写进业务代码的程度可读性也好别人接手你代码的时候不会对着几十行重复模板发呆。如果要我给一个学习路径那一定是先照着这篇文章的示例把读写跑通再找一段真实的业务XML报文来做练习最后去读几个开源项目的XML解析部分。踩坑不要怕最常见的那几个坑我已经帮你提前标记了真再遇到新的问题先按“编码、依赖、结构”这三类对号入座大概率能找出方向。最后再分享一个小经验如果你只是要从一个XML里取出某一个字段比如只读一个版本号就别把Dom4j整套引进来直接用JDK自带的XML解析或者简单的字符串截取就够了。工具不在多用对地方才算本事。Dom4j的精华在于处理那些结构复杂、需要反复读写、层级深的XML——那才是它的主场。