Java XML转Map工具类,如何实现递归解析XML为嵌套Map结构?

2026-04-29 13:213阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1333个文字,预计阅读时间需要6分钟。

Java XML转Map工具类,如何实现递归解析XML为嵌套Map结构?

Java原生没有直接将XML转换成嵌套Map的标准API,通常可以使用硬编码的方式,或者使用JAXB或第三方库(如xmltojson)来实现。在处理属性/文本混合、同名节点、空元素等复杂场景时,最可控的方式是使用DocumentBuilder解析XML生成Document,然后编写一个轻量级递归函数遍历节点。

关键不是“能不能转”,而是“怎么让 <user id="123"><name>Tom</name></user> 变成 {"user": {"@id": "123", "name": "Tom"}} 这种结构——属性加 @ 前缀、文本内容用 #text 键、子节点自动嵌套,都得手动约定清楚。

  • 别用 SAXParser:回调式写法对嵌套结构维护成本高,容易漏层级
  • 别跳过 DocumentBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true):防 XXE 攻击,尤其输入不可信时
  • 空元素如 <email/> 建议统一转成 {"email": null},而不是忽略或空字符串,否则下游 map.get("email") == null 语义模糊

parseNode 递归函数里必须处理三类节点:Element、Text、Attribute

XML 节点类型不只有 ElementText 节点(即标签内纯文本)和 Attribute(属性)必须分开处理,否则会把属性值当子节点、把换行当内容。

典型错误是只遍历 getChildNodes(),结果把文本节点(含缩进和换行)也塞进 Map,造成 {"#text": "\n "} 这种脏数据。

立即学习“Java免费学习笔记(深入)”;

  • node.getNodeType() == Node.ELEMENT_NODE 过滤,跳过 TEXT_NODECOMMENT_NODE
  • 属性用 element.getAttributes() 单独取,每个 Attr 的 key 是 "@" + attr.getName(),value 是 attr.getValue()
  • 元素内的纯文本(非子标签)要合并:遍历所有直接子 TEXT_NODEtrim() 后拼接,空则设为 null,键固定为 "#text"

示例片段:

private Map<String, Object> parseNode(Node node) { if (node.getNodeType() != Node.ELEMENT_NODE) return Collections.emptyMap(); Element elem = (Element) node; Map<String, Object> map = new LinkedHashMap<>(); // 处理属性 NamedNodeMap attrs = elem.getAttributes(); for (int i = 0; i < attrs.getLength(); i++) { Attr attr = (Attr) attrs.item(i); map.put("@" + attr.getName(), attr.getValue()); } // 处理子节点 NodeList children = elem.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { String key = child.getNodeName(); Object val = parseNode(child); // 同名子节点超过 1 个 → 转为 List if (map.containsKey(key)) { Object existing = map.get(key); if (existing instanceof List) { ((List) existing).add(val); } else { map.put(key, Arrays.asList(existing, val)); } } else { map.put(key, val); } } } // 处理本元素的文本内容 String text = elem.getTextContent().trim(); if (!text.isEmpty()) { map.put("#text", text); } return map; }

同名子节点(如多个 <item>)必须转成 List,不能覆盖

这是最容易被忽略的坑:XML 允许多个同名子节点,但 Map 的 key 是唯一的。如果代码里直接 map.put("item", value),后一个会覆盖前一个,数据直接丢。

判断逻辑很简单——插入前先查 key 是否已存在。但要注意,第一次是单个 Object,第二次才升格为 List<Object>,第三次往 list 里 add 就行。

  • 别用 instanceof ArrayList 判定,用 map.get(key) instanceof List 更稳妥
  • 如果业务确定某个标签永远只有一个,可加白名单跳过 list 化(比如 "config"),但默认行为必须保守
  • 测试用例一定要包含 <list><item>a</item><item>b</item></list>,验证输出是 {"list": {"item": ["a", "b"]}}

字符编码和命名空间不处理会导致解析失败或乱码

XML 声明里带 encoding="GBK",但 DocumentBuilder 默认按 UTF-8 读,字节流一解码就错;还有带命名空间的 XML(<ns:root xmlns:ns="http://example.com">),不设置 setNamespaceAware(true)getNodeName() 返回的是 ns:root 还是 root 都不确定。

  • 构造 InputSource 时显式指定编码:

    InputSource src = new InputSource(new ByteArrayInputStream(xmlBytes)); src.setEncoding("UTF-8"); // 或从 XML 声明里动态提取

  • 必须调用 dbFactory.setNamespaceAware(true),否则带前缀的标签名解析异常,且 getAttributes() 可能漏掉命名空间声明属性
  • 如果 XML 没声明 encoding,又不确定来源编码,建议统一转 UTF-8 再解析,避免依赖平台默认编码

复杂点在于,真实 XML 往往混合了 CDATA、实体引用()、注释,这些在 <code>getTextContent() 中已自动解码,但如果你改用 getNodeValue() 就会拿到原始未解码字符串——这点很容易被忽略,导致   显示成字面量。

标签:xml解析

本文共计1333个文字,预计阅读时间需要6分钟。

Java XML转Map工具类,如何实现递归解析XML为嵌套Map结构?

Java原生没有直接将XML转换成嵌套Map的标准API,通常可以使用硬编码的方式,或者使用JAXB或第三方库(如xmltojson)来实现。在处理属性/文本混合、同名节点、空元素等复杂场景时,最可控的方式是使用DocumentBuilder解析XML生成Document,然后编写一个轻量级递归函数遍历节点。

关键不是“能不能转”,而是“怎么让 <user id="123"><name>Tom</name></user> 变成 {"user": {"@id": "123", "name": "Tom"}} 这种结构——属性加 @ 前缀、文本内容用 #text 键、子节点自动嵌套,都得手动约定清楚。

  • 别用 SAXParser:回调式写法对嵌套结构维护成本高,容易漏层级
  • 别跳过 DocumentBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true):防 XXE 攻击,尤其输入不可信时
  • 空元素如 <email/> 建议统一转成 {"email": null},而不是忽略或空字符串,否则下游 map.get("email") == null 语义模糊

