第十篇:Ktor Custom Client Plugin:从 OkHttp Interceptor 真正理解请求与响应生命周期

前面我们已经学习了:

复制代码
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 转换等不同阶段通过 onRequestonResponsetransformRequestBodytransformResponseBody 以及 SendSendingRequest 等 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 顺序里明确包括 SetupRequestonRequesttransformRequestBodySendSendingRequestonResponsetransformResponseBody;其中 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 就是这样定义的:它创建一个可以安装进 HttpClientClientPlugin,Plugin 内部可以定义 onRequestonResponse 等 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,例如 TextContentByteArrayContentFormDataContent;如果当前转换器不适用,则返回 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 文档就用 AttributeKeySendingRequest 中保存开始时间,再在 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 官方文档对 SendSendingRequest 的区别正是如此定义。


五十九、再和 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 生命周期模型上的一个真实案例。

相关推荐
mmsx20 小时前
基于 Android 的校园信息管理系统源码
android·java·okhttp
消失的旧时光-194320 小时前
第八篇:Ktor 异常体系:断网、Timeout、HTTP、JSON 与业务错误如何统一成 AppError
异常处理·ktor·kmp
消失的旧时光-19431 天前
补充篇 8.1:Ktor/KMP 断网处理:为什么请求前要先判断网络状态?
ktor·kmp·networkclient
消失的旧时光-19431 天前
第七篇:Ktor 统一响应模型:ApiResponse、业务 code 与 data 解包
ktor·kmp·net
Full Stack Developme8 天前
跨站请求伪造 (CSRF) 是什么 设计及工作原理
前端·okhttp·csrf
消失的旧时光-194310 天前
第一篇:Ktor Client 到底是什么?从 Retrofit 迁移理解 Ktor 网络请求架构
网络·架构·retrofit·ktor·dsl
weixin_4407841110 天前
【OkHttp实现原理】
android·java·okhttp
小书房15 天前
KMP跨平台之数据库
数据库·kmp
带刺的坐椅18 天前
跳出 Fatjar 的束缚:Solon 插件的体外扩展(E-Spi)与热插拔(H-Spi)
java·jar·solon·plugin·fatjar