前面我们已经学习了:

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 顺序里明确包括 SetupRequestonRequesttransformRequestBodySendSendingRequestonResponse 和 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 内部可以定义 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,例如 TextContentByteArrayContent 或 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 HeaderApplication InterceptoronRequest / DefaultRequest
每次真实发送前观察Network/自定义 InterceptorSendingRequest
修改 Request Body 表达重建 RequestBodytransformRequestBody
真正执行并检查 Responsechain.proceed()Send / HttpSend
查看 Response Status/HeaderResponseonResponse
转换 Response Body读取/重建 ResponseBodytransformResponseBody
Retry自定义/内部 RetryHttpRequestRetry / Send
AuthInterceptor + AuthenticatorAuth / Send
HTTP LoggingHttpLoggingInterceptorLogging
JSONRetrofit ConverterContentNegotiation

注意:

这是一张“职责映射表”,不是源码一一对应表。


四十一、那完全自定义 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 生命周期模型上的一个真实案例。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