Skip to content

自定义客户端插件

从 v2.2.0 开始,Ktor 提供了一个新的 API 来创建自定义客户端插件。一般来说,此 API 不需要理解 Ktor 内部概念,例如流水线、阶段等。 相反,您可以使用 onRequestonResponse 等一组处理器,访问请求和响应处理的不同阶段。

创建并安装您的第一个插件

在本节中,我们将演示如何创建并安装您的第一个插件,该插件会向每个请求添加自定义标头:

  1. 要创建插件,请调用 createClientPlugin 函数,并将插件名称作为实参传递:

    kotlin
    package com.example.plugins
    
    import io.ktor.client.plugins.api.*
    
    val CustomHeaderPlugin = createClientPlugin("CustomHeaderPlugin") {
        // 配置插件 ...
    }

    此函数返回 ClientPlugin 实例,该实例将用于安装插件。

  2. 要向每个请求追加自定义标头,您可以使用 onRequest 处理器,它提供对请求形参的访问:

    kotlin
    package com.example.plugins
    
    import io.ktor.client.plugins.api.*
    
    val CustomHeaderPlugin = createClientPlugin("CustomHeaderPlugin") {
        onRequest { request, _ ->
            request.headers.append("X-Custom-Header", "Default value")
        }
    }
  3. 安装插件,请将创建的 ClientPlugin 实例传递给客户端配置代码块内的 install 函数:

    kotlin
    import com.example.plugins.*
    
    val client = HttpClient(CIO) {
        install(CustomHeaderPlugin)
    }

您可以在这里找到完整示例:CustomHeader.kt。 在以下章节中,我们将探讨如何提供插件配置以及处理请求和响应。

提供插件配置

上一节演示了如何创建插件,该插件会向每个请求追加预定义自定义标头。让我们让这个插件更有用,并提供一个配置,用于传递任何自定义标头名称和值:

  1. 首先,您需要定义一个配置类:

    kotlin
    class CustomHeaderPluginConfig {
        var headerName: String = "X-Custom-Header"
        var headerValue: String = "Default value"
    }
  2. 要在插件中使用此配置,请将配置类引用传递给 createClientPlugin

    kotlin
    import io.ktor.client.plugins.api.*
    
    val CustomHeaderConfigurablePlugin = createClientPlugin("CustomHeaderConfigurablePlugin", ::CustomHeaderPluginConfig) {
        val headerName = pluginConfig.headerName
        val headerValue = pluginConfig.headerValue
    
        onRequest { request, _ ->
            request.headers.append(headerName, headerValue)
        }
    }

    鉴于插件配置字段是可变的,建议将其保存在局部变量中。

  3. 最后,您可以按如下方式安装和配置插件:

    kotlin
    val client = HttpClient(CIO) {
        install(CustomHeaderConfigurablePlugin) {
            headerName = "X-Custom-Header"
            headerValue = "Hello, world!"
        }
    }

您可以在这里找到完整示例:CustomHeaderConfigurable.kt

处理请求和响应

自定义插件提供对处理请求和响应不同阶段的访问,方法是使用一组专用处理器,例如:

  • onRequestonResponse 分别允许您处理请求和响应。
  • transformRequestBodytransformResponseBody 可用于对请求和响应正文应用必要的转换。

还有 on(...) 处理器,它允许您调用可能有助于处理调用其他阶段的特定钩子。 下表列出了所有处理器及其执行顺序:

处理器 描述
onRequest

此处理器为每个 HTTP

请求
了解如何发出请求并指定各种请求形参:请求 URL、HTTP 方法、标头和请求正文。
执行,并允许您修改它。

示例:自定义标头

transformRequestBody

允许您转换请求正文。 在此处理器中,您需要将正文序列化为 OutgoingContent (例如,`TextContent`、`ByteArrayContent` 或 `FormDataContent`), 或者如果您的转换不适用,则返回 `null`。

示例:数据转换

onResponse

此处理器为每个传入的 HTTP

响应
了解如何发出请求并指定各种请求形参:请求 URL、HTTP 方法、标头和请求正文。
执行,并允许您 以各种方式探查它:记录响应、保存 cookie 等。

