1. 简介

Groovy 提供了多种处理 XML 的方法,支持对 XML 结构进行解析、修改、增删、替换等操作。本文将介绍如何使用 Groovy 的核心类库,如 XmlParserXmlSlurperMarkupBuilder,来高效处理 XML 数据。

我们还将展示如何从零构建 XML 结构,以及在实际开发中使用这些类库时的一些常见坑点,帮助你快速上手并避免踩坑。


2. 定义模型

我们先定义一个 XML 示例文件 articles.xml,内容如下:

<articles>
    <article>
        <title>First steps in Java</title>
        <author id="1">
            <firstname>Siena</firstname>
            <lastname>Kerr</lastname>
        </author>
        <release-date>2018-12-01</release-date>
    </article>
    <article>
        <title>Dockerize your SpringBoot application</title>
        <author id="2">
            <firstname>Jonas</firstname>
            <lastname>Lugo</lastname>
        </author>
        <release-date>2018-12-01</release-date>
    </article>
    <article>
        <title>SpringBoot tutorial</title>
        <author id="3">
            <firstname>Daniele</firstname>
            <lastname>Ferguson</lastname>
        </author>
        <release-date>2018-06-12</release-date>
    </article>
    <article>
        <title>Java 12 insights</title>
        <author id="1">
            <firstname>Siena</firstname>
            <lastname>Kerr</lastname>
        </author>
        <release-date>2018-07-22</release-date>
    </article>
</articles>

然后在 Groovy 代码中加载这个 XML 文件:

def xmlFile = getClass().getResourceAsStream("articles.xml")

3. XmlParser

XmlParser 是 Groovy 中用于解析 XML 并对其进行操作的常用类之一。

3.1. 读取 XML

使用 XmlParser 解析 XML 文件非常简单:

def articles = new XmlParser().parse(xmlFile)

解析后,我们可以使用 GPath 表达式访问 XML 的属性和值。例如:

✅ 示例测试代码:

def "Should read XML file properly"() {
    given: "XML file"

    when: "Using XmlParser to read file"
    def articles = new XmlParser().parse(xmlFile)

    then: "Xml is loaded properly"
    articles.'*'.size() == 4
    articles.article[0].author.firstname.text() == "Siena"
    articles.article[2].'release-date'.text() == "2018-06-12"
    articles.article[3].title.text() == "Java 12 insights"
    articles.article.find { it.author.'@id'.text() == "3" }.author.firstname.text() == "Daniele"
}

⚠️ 注意articles.article[0].author.firstname.text() 表示获取第一个 article 的作者名字。

3.2. 添加节点

我们可以使用 NodeBuilder 构建新节点,并通过 append() 方法添加到现有 XML 中:

def "Should add node to existing xml using NodeBuilder"() {
    given: "XML object"
    def articles = new XmlParser().parse(xmlFile)

    when: "Adding node to xml"
    def articleNode = new NodeBuilder().article(id: '5') {
        title('Traversing XML in the nutshell')
        author {
            firstname('Martin')
            lastname('Schmidt')
        }
        'release-date'('2019-05-18')
    }
    articles.append(articleNode)

    then: "Node is added to xml properly"
    articles.'*'.size() == 5
    articles.article[4].title.text() == "Traversing XML in the nutshell"
}

3.3. 修改节点

修改节点值也很简单,直接设置 value 属性即可:

def "Should modify node"() {
    given: "XML object"
    def articles = new XmlParser().parse(xmlFile)

    when: "Changing value of one of the nodes"
    articles.article.each { it.'release-date'[0].value = "2019-05-18" }

    then: "XML is updated"
    articles.article.findAll { it.'release-date'.text() != "2019-05-18" }.isEmpty()
}

3.4. 替换节点

使用 replaceNode() 方法可以替换整个节点:

def "Should replace node"() {
    given: "XML object"
    def articles = new XmlParser().parse(xmlFile)

    when: "Replacing node"
    def articleNode = new NodeBuilder().article(id: '5') {
        title('Traversing XML in the nutshell')
        author {
            firstname('Martin')
            lastname('Schmidt')
        }
        'release-date'('2019-05-18')
    }
    articles.article[0].replaceNode(articleNode)

    then: "Node is added to xml properly"
    articles.'*'.size() == 4
    articles.article[0].title.text() == "Traversing XML in the nutshell"
}

3.5. 删除节点

删除节点时需要注意,不能直接调用 removeAll(),而应先筛选出符合条件的节点,再逐个删除:

def "Should remove article from xml"() {
    given: "XML object"
    def articles = new XmlParser().parse(xmlFile)

    when: "Removing all articles but the ones with id==3"
    articles.article
      .findAll { it.author.'@id'.text() != "3" }
      .each { articles.remove(it) }

    then: "There is only one article left"
    articles.children().size() == 1
    articles.article[0].author.'@id'.text() == "3"
}