parseNode 递归函数里必须处理三类节点:Element、Text、Attribute

XML 节点类型不只有 ElementText 节点(即标签内纯文本)和 Attribute(属性)必须分开处理,否则会把属性值当子节点、把换行当内容。

典型错误是只遍历 getChildNodes(),结果把文本节点(含缩进和换行)也塞进 Map,造成 {"#text": "\n "} 这种脏数据。

立即学习“Java免费学习笔记(深入)”;

  • node.getNodeType() == Node.ELEMENT_NODE 过滤,跳过 TEXT_NODECOMMENT_NODE
  • 属性用 element.getAttributes() 单独取,每个 Attr 的 key 是 "@" + attr.getName(),value 是 attr.getValue()
  • 元素内的纯文本(非子标签)要合并:遍历所有直接子 TEXT_NODEtrim() 后拼接,空则设为 null,键固定为 "#text"

示例片段:

private Map<String, Object> parseNode(Node node) { if (node.getNodeType() != Node.ELEMENT_NODE) return Collections.emptyMap(); Element elem = (Element) node; Map<String, Object> map = new LinkedHashMap<>(); // 处理属性 NamedNodeMap attrs = elem.getAttributes(); for (int i = 0; i < attrs.getLength(); i++) { Attr attr = (Attr) attrs.item(i); map.put("@" + attr.getName(), attr.getValue()); } // 处理子节点 NodeList children = elem.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { String key = child.getNodeName(); Object val = parseNode(child); // 同名子节点超过 1 个 → 转为 List if (map.containsKey(key)) { Object existing = map.get(key); if (existing instanceof List) { ((List) existing).add(val); } else { map.put(key, Arrays.asList(existing, val)); } } else { map.put(key, val); } } } // 处理本元素的文本内容 String text = elem.getTextContent().trim(); if (!text.isEmpty()) { map.put("#text", text); } return map; }

同名子节点(如多个 <item>)必须转成 List,不能覆盖

这是最容易被忽略的坑:XML 允许多个同名子节点,但 Map 的 key 是唯一的。如果代码里直接 map.put("item", value),后一个会覆盖前一个,数据直接丢。

判断逻辑很简单——插入前先查 key 是否已存在。但要注意,第一次是单个 Object,第二次才升格为 List<Object>,第三次往 list 里 add 就行。

  • 别用 instanceof ArrayList 判定,用 map.get(key) instanceof List 更稳妥
  • 如果业务确定某个标签永远只有一个,可加白名单跳过 list 化(比如 "config"),但默认行为必须保守
  • 测试用例一定要包含 <list><item>a</item><item>b</item></list>,验证输出是 {"list": {"item": ["a", "b"]}}

字符编码和命名空间不处理会导致解析失败或乱码

XML 声明里带 encoding="GBK",但 DocumentBuilder 默认按 UTF-8 读,字节流一解码就错;还有带命名空间的 XML(<ns:root xmlns:ns="http://example.com">),不设置 setNamespaceAware(true)getNodeName() 返回的是 ns:root 还是 root 都不确定。

  • 构造 InputSource 时显式指定编码:

    InputSource src = new InputSource(new ByteArrayInputStream(xmlBytes)); src.setEncoding("UTF-8"); // 或从 XML 声明里动态提取

  • 必须调用 dbFactory.setNamespaceAware(true),否则带前缀的标签名解析异常,且 getAttributes() 可能漏掉命名空间声明属性
  • 如果 XML 没声明 encoding,又不确定来源编码,建议统一转 UTF-8 再解析,避免依赖平台默认编码

复杂点在于,真实 XML 往往混合了 CDATA、实体引用()、注释,这些在 <code>getTextContent() 中已自动解码,但如果你改用 getNodeValue() 就会拿到原始未解码字符串——这点很容易被忽略,导致   显示成字面量。

标签:xml解析