示例:日志记录标头响应时间

transformResponseBody

允许您转换响应正文。 此处理器为每个 `HttpResponse.body` 调用而调用。 您需要将正文反序列化为 `requestedType` 的实例, 或者如果您的转换不适用,则返回 `null`。

示例:数据转换

onClose 允许您清理此插件分配的资源。 此处理器在客户端[关闭](#close-client)时调用。
处理器 描述
on(SetupRequest) `SetupRequest` 钩子在请求处理中首先执行。
onRequest

此处理器为每个 HTTP

请求
了解如何发出请求并指定各种请求形参:请求 URL、HTTP 方法、标头和请求正文。
执行,并允许您修改它。

示例:自定义标头

transformRequestBody

允许您转换请求正文。 在此处理器中,您需要将正文序列化为 OutgoingContent (例如,`TextContent`、`ByteArrayContent` 或 `FormDataContent`), 或者如果您的转换不适用,则返回 `null`。

示例:数据转换

on(Send)

`Send` 钩子提供了探查响应并在需要时启动额外请求的能力。 这对于处理重定向、重试请求、身份验证等可能很有用。

示例:身份验证

on(SendingRequest)

`SendingRequest` 钩子为每个请求执行, 即使它不是由用户发起的。 例如,如果请求导致重定向,`onRequest` 处理器将仅为原始请求执行,而 `on(SendingRequest)` 将为原始请求和重定向请求都执行。 同样,如果您使用 `on(Send)` 发起额外请求,处理器的顺序将如下所示:

Console

示例:日志记录标头响应时间

onResponse

此处理器为每个传入的 HTTP

响应
了解如何发出请求并指定各种请求形参:请求 URL、HTTP 方法、标头和请求正文。
执行,并允许您 以各种方式探查它:记录响应、保存 cookie 等。

示例:日志记录标头响应时间

transformResponseBody

允许您转换响应正文。 此处理器为每个 `HttpResponse.body` 调用而调用。 您需要将正文反序列化为 `requestedType` 的实例, 或者如果您的转换不适用,则返回 `null`。

示例:数据转换

onClose 允许您清理此插件分配的资源。 此处理器在客户端[关闭](#close-client)时调用。

共享调用状态

自定义插件允许您共享与调用相关的任何值,以便您可以在处理此调用的任何处理器内部访问此值。 此值以具有唯一键的属性形式存储在 call.attributes 集合中。 下面的示例演示了如何使用属性来计算发送请求和接收响应之间的时间:

kotlin
import io.ktor.client.plugins.api.*
import io.ktor.util.*

val ResponseTimePlugin = createClientPlugin("ResponseTimePlugin") {
    val onCallTimeKey = AttributeKey<Long>("onCallTimeKey")
    on(SendingRequest) { request, content ->
        val onCallTime = System.currentTimeMillis()
        request.attributes.put(onCallTimeKey, onCallTime)
    }

    onResponse { response ->
        val onCallTime = response.call.attributes[onCallTimeKey]
        val onCallReceiveTime = System.currentTimeMillis()
        println("Read response delay (ms): ${onCallReceiveTime - onCallTime}")
    }
}

您可以在这里找到完整示例:ResponseTime.kt

访问客户端配置

您可以使用 client 属性访问客户端配置,该属性返回 HttpClient 实例。 下面的示例展示了如何获取客户端使用的代理地址

kotlin
import io.ktor.client.plugins.api.*

val SimplePlugin = createClientPlugin("SimplePlugin") {
    val proxyAddress = client.engineConfig.proxy?.address()
    println("Proxy address: $proxyAddress")
}

示例

下面的代码示例演示了自定义插件的几个示例。 您可以在这里找到最终项目:client-custom-plugin

自定义标头

展示了如何创建向每个请求添加自定义标头的插件:

kotlin
package com.example.plugins

import io.ktor.client.plugins.api.*

val CustomHeaderConfigurablePlugin = createClientPlugin("CustomHeaderConfigurablePlugin", ::CustomHeaderPluginConfig) {
    val headerName = pluginConfig.headerName
    val headerValue = pluginConfig.headerValue

    onRequest { request, _ ->
        request.headers.append(headerName, headerValue)
    }
}

class CustomHeaderPluginConfig {
    var headerName: String = "X-Custom-Header"
    var headerValue: String = "Default value"
}

日志记录标头

演示了如何创建记录请求和响应标头的插件:

kotlin
package com.example.plugins

import io.ktor.client.plugins.api.*

val LoggingHeadersPlugin = createClientPlugin("LoggingHeadersPlugin") {
    on(SendingRequest) { request, content ->
        println("Request headers:")
        request.headers.entries().forEach { entry ->
            printHeader(entry)
        }
    }

    onResponse { response ->
        println("Response headers:")
        response.headers.entries().forEach { entry ->
            printHeader(entry)
        }
    }
}

private fun printHeader(entry: Map.Entry<String, List<String>>) {
    var headerString = entry.key + ": "
    entry.value.forEach { headerValue ->
        headerString += "${headerValue};"
    }
    println("-> $headerString")
}

响应时间

展示了如何创建测量发送请求和接收响应之间时间的插件:

kotlin
package com.example.plugins

import io.ktor.client.plugins.api.*
import io.ktor.util.*

val ResponseTimePlugin = createClientPlugin("ResponseTimePlugin") {
    val onCallTimeKey = AttributeKey<Long>("onCallTimeKey")
    on(SendingRequest) { request, content ->
        val onCallTime = System.currentTimeMillis()
        request.attributes.put(onCallTimeKey, onCallTime)
    }

    onResponse { response ->
        val onCallTime = response.call.attributes[onCallTimeKey]
        val onCallReceiveTime = System.currentTimeMillis()
        println("Read response delay (ms): ${onCallReceiveTime - onCallTime}")
    }
}

数据转换

展示了如何使用 transformRequestBodytransformResponseBody 钩子转换请求和响应正文:

kotlin
package com.example.plugins

import com.example.model.*
import io.ktor.client.plugins.api.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.utils.io.*

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
        }
    }
    transformResponseBody { response, content, requestedType ->
        if (requestedType.type == User::class) {
            val receivedContent = content.readUTF8Line()!!.split(";")
            User(receivedContent[0], receivedContent[1].toInt())
        } else {
            content
        }
    }
}
kotlin
package com.example

