Kotlin 解析 XML 的典型场景与选型思路
在 Kotlin 项目中,XML 仍然经常出现在配置文件、接口响应、模板数据、旧系统交换格式等位置。无论是 Android 应用读取本地配置,还是服务端程序处理遗留接口,都会遇到把 XML 文本转换为对象或结构化数据的需求。解析方式的选择,会直接影响内存占用、代码复杂度以及后续维护成本。

常见需求可以分成几类。第一类是读取本地配置,这类文档通常较小,更看重代码直观程度。第二类是处理网络响应,服务端可能返回 XML 格式数据,需要快速转换成业务模型。第三类是对接第三方系统,文档结构可能较复杂,甚至包含命名空间、属性节点和嵌套列表。不同需求下,最适合的解析器并不相同。
- 小型文档优先考虑可读性和开发效率。
- 大型文档优先考虑低内存和顺序解析能力。
- 模型结构稳定时,优先考虑对象映射。
- Android 平台可以优先考虑系统熟悉的解析接口。
从技术路线看,Kotlin 可以直接复用 Java 生态中的 XML 处理能力,也可以使用 Android 平台提供的解析接口。常见方案包括拉式解析、文档对象模型解析、事件驱动解析,以及基于第三方库的对象映射。理解这些方案的差异,有助于在实际项目中做出更合适的取舍。
使用 XmlPullParser 实现低内存拉式解析
XmlPullParser 是一种拉式解析方式。它不会把整个文档一次性装入内存,而是由开发者主动驱动解析器向后移动。解析器在前进过程中会不断报告当前事件,例如开始文档、开始标签、文本内容、结束标签和结束文档。开发者只需要在关键事件上提取数据即可。
在 Android 开发场景中,XmlPullParser 是比较常见的选择。它本身占用资源较少,也适合处理体积较大的 XML 数据。相比完全手写字符串截取,它能够正确处理标签嵌套、文本内容和结束边界,可靠性更高。相比事件驱动解析,它的代码流程又更接近顺序阅读,状态管理相对容易理解。
下面的示例把一段包含多个 <user> 节点的 XML 字符串解析为 List<User> 结果。示例使用 data class 保存结果,并在遇到结束标签时把临时字段组装成完整对象。
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory
import java.io.StringReader
// 定义用户数据模型
data class User(
val id: Int,
val name: String,
val age: Int
)
// 使用 XmlPullParser 顺序读取节点
fun parseUsersWithPullParser(xml: String): List<User> {
val factory = XmlPullParserFactory.newInstance()
val parser = factory.newPullParser()
parser.setInput(StringReader(xml))
val users = mutableListOf<User>()
var eventType = parser.eventType
var id = 0
var name = ""
var age = 0
while (eventType != XmlPullParser.END_DOCUMENT) {
when (eventType) {
XmlPullParser.START_TAG -> {
when (parser.name) {
"id" -> id = parser.nextText().toIntOrNull() ?: 0
"name" -> name = parser.nextText()
"age" -> age = parser.nextText().toIntOrNull() ?: 0
}
}
XmlPullParser.END_TAG -> {
if (parser.name == "user") {
users.add(User(id, name, age))
id = 0
name = ""
age = 0
}
}
}
eventType = parser.next()
}
return users
}
fun main() {
val xml = """
<users>
<user>
<id>1</id>
<name>张三</name>
<age>28</age>
</user>
<user>
<id>2</id>
<name>李四</name>
<age>32</age>
</user>
</users>
""".trimIndent()
println(parseUsersWithPullParser(xml))
}
这个示例中,nextText 用于读取当前元素的文本内容,eventType 用于判断解析器当前所处的位置。为了减少空值或非法数字带来的异常,示例使用 toIntOrNull 做安全转换。实际项目中,如果 XML 来自文件或网络,也可以把 StringReader 替换成 InputStream,并保持相同的事件处理逻辑。
使用 DOM 与 SAX 处理结构化文档
DOM 解析会把整个 XML 文档读取到内存中,并构建成一棵节点树。它的优势是结构清晰,开发者可以通过元素名快速查找节点,也可以在文档中反复遍历。对于体积较小、结构较稳定的 XML 文件,DOM 往往是最容易编写和维护的方案。
在 Kotlin 中,DOM 解析通常借助 Java 标准库中的 DocumentBuilderFactory 完成。先创建 DocumentBuilder,再把输入源交给解析器,最后得到 Document 对象。得到文档树之后,可以使用 getElementsByTagName 查找节点,再读取其中的文本内容。
import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import org.xml.sax.InputSource
import java.io.StringReader
data class User(
val id: Int,
val name: String,
val age: Int
)
// 使用 DOM 将 XML 文档加载为树结构
fun parseUsersWithDom(xml: String): List<User> {
val factory = DocumentBuilderFactory.newInstance()
val builder = factory.newDocumentBuilder()
val document: Document = builder.parse(InputSource(StringReader(xml)))
document.documentElement.normalize()
val users = mutableListOf<User>()
val userNodes: NodeList = document.getElementsByTagName("user")
for (index in 0 until userNodes.length) {
val node = userNodes.item(index)
if (node.nodeType != Node.ELEMENT_NODE) {
continue
}
val element = node as Element
val id = element.getElementsByTagName("id").item(0)?.textContent?.toIntOrNull() ?: 0
val name = element.getElementsByTagName("name").item(0)?.textContent.orEmpty()
val age = element.getElementsByTagName("age").item(0)?.textContent?.toIntOrNull() ?: 0
users.add(User(id, name, age))
}
return users
}
fun main() {
val xml = """
<users>
<user>
<id>1</id>
<name>张三</name>
<age>28</age>
</user>
<user>
<id>2</id>
<name>李四</name>
<age>32</age>
</user>
</users>
""".trimIndent()
println(parseUsersWithDom(xml))
}
SAX 解析与 DOM 的思路不同。SAX 不会在内存中保存完整文档,而是在读取过程中触发事件。开发者需要继承 DefaultHandler,并在开始元素、文本内容和结束元素等回调中维护自己的状态。由于它不需要构建整棵树,所以适合处理大型 XML 文件。
SAX 的代价是代码复杂度更高。开发者必须清楚当前位于哪个节点,也要在合适的时机重置临时状态。如果 XML 结构较深,或者需要同时处理属性、命名空间和列表包装节点,处理器中的状态判断会变得更复杂。因此,SAX 更适合只读取一次、不需要随机访问节点的场景。
import javax.xml.parsers.SAXParserFactory
import org.xml.sax.Attributes
import org.xml.sax.InputSource
import org.xml.sax.helpers.DefaultHandler
import java.io.StringReader
data class User(
val id: Int,
val name: String,
val age: Int
)
// 自定义 SAX 处理器,按事件收集节点内容
class UserSaxHandler : DefaultHandler() {
private val users = mutableListOf<User>()
private val buffer = StringBuilder()
private var id = 0
private var name = ""
private var age = 0
override fun startElement(
uri: String?,
localName: String?,
qName: String?,
attributes: Attributes?
) {
buffer.setLength(0)
}
override fun characters(ch: CharArray?, start: Int, length: Int) {
if (ch == null) {
return
}
buffer.append(ch, start, length)
}
override fun endElement(uri: String?, localName: String?, qName: String?) {
val content = buffer.toString().trim()
when (qName) {
"id" -> id = content.toIntOrNull() ?: 0
"name" -> name = content
"age" -> age = content.toIntOrNull() ?: 0
"user" -> {
users.add(User(id, name, age))
id = 0
name = ""
age = 0
}
}
buffer.setLength(0)
}
fun getResult(): List<User> = users
}
fun parseUsersWithSax(xml: String): List<User> {
val factory = SAXParserFactory.newInstance()
val parser = factory.newSAXParser()
val handler = UserSaxHandler()
parser.parse(InputSource(StringReader(xml)), handler)
return handler.getResult()
}
fun main() {
val xml = """
<users>
<user>
<id>1</id>
<name>张三</name>
<age>28</age>
</user>
<user>
<id>2</id>
<name>李四</name>
<age>32</age>
</user>
</users>
""".trimIndent()
println(parseUsersWithSax(xml))
}
使用第三方库完成对象映射
当 XML 结构和业务模型比较稳定时,手写解析逻辑会显得重复。此时可以考虑第三方映射库,例如 Jackson XML 或 Simple XML。这类库通常通过注解描述 XML 节点和 Kotlin 类之间的对应关系,然后把解析过程交给框架完成。
以 Jackson XML 为例,开发者可以使用 @JacksonXmlRootElement 描述根节点,使用 @JacksonXmlProperty 描述字段对应的节点名,使用 @JacksonXmlElementWrapper 处理列表包装关系。完成映射配置后,只需要调用 XmlMapper 的 readValue 方法,就可以把 XML 字符串转换成对象。
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement
// 使用前需要在构建脚本中加入 Jackson XML 相关依赖
@JacksonXmlRootElement(localName = "users")
data class Users(
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "user")
val user: List<User>? = null
)
data class User(
@JacksonXmlProperty(localName = "id")
val id: Int = 0,
@JacksonXmlProperty(localName = "name")
val name: String = "",
@JacksonXmlProperty(localName = "age")
val age: Int = 0
)
// 使用 XmlMapper 将 XML 字符串映射为对象
fun parseUsersWithJackson(xml: String): List<User> {
val mapper = XmlMapper()
return mapper.readValue(xml, Users::class.java).user.orEmpty()
}
fun main() {
val xml = """
<users>
<user>
<id>1</id>
<name>张三</name>
<age>28</age>
</user>
<user>
<id>2</id>
<name>李四</name>
<age>32</age>
</user>
</users>
""".trimIndent()
println(parseUsersWithJackson(xml))
}
第三方库的优势是代码量少,模型清晰,适合服务端项目或依赖体积不受严格限制的项目。不过,在 Android 等对包体积和启动速度敏感的环境中,需要评估依赖大小、反射开销和兼容性。对于简单配置解析,引入完整映射框架未必是最划算的选择。
| 解析方式 | 内存占用 | 编码复杂度 | 适用场景 |
|---|---|---|---|
| XmlPullParser | 低 | 中等 | Android 或顺序读取中大型 XML |
| DOM | 高 | 低 | 小型 XML,需要随机访问节点 |
| SAX | 低 | 较高 | 大型 XML,只按顺序消费数据 |
| Jackson XML | 取决于映射过程 | 低 | 对象映射,服务端或依赖允许的项目 |
从表中可以看出,没有一种方案适合所有场景。XmlPullParser 在内存和可读性之间比较平衡,DOM 更适合小文档,SAX 更适合大文档,第三方库更适合对象映射。选型时应优先明确文档大小、访问方式、运行平台和依赖限制。
总结与工程建议
在实际项目中,建议先判断 XML 的规模和访问模式。如果文档较小,并且需要频繁查询节点,DOM 很合适。如果文档较大,并且只能顺序消费,XmlPullParser 或 SAX 更稳妥。如果项目运行在 Android 平台,优先使用平台熟悉的解析接口,可以减少额外依赖。
其次要考虑维护成本。对于一次性脚本或简单配置,代码越直观越好。对于长期维护的业务系统,最好把 XML 结构映射成明确的 Kotlin 数据模型,避免在多个模块中重复书写节点名和类型转换。使用 data class 承载结果,也能让后续测试和调试更容易。
最后需要注意健壮性。无论选择哪种解析方式,都应处理缺失节点、空文本、非法数字和编码异常。对于来自外部系统的数据,还应关注 XML 安全配置,避免不必要的外部实体解析。如今 XML 已不是唯一的数据交换格式,但在遗留系统和企业接口中仍然常见,掌握多种解析方式能够让 Kotlin 项目更从容地应对真实需求。
KotlinXML解析DOM解析SAX解析XmlPullParser修改时间:2026-07-11 05:03:13