4. XmlSlurper

XmlSlurper 是另一个用于处理 XML 的类,它提供了更简洁的 API。

4.1. 读取 XML

XmlParser 类似,使用方式如下:

def "Should read XML file properly"() {
    given: "XML file"

    when: "Using XmlSlurper to read file"
    def articles = new XmlSlurper().parse(xmlFile)

    then: "Xml is loaded properly"
    articles.'*'.size() == 4
    articles.article[0].author.firstname == "Siena"
    articles.article[2].'release-date' == "2018-06-12"
    articles.article[3].title == "Java 12 insights"
    articles.article.find { it.author.'@id' == "3" }.author.firstname == "Daniele"
}

4.2. 添加节点

添加节点方式类似,但更简洁:

def "Should add node to existing xml"() {
    given: "XML object"
    def articles = new XmlSlurper().parse(xmlFile)

    when: "Adding node to xml"
    articles.appendNode {
        article(id: '5') {
            title('Traversing XML in the nutshell')
            author {
                firstname('Martin')
                lastname('Schmidt')
            }
            'release-date'('2019-05-18')
        }
    }

    articles = new XmlSlurper().parseText(XmlUtil.serialize(articles))

    then: "Node is added to xml properly"
    articles.'*'.size() == 5
    articles.article[4].title == "Traversing XML in the nutshell"
}

⚠️ 注意:结构修改后需重新解析。

4.3. 修改节点

修改节点值更简单,直接赋值即可:

def "Should modify node"() {
    given: "XML object"
    def articles = new XmlSlurper().parse(xmlFile)

    when: "Changing value of one of the nodes"
    articles.article.each { it.'release-date' = "2019-05-18" }

    then: "XML is updated"
    articles.article.findAll { it.'release-date' != "2019-05-18" }.isEmpty()
}

4.4. 替换节点

替换节点使用 replaceNode() 方法:

def "Should replace node"() {
    given: "XML object"
    def articles = new XmlSlurper().parse(xmlFile)

    when: "Replacing node"
    articles.article[0].replaceNode {
        article(id: '5') {
            title('Traversing XML in the nutshell')
            author {
                firstname('Martin')
                lastname('Schmidt')
            }
            'release-date'('2019-05-18')
        }
    }

    articles = new XmlSlurper().parseText(XmlUtil.serialize(articles))

    then: "Node is replaced properly"
    articles.'*'.size() == 4
    articles.article[0].title == "Traversing XML in the nutshell"
}

4.5. 删除节点

删除节点可以使用 replaceNode({}) 实现:

def "Should remove article from xml"() {
    given: "XML object"
    def articles = new XmlSlurper().parse(xmlFile)

    when: "Removing all articles but the ones with id==3"
    articles.article
      .findAll { it.author.'@id' != "3" }
      .replaceNode {}

    articles = new XmlSlurper().parseText(XmlUtil.serialize(articles))

    then: "There is only one article left"
    articles.children().size() == 1
    articles.article[0].author.'@id' == "3"
}

5. XmlParser vs XmlSlurper

特性 XmlParser XmlSlurper
是否构建 DOM ✅ 是 ❌ 否
支持结构修改 ✅(但需重新解析)
性能 低(内存占用高) 高(适合大文件)
易用性 一般 更简洁
适用场景 小文件、需频繁修改 大文件、读取为主

建议:根据 XML 文件大小和操作需求选择合适的类。


6. MarkupBuilder

如果你需要从零构建 XML 结构,MarkupBuilder 是理想选择:

def "Should create XML properly"() {
    given: "Node structures"

    when: "Using MarkupBuilderTest to create xml structure"
    def writer = new StringWriter()
    new MarkupBuilder(writer).articles {
        article {
            title('First steps in Java')
            author(id: '1') {
                firstname('Siena')
                lastname('Kerr')
            }
            'release-date'('2018-12-01')
        }
        article {
            title('Dockerize your SpringBoot application')
            author(id: '2') {
                firstname('Jonas')
                lastname('Lugo')
            }
            'release-date'('2018-12-01')
        }
    }

    then: "Xml is created properly"
    XmlUtil.serialize(writer.toString()) == XmlUtil.serialize(xmlFile.text)
}

7. 总结

本文介绍了 Groovy 中处理 XML 的三种主要方式:

  • XmlParser:适合需要频繁修改 XML 的场景。
  • XmlSlurper:更适合读取大文件,语法更简洁。
  • MarkupBuilder:构建 XML 结构的利器。

在实际开发中,可以根据需求灵活选择,避免不必要的性能开销和代码复杂度。

完整示例代码可参考:GitHub 仓库(模拟地址)


原始标题:Working with XML in Groovy