自定义服务器插件
代码示例: custom-plugin
从 v2.0.0 开始,Ktor 提供了一个用于创建自定义插件的新 API。通常,此 API 不需要了解 Ktor 的内部概念,例如流水线 (pipelines)、阶段 (phases) 等。相反,您可以使用 onCall、onCallReceive 和 onCallRespond 处理程序来访问处理请求和响应的不同阶段。
本主题中描述的 API 适用于 v2.0.0 及更高版本。对于旧版本,您可以使用基础 API。
创建并安装您的第一个插件
在本节中,我们将演示如何创建并安装您的第一个插件。 您可以使用在创建、打开并运行新的 Ktor 项目教程中创建的应用程序作为起始项目。
要创建插件,请调用 createApplicationPlugin 函数并传递插件名称:
kotlinimport io.ktor.server.application.* val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") { println("SimplePlugin is installed!") }此函数返回
ApplicationPlugin实例,该实例将在下一步中用于安装插件。还有一个 createRouteScopedPlugin 函数,允许您创建可以安装到特定路由的插件。
要安装插件,请在应用程序的初始化代码中将创建的
ApplicationPlugin实例传递给install函数:kotlinfun Application.module() { install(SimplePlugin) }最后,运行您的应用程序,以在控制台输出中查看插件的问候语:
Bash2021-10-14 14:54:08.269 [main] INFO Application - Autoreload is disabled because the development mode is off. SimplePlugin is installed! 2021-10-14 14:54:08.900 [main] INFO Application - Responding at http://0.0.0.0:8080
您可以在此处找到完整的示例:SimplePlugin.kt。 在接下来的章节中,我们将了解如何处理不同阶段的调用并提供插件配置。
处理调用
在您的自定义插件中,您可以使用一组处理程序来处理请求和响应,这些处理程序提供了对调用不同阶段的访问:
- onCall 允许您获取请求/响应信息、修改响应参数(例如,追加自定义标头)等。
- onCallReceive 允许您获取并转换从客户端接收的数据。
- onCallRespond 允许您在将数据发送到客户端之前对其进行转换。
- on(...) 允许您调用特定的钩子,这些钩子对于处理调用的其他阶段或调用期间发生的异常可能很有用。
- 如果需要,您可以使用
call.attributes在不同处理程序之间共享调用状态。
onCall
onCall 处理程序接受 ApplicationCall 作为 lambda 实参。这允许您访问请求/响应信息并修改响应参数(例如,追加自定义标头)。如果您需要转换请求/响应体,请使用 onCallReceive/onCallRespond。
示例 1:请求日志记录
下面的示例展示了如何使用 onCall 创建一个用于记录传入请求的自定义插件:
val RequestLoggingPlugin = createApplicationPlugin(name = "RequestLoggingPlugin") {
onCall { call ->
call.request.origin.apply {
println("Request URL: $scheme://$localHost:$localPort$uri")
}
}
}如果您安装此插件,应用程序将在控制台中显示请求的 URL,例如:
Request URL: http://0.0.0.0:8080/
Request URL: http://0.0.0.0:8080/index示例 2:自定义标头
此示例演示了如何创建一个为每个响应追加自定义标头的插件:
val CustomHeaderPlugin = createApplicationPlugin(name = "CustomHeaderPlugin") {
onCall { call ->
call.response.headers.append("X-Custom-Header", "Hello, world!")
}
}结果,所有响应都将添加一个自定义标头:
HTTP/1.1 200 OK
X-Custom-Header: Hello, world!请注意,此插件中的自定义标头名称和值是硬编码的。您可以通过提供用于传递所需自定义标头名称/值的配置来使此插件更加灵活。
onCallReceive
onCallReceive 处理程序提供了 transformBody 函数,允许您转换从客户端接收的数据。假设客户端发起了一个示例 POST 请求,其主体中包含 10 作为 text/plain:
POST http://localhost:8080/transform-data
Content-Type: text/plain
10要将此主体接收为整数值,您需要为 POST 请求创建一个路由处理程序,并调用带有 Int 参数的 call.receive:
post("/transform-data") {
val data = call.receive<Int>()
}现在让我们创建一个插件,它将主体接收为整数值并将其加 1。为此,我们需要在 onCallReceive 内部处理 transformBody,如下所示:
val DataTransformationPlugin = createApplicationPlugin(name = "DataTransformationPlugin") {
onCallReceive { call ->
transformBody { data ->
if (requestedType?.type == Int::class) {
val line = data.readUTF8Line() ?: "1"
line.toInt() + 1
} else {
data
}
}
}
}上述代码段中的 transformBody 工作原理如下:
TransformBodyContext是一个 lambda 接收器,包含有关当前请求的类型信息。在上面的示例中,TransformBodyContext.requestedType属性用于检查请求的数据类型。data是一个 lambda 实参,允许您将请求体接收为 ByteReadChannel 并将其转换为所需的类型。在上面的示例中,ByteReadChannel.readUTF8Line用于读取请求体。- 最后,您需要转换并返回数据。在我们的示例中,将接收到的整数值加
1。
您可以在此处找到完整的示例:DataTransformationPlugin.kt。
onCallRespond
onCallRespond 也提供了 transformBody 处理程序,允许您转换要发送到客户端的数据。当在路由处理程序中调用 call.respond 函数时,会执行此处理程序。让我们继续 onCallReceive 中的示例,其中在 POST 请求处理程序中接收了一个整数值:
post("/transform-data") {
val data = call.receive<Int>()
call.respond(data)
}调用 call.respond 会触发 onCallRespond,这反过来允许您转换要发送到客户端的数据。例如,下面的代码段显示了如何将初始值加 1:
onCallRespond { call ->
transformBody { data ->
if (data is Int) {
(data + 1).toString()
} else {
data
}
}
}您可以在此处找到完整的示例:DataTransformationPlugin.kt。
其他有用的处理程序
除了 onCall、onCallReceive 和 onCallRespond 处理程序外,Ktor 还提供了一组特定的钩子,这些钩子对于处理调用的其他阶段可能很有用。 您可以使用接受 Hook 作为参数的 on 处理程序来处理这些钩子。 这些钩子包括:
CallSetup在处理调用的第一步被调用。ResponseBodyReadyForSend当响应体通过所有转换并准备发送时被调用。ResponseSent当响应成功发送到客户端时被调用。CallFailed当调用因异常而失败时被调用。- AuthenticationChecked 在身份验证凭据检查后执行。以下示例展示了如何使用此钩子来实现授权:custom-plugin-authorization。
下面的示例展示了如何处理 CallSetup:
on(CallSetup) { call->
// ...
}还有一个
MonitoringEvent钩子,允许您处理应用程序事件,例如应用程序启动或关闭。
共享调用状态
自定义插件允许您共享与调用相关的任何值,因此您可以在处理此调用的任何处理程序内部访问此值。此值作为具有唯一键的特性存储在 call.attributes 集合中。下面的示例演示了如何使用特性来计算接收请求和读取主体之间的时间:
val DataTransformationBenchmarkPlugin = createApplicationPlugin(name = "DataTransformationBenchmarkPlugin") {
val onCallTimeKey = AttributeKey<Long>("onCallTimeKey")
onCall { call ->
val onCallTime = System.currentTimeMillis()
call.attributes.put(onCallTimeKey, onCallTime)
}
onCallReceive { call ->
val onCallTime = call.attributes[onCallTimeKey]
val onCallReceiveTime = System.currentTimeMillis()
println("Read body delay (ms): ${onCallReceiveTime - onCallTime}")
}
}如果您发起 POST 请求,插件会在控制台中打印延迟:
Request URL: http://localhost:8080/transform-data
Read body delay (ms): 52您可以在此处找到完整的示例:DataTransformationBenchmarkPlugin.kt。
您还可以在路由处理程序中访问调用特性。
处理应用程序事件
on 处理程序提供了使用 MonitoringEvent 钩子来处理与应用程序生命周期相关的事件的能力。 例如,您可以将以下预定义事件传递给 on 处理程序:
ApplicationStartingApplicationStartedApplicationStopPreparingApplicationStoppingApplicationStopped
下面的代码段展示了如何使用 ApplicationStopped 处理应用程序关闭:
package com.example.plugins
import io.ktor.events.EventDefinition
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.application.hooks.*
val ApplicationMonitoringPlugin = createApplicationPlugin(name = "ApplicationMonitoringPlugin") {
on(MonitoringEvent(ApplicationStarted)) { application ->
application.log.info("Server is started")
}
on(MonitoringEvent(ApplicationStopped)) { application ->
application.log.info("Server is stopped")
// 释放资源并取消订阅事件
application.monitor.unsubscribe(ApplicationStarted) {}
application.monitor.unsubscribe(ApplicationStopped) {}
}
on(ResponseSent) { call ->
if (call.response.status() == HttpStatusCode.NotFound) {
this@createApplicationPlugin.application.monitor.raise(NotFoundEvent, call)
}
}
}
val NotFoundEvent: EventDefinition<ApplicationCall> = EventDefinition()这对于释放应用程序资源可能很有用。
提供插件配置
自定义标头示例演示了如何创建一个为每个响应追加预定义自定义标头的插件。让我们让这个插件更有用,并提供一个用于传递所需自定义标头名称/值的配置。
首先,您需要定义一个配置类:
kotlinclass PluginConfiguration { var headerName: String = "Custom-Header-Name" var headerValue: String = "Default value" }要在插件中使用此配置,请将配置类引用传递给
createApplicationPlugin:kotlinval CustomHeaderPlugin = createApplicationPlugin( name = "CustomHeaderPlugin", createConfiguration = ::PluginConfiguration ) { val headerName = pluginConfig.headerName val headerValue = pluginConfig.headerValue pluginConfig.apply { onCall { call -> call.response.headers.append(headerName, headerValue) } } }鉴于插件配置字段是可变的,建议将它们保存在局部变量中。
最后,您可以按如下方式安装并配置插件:
kotlininstall(CustomHeaderPlugin) { headerName = "X-Custom-Header" headerValue = "Hello, world!" }
您可以在此处找到完整的示例:CustomHeaderPlugin.kt。
在文件中配置
Ktor 允许您在配置文件中指定插件设置。 让我们看看如何为 CustomHeaderPlugin 实现这一点:
首先,将带有插件设置的新组添加到
application.conf或application.yaml文件中:shellhttp { custom_header { header_name = X-Another-Custom-Header header_value = Some value } }yamlhttp: custom_header: header_name: X-Another-Custom-Header header_value: Some value在我们的示例中,插件设置存储在
http.custom_header组中。要访问配置文件属性,请将
ApplicationConfig传递给配置类构造函数。tryGetString函数返回指定的属性值:kotlinclass CustomHeaderConfiguration(config: ApplicationConfig) { var headerName: String = config.tryGetString("header_name") ?: "Custom-Header-Name" var headerValue: String = config.tryGetString("header_value") ?: "Default value" }最后,将
http.custom_header值分配给createApplicationPlugin函数的configurationPath参数:kotlinval CustomHeaderPluginConfigurable = createApplicationPlugin( name = "CustomHeaderPluginConfigurable", configurationPath = "http.custom_header", createConfiguration = ::CustomHeaderConfiguration ) { val headerName = pluginConfig.headerName val headerValue = pluginConfig.headerValue pluginConfig.apply { onCall { call -> call.response.headers.append(headerName, headerValue) } } }
您可以在此处找到完整的示例:CustomHeaderPluginConfigurable.kt。
访问应用程序设置
配置
您可以使用 applicationConfig 属性访问您的服务器配置,该属性返回 ApplicationConfig 实例。下面的示例显示了如何获取服务器使用的地址和端口:
val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
val host = applicationConfig?.host
val port = applicationConfig?.port
println("Listening on $host:$port")
}环境
要访问应用程序的环境,请使用 environment 属性。例如,此属性允许您确定是否启用了开发模式:
val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
val isDevMode = environment?.developmentMode
onCall { call ->
if (isDevMode == true) {
println("handling request ${call.request.uri}")
}
}
}杂项
存储插件状态
要存储插件的状态,您可以从处理程序 lambda 中捕获任何值。请注意,建议通过使用并发数据结构和原子数据类型使所有状态值保持线程安全:
val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
val activeRequests = AtomicInteger(0)
onCall {
activeRequests.incrementAndGet()
}
onCallRespond {
activeRequests.decrementAndGet()
}
}数据库
我可以在自定义插件中使用可挂起的数据库吗?
可以。所有处理程序都是挂起函数,因此您可以在插件内部执行任何可挂起的数据库操作。但别忘了为特定调用释放资源(例如,通过使用 on(ResponseSent))。
如何在自定义插件中使用阻塞式数据库?
由于 Ktor 使用协程和挂起函数,向阻塞式数据库发起请求可能会很危险,因为执行阻塞调用的协程可能会被阻塞,然后永远挂起。为了防止这种情况,您需要创建一个单独的 CoroutineContext:
kotlinval databaseContext = newSingleThreadContext("DatabaseThread")然后,一旦创建了上下文,请将对数据库的每个调用包装到
withContext调用中:kotlinonCall { withContext(databaseContext) { database.access(...) // 对数据库的一些调用 } }
