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

忽略元素顺序校验XML文件相等:XMLUnit失效后的方案咨询

XML全层级排序后比较的解决方案(无需预先知晓结构)

现有两段仅<elem1>元素顺序不同的XML,需要编写测试证明二者相等。使用XMLUnit工具时,尝试ElementSelectors.byNameAndText和ElementSelectors.byNameAndAllAttributes两种匹配规则均未成功,且现有方案多需要预先知晓XML结构,求无需预定义结构、可对XML所有层级元素统一排序后再比较的方法及可用库。

待比较的XML示例:

var xml1 = """
      <root>
          <elem1>
            <elem2 attr="a"/>
          </elem1>
          <elem1>
            <elem2 attr="b"/>
          </elem1>
      </root>
    """;
    
var xml2 = """
      <root>
          <elem1>
            <elem2 attr="b"/>
          </elem1>
          <elem1>
            <elem2 attr="a"/>
          </elem1>
      </root>
    """;

此前尝试的测试代码:

// 尝试规则1
assertThat(xml1, isSimilarTo(xml2)
        .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));

// 尝试规则2
assertThat(xml1, isSimilarTo(xml2)
        .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndAllAttributes)));

解决方案

1. 基于XMLUnit实现全层级动态排序匹配

无需预定义XML结构,通过自定义ElementSelector结合递归排序逻辑,让XMLUnit能自动匹配排序后的子元素:

自定义匹配工具类

import org.xmlunit.diff.*;
import org.w3c.dom.Element;
import java.util.*;
import java.util.stream.Collectors;

public class XmlFullHierarchyComparator {

    // 自定义递归匹配选择器:自动排序子元素后比较
    public static ElementSelector recursiveByNameAndContent() {
        return (e1, e2) -> {
            // 先匹配元素名
            if (!e1.getNodeName().equals(e2.getNodeName())) {
                return false;
            }
            // 匹配属性
            if (!compareAttributes(e1, e2)) {
                return false;
            }
            // 匹配文本内容(去除空白)
            String text1 = Optional.ofNullable(e1.getTextContent()).orElse("").trim();
            String text2 = Optional.ofNullable(e2.getTextContent()).orElse("").trim();
            if (!text1.equals(text2)) {
                return false;
            }
            // 递归排序并匹配子元素
            List<Element> sortedChildren1 = getSortedChildElements(e1);
            List<Element> sortedChildren2 = getSortedChildElements(e2);
            if (sortedChildren1.size() != sortedChildren2.size()) {
                return false;
            }
            for (int i = 0; i < sortedChildren1.size(); i++) {
                if (!recursiveByNameAndContent().match(sortedChildren1.get(i), sortedChildren2.get(i))) {
                    return false;
                }
            }
            return true;
        };
    }

    // 按「元素名+属性+文本」排序子元素
    private static List<Element> getSortedChildElements(Element parent) {
        return Arrays.stream(parent.getElementsByTagName("*"))
                .map(node -> (Element) node)
                .sorted((e1, e2) -> {
                    int nameCompare = e1.getNodeName().compareTo(e2.getNodeName());
                    if (nameCompare != 0) return nameCompare;

                    String attrs1 = getSortedAttributesString(e1);
                    String attrs2 = getSortedAttributesString(e2);
                    int attrCompare = attrs1.compareTo(attrs2);
                    if (attrCompare != 0) return attrCompare;

                    String text1 = Optional.ofNullable(e1.getTextContent()).orElse("").trim();
                    String text2 = Optional.ofNullable(e2.getTextContent()).orElse("").trim();
                    return text1.compareTo(text2);
                })
                .collect(Collectors.toList());
    }

    // 将属性按键排序后拼接成字符串,用于排序比较
    private static String getSortedAttributesString(Element elem) {
        return elem.getAttributes().getLength() == 0 ? "" :
                Arrays.stream(elem.getAttributes())
                        .map(node -> (org.w3c.dom.Attr) node)
                        .sorted(Comparator.comparing(org.w3c.dom.Attr::getName))
                        .map(attr -> attr.getName() + "=" + attr.getValue())
                        .collect(Collectors.joining(";"));
    }

    // 比较两个元素的属性是否完全一致
    private static boolean compareAttributes(Element e1, Element e2) {
        if (e1.getAttributes().getLength() != e2.getAttributes().getLength()) {
            return false;
        }
        return Arrays.stream(e1.getAttributes())
                .map(node -> (org.w3c.dom.Attr) node)
                .allMatch(attr -> attr.getValue().equals(e2.getAttribute(attr.getName())));
    }
}

使用自定义选择器的测试代码

assertThat(xml1, isSimilarTo(xml2)
        .withNodeMatcher(new DefaultNodeMatcher(XmlFullHierarchyComparator.recursiveByNameAndContent())));

2. 基于DOM4J的XML归一化方案

先将两段XML全层级排序生成结构完全一致的归一化XML,再直接比较字符串:

归一化工具类

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import java.util.*;

public class XmlNormalizer {

    // 生成全层级排序后的归一化XML字符串
    public static String normalizeXml(String xml) throws DocumentException {
        Document doc = DocumentHelper.parseText(xml);
        sortElementsRecursively(doc.getRootElement());
        return doc.asXML();
    }

    // 递归排序所有层级的元素
    private static void sortElementsRecursively(Element element) {
        // 排序当前元素的子元素
        List<Element> children = new ArrayList<>(element.elements());
        children.sort((e1, e2) -> {
            int nameCompare = e1.getName().compareTo(e2.getName());
            if (nameCompare != 0) return nameCompare;

            String attrs1 = getSortedAttributesString(e1);
            String attrs2 = getSortedAttributesString(e2);
            int attrCompare = attrs1.compareTo(attrs2);
            if (attrCompare != 0) return attrCompare;

            return e1.getTextTrim().compareTo(e2.getTextTrim());
        });
        // 替换为排序后的子元素
        element.elements().clear();
        element.add(children);
        // 递归处理子元素的子节点
        for (Element child : children) {
            sortElementsRecursively(child);
        }
    }

    // 将属性按键排序后拼接成字符串
    private static String getSortedAttributesString(Element elem) {
        if (elem.attributeCount() == 0) return "";
        List<org.dom4j.Attribute> attrs = new ArrayList<>(elem.attributes());
        attrs.sort(Comparator.comparing(org.dom4j.Attribute::getName));
        StringBuilder sb = new StringBuilder();
        for (org.dom4j.Attribute attr : attrs) {
            sb.append(attr.getName()).append("=").append(attr.getValue()).append(";");
        }
        return sb.toString();
    }
}

测试代码

String normalizedXml1 = XmlNormalizer.normalizeXml(xml1);
String normalizedXml2 = XmlNormalizer.normalizeXml(xml2);
assertEquals(normalizedXml1, normalizedXml2);

内容的提问来源于stack exchange,提问作者ka3ak

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 16:41:01