You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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中数据被意外移除

解决方案

针对两个问题分别做了对应修复:

  1. 重复标签键冲突修复:遍历解析XML阶段,为重复出现的同路径标签追加出现次数编号,生成带序号的唯一标签路径标识,从根源避免键重复导致的内容覆盖
  2. 排序丢数据问题修复:放弃直接使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 02:48:31