别再说 Kotlin Serialization 难用了!
我不止一次见到有开发者吐槽 Kotlin Serialization
难用。尤其是 Java
开发者将它与 Jackson
\ Gson
来对比。这种印象主要源于对其工作原理的误解,Kotlin Serialization
并不依赖运行时反射机制来完成序列化/反序列化操作。
这个设计选择是经过深思熟虑的:Kotlin
是一个多平台语言,意味着同一份代码可以编译到 JVM
、Android
、Native
、JavaScript
等不同平台。而反射机制在各个平台的实现和性能特征差异很大,有些平台甚至完全不支持反射。因此,Kotlin Serialization
选择了一个更优雅的解决方案:通过编译器插件在编译期生成序列化代码。
这种方案带来了几个显著优势:
- 跨平台兼容性:生成的代码可以在所有支持的平台上运行
- 更好的性能:避免了运行时反射带来的性能开销
- 编译期类型安全:序列化错误在编译期就能被发现
接下来让我们看看在 Compose Multiplatform
项目中如何使用 Kotlin Serialization
。
初始化 Compose Multiplatform
项目可以查看我之前的文章。所有源代码基于我开源项目 crosspaste-desktop
快速开始
1. Gradle 配置
首先需要在项目中添加 Kotlin Serialization
插件和依赖:
[versions]
kotlin = "2.0.21"
[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
[plugins]
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
plugins {
...
alias(libs.plugins.kotlinSerialization)
...
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.serialization.json)
}
}
}
2. 基础使用
让我们通过一个包含单元测试的示例来详细了解 Kotlin Serialization
的基础功能。这个示例不仅展示了基本用法,还通过测试用例确保了序列化和反序列化的正确性:
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
// @Serializable 注解告诉编译器需要为这个类生成序列化代码
@Serializable
data class User(
// @SerialName 注解允许我们自定义序列化后的字段名
// 这在与后端 API 对接时特别有用,比如 MongoDB 默认使用 _id 作为主键
@SerialName("_id")
val id: Int,
val name: String,
val email: String
)
class JsonTest {
@Test
fun testJson() {
// 创建测试数据
val user = User(1, "张三", "zhangsan@example.com")
// 序列化为 JSON 字符串
// 注意这里直接使用 Json.encodeToString(user),不需要显式传入序列化器
// 这是因为 @Serializable 注解会在编译期自动生成所需的序列化器
val jsonString = Json.encodeToString(user)
// 验证序列化结果
// 可以看到 id 字段被序列化为 _id,这是因为我们使用了 @SerialName 注解
assertEquals(
"{\"_id\":1,\"name\":\"张三\",\"email\":\"zhangsan@example.com\"}",
jsonString
)
// 从 JSON 字符串反序列化
// 使用泛型函数 decodeFromString<User> 来指定目标类型
// Kotlin 的类型推断能够自动处理大部分情况
val decodedUser = Json.decodeFromString<User>(jsonString)
// 验证反序列化结果
// 通过比较原始对象和反序列化后的对象,确保整个序列化过程的正确性
assertEquals(user, decodedUser)
}
}
3. 自定义序列化
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
@Serializable
data class Post(
val id: Int,
val title: String,
// 使用 @Serializable 注解指定该字段使用自定义序列化器
@Serializable(with = LocalDateTimeIso8601Serializer::class)
val createTime: LocalDateTime
)
// 自定义序列化器需要实现 KSerializer 接口
object LocalDateTimeIso8601Serializer : KSerializer<LocalDateTime> {
// descriptor 定义了这个类型在序列化时的基本信息
// 这里我们将 LocalDateTime 序列化为字符串类型
override val descriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
// 实现序列化逻辑:如何将对象转换为基本类型
override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.toString())
}
// 实现反序列化逻辑:如何从基本类型恢复对象
override fun deserialize(decoder: Decoder): LocalDateTime {
return LocalDateTime.parse(decoder.decodeString())
}
}
class CustomJsonTest {
@Test
fun testCustomSerializer() {
val post = Post(1, "Hello", LocalDateTime(2021, 1, 1, 12, 0))
val jsonString = Json.encodeToString(post)
assertEquals(
"{\"id\":1,\"title\":\"Hello\",\"createTime\":\"2021-01-01T12:00\"}",
jsonString
)
val decodedPost = Json.decodeFromString<Post>(jsonString)
assertEquals(post, decodedPost)
}
}
实现自定义序列化器主要需要以下步骤:
- 定义序列化器类:
- 实现
KSerializer<T>
接口,其中T
是要序列化的类型 - 通常定义为
object
,因为序列化器通常是无状态的
- 提供序列化描述符:
- 实现
descriptor
属性 - 描述符定义了序列化后的数据类型(如字符串、数字等)
- 使用
PrimitiveSerialDescriptor
表示基本类型
- 实现序列化/反序列化方法:
serialize
:将对象转换为基本类型deserialize
:将基本类型转换回对象- 使用
encoder/decoder
提供的方法进行基本类型的编解码
- 应用序列化器:
- 使用
@Serializable(with = ...)
注解指定序列化器 - 可以针对特定字段使用不同的序列化策略
这种方式让我们能够:
- 完全控制序列化和反序列化的过程
- 将复杂类型转换为可序列化的基本类型
- 保持类型安全和编译时检查
4. 多态序列化
多态序列化是处理继承关系时的一个关键功能。当我们需要序列化一个可能包含多个子类型的基类或接口时,就需要用到多态序列化。这在处理插件系统、数据存储、网络传输等场景下特别有用。
composeApp/src/desktopMain/kotlin/com/crosspaste/utils /JsonUtils.desktop.kt
object DesktopJsonUtils : JsonUtils {
override val JSON: Json =
Json {
encodeDefaults = true
ignoreUnknownKeys = true
serializersModule =
SerializersModule {
polymorphic(PasteItem::class) {
subclass(ColorPasteItem::class)
subclass(FilesPasteItem::class)
subclass(HtmlPasteItem::class)
subclass(ImagesPasteItem::class)
subclass(RtfPasteItem::class)
subclass(TextPasteItem::class)
subclass(UrlPasteItem::class)
}
}
}
}
composeApp/src/commonMain/kotlin/com/crosspaste/dto/paste/SyncPasteCollection.kt
@Serializable
data class SyncPasteCollection(
val pasteItems: List<PasteItem>,
)
在这个例子中:
- 类型体系设计:
PasteItem
作为基类,统一抽象了不同类型的粘贴板内容- 各种具体实现(如
ColorPasteItem
、FilesPasteItem
等)处理不同的数据类型 @Serializable
注解和多态配置让这个类型体系可以被序列化和反序列化
- 应用场景:
- 网络同步:当用户在设备 A 复制内容时,可以将
PasteItem
序列化后发送到设备 B - 本地存储:可以将不同类型的粘贴板内容统一保存到本地数据库
- 实现优势:
- 类型安全:完整保留了类型信息,接收方可以安全地还原出正确的类型
- 扩展性好:添加新的粘贴板类型只需创建新的
PasteItem
子类并注册到SerializersModule
- 代码简洁:使用
List<PasteItem>
这样简单的数据结构就能处理所有类型的粘贴板内容
这种设计让 CrossPaste
能够优雅地处理各种类型的粘贴板内容,无论是文本、图片、文件还是富文本,都能在不同设备间可靠地传输和还原。这充分展示了 Kotlin Serialization
在实际项目中的应用价值。
总结
Kotlin Serialization
的设计选择反映了它的核心目标:为 Kotlin
多平台项目提供统一的序列化解决方案。它通过编译期代码生成而不是运行时反射来实现序列化,这带来了跨平台兼容性、类型安全性和优秀的性能表现。 然而,选择序列化库时需要根据具体场景来权衡:
如果你的项目是纯
JVM
环境:Jackson
/Gson
可能是更好的选择- 它们拥有更成熟的生态系统
- 使用起来更简单直观
- 有更丰富的功能支持和社区资源
如果你的项目涉及多平台开发:
Kotlin Serialization
是理想选择- 一套代码可以运行在所有平台
- 编译期保证类型安全
- 与
Kotlin
语言特性深度整合 - 更适合现代的
Kotlin-first
架构
归根结底,Kotlin Serialization
不是为了取代 Jackson
/Gson
,而是为了解决多平台序列化的问题。理解这一点,我们就不会把它简单地与 Java
序列化库进行比较,而是应该在合适的场景下使用合适的工具。选择技术栈时,项目的具体需求永远是最重要的考虑因素。