客戶端自訂外掛
程式碼範例: client-custom-plugin
從 v2.2.0 開始,Ktor 提供了一組新的 API,用於建立自訂客戶端外掛。一般而言,此 API 不需要瞭解 Ktor 內部概念,例如 pipelines、phases 等等。相反地,您可以使用一組處理常式(例如 onRequest
、onResponse
等等),存取處理請求和回應的不同階段。
建立並安裝您的第一個外掛
在本節中,我們將展示如何建立並安裝您的第一個外掛,該外掛會為每個請求新增自訂標頭:
若要建立外掛,請呼叫 createClientPlugin 函式並傳入外掛名稱作為引數:
kotlinpackage com.example.plugins import io.ktor.client.plugins.api.* val CustomHeaderPlugin = createClientPlugin("CustomHeaderPlugin") { // 設定外掛... }
此函式會傳回
ClientPlugin
實例,該實例將用於安裝外掛。若要為每個請求附加自訂標頭,您可以使用
onRequest
處理常式,它提供對請求參數的存取:kotlinpackage com.example.plugins import io.ktor.client.plugins.api.* val CustomHeaderPlugin = createClientPlugin("CustomHeaderPlugin") { onRequest { request, _ -> request.headers.append("X-Custom-Header", "Default value") } }
若要安裝外掛,請將建立的
ClientPlugin
實例傳遞給客戶端設定區塊內的install
函式:kotlinimport com.example.plugins.* val client = HttpClient(CIO) { install(CustomHeaderPlugin) }
您可以在此處找到完整範例:CustomHeader.kt。在以下章節中,我們將探討如何提供外掛設定以及處理請求和回應。
提供外掛設定
上一節展示了如何建立一個外掛,該外掛會為每個回應附加預定義的自訂標頭。讓我們使這個外掛更有用,並提供一個設定,用於傳遞任何自訂標頭名稱和值:
首先,您需要定義一個設定類別:
kotlinclass CustomHeaderPluginConfig { var headerName: String = "X-Custom-Header" var headerValue: String = "Default value" }
若要在外掛中使用此設定,請將設定類別參考傳遞給
createApplicationPlugin
:kotlinimport 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) } }
鑑於外掛設定欄位是可變的,建議將它們儲存到區域變數中。
最後,您可以如下所示安裝並設定此外掛:
kotlinval client = HttpClient(CIO) { install(CustomHeaderConfigurablePlugin) { headerName = "X-Custom-Header" headerValue = "Hello, world!" } }
您可以在此處找到完整範例:CustomHeaderConfigurable.kt。
處理請求和回應
自訂外掛使用一組專用處理常式,提供對處理請求和回應不同階段的存取,例如:
onRequest
和onResponse
分別允許您處理請求和回應。transformRequestBody
和transformResponseBody
可用於對請求和回應主體應用必要的轉換。
還有 on(...)
處理常式,它允許您呼叫特定的掛鉤 (hook),這對於處理呼叫的其他階段可能很有用。 下表列出了所有處理常式及其執行順序:
處理常式 | 描述 |
onRequest | 此處理常式會對每個 HTTP 請求 執行,並允許您修改它。 了解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求的主體。 範例:自訂標頭 |
transformRequestBody | 允許您轉換請求主體。 在此處理常式中,您需要將主體序列化為 OutgoingContent (例如, 範例:資料轉換 |
onResponse | 此處理常式會對每個傳入的 HTTP 回應 執行,並允許您以各種方式檢查它:記錄回應、儲存 Cookie 等等。 了解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求的主體。 |
transformResponseBody | 允許您轉換回應主體。 每次呼叫 範例:資料轉換 |
onClose | 允許您清理此外掛所分配的資源。 當客戶端關閉時,會呼叫此處理常式。 |
處理常式 | 描述 |
on(SetupRequest) | 在請求處理中,SetupRequest 掛鉤會最先執行。 |
onRequest | 此處理常式會對每個 HTTP 請求 執行,並允許您修改它。 了解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求的主體。 範例:自訂標頭 |
transformRequestBody | 允許您轉換請求主體。 在此處理常式中,您需要將主體序列化為 OutgoingContent (例如, 範例:資料轉換 |
on(Send) |
範例:驗證 |
on(SendingRequest) | 即使請求不是由使用者啟動, Console |
onResponse | 此處理常式會對每個傳入的 HTTP 回應 執行,並允許您以各種方式檢查它:記錄回應、儲存 Cookie 等等。 了解如何發出請求並指定各種請求參數:請求 URL、HTTP 方法、標頭以及請求的主體。 |
transformResponseBody | 允許您轉換回應主體。 每次呼叫 範例:資料轉換 |
onClose | 允許您清理此外掛所分配的資源。 當客戶端關閉時,會呼叫此處理常式。 |
共用呼叫狀態
自訂外掛允許您共用與呼叫相關的任何值,以便您能夠在任何處理此呼叫的處理常式內部存取此值。此值會儲存為在 call.attributes
集合中帶有唯一的鍵的屬性。以下範例展示了如何使用屬性計算傳送請求和接收回應之間的時間:
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 實例。以下範例展示了如何取得客戶端使用的代理位址:
import io.ktor.client.plugins.api.*
val SimplePlugin = createClientPlugin("SimplePlugin") {
val proxyAddress = client.engineConfig.proxy?.address()
println("Proxy address: $proxyAddress")
}
範例
以下程式碼範例展示了自訂外掛的幾個範例。您可以在此處找到最終專案:client-custom-plugin。
自訂標頭
展示如何建立為每個請求新增自訂標頭的外掛:
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"
}
記錄標頭
展示如何建立記錄請求和回應標頭的外掛:
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")
}
回應時間
展示如何建立測量傳送請求和接收回應之間時間的外掛:
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}")
}
}
資料轉換
展示如何使用 transformRequestBody
和 transformResponseBody
掛鉤轉換請求和回應主體:
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
}
}
}
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}")
}
}
package com.example.model
data class User(val name: String, val age: Int)
在此處可以找到完整範例:client-custom-plugin-data-transformation。
驗證
一個 Ktor 範例專案,展示如何使用 on(Send)
掛鉤,在從伺服器收到未經授權的回應時,將承載權杖新增到 Authorization
標頭:
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 = ""
}
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。