在 Java 的世界中,有很多种 json library 任君挑选,其中最多人使用的应该是 Jackson 及 Gson。我过往都是使用 Jackson,因为这是 Spring Boot 及 Play Framework 的预设偏好。因为我几乎没使用过其它的 library,所以也无法做详尽的功能比较及推荐,不过我个人认为除非你需要处理数量或内容庞大的 json 资料,那麽可以参考 Github 上的 benchmark 结果做为挑选依据,要不然其实就用预设的就好,基本上你需要或想得到的功能都已经有实作了。
至於 Ktor ContentNegotiation Plugin 当然也有支援 Jackson 及 Gson,不过预设支援的还有自家 JetBrains 开发的 kotlinx.serialization,kotlinx.serialization 的特点在於
虽然我目前的需求不需要跨平台、又只需要转换 json 资料,但基於做 side project 就是要学习新技术的精神,而且未来跨平台开发会是一个趋势,同时也想知道 Complier Plugin 的方式与 Reflection 的效能比较,所以决定使用 kotlinx.serialization !
使用 kotlinx.serialization 在开发上最大的差别就是它会要求或限制你的程序写法,不像 Reflection 方式那样无脑使用。例如只有 Backing fields 才会被序列化,所以如果你使用 delegated property 或是 getter function 那就不行。还有类别一定要加上 @Serializable annotation 或是已内建支援的型态才能转换,所以如果要转换的物件类别,存在任一个变数的型态不能转换的话,那就无法「编译」程序码了。例如针对 java.util.UUID 要另外写 CustomSerializer 实作 serialize / deserialize 函式,然後在变数宣告加上 @Serializable(with = UUIDSerializer::class)
才能编译,否则 Compiler Plugin 根本不知道怎麽产生程序码。如果是 Jackson 的话,几乎任何物件都能够在执行期透过反射转换,除非产生的 json 不是你想要的,例如 UUID 会被序列化为 mostSigBits, leastSigBits 2个属性值的 JsonObject,而非一个字串,此时我们要自己写 CustomSerializer 再注册到 Jackson。
上面的例子只是众多限制之一,尤其处理继承多型的类别更是要小心,建议如果你是初次使用 kotlinx.serialization,一定要先把官方说明文件大略看过一遍,至少有个印象,否则你会遇到一堆 Compiler Plugin 无法编译的错误,或是发现怎麽有些属性没被 serialize,然後就想要放弃了…XD。另外还要关注一下 kotlinx.serialization 新版本,因为随着版本的进化,过去的限制可能会被解除或有条件松绑,此时升级一下也是不错的。
以上对 kotlinx.serialization 有所认识之後,接下来说明 Ktor 使用 kotlinx.serialization 处理 request json deserialization 及 response json serialization 的实作技巧。
我在 ApplicationCall 类别加上 receiveAndValidateBody
extension function,透过 kotlinx.serialization 的 serializer() 取得 reified type parameter T 类别的 serializer,藉此达到类似 reflection 的效果,这样就可以处理每一种 T 型态的类别。但要小心 T 类别必须是 Serializable,否则会丢出 SerializationException,而且目前使用 serializer() 必须标注 InternalSerializationApi
@OptIn(InternalSerializationApi::class)
suspend inline fun <reified T : Form<*>> ApplicationCall.receiveAndValidateBody(location: Location? = null): T {
val form = try {
json.decodeFromString(T::class.serializer(), receiveUTF8Text())
} catch (e: Throwable) {
throw RequestException(InfraResponseCode.BAD_REQUEST_BODY, "can't deserialize from json: ${e.message}", e)
}
//... 以下省略
}
我在 ApplicationCall 类别加上 respond(responseDTO: ResponseDTO)
extension function。ResponseDTO 设计上考虑多种回应格式,最简单的格式 DataResponseDTO 就直接继承 ResponseDTO,然後加上 @SerialName("data") 作为 classDiscriminator property 的值。
为了简化建立 DataResponseDTO 物件的写法,在这里使用 invoke operator overloading 达到直接呼叫 constructor 一样的写法 call.respond(DataResponseDTO(loginResponse))
这样子就不必另外再定义 factory method,而且还可以拿到 refied type parameter T,然後再取得 T 的 serializer 了
suspend fun ApplicationCall.respond(responseDTO: ResponseDTO) {
respond(responseDTO.code.httpStatusCode, responseDTO)
}
@Serializable
sealed class ResponseDTO {
abstract val code: ResponseCode
abstract val message: String?
abstract val data: JsonElement?
}
@Serializable
@SerialName("data")
class DataResponseDTO(
override val code: ResponseCode = InfraResponseCode.OK,
override val message: String? = null,
override val data: JsonElement
) : ResponseDTO() {
companion object {
inline operator fun <reified T : Any> invoke(data: T, message: String? = null): DataResponseDTO {
return DataResponseDTO(data = json.encodeToJsonElement(T::class.serializer(), data), message = message)
}
inline operator fun <reified T : Any> invoke(data: List<T>, message: String? = null): DataResponseDTO {
return DataResponseDTO(data = JsonArray(data.map { json.encodeToJsonElement(T::class.serializer(), it) }), message = message)
}
}
}
更多 kotlinx.serialization 的实作程序码可参考Github repo,包含了许多常见资料型态的 CustomSerializer,例如 BigDecimal 及 java.time.* 的 ZonedDateTime…等。还有2个 json object 的 deepMerge 操作。
<<: [Day8] Android - Kotlin笔记:JetPack - Core KTX
简单型别 + 字串 简单型别定义 Case01 - 简单型别 Controller 预期 post ...
衔接昨日 Part 5 - AutoVC 这部分我们暂时先参考官网 model_vc.py 即可 P...
昨天没有写完的 Array & Slice(上) ,今天要来把补完进度。 那我们就开始吧 ─...
角色情境 小明同时学会输入指令操作着终端机、 以及透过滑鼠操作着图像化介面的 Sourcetree ...
如何处理定期的需求 相信很多人会遇到需要定期做某些事情的状况,例如每分钟去计算一次资料,或者一分钟跟...