Skip to content

自訂用戶端外掛程式

程式碼範例 client-custom-plugin

從 v2.2.0 開始,Ktor 提供了一套新的 API 用於建立自訂用戶端外掛程式。一般而言,這套 API 不需要瞭解 Ktor 的內部概念,例如管線(pipelines)、階段(phases)等等。 相反地,您可以使用一組處理常式(例如 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(...) 處理常式,允許您叫用特定的掛勾(hooks),這對於處理呼叫的其他階段可能很有用。 下表按執行順序列出了所有處理常式:

處理常式 說明
onRequest

此處理常式會針對每個 HTTP

請求
瞭解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求主體。
執行,並允許您對其進行修改。

範例:自訂標頭

transformRequestBody

允許您轉換 請求主體。 在此處理常式中,您需要將主體序列化為 OutgoingContent (例如 TextContentByteArrayContentFormDataContent), 如果您的轉換不適用,則傳回 null

範例:資料轉換

onResponse

此處理常式會針對每個傳入的 HTTP

回應
瞭解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求主體。
執行,並允許您以各種方式檢查它:記錄回應、儲存 Cookie 等等。

範例:記錄標頭回應時間

transformResponseBody

允許您轉換 回應主體。 此處理常式會針對每個 HttpResponse.body 呼叫叫用。 您需要將主體反序列化為 requestedType 的執行個體,如果您的轉換不適用,則傳回 null

範例:資料轉換

onClose 允許您清理此外掛程式分配的資源。當用戶端被關閉時,會呼叫此處理常式。
處理常式 說明
on(SetupRequest)SetupRequest 掛鉤在請求處理中首先執行。
onRequest

此處理常式會針對每個 HTTP

請求
瞭解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求主體。
執行,並允許您對其進行修改。

範例:自訂標頭

transformRequestBody

允許您轉換 請求主體。 在此處理常式中,您需要將主體序列化為 OutgoingContent (例如 TextContentByteArrayContentFormDataContent), 如果您的轉換不適用,則傳回 null

範例:資料轉換

on(Send)

Send 掛鉤提供了檢查回應並在需要時啟動額外請求的能力。這對於處理重新導向、重試請求、身分驗證等可能很有用。

範例:身分驗證

on(SendingRequest)

SendingRequest 掛鉤會針對每個請求執行,即使它不是由使用者啟動的。 例如,如果一個請求導致了重新導向,onRequest 處理常式將僅針對原始請求執行,而 on(SendingRequest) 將針對原始請求和重新導向的請求都執行。 同樣地,如果您使用 on(Send) 啟動了額外請求,處理常式的順序將如下:

Console

範例:記錄標頭回應時間

onResponse

此處理常式會針對每個傳入的 HTTP

回應
瞭解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求主體。
執行,並允許您以各種方式檢查它:記錄回應、儲存 Cookie 等等。

範例:記錄標頭回應時間

transformResponseBody

允許您轉換 回應主體。 此處理常式會針對每個 HttpResponse.body 呼叫叫用。 您需要將主體反序列化為 requestedType 的執行個體,如果您的轉換不適用,則傳回 null

範例:資料轉換

onClose 允許您清理此外掛程式分配的資源。當用戶端被關閉時,會呼叫此處理常式。

共享呼叫狀態

自訂外掛程式允許您共享任何與呼叫相關的值,以便您可以在處理此呼叫的任何處理常式中存取此值。此值作為具有唯一金鑰的屬性儲存在 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) 掛鉤,在收到來自伺服器的未經授權回應時,將載體權杖(bearer token)新增至 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