1. XOM到底是个什么包:先把“包”和“库”这两个概念分开
如果你是在沿依赖链排查问题,突然看到一行xom:xom,第一反应大概率是“这是不是哪个团队自己封装的工具包”,甚至怀疑是不是某个内部中间件的简写。我第一次遇到它,是在一份第三方组件的依赖清单里,当时项目里已经有 DOM、有 JAXB,又冒出个 XOM,本能地觉得是重复造轮子。真正把源码翻了一遍,才发现这个判断错得离谱——XOM 是一个有明确设计主张、历史相当悠久的开源 Java XML API,它的包名是nu.xom,核心作者是 XML 领域的老派实践者,整套 API 的出发点和 DOM 完全不是一回事。
所以标题里那个问题——“java中xom是啥包”——其实包含两层含义需要拆开:一层是工程层面的,即xom:xom这个 Maven 坐标对应的是什么组件、要不要引、会不会和现有依赖打架;另一层是设计层面的,即这套 API 存在的意义是什么,既然 Java 平台自带了 DOM 和 SAX,为什么还要多学一套。这两个层面不解决,代码写出来一定是别扭的,因为你会在该用 XPath 的地方写 for 循环,在该复用节点的地方疯狂 new。
XOM 的全称是 XML Object Model,直译过来就是“XML 对象模型”,这个名字听起来和 DOM(Document Object Model)撞车严重,但它的核心卖点恰恰是“比 DOM 更严格地守住 XML 规范”。在 XOM 的世界观里,一个在内存里存在的文档树,如果本身就不合法,那它就不该被构建出来。比如你不能创建一个名字里带空格或者以数字开头的元素,不能往属性值里塞未经转义的尖括号,不能在一个不允许多根的文档里挂两个根节点——这些在 DOM 里基本都是“运行时再说”的事情,在 XOM 里是创建那一刻直接抛异常。这个设计选择会带来写代码习惯上的巨大改变,后面会详细展开。
这篇文章适合谁看:如果你手上有个老项目依赖了 XOM,需要读懂、维护甚至替换它;或者你在评估 Java 生态里到底该用哪套 XML 处理方案,想做一次理性的横向比较;又或者你单纯好奇一个“较真型”API 长什么样,想看看把规范约束前置到对象模型里会是什么效果——这三种诉求,下面的内容都能对上。我不打算只罗列 API,而是按“这东西为什么长这样、实际怎么用、坑在哪”的顺序讲,读完你应该能独立判断自己的项目该不该用它。
2. 拆开 nu.xom 这个包:类族、坐标与它的设计性格
2.1 Maven 坐标与依赖引入的实际情况
先解决最实际的工程问题。XOM 的 Maven 坐标是xom:xom,groupId 和 artifactId 同名,这在新式项目里不常见,但它的历史足够久,久到当年这种命名方式还挺流行。版本线上,1.3.x 是目前主要维护的分支,1.3.9 是社区里被引用得比较多的一个稳定版本。如果你在依赖树里看到的是 1.1 或者 1.2.0 这种,那基本可以判定是很老的第三方库带进来的传递依赖,值得单独评估一下要不要升。
<dependency> <groupId>xom</groupId> <artifactId>xom</artifactId> <version>1.3.9</version> </dependency>这个依赖本身非常轻,没有拖家带口拉一堆东西进来,这一点比某些 XML 方案要好。但要注意一个细节:XOM 在做 XSLT 转换时要依赖外部的 XSLT 处理器,那部分能力不在核心包里,用的时候需要额外引入相应实现。也就是说,只引xom:xom你能做的解析、遍历、XPath 查询、序列化都不受影响,一旦涉及样式表转换,就得补依赖。
另一个常见问题是依赖冲突。XOM 内部是基于 SAX 事件流往上建树的,它需要底层的 XML 解析器来干活,在 JDK 环境里通常走 JDK 自带的那套。如果你的项目里同时引入了其它 XML 解析实现,偶尔会出现解析行为差异,表现为同一个文件在本地跑得好好的,到了某个环境里报结构异常。我遇到过一次,排查到最后是解析器实现不同导致的字符规范化位置不一致,虽然根因不在 XOM,但因为 XOM 对规范性要求高,它会把别人容忍的问题直接暴露出来。
2.2 包内类族:六个类撑起整个对象模型
nu.xom这个包名里的nu不是缩写,也不是什么版本号,是作者当年选的一个短名字,图的是包名短、类名短、写起来省事。这个包里的类其实不多,真正天天打交道的就那么几个,把它们的关系捋清楚,整个 API 就通透了。
最顶层是抽象类Node,所有节点都继承它,包括Document、Element、Attribute、Text、Comment、ProcessingInstruction、DocType。这里有个和 DOM 显著不同的地方:XOM 把Attribute也当作Node的子类,但属性并不出现在子节点列表里,它挂在自己的宿主元素上。这个设计意味着你不能像遍历子元素那样遍历属性,必须单独走getAttributeCount()和getAttribute(int)这套接口。
再往下一层是抽象类ParentNode,它继承自Node,Document和Element都继承它。所以“往里面插节点”“从里面删节点”这些操作定义在ParentNode上,insertChild(Node, int)、appendChild(Node)、removeChild(Node)、replaceChild(Node, Node)这几个方法就是主要的修改入口。理解这层继承关系很关键,因为很多人第一次用 XOM 时会去找Document.addElement之类的方法,结果找不到,真正的方法名是appendChild。
Document有且只有一个根元素,通过getRootElement()拿到。想往Document上直接挂Element可以,但必须是第一个,第二个直接抛异常。Element是最忙的类,负责属性管理、子元素遍历、命名空间读写。Text表示文本节点,getValue()取内容。Comment和ProcessingInstruction用得不多但确实存在,序列化时会原样输出。
有个类不在nu.xom的核心树里,但几乎每个实际项目都会用到,就是Builder。它是解析入口,负责把字节流、文件、字符串变成Document。还有一个容易被忽略的Serializer,专门负责把树写回去。这两个类一进一出,构成了 XOM 的完整闭环。
2.3 它最鲜明的性格:把 XML 规范前置到对象模型
XOM 最值得说的设计决定,是把“文档合法性”这件事从校验阶段提前到了构建阶段。在 DOM 里,你可以创建一个名字叫"1abc"的元素,代码照跑不误,直到你把它序列化出去或者做 schema 校验,问题才浮出水面。XOM 不给你这个机会,new Element("1abc")这一行直接抛IllegalNameException。
这个约束覆盖面比想象中广。元素名和属性名不能是 XML 规范里定义的保留字;属性值里的&和<会自动转义,你不需要手动处理;文本节点里的回车换行符会被规范化成换行符,因为 XML 规范要求解析器这么做;注释内容不能包含--,也不能以-结尾。这些规则如果靠人肉记,早晚出错,XOM 把校验器直接焊在了构造函数里。
代价是什么?代价是你在做数据清洗、做“宽容解析”的时候会觉得它很烦。有些上游系统吐出来的 XML 本身就不太干净,比如标签名带了个非法字符,DOM 能忍,XOM 直接拒绝。这种时候要么在进入 XOM 之前先做一轮修复,要么就换更宽容的解析方式。我的经验是:如果数据源是可控的、规范的,XOM 的严格是纯收益;如果数据源是外部的、质量参差不齐的,那就得在入口处加一层预处理,别指望它帮你兜底。
还有一个连带影响值得提前知道:XOM 里节点的名字和命名空间在创建后不可更改,Element和Attribute都没有setName这类方法。想改名只能新建一个节点,把内容搬过去,再把旧的换掉。这个设定一开始很反直觉,但用久了会发现它避免了大量“改了名字忘了同步命名空间”的低级错误。
3. 把 XOM 用起来:Builder 入口、异常分支与三种遍历写法
3.1 Builder 是唯一入口,异常必须分两支接
所有解析动作都从Builder开始。它提供了一组重载的build方法,可以接File、InputStream、Reader、String,基本覆盖了日常场景。用字符串直接解析在单元测试里特别方便,不用造临时文件。
import nu.xom.Builder; import nu.xom.Document; import nu.xom.ParsingException; import nu.xom.ValidityException; public class ParseDemo { public static Document load(String xml) { Builder builder = new Builder(); try { return builder.build(xml); } catch (ValidityException e) { // 文档结构完整,但不满足 DTD 约束 return e.getDocument(); } catch (ParsingException e) { // 连基本的良构性都不满足,解析失败 throw new IllegalStateException( "XML 解析失败,行 " + e.getLineNumber() + " 列 " + e.getColumnNumber(), e); } catch (Exception e) { throw new IllegalStateException("读取 XML 时发生 IO 异常", e); } } }这段代码里最关键的是两个异常分支的区别。ParsingException表示文档连“良构”都达不到,比如标签没闭合、属性没加引号、出现非法字符,这时候拿不到任何可用的树,只能报错。ValidityException则温和得多:文档本身是良构的,你照样能拿到一棵完整的树,只是它不满足文档里声明的 DTD 约束。这个区分非常实用,因为它让你可以决定“结构不对但能用”的数据是丢弃还是降级处理。
实际项目里我一般会把这个判断封装成配置项。对外部来源的数据,允许ValidityException通过并记一条告警;对内部生成的数据,ValidityException一律当成错误,因为那说明上游的生成逻辑有 bug。
Builder还支持设置错误处理器,用来接管解析过程中的警告和错误回调。如果你需要在解析时收集所有问题而不是遇到第一个就中断,这个口子是必要的。
3.2 从根节点往下走:三种遍历方式各有适用场景
拿到Document之后,第一件事通常是定位数据。XOM 提供了三种层次的遍历手段,从粗到细分别是按名字取子元素、按下标遍历、以及访问者模式。
最常用的是按名字取。Element.getFirstChildElement(String name)返回第一个匹配的子元素,没有就返回null;getChildElements(String name)返回一个Elements集合。这里有个必须提醒的点:Elements继承了java.util.ArrayList,所以它有size()、get(int),也能用增强 for 循环,但它并不是一个纯粹的List实现,某些列表操作在上面是无效的。这是 XOM 里少见的、为了便利性做的妥协,知道就行,别拿它当通用集合用。
Element root = doc.getRootElement(); Element book = root.getFirstChildElement("book"); if (book != null) { Elements authors = book.getChildElements("author"); for (int i = 0; i < authors.size(); i++) { Element author = (Element) authors.get(i); String name = author.getAttributeValue("name"); String text = author.getValue(); System.out.println(name + " -> " + text); } }注意getAttributeValue(String)这个方法有个隐藏限制:它只对没有命名空间的属性有效。一旦属性带了前缀,这个方法返回null,你得改用带两个参数的版本getAttributeValue(String localName, String uri)。这个坑非常隐蔽,因为返回null不会报错,只会在业务逻辑里表现为“字段莫名其妙取不到”。
第二种是按下标遍历全部子节点,用getChildCount()配合getChild(int),或者getChildElements()的无参版本其实不存在,得自己加类型判断。这种写法在结构不固定的场景下才用,比如你要处理任意层级的混合内容。写的时候一定要做instanceof判断,因为子节点里可能混着Text、Comment,直接强转Element会炸。
第三种是NodeVisitor。它是一个抽象类,你继承它并重写startElement、endElement、text等方法,然后调用root.accept(visitor)就能走一遍深度优先遍历。这种写法在“需要统计整棵树的信息”“需要做全局替换”这类场景下比手写递归清爽得多。
3.3 一个容易忽略的细节:内容相等的节点不等于 equals
这里插一个必须单独拎出来说的坑。XOM 的Node系列没有重写equals,也就是说,两个内容完全一样的Element,用equals比较返回的是false。如果你在业务代码里用Set<Element>或者Map<Element, Something>做去重、做缓存,结果会完全不符合预期——因为走的是对象标识哈希。
正确的比较方式是比较序列化结果,或者逐层比较节点名、命名空间、属性和子节点。写工具方法的话,a.toXML().equals(b.toXML())是最省事的,代价是每次比较都要序列化一遍,在循环里用会明显拖慢速度。如果比较频繁,建议自己写一个递归比较器,先比名字和命名空间,再比属性数量,最后比子节点,短路掉大部分不相等的情况。
复制节点也有讲究。Node.copy()是深拷贝,会把整棵子树复制一份新的,但复制出来的节点没有父节点,需要自己挂上去。而appendChild的行为是“移动”而不是“复制”——如果你把一棵已经挂在 A 下面的子树appendChild到 B 下面,XOM 会先把它从 A 上摘下来。这个行为设计上是为了保证一个节点只有一个父亲,但对不熟悉的开发者来说很像“数据凭空消失”,我第一次遇到时盯着日志看了半天才发现是位移。
4. XPath 与命名空间:XOM 里最省代码的两块
4.1 query 方法把嵌套循环压成一行
如果说 XOM 有哪个功能最能让人产生“这库值了”的感觉,那一定是内置的 XPath 支持。Node上直接就有query(String xpath)和query(String xpath, XPathContext context),返回一个Nodes集合,同样支持size()和get(int)。
对比一下就很直观。假设要取所有 id 为 1 的书名,手写遍历得这么来:
Element root = doc.getRootElement(); Elements books = root.getChildElements("book"); String target = null; for (int i = 0; i < books.size(); i++) { Element book = (Element) books.get(i); if ("1".equals(book.getAttributeValue("id"))) { Element name = book.getFirstChildElement("name"); if (name != null) { target = name.getValue(); break; } } }用 XPath 就是:
Nodes nodes = doc.query("/catalog/book[@id='1']/name/text()"); String target = nodes.size() > 0 ? nodes.get(0).getValue() : null;行数从十几行降到两行,可读性差距更大。而且 XPath 的表达能力强得多,[last()]、[position() > 2]、contains()、starts-with()这些谓词直接就能用,不用自己拼条件判断。
但 XPath 也有它的边界,不能无脑上。一是表达式写错了不会在编译期报错,只会抛XPathException,而且错误信息往往不够精确,字符串拼接里少个引号能让人找半天。我的习惯是把 XPath 表达式抽成常量,写一个通用的查询方法统一处理异常,出错时把表达式原文打进日志。二是复杂表达式的性能不一定比手写遍历好,尤其是那种带多层//的查询,在大文档上会明显变慢,因为要扫全树。
提示:
Nodes里的节点是树中的原始引用,不是副本。从Nodes里取出来的Element改了,原文档也跟着变。如果你需要“查询结果快照”,得自己逐个copy()。
4.2 命名空间在 XOM 里不是可选项
Java 生态里很多 XML 库对命名空间的态度是“你可以不用,用了也别出事”,XOM 的态度则是“你必须正视它”。这个差异在多命名空间混用的文档上体现得淋漓尽致。
创建带命名空间的元素,构造时就要把 URI 传进去:
String ns = "http://example.com/schema/order"; Element order = new Element("order", ns); Element item = new Element("item", ns); order.appendChild(item); Document doc = new Document(order);读取时,判断元素属于哪个命名空间要用getNamespaceURI(),而不是靠前缀字符串猜。前缀只是别名,同一个 URI 在不同位置可以用不同前缀,甚至可以用默认命名空间不写前缀。拿前缀做判断是新手最常见的错误,在测试环境可能一直能跑,一旦上游换了个前缀就全线崩溃。
XPath 查询里涉及命名空间的节点,必须通过XPathContext做前缀映射,否则表达式里的ns:item根本匹配不到东西:
XPathContext ctx = new XPathContext("o", "http://example.com/schema/order"); Nodes items = doc.query("//o:item[o:qty > 10]", ctx);这里有个实操建议:XPathContext每次查询用一个新的就行,它的构造很轻,不要想着做成全局单例复用,因为里面挂的是变量绑定,复用容易串味。还有一点,XPath 里访问属性是不带命名空间前缀的,除非属性本身真有命名空间——而 XML 规范里属性的命名空间不继承默认命名空间,所以<item xmlns="...">下面的id="1"属性其实是在空命名空间里。这个规则记不住没关系,只要记住“属性取不到就先怀疑命名空间”,基本能定位大部分问题。
5. 造文档与写出去:XOM 的另一半价值
5.1 从零拼一棵树,以及为什么不用自己拼字符串
很多人处理 XML 输出时习惯手写字符串拼接,图的是快,结果在遇到特殊字符、中文、嵌套引号的时候反复翻车。XOM 的构建 API 虽然啰嗦一点,但它把所有转义和合法性检查都包了,长期看省事得多。
Element root = new Element("response"); root.addAttribute(new Attribute("code", "0")); Element msg = new Element("message"); msg.appendChild("操作成功 & 数据已同步"); root.appendChild(msg); Element data = new Element("data"); data.appendChild("张三"); root.appendChild(data); Document doc = new Document(root); System.out.println(doc.toXML());注意msg里那个&号,我故意没转义,XOM 会自动处理成&。这就是把规范写进 API 的好处,你不需要在每个拼接点都想着“这里要不要 escape”。反过来,如果你手动拼字符串漏了一次,产出物在浏览器里可能看着正常,在严格的解析器里直接报错,排查成本高得多。
Document的构造也有约束:只有一个根元素。想造多根的文档,会直接抛异常。需要输出多个顶层对象时,正确做法是包一个容器元素,或者用多个独立文档分别序列化。这个约束不讨喜但符合规范,早点接受比对抗它省时间。
节点操作上还有几个常用方法值得记一下。removeChild(Node)可以把自己从父节点上摘掉;replaceChild(Node newNode, Node oldNode)做替换;getParent()反查父节点。这几个方法配合起来,做“删除符合条件的节点”这类批量修改很方便,但要注意边遍历边删会打乱下标,稳妥的写法是先把要删的节点收集到列表里,遍历完再统一删。
5.2 Serializer 的编码坑:中文乱码大多出在这一步
写文档出来用的是Serializer。它的基本用法是构造时指定输出目标和编码,然后write一下:
Serializer serializer = new Serializer( new FileOutputStream("out.xml"), "UTF-8"); serializer.setIndent(2); serializer.setMaxLength(100); serializer.write(doc); serializer.flush(); serializer.close();setIndent(2)控制缩进空格数,setMaxLength控制每行最大长度,超出会折行。这两个参数影响的是可读性,不影响语义,但在需要人工比对输出结果的场景里,设一下会舒服很多。
真正容易出问题的是编码。Serializer有两个构造分支:接OutputStream和接Writer。接OutputStream时,构造里传的编码参数是生效的,会写进 XML 声明的encoding属性,也用于实际编码转换。接Writer时,编码参数会被忽略,最终用什么编码完全取决于外面那层Writer是怎么构造的。
这就是中文乱码的经典来源:有人包了一层new OutputStreamWriter(fos),没指定字符集,走了平台默认编码,在某个环境里是 GBK,在另一个环境里是 UTF-8,同一份代码表现不一致。修法很简单,要么别包Writer,直接给Serializer传流;要么在OutputStreamWriter上明确写死"UTF-8"。
// 不推荐:依赖平台默认编码 Serializer s1 = new Serializer( new OutputStreamWriter(new FileOutputStream("a.xml")), "UTF-8"); // 推荐:编码从流这一层就定死 Serializer s2 = new Serializer( new FileOutputStream("b.xml"), "UTF-8");还有一个小细节:序列化完成后一定要flush()或close()。Serializer内部有缓冲,不刷新的话文件可能是空的或者被截断。这类问题在本地测试时偶尔能过(因为 JVM 退出会刷盘),到了容器环境里就变成了“文件生成成功但内容缺失”,排查方向很容易跑偏到别处。
6. 横向选型:XOM、DOM、JDOM、StAX、JAXB 摆在一起
6.1 五套方案的能力对照
到了选型环节,光看文档描述没用,得按实际面临的问题维度去比。下面这张表是我根据自己的使用经验整理的,覆盖了日常决策最关心的几个点。
| 维度 | XOM | DOM | JDOM | StAX | JAXB |
|---|---|---|---|---|---|
| 模型类型 | 树模型 | 树模型 | 树模型 | 流模型 | 绑定模型 |
| 规范严格度 | 高,构建即校验 | 低,宽容 | 中 | 取决于实现 | 取决于实现 |
| 内存占用 | 中等,节点较轻 | 高 | 中 | 极低 | 中高 |
| XPath 支持 | 内置,开箱可用 | 需额外代码 | 需额外依赖 | 不适用 | 不适用 |
| 命名空间处理 | 一等公民 | 支持但繁琐 | 支持 | 支持 | 由注解驱动 |
| 大文件友好度 | 一般 | 差 | 一般 | 优秀 | 一般 |
| 学习曲线 | 平缓 | 陡 | 平缓 | 中等 | 陡(需懂注解映射) |
| 典型用途 | 中小文档处理、转换 | 遗留系统、标准接口 | 轻量树操作 | 超大文件、流式处理 | 强类型对象映射 |
这张表里最值得说的是 XOM 和 JDOM 的关系。两者定位接近,都是“比 DOM 好用的树模型”,但性格不同:JDOM 更宽容、更贴近 Java 集合的使用习惯;XOM 更严格、更贴近 XML 规范。如果你的数据来源规范、你希望错误尽早暴露,选 XOM;如果你的数据来源复杂、需要容错,JDOM 或者宽容模式的 DOM 更合适。
XOM 和 JAXB 的关系则不是竞争而是互补。JAXB 解决的是“XML 到强类型对象”的映射问题,适合结构固定、需要频繁读写的业务数据。XOM 解决的是“XML 树到 XML 树”的操作问题,适合结构不固定、需要查询和变换的场景。我见过一个项目把两者混着用:入口用 XOM 做结构校验和清洗,清洗完再转成对象给业务层,各取所长,效果不错。
6.2 什么情况下我会主动选 XOM
说几个具体的判断标准,都是踩过坑之后总结的。
第一,文档规模在几十 MB 以内,且需要多次随机访问。这种场景树模型的优势最大,XOM 的节点结构比 DOM 紧凑,同样一份文档加载进来,堆占用通常更低。超过这个量级就该考虑 StAX 了,再怎么优化树模型也顶不住内存压力。
第二,需要做 XML 到 XML 的结构变换。XOM 的 XPath 加序列化组合,配合节点复制和移动,写变换逻辑比用 DOM 顺手很多。尤其是“按条件筛选节点、重组层级、输出新文档”这类操作,代码量能压到 DOM 的三分之一左右。
第三,需要保证输出一定合法。这个场景下 XOM 的价值无可替代——因为它从创建那一刻就在保证合法性,序列化出来的东西基本不可能不合规。如果你的输出要给下游系统做严格解析,用它等于省掉了一轮校验。
反过来说,三种情况我会避开 XOM。需要处理 XML Schema 校验的场景,XOM 在这方面支持有限,老老实实用支持 schema 的方案;需要处理 GB 级日志流或数据导出的场景,直接上流式处理;团队里没人熟悉这套 API 且项目周期很紧的场景,用大家都会的方案更稳妥,技术选型不能只看优劣,还得看维护成本。
7. 实战里真正会绊倒人的几个地方
7.1 父子关系与节点移动的连锁反应
前面提过appendChild是移动而非复制,这个行为在单点操作时不难理解,放到批量处理里就容易出事。典型场景是“把符合条件的节点从原位置搬到新位置”,如果边遍历边搬,遍历的下标会因为你动了树结构而错乱,表现为“有一半节点没被处理”。
安全的写法是两段式:先遍历收集,再统一搬迁。收集的时候用Nodes或者普通的ArrayList都行,但要注意收集的是节点引用,一旦搬迁完成,原树里的位置就空了,如果你后续还要依赖原有顺序,得在收集阶段就把顺序信息记下来。
Element source = doc.getRootElement(); Elements targets = source.getChildElements("item"); List<Element> pending = new ArrayList<>(); for (int i = 0; i < targets.size(); i++) { Element e = (Element) targets.get(i); if ("expired".equals(e.getAttributeValue("status"))) { pending.add(e); } } Element trash = new Element("archived"); for (Element e : pending) { // appendChild 会自动从原父节点摘除 trash.appendChild(e); } source.appendChild(trash);另外要留意,removeChild之后节点的父引用会被清空,但节点本身还在,可以重新挂到别处。这个特性可以用来做“临时摘下再放回”的操作,但别忘了放回去,否则数据就悬空了,既不报错也不在树里,查起来很费劲。
7.2 大文件、内存与并发下的真实边界
XOM 是树模型,整份文档都要进内存,这个前提决定了它的适用边界。我做过一次粗略测量,加载一份 40 MB 左右的 XML,堆内存峰值大概在原文件的六到八倍,具体倍数跟节点密度强相关——节点越碎、属性越多,膨胀越厉害。所以如果你的堆上限只有 512 MB,实际能安全处理的文档大小可能就三十来 MB,再大就得上流式方案或者做分片。
并发方面,Builder、Serializer都不建议跨线程共享,每次用新建一个最省心,它们的构造成本很低。Document和Element本身也不是线程安全的,多线程同时改同一棵树一定会出问题。如果确实需要并发读取,可以在解析完成后把树冻结起来只读,或者干脆每个线程各自解析一份——后者听着浪费,但在文档不大、线程数不多的情况下,比加锁简单得多。
最后一个不太常被提起但值得关注的点是外部实体处理。XOM 较新的版本在解析时对外部实体的加载做了限制,这对安全性是好事。但如果你的项目里用的是很老的版本,或者解析的数据来自不可信来源,建议在进入 XOM 之前就做一层输入检查,别把来路不明的文档直接喂进去。我在做接口对接时养成的习惯是:任何外部传入的 XML,先做大小限制和结构预检,再交给解析器,这层防护比事后追查划算得多。
我个人在实际操作中的体会是,XOM 这类库的价值不在“功能多”,而在“约束狠”。它逼着你在写代码的时候就把数据的规范性想清楚,短期看着啰嗦,长期省下的是排查线上脏数据的那些深夜。如果你的项目正被各种格式诡异的 XML 折腾得够呛,不妨拿一份真实数据丢给它跑一遍,报出来的那些异常,往往就是上游藏了很久的问题。