前面我们已经学习了:
Logging
HttpTimeout
HttpRequestRetry
ContentNegotiation
DefaultRequest
这些 Ktor 官方已经实现好的 Plugin。
比如:
install(Logging)
install(HttpTimeout)
install(HttpRequestRetry)
只要:
install
↓
配置
↓
使用
就可以获得对应能力。
但是到这里,其实只能说明:
我们已经会"使用 Ktor"。
真正理解 Ktor,需要继续往下一层:
为什么 Logging 可以看到 Request / Response?
为什么 Retry 可以重新发送 Request?
为什么 ContentNegotiation 可以修改 Request Body 和 Response Body?
如果公司有自己的:
请求签名
Body 加密
Response 解密
TraceId
特殊协议
请求统计
应该怎么做?
这就必须进入 Ktor Client 真正核心的扩展机制:
Custom Client Plugin
+
Request / Response 生命周期
+
Hook
Ktor 3.5.2 当前官方提供 createClientPlugin() 创建自定义 Client Plugin,并把请求、响应、Body 转换等不同阶段通过 onRequest、onResponse、transformRequestBody、transformResponseBody 以及 Send、SendingRequest 等 Hook 暴露出来。
这一篇,我们先从 Android 开发最熟悉的:
OkHttp Interceptor
重新理解 Ktor。
一、为什么先讲 OkHttp?
因为 OkHttp 的心智模型非常直观。
以前 Android:
Retrofit
↓
OkHttp
↓
Interceptor Chain
↓
Network
你写一个:
class MyInterceptor :
Interceptor {
override fun intercept(
chain: Interceptor.Chain,
): Response {
val request =
chain.request()
// Request 前处理
val response =
chain.proceed(
request
)
// Response 后处理
return response
}
}
基本就可以理解整个拦截器模型:
Request
↓
Interceptor
↓
做事情
↓
chain.proceed()
↓
后面的 Interceptor
↓
Network
↓
Response
↑
继续回到当前 Interceptor
所以一个 intercept() 同时拥有:
Request 前
+
Response 后
两个方向的控制权。
这也是为什么 OkHttp 自定义 Interceptor 自由度特别高。
二、先恢复 OkHttp 整条责任链
当前 OkHttp 的 RealCall 在构建完整 Interceptor Chain 时,顺序仍然是:
Application Interceptors
↓
RetryAndFollowUpInterceptor
↓
BridgeInterceptor
↓
CacheInterceptor
↓
ConnectInterceptor
↓
Network Interceptors
↓
CallServerInterceptor
也就是说,OkHttp 内部核心是 5 个内置 Interceptor ,同时在前后分别允许开发者插入 Application Interceptor 和 Network Interceptor。当前源码的 getResponseWithInterceptorChain() 就是按照这个顺序组装。
完整一点:
Retrofit
↓
Application Interceptor
↓
RetryAndFollowUpInterceptor
↓
BridgeInterceptor
↓
CacheInterceptor
↓
ConnectInterceptor
↓
Network Interceptor
↓
CallServerInterceptor
↓
Socket / Server
Response 则反方向回来:
Server
↓
CallServerInterceptor
↓
Network Interceptor
↓
ConnectInterceptor
↓
CacheInterceptor
↓
BridgeInterceptor
↓
RetryAndFollowUpInterceptor
↓
Application Interceptor
↓
Retrofit
这就是典型的:
责任链模式。
三、OkHttp 第一个核心 Interceptor:RetryAndFollowUpInterceptor
先看名字:
Retry
+
Follow Up
它负责的是:
当前请求没有正常结束时,还要不要继续产生后续请求。
例如:
Request
↓
网络连接失败
↓
判断能否恢复
↓
可能 Retry
或者:
Request
↓
302 Redirect
↓
生成新的 Request
↓
继续请求
再比如认证挑战等后续请求,也属于这个方向。
所以可以把它记成:
RetryAndFollowUpInterceptor = "这次请求结束了吗?如果没有,我还需要继续发什么?"
四、第二个:BridgeInterceptor
Bridge:
桥
它连接的是:
应用层 Request
和:
真正 HTTP 网络请求
例如你业务只写:
Request.Builder()
.url(url)
.build()
真正发 HTTP 时还涉及:
Host
Connection
Content-Type
Content-Length
Transfer-Encoding
Cookie
Accept-Encoding
User-Agent
Response 回来后还可能涉及:
Cookie
gzip 解压
等 HTTP 层处理。
因此可以记:
BridgeInterceptor = 应用层 Request/Response 与标准 HTTP 网络协议之间的桥梁。
五、第三个:CacheInterceptor
顾名思义:
Cache
主要负责:
这次 Request
↓
缓存能不能直接满足?
例如:
GET /users
↓
CacheInterceptor
↓
缓存仍然有效
↓
直接返回 Response
可能:
根本不访问服务器
也可能:
本地有缓存
↓
需要服务器验证
↓
条件请求
↓
304
↓
继续使用缓存内容
所以:
CacheInterceptor 决定这次 Response 应该来自缓存、服务器,还是缓存和服务器共同决定。
六、第四个:ConnectInterceptor
到了这里,开始真正接近:
连接
主要解决:
这次 Request 到底通过哪条连接发送?
例如:
ConnectionPool
↓
有现成可复用连接?
/ \
有 没有
↓ ↓
复用 建立新连接
建立新连接又可能涉及:
DNS
↓
TCP
↓
TLS
↓
Connection
所以可以记:
ConnectInterceptor = 给 Request 准备真正的网络连接。
七、第五个:CallServerInterceptor
这已经来到最靠近服务器的一层。
负责真正:
写 Request Header
写 Request Body
读取 Response Header
读取 Response Body
也就是:
HTTP I/O
所以:
CallServerInterceptor = 真正把 HTTP Request 发给服务器,并读取 HTTP Response。
八、五个核心拦截器怎么快速记?
可以直接记一句:
Retry
↓
要不要继续?
Bridge
↓
把业务请求补成 HTTP 请求
Cache
↓
缓存能不能解决?
Connect
↓
找到网络连接
CallServer
↓
真正收发 HTTP
缩成:
重试 → 桥接 → 缓存 → 连接 → 通信。
九、Application Interceptor 又是什么?
我们平时:
OkHttpClient.Builder()
.addInterceptor(
MyInterceptor()
)
添加的是:
Application Interceptor
当前 OkHttp 源码中:
client.interceptors
被放在所有五个内部核心 Interceptor 之前。
所以:
MyInterceptor
↓
RetryAndFollowUp
↓
Bridge
↓
Cache
↓
Connect
↓
CallServer
这就是为什么 Application Interceptor 非常适合:
公共 Header
Token
日志
签名
统一 Request 修改
等应用级横切逻辑。
十、Network Interceptor 又是什么?
如果:
.addNetworkInterceptor(
MyNetworkInterceptor()
)
位置就不同:
ConnectInterceptor
↓
Network Interceptor
↓
CallServerInterceptor
当前 OkHttp 链路源码就是这样插入 client.networkInterceptors 的。
它更接近:
真正网络 Request / Response
而不是整个业务 Call。
所以:
Application Interceptor
与:
Network Interceptor
不能简单理解成:
两个名字不同但完全一样
它们观察的层级不同。
十一、HttpLoggingInterceptor 其实就是官方帮你写好的 Interceptor
前面我们讲过:
val logging =
HttpLoggingInterceptor().apply {
level =
HttpLoggingInterceptor
.Level
.BODY
}
OkHttpClient.Builder()
.addInterceptor(logging)
本质:
HttpLoggingInterceptor
↓
就是一个 Interceptor
Square 帮你实现了:
读取 Request
↓
打印
↓
proceed()
↓
读取 Response
↓
打印
所以:
HttpLoggingInterceptor
不是某种神秘的 OkHttp 特殊能力。
它本质仍然建立在:
Interceptor
扩展机制之上。
这点非常重要。
十二、于是你就可以理解"官方能力"和"底层扩展能力"
OkHttp:
HttpLoggingInterceptor
↓
官方已经写好的功能
底层:
Interceptor
↓
真正的扩展机制
同理 Ktor:
Logging
HttpTimeout
HttpRequestRetry
Auth
ContentNegotiation
↓
官方已经实现好的 Plugin
底层:
Client Plugin
+
Hook
真正的扩展机制。
这就是这一篇最核心的第一层认知。
十三、自定义 OkHttp Interceptor 为什么这么自由?
例如:
class CustomInterceptor :
Interceptor {
override fun intercept(
chain: Interceptor.Chain,
): Response {
val oldRequest =
chain.request()
val newRequest =
oldRequest
.newBuilder()
.header(
"X-Trace-Id",
createTraceId(),
)
.build()
val start =
System.currentTimeMillis()
val response =
chain.proceed(
newRequest
)
val duration =
System.currentTimeMillis()
- start
println(
"duration=$duration"
)
return response
}
}
一个函数里:
拿 Request
↓
修改 Request
↓
记录开始时间
↓
proceed
↓
得到 Response
↓
计算耗时
↓
检查 Response
↓
return
全部可以完成。
所以 OkHttp 的思维非常像:
一个巨大的可控制节点
十四、那 Ktor 为什么没有一个完全一样的 intercept()?
因为 Ktor 选择了另一套扩展模型。
不是:
给你一个 intercept()
↓
所有事情都塞里面
而是:
把一次 Client Call
拆成不同阶段
↓
每个阶段提供 Handler / Hook
Ktor 3.5.2 官方对 Custom Client Plugin 的描述也非常明确:新 API 不要求开发者直接操作内部 Pipeline phase,而是提供一组针对 Request、Response 不同处理阶段的 Handler。
也就是说:
OkHttp
一个 intercept()
↓
自己控制前后
Ktor
多个生命周期 Handler / Hook
↓
选择正确阶段
十五、Ktor 的真正核心心智模型
先看一个学习用简化图:
HttpClient
Request
↓
SetupRequest
↓
onRequest
↓
transformRequestBody
↓
on(Send)
↓
on(SendingRequest)
↓
Engine
↓
HTTP
↓
Response
↓
onResponse
↓
transformResponseBody
↓
body<T>()
注意:
这是一张帮助理解职责的简化生命周期图,不等于 Ktor 所有内部 Pipeline 实现细节。
官方当前给出的 Custom Plugin handler 顺序里明确包括 SetupRequest、onRequest、transformRequestBody、Send、SendingRequest、onResponse 和 transformResponseBody;其中 transformResponseBody 是在调用 HttpResponse.body() 时参与转换。
十六、第一层:SetupRequest
Ktor 提供:
on(SetupRequest) {
...
}
官方当前说明:
SetupRequest是 Request 处理过程中最先执行的 Hook。
可以先理解:
Request 生命周期
↓
非常早期的 Setup 阶段
普通业务自定义 Plugin 其实不一定需要直接使用它。
大部分常见 Request 修改:
Header
URL
Attributes
使用:
onRequest
已经足够。
十七、onRequest:最容易理解的 Hook
例如:
val TracePlugin =
createClientPlugin(
"TracePlugin"
) {
onRequest {
request,
_,
->
request.headers.append(
"X-Trace-Id",
createTraceId(),
)
}
}
安装:
HttpClient {
install(TracePlugin)
}
于是每次:
client.get()
client.post()
都会经过:
onRequest
官方当前定义就是:onRequest 在每一次 HttpClient.request 创建 HTTP Request 时执行,可以修改 HttpRequestBuilder。
所以适合:
Header
URL
Query
Attributes
Request 级配置
十八、这和 OkHttp Application Interceptor 有什么感觉上的对应?
例如 OkHttp:
val request =
chain.request()
.newBuilder()
.header(
"X-Trace-Id",
traceId,
)
.build()
Ktor:
onRequest { request, _ ->
request.headers.append(
"X-Trace-Id",
traceId,
)
}
所以在:
修改普通 Request 配置
这个场景下:
OkHttp Application Interceptor
≈
Ktor onRequest
但只能说:
职责类似。
不能说两个生命周期完全相同。
十九、createClientPlugin 到底创建了什么?
最简单:
val CustomHeaderPlugin =
createClientPlugin(
"CustomHeaderPlugin"
) {
onRequest {
request,
_,
->
request.headers.append(
"X-App-Version",
"1.0.0",
)
}
}
createClientPlugin() 返回:
ClientPlugin
然后:
install(CustomHeaderPlugin)
安装到某个:
HttpClient
上。
当前官方 API 就是这样定义的:它创建一个可以安装进 HttpClient 的 ClientPlugin,Plugin 内部可以定义 onRequest、onResponse 等 Handler。
二十、Plugin 还可以有自己的 Config
例如:
class TracePluginConfig {
var headerName:
String = "X-Trace-Id"
var enabled:
Boolean = true
}
然后:
val TracePlugin =
createClientPlugin(
name = "TracePlugin",
createConfiguration =
::TracePluginConfig,
) {
val headerName =
pluginConfig.headerName
val enabled =
pluginConfig.enabled
onRequest {
request,
_,
->
if (!enabled) {
return@onRequest
}
request.headers.append(
headerName,
createTraceId(),
)
}
}
安装:
install(TracePlugin) {
headerName =
"X-Request-Id"
enabled =
true
}
这就开始像官方:
install(Logging) {
...
}
install(HttpTimeout) {
...
}
了。
因为:
官方 Plugin 和你自己写的 Custom Plugin,在"Plugin + Config + install"这个设计上本质是一套思想。
Ktor 官方也建议把 PluginConfig 中的可变配置值在 Plugin 创建阶段保存到局部变量中使用。
二十一、第二个关键 Hook:transformRequestBody
现在进入真正重要的地方。
假设:
client.post("/user") {
setBody(
User(
name = "Tom",
age = 18,
)
)
}
这里:
User
是业务对象。
但网络不能直接发送:
Kotlin User 对象
最终必须变成:
JSON
Text
ByteArray
FormData
OutgoingContent
Ktor 提供:
transformRequestBody {
request,
content,
bodyType,
->
...
}
官方说明:如果你的 Plugin 要处理该 Body,需要把它转换成 OutgoingContent,例如 TextContent、ByteArrayContent 或 FormDataContent;如果当前转换器不适用,则返回 null。
二十二、一个官方思路的 Body 转换例子
例如有:
data class User(
val name: String,
val age: Int,
)
我们不使用 JSON,而规定发送:
Tom;18
可以:
val DataTransformationPlugin =
createClientPlugin(
"DataTransformationPlugin"
) {
transformRequestBody {
request,
content,
bodyType,
->
if (
bodyType?.type ==
User::class
) {
val user =
content as User
TextContent(
text =
"${user.name};${user.age}",
contentType =
ContentType.Text.Plain,
)
} else {
null
}
}
}
于是:
User("Tom", 18)
↓
transformRequestBody
↓
TextContent("Tom;18")
↓
Engine
Ktor 官方 Custom Client Plugin 文档当前就使用同类 User -> TextContent 示例解释 Request Body Transformation。
二十三、这和 ContentNegotiation 是什么关系?
现在就能理解:
ContentNegotiation
为什么也是 Plugin。
例如:
User
↓
ContentNegotiation
↓
kotlinx.serialization
↓
JSON OutgoingContent
它本质也在参与:
Request / Response Body 转换
Ktor 官方对 ContentNegotiation 的职责就是发送时序列化、接收时反序列化。
所以:
ContentNegotiation
不是某种脱离 Plugin 机制的特殊功能。
它仍然是在:
HttpClient 生命周期
中增加:
Body 转换能力
二十四、所以不要自己重新造 JSON Converter
如果你有:
DTO
↔
JSON
直接:
install(ContentNegotiation) {
json(...)
}
就够了。
不要为了练 Custom Plugin:
transformRequestBody
↓
自己手写 JSON
transformResponseBody
↓
自己 JSON.parse
把官方已经解决好的:
ContentNegotiation
重新实现一遍。
Custom Plugin 应该用在:
项目特有能力
而不是:
重新造官方基础设施
二十五、第三个重要 Hook:SendingRequest
这个 Hook 很容易和:
onRequest
混。
区别非常重要。
官方当前说明:
onRequest
针对原始 HttpClient.request;
而:
SendingRequest
针对实际发送的每一次 Request。
如果发生:
Redirect
那么:
onRequest
↓
原始 Request 执行一次
但:
SendingRequest
↓
原始请求执行
↓
Redirect 后新请求也执行
如果 Send 发起额外请求,同样会再次出现 SendingRequest。
二十六、用 Redirect 就很好理解
假设:
GET /old
↓
302
↓
GET /new
那么:
业务调用:
client.get("/old")
onRequest 更接近:
用户发起的原始 Call
而:
SendingRequest
看到:
Send #1
GET /old
Send #2
GET /new
所以可以记:
onRequest 看"这次业务 Request",SendingRequest 看"真正每一次发送"。
这是一个非常重要的区别。
二十七、Retry 场景下 SendingRequest 更容易理解
例如:
Request
↓
503
↓
Retry
↓
Request
业务调用只有:
一次
但真正 Send:
两次
那么:
onRequest
↓
更接近一次业务请求
而:
SendingRequest
↓
每一次真实发送
这也是为什么:
RetryCount
AttemptId
每次 Send 日志
之类的信息,更值得关注 Sending 阶段。
二十八、第四个核心 Hook:Send
这一个是最接近 OkHttp:
chain.proceed()
思维的 Hook。
当前 Ktor API 对 Send 的说明非常直接:
它可以检查 Response,并在需要时发起额外 Request,典型用途包括 Redirect、Retry、Authentication。
概念:
Request
↓
Send
↓
拿到 Response
↓
检查
↓
需要吗?
/ \
否 是
↓ ↓
返回 再发 Request
这就非常接近:
val response =
chain.proceed(request)
if (...) {
return chain.proceed(
newRequest
)
}
return response
这种 OkHttp 思想。
二十九、这也解释了 Retry 为什么可以是 Plugin
之前我们学:
HttpRequestRetry
感觉像:
Ktor 神奇地知道怎么重新请求
现在就能理解:
Request
↓
Send
↓
Response / Exception
↓
判断 Retry Condition
↓
再次 Send
所以 Retry 的核心能力依赖:
发送生命周期控制
而不是:
在 ViewModel catch 后再手写一次 get()
三十、Auth 也一样
下一篇我们要学:
Auth
尤其:
Bearer
401
Refresh Token
现在提前就能猜到:
Request
↓
加 AccessToken
↓
Send
↓
401
↓
Refresh Token
↓
更新 Token
↓
再次 Send 原 Request
所以 Auth 为什么也是 Plugin?
因为它需要:
Request 前
+
Response 后
+
必要时再次发送
这一整套生命周期能力。
你把这一篇理解透以后,下一篇 Auth 会明显简单很多。
三十一、Ktor 还有 HttpSend.intercept
如果你特别怀念 OkHttp:
chain.proceed()
Ktor 还有一个更直观的 API:
client
.plugin(HttpSend)
.intercept { request ->
val call =
execute(request)
call
}
官方当前文档甚至直接使用:
execute(request)
↓
检查 Response
↓
不符合条件
↓
execute(request)
演示手动 Retry。HttpSend 不需要额外 install,通过 client.plugin(HttpSend) 就可以获取并拦截实际发送流程。
这从代码形态上非常像:
OkHttp
chain.proceed(request)
Ktor HttpSend
execute(request)
但依然不要认为两者实现模型完全相同。
三十二、第五个 Handler:onResponse
收到:
HttpResponse
以后,可以:
onResponse { response ->
println(
response.status
)
}
官方当前说明 onResponse 会针对进入 Client 的 HTTP Response 执行,可用于:
检查 Response
记录日志
保存 Cookie
等观察行为。
所以非常适合:
Status
Header
RequestId
Server Trace
耗时统计
这种:
观察 Response
的场景。
三十三、onResponse 和 transformResponseBody 不一样
这一点特别重要。
onResponse:
我收到了一个 Response
↓
我要看看它
而:
transformResponseBody
是:
我调用 body<T>()
↓
这个 Body 到底怎么变成 T?
两者的职责完全不同。
三十四、第六个核心 Handler:transformResponseBody
例如服务器返回:
Tom;18
客户端调用:
response.body<User>()
那么自定义 Plugin 可以:
transformResponseBody {
response,
content,
requestedType,
->
if (
requestedType.type ==
User::class
) {
val text =
content.readLine()
?: return@transformResponseBody null
val values =
text.split(";")
User(
name = values[0],
age =
values[1]
.toInt(),
)
} else {
null
}
}
于是:
ByteReadChannel
↓
transformResponseBody
↓
User
Ktor 当前官方文档明确说明:transformResponseBody 会在 HttpResponse.body() 时调用,需要把原始 ByteReadChannel 转换为请求的目标类型;不适用时可以返回 null。官方 Custom Plugin 示例同样展示了 Tom;18 -> User 的转换。
三十五、现在终于能理解 ContentNegotiation 的另一半
服务器:
{
"name": "Tom",
"age": 18
}
业务:
response.body<User>()
中间:
Response Body
↓
ContentNegotiation
↓
kotlinx.serialization
↓
User
所以 ContentNegotiation 本质上就在做:
Request Body Transformation
+
Response Body Transformation
这也是为什么前面说:
不要只把 ContentNegotiation 理解成 Retrofit ConverterFactory 的名字替换。
它真正是 Ktor Client 生命周期中的一个 Plugin 能力。
三十六、Request 和 Response 的完整心智模型
现在可以画出一条更完整的链:
业务层
ApiService
↓
NetworkClient
↓
HttpClient
↓
──────────────────────────
Request 生命周期
──────────────────────────
SetupRequest
↓
onRequest
↓
Request Body Transformation
↓
Send
↓
SendingRequest
↓
Engine
↓
──────────────────────────
真实网络
──────────────────────────
HTTP
↓
──────────────────────────
Response 生命周期
──────────────────────────
HttpResponse
↓
onResponse
↓
Response Body Transformation
↓
body<T>()
↓
──────────────────────────
业务数据
──────────────────────────
T
这张图是第十篇最重要的图之一。
三十七、但 Retry / Redirect 会让它不再是一条单线
这也是 Ktor 比简单流程图更复杂的地方。
例如:
业务 Request
↓
onRequest
↓
Send
↓
SendingRequest #1
↓
503
↓
再次 Send
↓
SendingRequest #2
↓
200
所以:
一个业务 Call
完全可能对应:
多次实际 Send
Ktor 官方文档也特别指出:当 Send 发起额外请求时,SendingRequest 会针对每次实际发送继续执行,而 onRequest对应原始请求。
三十八、这正是 OkHttp 与 Ktor 心智模型的关键差异
OkHttp:
Request
↓
Interceptor A
↓
Interceptor B
↓
Interceptor C
↓
Network
↓
Response
↑
Interceptor C
↑
Interceptor B
↑
Interceptor A
核心思维:
一条责任链。
Ktor:
HttpClient Call
↓
不同生命周期阶段
↓
不同 Plugin
↓
每个 Plugin 注册对应 Hook
核心思维:
生命周期 + Hook + Plugin。
三十九、所以不能再说"Ktor Plugin 就是 Interceptor"
这个类比只能帮助入门。
比较准确的是:
OkHttp Interceptor
≈
一种统一的横切扩展入口
而:
Ktor Client Plugin
≈
一组挂载在不同 Client 生命周期上的横切能力
所以 Ktor Plugin 能做的范围实际上比:
intercept(Request → Response)
这个单一模型更分散、更明确。
四十、做一张真正的对照表
| 需求 | OkHttp 常见做法 | Ktor 常见入口 |
|---|---|---|
| 修改 Request Header | Application Interceptor | onRequest / DefaultRequest |
| 每次真实发送前观察 | Network/自定义 Interceptor | SendingRequest |
| 修改 Request Body 表达 | 重建 RequestBody | transformRequestBody |
| 真正执行并检查 Response | chain.proceed() |
Send / HttpSend |
| 查看 Response Status/Header | Response |
onResponse |
| 转换 Response Body | 读取/重建 ResponseBody | transformResponseBody |
| Retry | 自定义/内部 Retry | HttpRequestRetry / Send |
| Auth | Interceptor + Authenticator | Auth / Send |
| HTTP Logging | HttpLoggingInterceptor |
Logging |
| JSON | Retrofit Converter | ContentNegotiation |
注意:
这是一张"职责映射表",不是源码一一对应表。
四十一、那完全自定义 Plugin 能做什么?
例如:
TraceId
请求统计
Request Header
请求签名
特殊协议
Body Transformation
Response Validation
Response Transformation
特殊 Retry
自定义认证
都可以。
真正应该问的已经不是:
"Ktor 有没有 Interceptor?"
而是:
"我的逻辑应该插在哪个生命周期?"
这才是 Ktor 的正确问题。
四十二、举例:TraceId 应该放哪?
需求:
每个业务请求
↓
生成 TraceId
↓
放 Header
↓
Response 回来计算耗时
很自然:
onRequest
↓
生成 TraceId
onResponse
↓
记录结果
但还需要:
Request 和 Response
如何共享 TraceId / 开始时间?
Ktor 提供:
Attributes
处理 Call 内状态共享。
官方 Custom Plugin 文档就用 AttributeKey 在 SendingRequest 中保存开始时间,再在 onResponse 中取出来计算 Response Time。
四十三、完整 Trace Plugin
先定义配置:
class TracePluginConfig {
var headerName:
String = "X-Trace-Id"
var log:
(String) -> Unit = {}
}
定义 Plugin:
val TracePlugin =
createClientPlugin(
name = "TracePlugin",
createConfiguration =
::TracePluginConfig,
) {
val headerName =
pluginConfig.headerName
val logger =
pluginConfig.log
val traceIdKey =
AttributeKey<String>(
"TraceId"
)
val startTimeKey =
AttributeKey<Long>(
"TraceStartTime"
)
onRequest {
request,
_,
->
val traceId =
createTraceId()
request.attributes.put(
traceIdKey,
traceId,
)
request.headers.append(
headerName,
traceId,
)
}
on(SendingRequest) {
request,
_,
->
request.attributes.put(
startTimeKey,
System.currentTimeMillis(),
)
}
onResponse { response ->
val attributes =
response.call
.request
.attributes
val traceId =
attributes[
traceIdKey
]
val startTime =
attributes[
startTimeKey
]
val duration =
System.currentTimeMillis()
- startTime
logger(
"traceId=$traceId, " +
"status=${response.status}, " +
"duration=${duration}ms"
)
}
}
安装:
HttpClient {
install(TracePlugin) {
headerName =
"X-Request-Id"
log = { message ->
AppLogger.d(
tag = "HTTP",
message = message,
)
}
}
}
这已经是一个真正:
可配置
可复用
可安装
的自定义 Client Plugin。
四十四、它和普通工具类的本质区别是什么?
如果只是:
fun addTraceId(
request: HttpRequestBuilder,
)
这只是:
一个函数
你每次请求都得主动调用。
而 Plugin:
install(TracePlugin)
之后:
整个 HttpClient
↓
自动获得该能力
所以:
普通函数
↓
调用者主动调用
Plugin
↓
生命周期自动触发
这就是 Plugin 的价值。
四十五、举例:请求签名应该怎么思考?
假设后端要求:
timestamp
+
method
+
path
+
body hash
+
secret
↓
HMAC
↓
X-Signature
以前 OkHttp:
Custom Interceptor
↓
拿 Request
↓
读 Body
↓
算 Signature
↓
重建 Request
Ktor 不应该第一反应:
找一个 intercept()
而应该先问:
Timestamp 在什么时候加?
Body 在什么时候已经有合适的表达?
Signature 最终应该写到哪里?
也就是说:
生命周期设计
优先于:
API 选择
四十六、特别注意:不要以为 transformRequestBody 一定拿到 JSON
这是一个非常容易踩的坑。
例如:
setBody(
User(...)
)
在某个 Body Transformation 阶段:
content
可能仍然是:
User
而不是:
JSON String
所以如果你的签名协议要求:
最终 JSON ByteArray
↓
SHA256
就必须认真考虑:
ContentNegotiation
你的 Plugin
安装顺序
具体 Hook
不能想当然:
transformRequestBody
↓
一定已经是 JSON
这就是理解生命周期的重要性。
四十七、Request 加密同样如此
需求:
DTO
↓
JSON
↓
AES
↓
Encrypted Body
↓
Server
真正要求的顺序是:
Serialization
↓
Encryption
↓
Send
而不是:
拿到 DTO
↓
不知道现在是什么阶段
↓
随便 encrypt()
所以 Custom Plugin 最难的并不是:
createClientPlugin()
这几个字。
真正难的是:
你的业务能力应该发生在生命周期的哪个阶段。
四十八、Response 解密也是一样
服务器:
Encrypted Bytes
↓
Client
↓
Decrypt
↓
JSON
↓
User
要求:
Decrypt
必须发生在:
JSON Deserialize
之前。
所以整个链路应该明确:
Network Bytes
↓
Decrypt
↓
JSON
↓
ContentNegotiation
↓
DTO
而不是:
DTO 都已经解析完了
↓
再想起来我要解密
这就是为什么 Body Transformation 的顺序特别重要。
四十九、Custom Plugin 不等于"什么都塞一个 Plugin"
这是另一个容易走向极端的地方。
不要写:
SuperNetworkPlugin
里面:
Header
Token
Logging
Retry
Timeout
JSON
Cache
Signing
Encryption
Error
最后又变成:
超级 Interceptor
好的设计仍然应该:
TracePlugin
SigningPlugin
EncryptionPlugin
ConnectivityPlugin
按职责拆。
原则:
一个 Plugin 表达一个相对稳定的横切能力。
五十、官方 Plugin 与 Custom Plugin 怎么选择?
很简单:
官方已经有成熟能力
例如:
Logging
Timeout
Retry
Auth
ContentNegotiation
Cookies
Compression
优先:
官方 Plugin
公司协议特有能力
例如:
公司自己的签名算法
特殊加密协议
Trace 规范
设备 ID 协议
机器人 Command Header
特定监控协议
考虑:
Custom Plugin
所以:
不要为了"更底层"而拒绝官方 Plugin。真正理解框架以后,反而更应该知道什么时候不需要自己造轮子。
五十一、现在回头看 Logging,会完全不一样
以前:
install(Logging)
你看到的是:
API
现在看到的是:
Logging Plugin
↓
挂入 Request / Response 生命周期
↓
观察 Request
↓
观察 Response
↓
输出日志
所以:
Logging
只是官方帮你写好的:
一个 Client Plugin
五十二、HttpRequestRetry 也一样
以前:
install(HttpRequestRetry)
只是:
API
现在:
Request
↓
Send
↓
Response / Throwable
↓
Retry Condition
↓
额外 Send
你开始知道它为什么可以工作。
五十三、ContentNegotiation 也一样
以前:
install(ContentNegotiation) {
json(...)
}
现在:
Request Object
↓
Body Transformation
↓
JSON OutgoingContent
Response Bytes
↓
Body Transformation
↓
JSON
↓
Object
所以它不是:
"一个 JSON API"
而是:
Body 生命周期能力
五十四、下一篇 Auth 也会一样
以后看到:
install(Auth) {
bearer {
...
}
}
不要只看:
loadTokens
refreshTokens
而应该看:
Request
↓
认证 Header
Response
↓
401
↓
Refresh
↓
再次 Send
这样就真正理解了。
五十五、OkHttp 与 Ktor 最核心的设计差异
现在可以给出一个最终总结。
OkHttp
核心心智模型:
责任链
每个 Interceptor:
Request
↓
intercept()
↓
proceed()
↓
Response
优势:
简单
直观
自由度高
Ktor
核心心智模型:
Client 生命周期
+
Plugin
+
Hook
不同阶段:
onRequest
transformRequestBody
Send
SendingRequest
onResponse
transformResponseBody
优势:
职责更加明确
多平台
可组合
不同生命周期独立扩展
五十六、不要比较"谁更强"
不是:
OkHttp Interceptor 更强
或者:
Ktor Plugin 更高级
而是:
两种不同扩展模型
OkHttp:
把自由度集中在 intercept()
Ktor:
把自由度拆到不同生命周期 Hook
理解这一点就够了。
五十七、这一篇真正应该掌握的不是 API
如果只是记:
createClientPlugin(...)
onRequest { ... }
onResponse { ... }
其实还是:
背 API
真正应该掌握的是:
遇到需求:
我要加 Header
脑子里想到:
Request 创建阶段
↓
onRequest
遇到:
我要对业务 Body 做特殊转换
想到:
transformRequestBody
遇到:
我要监控每一次真正发送
想到:
SendingRequest
遇到:
我要看到 Response 后决定再发一次
想到:
Send
遇到:
我要检查 Response Status
想到:
onResponse
遇到:
我要改变 body<T>() 的转换
想到:
transformResponseBody
这才叫:
掌握 Ktor Client。
五十八、本篇最重要的一张图
ApiService
↓
NetworkClient
↓
HttpClient
↓
┌────────────────────────────┐
│ Client Lifecycle │
│ │
│ SetupRequest │
│ ↓ │
│ onRequest │
│ ↓ │
│ transformRequestBody │
│ ↓ │
│ Send │
│ ↓ │
│ SendingRequest │
└────────────┬───────────────┘
↓
Engine
↓
HTTP
↓
┌────────────┴───────────────┐
│ HttpResponse │
│ ↓ │
│ onResponse │
│ ↓ │
│ transformResponseBody │
│ ↓ │
│ body<T>() │
└────────────┬───────────────┘
↓
T
如果出现:
Redirect
Retry
Auth Refresh
中间还可能发生:
Send
↓
SendingRequest
↓
Response
↓
再次 Send
所以一次业务调用不一定只对应一次真实网络发送。Ktor 官方文档对 Send 与 SendingRequest 的区别正是如此定义。
五十九、再和 OkHttp 最终对照一次
OkHttp Ktor
Application Interceptor onRequest / Plugin
↓ ↓
Request 修改 Request 修改
↓ ↓
chain.proceed() Send
↓ ↓
Connect / Network SendingRequest
↓ ↓
CallServer Engine
↓ ↓
HTTP HTTP
↓ ↓
Response HttpResponse
↓ ↓
Interceptor 返回方向 onResponse
↓
transformResponseBody
再次强调:
这是为了建立心智模型,不是源码类一一对应。
六十、本篇总结
这一篇真正完成了一个很重要的转换:
以前看 Ktor:
Logging
Timeout
Retry
Auth
ContentNegotiation
感觉是:
Ktor 给了我很多 API
↓
我学会怎么调用
现在应该变成:
HttpClient
↓
存在完整 Request / Response 生命周期
↓
Plugin 可以把逻辑挂到不同生命周期
↓
官方 Plugin
只是官方提前帮我实现好的能力
OkHttp 的核心:
Interceptor Chain
可以概括为:
Request
↓
Interceptor
↓
proceed
↓
Response
其中当前核心内部链路仍然包括:
RetryAndFollowUp
↓
Bridge
↓
Cache
↓
Connect
↓
CallServer
并允许 Application Interceptor 和 Network Interceptor 插入链路。
Ktor 的核心:
Client Lifecycle
+
Plugin
+
Hook
主要可以从:
SetupRequest
↓
onRequest
↓
transformRequestBody
↓
Send
↓
SendingRequest
↓
Engine
↓
onResponse
↓
transformResponseBody
建立心智模型。Ktor 3.5.2 当前官方 Custom Client Plugin API 就是通过这些 Handler/Hook 让开发者操作 Request、Response 和 Body,而不必直接操作内部 Pipeline phase。
因此以后不要再简单问:
"Ktor 有没有类似 OkHttp Interceptor 的东西?"
更准确的问题应该是:
"我的逻辑需要发生在 Ktor Request/Response 生命周期的哪个阶段?"
这句话就是这一篇最核心的知识。
当你能够回答这个问题以后:
Header
Trace
Logging
Signing
Encryption
Retry
Auth
Body Transformation
就不再是一堆孤立 API。
而会变成:
一个完整 HttpClient 生命周期
上的不同横切能力
这才是从:
会调用 Ktor
走向:
真正理解 Ktor
的分界线。
下一篇
第十一篇:《Ktor Auth:Bearer Token、Refresh Token 与 401 自动刷新到底怎么工作?》
有了这一篇的基础,下一篇不会只讲:
install(Auth) {
bearer {
loadTokens { ... }
refreshTokens { ... }
}
}
而是从生命周期理解:
Request
↓
读取 TokenProvider
↓
Authorization Header
↓
Send
↓
401
↓
Auth Plugin
↓
Refresh Token
↓
TokenProvider 更新
↓
重新 Send 原 Request
并重点解决:
为什么 AccessToken 是动态 Provider?
为什么 Refresh Client 经常要单独存在?
为什么 Refresh 请求不能再次触发 Refresh?
10 个接口同时 401 怎么办?
会不会同时 Refresh 10 次?
Refresh 和 HttpRequestRetry 是什么关系?
401 到底应该什么时候进入 AppError.Unauthorized?
到那时,Auth 就不再只是一个"官方 Plugin API",而会成为这一篇 Client 生命周期模型上的一个真实案例。