Java实现XML解析后按层级树形格式化展示方案
Java实现XML人性化树形展示的踩坑与完整方案
需求背景
做XML人性化易读展示功能时,最初的实现思路是:解析XML内容后,将点分隔的标签全路径作为键、标签对应文本值作为值存入LinkedHashMap<String,String>结构。展示规则如下:
- 当某标签包含2个及以上子节点时,将该标签的全路径作为分组标题
- 下属子标签与对应值统一做缩进展示
- 最终输出层级清晰的类树形结构
核心问题记录
开发过程中遇到两个直接影响功能正确性的问题:
- 重复标签覆盖问题:XML结构存在同路径重复同名标签时,受Map键唯一特性限制,后写入的数据会直接覆盖之前的内容,无法完整保留原始XML的结构顺序和全部节点内容
- TreeMap排序丢数据问题:初期采用
new TreeMap<>(Comparator.comparingInt(MyTag::getAppearanceOrder)实现按标签出现顺序排序时,由于部分MyTag对象的appearanceOrder属性值相同,TreeMap在比较器返回0时会判定两个键相等,直接覆盖旧条目,导致排序逻辑不一致,Map中数据被意外移除
解决方案
针对两个问题分别做了对应修复:
- 重复标签键冲突修复:遍历解析XML阶段,为重复出现的同路径标签追加出现次数编号,生成带序号的唯一标签路径标识,从根源避免键重复导致的内容覆盖
- 排序丢数据问题修复:放弃直接使用TreeMap作为排序容器,改为对Map的entry集调用
stream().sorted(Map.Entry.comparingByKey(...))方法按标签出现顺序排序后,通过collect()收集为LinkedHashMap保留排序结果,彻底解决排序不一致、数据丢失的问题
完整可运行实现代码
import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.util.*; import java.util.stream.Collectors; public class XmlPrettyFormatter { // 记录原始路径出现次数,用于生成唯一键、还原排序顺序 private static final Map<String, Integer> pathCountMap = new HashMap<>(); public static void main(String[] args) throws Exception { // 测试XML样例,包含重复同名标签 String testXml = """ <root> <user> <name>张三</name> <age>25</age> </user> <user> <name>李四</name> <age>30</age> <contact> <phone>13800138000</phone> <email>lisi@example.com</email> </contact> </user> </root> """; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.parse(new java.io.ByteArrayInputStream(testXml.getBytes())); doc.getDocumentElement().normalize(); Map<String, String> tagMap = new LinkedHashMap<>(); // 递归解析所有XML节点 parseNode(doc.getDocumentElement(), "", tagMap); // 按标签实际出现顺序排序,收集为LinkedHashMap固定顺序 Map<String, String> sortedTagMap = tagMap.entrySet().stream() .sorted(Comparator.comparingInt(entry -> { String originalPath = entry.getKey().replaceAll("\\[\\d+\\]$", ""); return pathCountMap.getOrDefault(originalPath, 0); })) .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (oldVal, newVal) -> oldVal, LinkedHashMap::new )); // 格式化输出类树形结构 System.out.println(formatToTree(sortedTagMap)); } private static void parseNode(Node node, String parentPath, Map<String, String> tagMap) { if (node.getNodeType() != Node.ELEMENT_NODE) { return; } String nodeName = node.getNodeName(); String currentPath = parentPath.isEmpty() ? nodeName : parentPath + "." + nodeName; // 同路径重复标签追加序号,生成唯一键 int count = pathCountMap.getOrDefault(currentPath, 0) + 1; pathCountMap.put(currentPath, count); String uniqueKey = count > 1 ? currentPath + "[" + count + "]" : currentPath; NodeList childNodes = node.getChildNodes(); int elementChildCount = 0; boolean hasTextChild = false; StringBuilder textValue = new StringBuilder(); for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { elementChildCount++; // 递归解析子节点时传入原始无序号路径,保证子节点路径正确性 parseNode(child, currentPath, tagMap); } else if (child.getNodeType() == Node.TEXT_NODE) { String text = child.getTextContent().trim(); if (!text.isEmpty()) { hasTextChild = true; textValue.append(text); } } } // 存入节点值,非叶子节点值为空 tagMap.put(uniqueKey, hasTextChild || elementChildCount == 0 ? textValue.toString() : ""); } private static String formatToTree(Map<String, String> sortedTagMap) { StringBuilder sb = new StringBuilder(); Set<String> printedGroup = new HashSet<>(); for (Map.Entry<String, String> entry : sortedTagMap.entrySet()) { String originalPath = entry.getKey().replaceAll("\\[\\d+\\]", ""); String value = entry.getValue(); String[] pathParts = originalPath.split("\\."); int depth = pathParts.length - 1; // 统计当前节点下直接子节点数量,判断是否为分组节点 long directChildCount = sortedTagMap.keySet().stream() .map(k -> k.replaceAll("\\[\\d+\\]", "")) .filter(k -> k.startsWith(originalPath + ".")) .map(k -> k.substring(originalPath.length() + 1).split("\\.")[0]) .distinct() .count(); // 打印分组标题 if (directChildCount >= 2 && !printedGroup.contains(originalPath)) { sb.append(" ".repeat(depth)) .append("▸ ") .append(pathParts[depth]) .append(entry.getKey().matches(".*\\[\\d+\\]$") ? entry.getKey().substring(entry.getKey().lastIndexOf("[")) : "") .append("\n"); printedGroup.add(originalPath); } // 打印叶子节点键值 if (!value.isEmpty()) { sb.append(" ".repeat(depth + 1)) .append("- ") .append(pathParts[depth]) .append(": ") .append(value) .append("\n"); } } return sb.toString(); } }
内容的提问来源于stack exchange,提问作者ZeeCos
相关产品推荐
相关产品推荐

