第三篇:Ktor ContentNegotiation 到底是什么?JSON 为什么能自动变成 Kotlin 对象?
上一篇我们已经会写最基本的 Ktor 请求client.get(/users) client.post(/users) { setBody(request) }响应也可以直接val user: User client .get(/users/1001) .body()第一次看到这里很容易产生一个疑问CreateUserRequest明明是 Kotlin 对象为什么setBody(request)以后就能发送 JSON反过来服务器明明返回的是 JSON 字符串为什么bodyUser()就能直接得到User真正参与这个过程的就是本篇要讲的ContentNegotiation kotlinx.serializationKtor Client 官方把ContentNegotiation作为客户端内容转换插件使用可以为 JSON、XML、CBOR、ProtoBuf 等内容注册序列化转换器对于 JSON可以选择 kotlinx.serialization、Gson、Jackson 等实现。但这里有一个特别容易混淆的地方ContentNegotiation 本身不是 JSON 解析库。它和kotlinx.serialization是两层不同的东西。理解这一点这篇基本就懂了一半。一、先说结论ContentNegotiation 到底是什么可以先用一句话理解ContentNegotiation 是 Ktor Client 中负责接入“内容转换能力”的 Plugin。比如install(ContentNegotiation) { json() }这段代码并不是在说ContentNegotiation 自己会解析 JSON更准确的结构是Ktor HttpClient ↓ ContentNegotiation ↓ 注册 JSON Converter ↓ kotlinx.serialization ↓ JSON ↔ Kotlin Object所以ContentNegotiation更像是一个转换机制的管理者 / 接入点真正负责Kotlin Object ↕ JSON的是后面的序列化实现。例如我们使用kotlinx.serializationKtor 官方当前推荐的 kotlinx JSON 集成需要ktor-client-content-negotiation和ktor-serialization-kotlinx-json同时 Kotlin 数据类序列化需要 Kotlin serialization compiler plugin。二、如果用 Retrofit 来类比就很好理解Android 开发中我们以前经常写Retrofit.Builder() .addConverterFactory( GsonConverterFactory.create() )为什么 Retrofit 接口可以GET(users/1001) suspend fun getUser(): User明明服务器返回的是{ id: 1001, name: Tom }最终却直接得到User因为中间存在Retrofit ↓ ConverterFactory ↓ Gson ↓ JSON → User到了 Ktor思想其实非常相似。Retrofit ConverterFactory Gson对应到Ktor ContentNegotiation kotlinx.serialization可以先这样建立映射Retrofit Ktor ConverterFactory ≈ ContentNegotiation Gson ≈ kotlinx.serialization这里的≈很重要。它只是帮助我们理解职责并不是说它们底层实现完全相同。三、ContentNegotiation 和 kotlinx.serialization 到底谁负责什么这是本篇最重要的地方。先看install(ContentNegotiation) { json() }可以拆成两层。第一层ContentNegotiation负责Ktor 的 Request / Response ↓ 应该使用哪个 Converter ↓ 什么时候进行序列化 什么时候进行反序列化也就是它把HTTP 请求响应和序列化系统连接起来。第二层kotlinx.serialization负责Kotlin Object ↕ JSON例如Serializable data class User( val id: Long, val name: String, )服务器 JSON{ id: 1001, name: Tom }kotlinx.serialization 才是真正负责把JSON ↓ User或者User ↓ JSON的工具。因此可以记成一句非常简单的话ContentNegotiation 管“什么时候转换、使用什么转换器”kotlinx.serialization 管“具体怎么转换”。Ktor 文档也把这两层分开先安装ContentNegotiationPlugin再通过json()、xml()等方式注册具体的序列化格式。四、先看发送请求setBody(request) 到底发生了什么假设我们有Serializable data class LoginRequest( val account: String, val password: String, )创建对象val request LoginRequest( account tom, password 123456, )然后client.post(/login) { contentType( ContentType.Application.Json ) setBody(request) }表面上看LoginRequest ↓ setBody() ↓ HTTP但真实过程更接近LoginRequest ↓ setBody(request) ↓ Ktor 发现这是 Kotlin Object ↓ 请求 Content-Type application/json ↓ ContentNegotiation ↓ 找到 JSON Converter ↓ kotlinx.serialization ↓ JSON ↓ HTTP Request Body最终真正发出去的是类似{ account: tom, password: 123456 }Ktor 官方的对象请求示例同样是在安装ContentNegotiation后通过contentType(ContentType.Application.Json)与setBody()配合发送序列化后的对象。五、所以 setBody() 自己会把对象变成 JSON 吗不会。这是一个很重要的误区。setBody()的职责更接近告诉 Ktor我要把这个东西作为 Request Body。比如setBody(hello)可以直接把字符串作为 Body。也可以setBody(byteArray)或者setBody(request)当传入的是 Kotlin 对象并且希望它作为 JSON 发出去时才需要ContentNegotiation JSON Converter来完成对象转换。Ktor 的setBody()支持多种 Body 类型而对象序列化需要安装相应的序列化能力。所以不要记成setBody() ↓ JSON 工具应该记成setBody() ↓ 设置 Request Body ContentNegotiation Serializer ↓ 负责必要的对象转换六、Content-Type 为什么重要刚才 POST 里还有一句contentType( ContentType.Application.Json )它最终对应 HTTPContent-Type: application/json这句话是在告诉服务器以及 Ktor 的内容转换机制这个 Request Body 的内容类型是 JSON。所以Content-Type ↓ 这个 Body 是什么格式例如application/json ↓ JSON还有text/plain ↓ 普通文本所以请求对象序列化时setBody(request) Content-Type: application/jsonKtor 才能够结合已经注册的 JSON Converter 来处理这个对象。Ktor 官方请求文档也明确使用contentType()来声明请求 Body 的媒体类型。七、再看响应body() 到底发生了什么服务器返回HTTP/1.1 200 OK Content-Type: application/jsonBody{ id: 1001, name: Tom }我们的代码val user: User client .get(/users/1001) .body()这个过程实际上是Server ↓ HTTP Response ↓ Content-Type: application/json ↓ JSON Body ↓ ContentNegotiation ↓ 找到 JSON Converter ↓ kotlinx.serialization ↓ User也就是JSON ↓ User这个转换不是HttpResponse 自己完成而是HttpResponse ↓ ContentNegotiation ↓ Serializer协作完成。Ktor 官方说明安装ContentNegotiation并注册对应序列化器后可以通过response.bodyT()将 Response Body 反序列化为 Kotlin 对象。八、为什么 body() 知道我要转换成 User例如val user: User response.body()或者更明显一点val user response.bodyUser()这里已经明确告诉 Ktor目标类型 ↓ User所以 Ktor 可以把JSON交给对应的 ConverterJSON ↓ kotlinx.serialization ↓ User如果换成val users response.bodyListUser()那么目标类型就是ListUser整个过程变成JSON Array ↓ kotlinx.serialization ↓ ListUser这也是后面我们写suspend inline fun reified T get(...): T的基础。九、如果不安装 ContentNegotiation 会怎么样假设只有val client HttpClient()然后val user client .get(/users/1001) .bodyUser()此时我们期待JSON ↓ User但 Ktor 并没有被告知JSON 应该使用什么 Converter也没有注册kotlinx.serialization JSON那么对象级的 JSON 自动转换就无法正常工作。所以需要val client HttpClient { install(ContentNegotiation) { json() } }这相当于告诉 Ktor以后遇到 JSON 内容 ↓ 可以使用这个 JSON Converter ↓ 完成对象序列化 / 反序列化这也是 Ktor 官方配置 JSON 序列化客户端的标准方式。十、完整配置长什么样一个基础配置可以是val client HttpClient { install(ContentNegotiation) { json() } }如果使用 kotlinx.serialization还通常会进一步配置Jsonval client HttpClient { install(ContentNegotiation) { json( Json { ignoreUnknownKeys true } ) } }结构可以理解成HttpClient ↓ install(ContentNegotiation) ↓ json(...) ↓ kotlinx.serialization 的 Json 配置所以这里实际上存在三层HttpClient ↓ ContentNegotiation Ktor Plugin ↓ json(...) ↓ kotlinx.serialization十一、ignoreUnknownKeys 是干什么的假设客户端定义Serializable data class User( val id: Long, val name: String, )但是服务器突然多返回一个字段{ id: 1001, name: Tom, avatar: xxx }客户端User里没有avatar如果项目希望忽略 DTO 中没有声明的额外 JSON 字段可以配置Json { ignoreUnknownKeys true }这样服务端增加一些客户端暂时不关心的字段时就不会因为“未知字段”而直接导致反序列化失败。这里要注意这是kotlinx.serialization 的 Json 配置不是 ContentNegotiation 自己的配置能力。也就是ContentNegotiation ↓ 负责接入 Json { ignoreUnknownKeys true } ↓ 属于 Serializer 的具体行为十二、为什么需要 Serializable例如Serializable data class User( val id: Long, val name: String, )这个Serializable也是kotlinx.serialization体系里的东西。它不是 Ktor 的注解。可以先理解成告诉 Kotlin serialization这个类型需要具备序列化和反序列化能力。对应工程中还需要应用 Kotlin Serialization Compiler Plugin。Ktor 官方 JSON serialization 示例也要求启用 Kotlin serialization plugin并添加对应 Ktor kotlinx JSON 模块。所以Serializable和ContentNegotiation不是一个框架层级。结构应该是Ktor │ └── ContentNegotiation ↓ 接入 Serializer ↓ kotlinx.serialization │ ├── Serializable ├── Json ├── encode └── decode十三、完整的发送流程现在把整个 POST 串起来。代码Serializable data class LoginRequest( val account: String, val password: String, )请求client.post(/login) { contentType( ContentType.Application.Json ) setBody( LoginRequest( account Tom, password 123456, ) ) }HttpClientHttpClient { install(ContentNegotiation) { json() } }完整流程LoginRequest ↓ Serializable ↓ setBody(request) ↓ Request Content-Type application/json ↓ ContentNegotiation ↓ 找到 JSON Converter ↓ kotlinx.serialization ↓ JSON ↓ Engine ↓ HTTP ↓ Server最终服务器收到{ account: Tom, password: 123456 }十四、完整的接收流程服务器返回{ id: 1001, name: Tom }响应 HeaderContent-Type: application/json客户端val user: User client .get(/users/1001) .body()完整流程Server ↓ HTTP ↓ Engine ↓ HttpResponse ↓ Content-Type: application/json ↓ bodyUser() ↓ ContentNegotiation ↓ JSON Converter ↓ kotlinx.serialization ↓ User所以现在bodyUser()就不再是什么“魔法”了。十五、如果后端返回的是统一 ApiResponse 呢实际项目里一般不会直接返回{ id: 1001, name: Tom }而可能是{ code: 0, msg: success, data: { id: 1001, name: Tom } }那么客户端可以定义Serializable data class ApiResponseT( val code: Int, val msg: String, val data: T, )然后val response client .get(/users/1001) .bodyApiResponseUser()转换过程JSON ↓ ContentNegotiation ↓ kotlinx.serialization ↓ ApiResponseUser其中外层 ↓ ApiResponse 内层 data ↓ User于是以后还可以bodyApiResponseListUser()甚至bodyApiResponseOrder()所以ContentNegotiation kotlinx.serialization 泛型最终就构成了正式项目中统一网络响应解析的基础。十六、ContentNegotiation 不只支持 JSON这里还有一个很容易被名字误导的地方。我们现在写的是install(ContentNegotiation) { json() }但 ContentNegotiation 并不是JSON Plugin它负责的是更广义的内容格式转换Ktor Client 官方当前支持通过该机制配置 JSON、XML、CBOR、ProtoBuf 等格式具体支持范围也与所选平台和序列化模块有关。所以理论上可以理解成ContentNegotiation │ ├── JSON ├── XML ├── CBOR └── ProtoBuf只是普通 App 后端 REST API 中JSON最常见所以我们大部分时候看到的都是json()十七、所以为什么名字叫 ContentNegotiation从 HTTP 本身看请求和响应都存在Content-Type Accept这些 Header 用来描述我发送的是什么格式 我希望接收什么格式例如Content-Type: application/json说明我发送的是 JSON而Accept: application/json表示我希望服务器返回 JSONKtor 的ContentNegotiationPlugin 就围绕这些内容类型将具体的 HTTP 内容和已经注册的序列化转换器连接起来。Ktor 官方文档也把该 Plugin 定义为客户端内容协商与序列化/反序列化的入口。所以这个名字可以理解为HTTP Content Type ↓ 选择合适的 Converter ↓ 转换内容十八、它和 Retrofit Converter 的差别怎么理解如果以前使用Retrofit Gson可以这样理解HTTP Response ↓ Retrofit Converter ↓ Gson ↓ Java / Kotlin ObjectKtorHTTP Response ↓ ContentNegotiation ↓ JSON Converter ↓ kotlinx.serialization ↓ Kotlin Object因此对于 Android 开发者来说ContentNegotiation最开始完全可以借助Retrofit Converter来理解。但是要注意Ktor Plugin 是作用于整个 HttpClient 生命周期中的能力ContentNegotiation 只是其中负责内容转换的一种 Plugin。整个 Ktor Client 可能还有HttpClient │ ├── DefaultRequest ├── ContentNegotiation ├── Logging ├── Auth ├── HttpTimeout └── HttpRequestRetry所以它是 Ktor Plugin 体系中的一个能力模块。十九、ContentNegotiation 和 Plugin 思维再联系起来第一篇我们说过Ktor 的 Plugin 更像是在 HttpClient 生命周期中提前安装某种能力。现在就很好理解了。你没有安装install(ContentNegotiation)那么 HttpClient 并没有自动 JSON 对象转换能力安装之后install(ContentNegotiation) { json() }相当于告诉这个 HttpClient以后处理 Request / Response 时 如果涉及 JSON 我已经为你准备好了 JSON 转换能力它不是你每次请求时手动调用Json.decode()而是提前安装HttpClient ↓ 挂上 JSON 转换能力 ↓ 以后每次请求/响应需要时自动参与这就是 Ktor Plugin 的设计思想。二十、这也是 Ktor 和 Retrofit 思维差异比较大的地方Retrofit 时代我们更习惯一个 Call ↓ 经过各种网络处理 ↓ 得到结果而 Ktor 更适合理解成HttpClient │ ├── 提前安装 JSON 能力 ├── 提前安装日志能力 ├── 提前安装 Auth 能力 ├── 提前安装 Timeout 能力 └── 提前安装 Retry 能力 ↓ 以后 Request / Response 经过生命周期时 相关 Plugin 自动参与因此install(ContentNegotiation)不是拦截一次请求做 JSON 转换。而是给这个 HttpClient 安装“内容转换能力”在后续请求和响应的相应阶段参与处理。这正是前面理解 Ktor Plugin 时最重要的地方。二十一、多 Client 项目怎么办上一篇我们已经知道正式项目可能存在NetworkClients │ ├── api ├── refresh ├── upload ├── download └── thirdParty那么每一个 Client 都可以拥有自己的 ContentNegotiation 配置。例如普通 APIHttpClient { install(ContentNegotiation) { json( Json { ignoreUnknownKeys true } ) } }第三方接口如果 JSON 规则不同可以使用另一套配置apiClient ↓ Json Config A thirdPartyClient ↓ Json Config B这也再次说明Plugin 是安装在具体 HttpClient 上的能力。不是整个 App 只能有一套。二十二、正式项目里应该放在哪里不要在每个 Api 中写install(ContentNegotiation)它应该属于NetworkClient 创建阶段例如internal fun createNetworkClient( config: NetworkConfig, ): HttpClient { return HttpClient { install(ContentNegotiation) { json( Json { ignoreUnknownKeys true } ) } } }然后createNetworkClient() ↓ 创建 HttpClient ↓ 安装 ContentNegotiation ↓ Client 长期复用以后UserApi OrderApi RepairApi都不需要知道JSON 到底怎么解析只需要client.get(...) .bodyUser()这就是网络基础设施和业务层解耦。二十三、几个最容易产生的误区误区一ContentNegotiation JSON 解析器不准确。应该是ContentNegotiation ↓ 内容转换 Plugin kotlinx.serialization ↓ 具体序列化实现误区二setBody() ↓ 自动把对象转 JSON不准确。应该是setBody() ↓ 设置 Body ContentNegotiation Serializer ↓ 对象 → JSON误区三bodyUser() ↓ HttpResponse 自己解析 JSON不准确。应该是HttpResponse ↓ ContentNegotiation ↓ JSON Converter ↓ Serializer ↓ User误区四Serializable ↓ Ktor 注解错误。它属于kotlinx.serialization不是 Ktor。误区五ContentNegotiation 只能处理 JSON也不对。JSON 只是最常见的一种内容格式Ktor 的内容转换机制还可以配置其他受支持的序列化格式。二十四、现在把整个网络转换流程串起来发送Kotlin Object ↓ setBody() ↓ ContentNegotiation ↓ JSON Converter ↓ kotlinx.serialization ↓ JSON ↓ HttpClient ↓ Engine ↓ Server响应Server ↓ Engine ↓ HttpResponse ↓ JSON ↓ ContentNegotiation ↓ JSON Converter ↓ kotlinx.serialization ↓ bodyT() ↓ Kotlin Object如果再把上一篇的知识放进来HttpClient │ ┌──────────┴──────────┐ ↓ ↓ Plugin Engine │ │ ↓ ↓ ContentNegotiation OkHttp │ Darwin ↓ │ kotlinx.serialization ↓ │ HTTP ↓ JSON ↔ Object整个 Ktor 网络体系就开始串起来了。二十五、从 Retrofit 迁移最后这样记如果以前是Retrofit ↓ GsonConverterFactory ↓ Gson ↓ JSON ↔ Object现在 KtorHttpClient ↓ ContentNegotiation ↓ json() ↓ kotlinx.serialization ↓ JSON ↔ Object所以可以暂时形成这个对应关系Retrofit Ktor ConverterFactory → ContentNegotiation Gson → kotlinx.serialization ResponseUser → response.bodyUser()这不是严格的一一对应但对于从 Retrofit 迁移到 Ktor 来说是非常好用的认知桥梁。二十六、本篇总结这一篇最重要的就是区分两个概念ContentNegotiation和kotlinx.serialization可以记成ContentNegotiation ↓ Ktor 的内容转换 Plugin ↓ 负责把 HTTP 与转换器连接起来 kotlinx.serialization ↓ 具体的序列化框架 ↓ 负责 Kotlin Object ↔ JSON发送请求Kotlin Object ↓ setBody() ↓ ContentNegotiation ↓ kotlinx.serialization ↓ JSON接收响应JSON ↓ ContentNegotiation ↓ kotlinx.serialization ↓ bodyT() ↓ Kotlin Object所以以后再看到install(ContentNegotiation) { json() }脑子里应该出现的不是安装一个 JSON 库而是给 HttpClient 安装“内容转换能力” ↓ 注册 JSON Converter ↓ 底层使用 kotlinx.serialization而看到setBody(request)应该想到设置请求 Body ↓ 需要时通过 ContentNegotiation 完成对象序列化看到response.bodyUser()应该想到读取 Response Body ↓ ContentNegotiation 根据内容类型找到 Converter ↓ kotlinx.serialization ↓ User最终一句话总结ContentNegotiation 负责让 Ktor 的请求/响应拥有“自动内容转换机制”kotlinx.serialization 则负责真正完成 Kotlin 对象和 JSON 之间的序列化与反序列化。理解这一层以后Ktor 中看似“自动”的setBody()和bodyT()其实就都能解释清楚了。下一篇《kotlinx.serialization 从零理解KMP 为什么不再依赖 Gson》下一篇脱离 Ktor 本身专门学习Serializable 是什么 序列化和反序列化到底是什么意思 Json.encodeToString() Json.decodeFromString() SerialName 有什么用 默认值怎么处理 nullable 怎么处理 ignoreUnknownKeys 是什么 泛型和嵌套对象怎么解析 为什么 kotlinx.serialization 特别适合 KMP也就是把这一篇里的ContentNegotiation ↓ kotlinx.serialization继续往下一层彻底拆开。