import com.example.model.*
import com.example.plugins.*
import com.example.server.*
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*

fun main() {
    startServer()
    runBlocking {
        val client = HttpClient(CIO) {
            install(DataTransformationPlugin)
        }
        val bodyAsText = client.post("http://0.0.0.0:8080/post-data") {
            setBody(User("John", 42))
        }.bodyAsText()
        val user = client.get("http://0.0.0.0:8080/get-data").body<User>()
        println("Userinfo: $bodyAsText")
        println("Username: ${user.name}, age: ${user.age}")
    }
}
kotlin
package com.example.model

data class User(val name: String, val age: Int)

您可以在这里找到完整示例:client-custom-plugin-data-transformation

身份验证

一个 Ktor 示例项目,展示了如何使用 on(Send) 钩子,以便在从服务器收到未经授权的响应时,向 Authorization 标头添加一个不记名令牌:

kotlin
package com.example.plugins

import io.ktor.client.plugins.api.*
import io.ktor.http.*

val AuthPlugin = createClientPlugin("AuthPlugin", ::AuthPluginConfig) {
    val token = pluginConfig.token

    on(Send) { request ->
        val originalCall = proceed(request)
        originalCall.response.run { // this: HttpResponse
            if(status == HttpStatusCode.Unauthorized && headers["WWW-Authenticate"]!!.contains("Bearer")) {
                request.headers.append("Authorization", "Bearer $token")
                proceed(request)
            } else {
                originalCall
            }
        }
    }
}

class AuthPluginConfig {
    var token: String = ""
}
kotlin
package com.example

import com.example.plugins.*
import com.example.server.*
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*

fun main() {
    startServer()
    runBlocking {
        val client = HttpClient(CIO) {
            install(AuthPlugin) {
                token = "abc123"
            }
        }
        val response = client.get("http://0.0.0.0:8080/")
        println(response.bodyAsText())
    }
}

您可以在这里找到完整示例:client-custom-plugin-auth