我不止一次見到有開發者吐槽 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
外掛和依賴:
gradle/libs.versions.toml
[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" }
composeApp/build.gradle.kts
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
序列化庫進行比較,而是應該在合適的場景下使用合適的工具。選擇技術棧時,專案的具體需求永遠是最重要的考慮因素。