前面我們已經討論過了大多數 Ktor 框架提供的功能。
從 v2.2.0 開始,Ktor 還提供了一個功能,就是允許開發者自己開發客製化的套件。
今天我們來看看開發的方式,以及 Ktor 如何允許使用者自己開發客製化套件。
根據官網教學,可以先寫一個 SimplePlugin
import io.ktor.server.application.*
val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
println("SimplePlugin is installed!")
}
定義好 SimplePlugin
之後,我們就可以像安裝其他套件一樣
fun Application.module() {
install(SimplePlugin)
}
這樣一來,運作這個套件時,我們就會看到 "SimplePlugin is installed!"
我們來看看 createApplicationPlugin
的實作
/**
* Creates an [ApplicationPlugin] that can be installed into an [Application].
*
* The example below creates a plugin that prints a requested URL each time your application receives a call:
* ```
* val RequestLoggingPlugin = createApplicationPlugin("RequestLoggingPlugin") {
* onCall { call ->
* println(call.request.uri)
* }
* }
*
* application.install(RequestLoggingPlugin)
* ```
*
* You can learn more from [Custom plugins](https://ktor.io/docs/custom-plugins.html).
*
* @param name A name of a plugin that is used to get an instance of the plugin installed to the [Application].
* @param body Allows you to define handlers ([onCall], [onCallReceive], [onCallRespond] and so on) that
* can modify the behaviour of an [Application] where your plugin is installed.
**/
public fun createApplicationPlugin(
name: String,
body: PluginBuilder<Unit>.() -> Unit
): ApplicationPlugin<Unit> = createApplicationPlugin(name, {}, body)
/**
* Creates an [ApplicationPlugin] that can be installed into an [Application].
*
* The example below creates a plugin that prints a requested URL each time your application receives a call:
* ```
* val RequestLoggingPlugin = createApplicationPlugin("RequestLoggingPlugin") {
* onCall { call ->
* println(call.request.uri)
* }
* }
*
* application.install(RequestLoggingPlugin)
* ```
*
* You can learn more from [Custom plugins](https://ktor.io/docs/custom-plugins.html).
*
* @param name A name of a plugin that is used to get its instance.
* @param createConfiguration Defines how the initial [PluginConfigT] of your new plugin can be created.
* Note that it may be modified later when a user of your plugin calls [Application.install].
* @param body Allows you to define handlers ([onCall], [onCallReceive], [onCallRespond] and so on) that
* can modify the behaviour of an [Application] where your plugin is installed.
*
**/
public fun <PluginConfigT : Any> createApplicationPlugin(
name: String,
createConfiguration: () -> PluginConfigT,
body: PluginBuilder<PluginConfigT>.() -> Unit
): ApplicationPlugin<PluginConfigT> = object : ApplicationPlugin<PluginConfigT> {
override val key: AttributeKey<PluginInstance> = AttributeKey(name)
override fun install(
pipeline: Application,
configure: PluginConfigT.() -> Unit
): PluginInstance {
return createPluginInstance(pipeline, pipeline, body, createConfiguration, configure)
}
}
到這邊會先將套件名稱的 name
定義成 AttributeKey
/**
* Specifies a key for an attribute in [Attributes]
* @param T is a type of the value stored in the attribute
* @param name is a name of the attribute for diagnostic purposes. Can't be blank
*/
public class AttributeKey<T : Any>(public val name: String) {
init {
if (name.isEmpty()) {
throw IllegalStateException("Name can't be blank")
}
}
override fun toString(): String = "AttributeKey: $name"
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as AttributeKey<*>
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
}
createApplicationPlugin.install
內的 createPluginInstance
定義則是
private fun <
PipelineT : ApplicationCallPipeline,
PluginConfigT : Any
> Plugin<PipelineT, PluginConfigT, PluginInstance>.createPluginInstance(
application: Application,
pipeline: ApplicationCallPipeline,
body: PluginBuilder<PluginConfigT>.() -> Unit,
createConfiguration: () -> PluginConfigT,
configure: PluginConfigT.() -> Unit
): PluginInstance {
val config = createConfiguration().apply(configure)
val currentPlugin = this
val pluginBuilder = object : PluginBuilder<PluginConfigT>(currentPlugin.key) {
override val application: Application = application
override val pipeline: ApplicationCallPipeline = pipeline
override val pluginConfig: PluginConfigT = config
}
pluginBuilder.setupPlugin(body)
return PluginInstance(pluginBuilder)
}
這邊會實作出一個 PluginBuilder
來協助設置
PluginBuilder
的註解與簽名如下
/**
* A utility class to build an [ApplicationPlugin] instance.
**/
@KtorDsl
@Suppress("UNUSED_PARAMETER", "DEPRECATION")
public abstract class PluginBuilder<PluginConfig : Any> internal constructor(
internal val key: AttributeKey<PluginInstance>
)
裡面的參數則是
/**
* A reference to the [Application] where the plugin is installed.
*/
public abstract val application: Application
/**
* A configuration of the current plugin.
*/
public abstract val pluginConfig: PluginConfig
/**
* A pipeline [PluginConfig] for the current plugin.
* See [Pipelines](https://ktor.io/docs/pipelines.html) for more information.
**/
internal abstract val pipeline: ApplicationCallPipeline
/**
* Allows you to access the environment of the currently running application where the plugin is installed.
**/
public val environment: ApplicationEnvironment? get() = pipeline.environment
可以看到,這裡面定義了 application
、pipeline
等等,這部分在前面接收請求或者送出回傳時都會使用到。
另外也定義了 environment = pipeline.environment
接著還定義了許多的列表,設置各種的攔截
internal val callInterceptions: MutableList<CallInterception> = mutableListOf()
internal val onReceiveInterceptions: MutableList<ReceiveInterception> = mutableListOf()
internal val onResponseInterceptions: MutableList<ResponseInterception> = mutableListOf()
internal val afterResponseInterceptions: MutableList<ResponseInterception> = mutableListOf()
internal val hooks: MutableList<HookHandler<*>> = mutableListOf()
然後我們就可以用 Builder.setupPlugin
來設置套件的行為
private fun <Configuration : Any, Builder : PluginBuilder<Configuration>> Builder.setupPlugin(
body: Builder.() -> Unit
) {
apply(body)
callInterceptions.forEach {
it.action(pipeline)
}
onReceiveInterceptions.forEach {
it.action(pipeline.receivePipeline)
}
onResponseInterceptions.forEach {
it.action(pipeline.sendPipeline)
}
afterResponseInterceptions.forEach {
it.action(pipeline.sendPipeline)
}
hooks.forEach { it.install(pipeline) }
}
這樣一來,我們就定義完了一個 ApplicationPlugin
能夠透過 Pipeline.install
進行安裝
/**
* Installs a [plugin] into this pipeline, if it is not yet installed.
*/
public fun <P : Pipeline<*, ApplicationCall>, B : Any, F : Any> P.install(
plugin: Plugin<P, B, F>,
configure: B.() -> Unit = {}
): F {
if (this is Route && plugin is BaseRouteScopedPlugin) {
return installIntoRoute(plugin, configure)
}
val registry = pluginRegistry
return when (val installedPlugin = registry.getOrNull(plugin.key)) {
null -> {
try {
val installed = plugin.install(this, configure)
registry.put(plugin.key, installed)
// environment.log.trace("`${plugin.name}` plugin was installed successfully.")
installed
} catch (t: Throwable) {
// environment.log.error("`${plugin.name}` plugin failed to install.", t)
throw t
}
}
plugin -> {
// environment.log.warning("`${plugin.name}` plugin is already installed")
installedPlugin
}
else -> {
throw DuplicatePluginException(
"Please make sure that you use unique name for the plugin and don't install it twice. " +
"Conflicting application plugin is already installed with the same key as `${plugin.key.name}`"
)
}
}
}
這段我們在前面看安裝 route
時已經看過,不過這次不是安裝 route
也就是
val installed = plugin.install(this, configure)
registry.put(plugin.key, installed)
// environment.log.trace("`${plugin.name}` plugin was installed successfully.")
installed
到這邊,我們概略地看過怎麼定義一個客製化套件
以及 install
該套件時會發生什麼事情。
明天我們再來看看客製化套件還可以做哪些事情