项目工程中使用 XML 作为配置文件很常见,比如 Android 项目中的 AndroidManifest.xml 和 Maven 项目中的 pom.xml,我们在编写脚本工具时经常需要对这类 XML 文件进行读写操作。

XML 的解析有很多种方法,但总体来说都比 JSON 要复杂。今天来介绍一个我常用的 XML 解析库——Dom4J。

为了避免引起逻辑上的混乱,下面我会分几个例子介绍其用法。

添加依赖:

dependencies {
    implementation("org.dom4j:dom4j:2.1.4")
}

首先看如何读取 XML,一般情况下我们会读取 XML 文件:

fun parseXml(file: File) {
    val document = SAXReader().read(file)
    // ...
}

SAXReader 从 SAX 解析事件创建 Dom4J 树,通过 read() 方法读取 XML 并生成一个 Document 对象。

read() 不仅支持 File 类型,其他的如 URLInputStreamReader 等也是可以的。

如果希望从字符串中解析,可以换一种方式:

fun parseXml(xml: String) {
    val document = DocumentHelper.parseText(xml)
    // ...
}

读取 XML 内容后,我们通过这个 Document 对象,就可以获取到里面的节点。

举个例子,我们解析如下 XML:

<school>
    <student>
        <name>Tom</name>
        <age>18</age>
    </student>
</school>

代码如下:

fun parseDocument(document: Document) {
    val rootElement = document.rootElement
    val student = rootElement.element("student")
    val name = student.elementText("name")
    val age = student.element("age").text
    // ...
}

通过 DocumentgetRootElement() 方法拿到根节点,每个节点都是一个 Element 对象,调用其 element() 方法可以获取子节点。最内层的节点,我们有两种方式获取它的值,一种是对该节点的父节点调用 elementText() 方法,可以直接根据子节点的名称获取到值;另一种是先获取到该子节点 Element 对象再调用 getText() 方法获取值。所获取到的值都是 String 类型,按需转换。

在 Android 中我们常见的配置往往是通过属性配置的,比如:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="warp_content"
        android:background="@color/white"
        tools:background="@color/black" />
</FrameLayout>

这是一个布局文件,解析其属性的方法如下:

fun parseDocument(document: Document) {
    val rootElement = document.rootElement
    val textView = rootElement.element("TextView")
    val id = textView.attributeValue("id")
    // ...
}

获取到对应元素的 Element 后,调用 attributeValue() 方法传入属性名获取对应的值,可以看到,虽然该 XML 中每个属性都指定了命名空间,也就是前面的 android:,但我们并不需要传入完整的 android:id 才能获取到该属性的值,恰恰相反,如果把命名空间也写上反而获取不到,这是因为命名空间需要额外配置。

相信你已经注意到,上方对于 background 属性,我在两个命名空间下都有定义,分别是 android:backgroundtools:background,如果直接调用 attributeValue("background"),你猜它会获取到哪个值?

实际上,哪个定义排在前面,它就会获取哪个属性的值。

对于不同命名空间的属性,我们可以通过以下方法进行获取:

fun parseDocument(document: Document) {
    val rootElement = document.rootElement
    val toolsNamespace = Namespace.get("tools", "http://schemas.android.com/tools")
    val androidNamespace = Namespace.get("android", "http://schemas.android.com/apk/res/android")
    val textView = rootElement.element("TextView")
    val toolsBackground = textView.attributeValue(QName.get("background", toolsNamespace))
    val androidBackground = textView.attributeValue(QName.get("background", androidNamespace))
    // ...
}

先传入前缀和其 Uri 构建对应的命名空间 Namespace,获取属性值时,我们不再简单粗暴地传入属性名,而是通过 Namespace 构建 QNameQName 是 XML 元素或属性的限定名称值,它能够帮助我们根据命名空间解析正确的值。

对于一些相同标签的组,我们无法直接通过标签名直接获取到我们想要的值,可以一次性把这个组下所有的标签都获取到,再根据条件筛选。

比如 Android 中用到的:

<application xmlns:android="http://schemas.android.com/apk/res/android">
    <activity android:name=".MainActivity" />
    <activity android:name=".SplashActivity" />
    <activity android:name=".AboutActivity" />
</application>

通过 Elementelements() 可以获取其所有的子节点。

fun parseDocument(document: Document) {
    val rootElement = document.rootElement
    val activities = rootElement.elements()
    activities.forEach {
        if (it.attribute("name").text.contains("MainActivity")) {
            // ...
        }
    }
}

获取所有属性也是一样的操作,调用 attributes() 即可,不再赘述。

另外还可以通过 elementIterator()attributeIterator() 获取迭代器,使用迭代器遍历来完成操作。

以上就是使用 Dom4J 对 XML 的读操作,在这几个例子中我们可以发现,主要操作的几个对象无非就是 DocumentElementAttribute,接下来的写操作大同小异。

fun createDocument(): Document {
    val document = DocumentHelper.createDocument()
    val root = document.addElement("root")
    val author = root.addElement("author")
        .addAttribute("location", "UK")
        .addText("James Strachan")
    return document
}

节点、属性的添加与修改,都可以通过 addElement()addAttribute() 来完成,需要注意的是,如果涉及到命名空间,依旧要借助 QName

为了方便日志打印或其他操作,可以将 Document 转为 XML 字符串:

fun convertToXml(document: Document) {
    val text = document.asXML()
    // ...
}

作为脚本,最重要的当然是回写到文件:

fun write(document: Document) {
    FileWriter("output.xml").use { 
        val writer = XMLWriter(it)
        writer.write(document)
        writer.close()
    }
}

根据路径创建 FileWriter,再创建出 XMLWriter 并调用 write()Document 写入到路径所对应的文件中,最后不要忘了调用 close()

除了写入到文件中外,还可以输出到控制台:

fun log(document: Document) {
    val format = OutputFormat.createPrettyPrint()
    val writer = XMLWriter(System.out, format)
    writer.write(document)
    writer.close()
}

这里同时还创建了一个格式美化的 OutputFormat,创建 XMLWriter 不再借助 FileWriter,而是传入 System.out,相信任何一个 Java 开发者对这个都不陌生。

以上就是 Dom4J 的常见用法,更详细的介绍可以查阅官方文档