11、Kotlin序列化与反射:kotlinx.serialization的注解处理、自定义序列化器

说到序列化,很多Android开发者的第一反应是Gson或者Moshi。但自从Kotlin官方推出了kotlinx.serialization,我个人就彻底转向了它。为什么?因为它跟Kotlin的语言特性结合得太紧密了——尤其是反射和注解处理这块。

说白了,kotlinx.serialization不是运行时反射,而是编译期通过注解处理器生成序列化代码。这意味着性能更好,而且没有反射带来的那些坑。今天我们就来深入聊聊它的注解处理机制,以及如何编写自定义序列化器。

11.1 kotlinx.serialization 的注解处理机制

先看一个最简单的例子:

@Serializable
data class User(
    val name: String,
    val age: Int,
    @Transient val password: String = ""
)

这个@Serializable注解,就是整个序列化流程的入口。编译器看到它后,会自动生成一个UserSerializer对象。你不需要手动写任何序列化代码。

我在项目中遇到过一个问题:团队里有人给一个很大的数据类加了@Serializable,结果编译时间明显变长了。这是因为注解处理器需要遍历所有属性,生成对应的序列化逻辑。对于几十个字段的类,这个开销是肉眼可见的。

核心原理:kotlinx.serialization 使用 Kotlin 符号处理 API(KSP)或轻量级注解处理器(KAPT)在编译期解析注解,生成序列化器代码。生成的代码是纯 Kotlin,不依赖反射。

11.2 常用注解详解

除了@Serializable,还有几个注解你一定会用到:

注解 作用 使用场景
@SerialName 指定序列化后的字段名 后端字段名与Kotlin属性名不一致时
@Transient 排除某个字段 密码、临时缓存等不需要序列化的字段
@Required 标记字段为必填 反序列化时缺少该字段会抛异常
@EncodeDefault 序列化时输出默认值 默认情况下,等于默认值的字段会被省略

举个例子,后端返回的字段名是user_name,但你的Kotlin属性是userName

@Serializable
data class UserResponse(
    @SerialName("user_name") val userName: String,
    val age: Int
)

嗯,这里要注意:@SerialName不仅影响序列化,也影响反序列化。我曾经因为拼写错误,导致线上数据解析失败——user_name写成了username,排查了半天才发现。

11.3 自定义序列化器

有些场景下,默认的序列化行为满足不了需求。比如你有一个日期字段,想序列化成时间戳而不是字符串。这时候就需要自定义序列化器。

实现一个自定义序列化器,需要继承KSerializer接口:

object DateSerializer : KSerializer<Date> {
    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG)

    override fun serialize(encoder: Encoder, value: Date) {
        encoder.encodeLong(value.time)
    }

    override fun deserialize(decoder: Decoder): Date {
        return Date(decoder.decodeLong())
    }
}

然后在使用时通过@Serializable注解指定:

@Serializable
data class Event(
    val name: String,
    @Serializable(with = DateSerializer::class) val createdAt: Date
)

你想想看,这样做的好处是什么?序列化逻辑和业务逻辑完全解耦了。哪天后端改了时间格式,你只需要改DateSerializer这一个地方。

个人建议:自定义序列化器最好写成object单例,避免每次序列化都创建新实例。如果你的序列化器需要参数,可以用类而不是object,但要注意线程安全。

11.4 复合对象的自定义序列化

有时候你需要序列化一个复杂的对象结构。比如一个Result<T>泛型类:

@Serializable
class Result<T>(val code: Int, val data: T, val message: String)

直接加@Serializable会报错,因为泛型参数T的类型在编译期不确定。这时候需要自定义序列化器来处理泛型:

class ResultSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<Result<T>> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Result") {
        element<Int>("code")
        element("data", dataSerializer.descriptor)
        element<String>("message")
    }

    override fun serialize(encoder: Encoder, value: Result<T>) {
        encoder.encodeStructure(descriptor) {
            encodeIntElement(descriptor, 0, value.code)
            encodeSerializableElement(descriptor, 1, dataSerializer, value.data)
            encodeStringElement(descriptor, 2, value.message)
        }
    }

    override fun deserialize(decoder: Decoder): Result<T> {
        return decoder.decodeStructure(descriptor) {
            var code = 0
            var data: T? = null
            var message = ""
            while (true) {
                when (val index = decodeElementIndex(descriptor)) {
                    0 -> code = decodeIntElement(descriptor, 0)
                    1 -> data = decodeSerializableElement(descriptor, 1, dataSerializer)
                    2 -> message = decodeStringElement(descriptor, 2)
                    CompositeDecoder.DECODE_DONE -> break
                }
            }
            Result(code, data!!, message)
        }
    }
}

这段代码看起来有点长,但结构很清晰。核心就是encodeStructuredecodeStructure这两个方法,它们保证了序列化和反序列化的顺序一致。

避坑指南:我曾经在descriptor中定义的字段顺序,与serialize/deserialize中操作的索引不一致,导致反序列化时数据错乱。记住:索引从0开始,且必须与buildClassSerialDescriptor中的声明顺序完全一致。

11.5 知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

Kotlin序列化与反射:知识体系 kotlinx.serialization 注解处理机制 @Serializable 触发编译期处理 KSP/KAPT 生成序列化器代码 零反射,高性能 常用注解详解 @SerialName 字段映射 @Transient 排除字段 @Required 必填校验 @EncodeDefault 默认值输出 自定义序列化器 KSerializer 接口实现 descriptor 定义结构 serialize/deserialize 方法 泛型序列化器支持 核心要点 • 编译期处理,无反射开销 • 注解驱动,声明式编程 • 自定义序列化器实现灵活控制 • 泛型支持,类型安全

11.6 实战中的一些建议

最后分享几个我在实际项目中的经验:

  • 优先使用默认序列化器——除非有特殊需求,否则不要轻易自定义。默认的已经覆盖了90%的场景。
  • 注意版本兼容性——kotlinx.serialization的版本需要和Kotlin版本匹配。我见过因为版本不匹配导致的诡异编译错误。
  • 测试你的序列化器——尤其是自定义的,一定要写单元测试。我曾经漏掉了一个字段,导致反序列化时一直拿不到数据。
  • 利用@Transient优化性能——对于不需要序列化的字段,比如缓存数据、临时状态,记得加上这个注解。

说白了,kotlinx.serialization的设计哲学就是「约定优于配置」。你只需要告诉它「我要序列化这个类」,剩下的它帮你搞定。当你需要更精细的控制时,自定义序列化器又给了你足够的灵活性。这种平衡,正是它吸引我的地方。


公众号:蓝海资料掘金营,微信deep3321