自定義套件除了前面所說的,可以設置行為以及事件之外
Ktor 還提供了設置套件參數的方式
根據官網,我們可以看到教學是先定義了參數類別
class PluginConfiguration {
var headerName: String = "Custom-Header-Name"
var headerValue: String = "Default value"
}
然後我們就可以在套件內使用
val CustomHeaderPlugin = createApplicationPlugin(
name = "CustomHeaderPlugin",
createConfiguration = ::PluginConfiguration
) {
val headerName = pluginConfig.headerName
val headerValue = pluginConfig.headerValue
pluginConfig.apply {
onCall { call ->
call.response.headers.append(headerName, headerValue)
}
}
}
我們再次看看 createApplicationPlugin
的簽名
public fun <PluginConfigT : Any> createApplicationPlugin(
name: String,
createConfiguration: () -> PluginConfigT,
body: PluginBuilder<PluginConfigT>.() -> Unit
): ApplicationPlugin<PluginConfigT>
這邊我們可以看到 createConfiguration
的型態為 () -> PluginConfigT
由於 Ktor 事先不知道我們所定義的 PluginConfiguration
類別
因此這邊採用泛型的方式,來定義 createConfiguration
的回傳型態
另外這邊並不是直接送入一個實體物件,而是送入該類別的 callable references
所以寫法是
createConfiguration = ::PluginConfiguration
參考官網 Constructor references 的教學,這樣寫可以直接送入一個() -> PluginConfiguration
的函數。
這樣一來,我們就可以在安裝套件時,調整對應的參數設置
install(CustomHeaderPlugin) {
headerName = "X-Custom-Header"
headerValue = "Hello, world!"
}
我們來看看 PluginConfiguration
這個物件,
是怎麼被送入我們自定義 CustomHeaderPlugin
內的。
首先,我們重新看 createApplicationPlugin
的定義
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)
}
}
這邊使用了 Kotlin 的 object
關鍵字宣告了一個匿名類別,這邊會直接變成 ApplicationPlugin
的建構函數
ApplicationPlugin
定義如下
/**
* Defines a [Plugin](https://ktor.io/docs/plugins.html) that is installed into Application.
* @param TConfiguration is the configuration object type for this Plugin
*/
public interface ApplicationPlugin<out TConfiguration : Any> :
BaseApplicationPlugin<Application, TConfiguration, PluginInstance>
BaseApplicationPlugin
是
/**
* Defines a [Plugin](https://ktor.io/docs/plugins.html) that is installed into Application.
* @param TPipeline is the type of the pipeline this plugin is compatible with
* @param TConfiguration is the configuration object type for this Plugin
* @param TPlugin is the instance type of the Plugin object
*/
public interface BaseApplicationPlugin<
in TPipeline : Pipeline<*, ApplicationCall>,
out TConfiguration : Any,
TPlugin : Any
> : Plugin<TPipeline, TConfiguration, TPlugin>
Plugin
則是
/**
* Defines an installable [Plugin](https://ktor.io/docs/plugins.html).
* @param TPipeline is the type of the pipeline this plugin is compatible with
* @param TConfiguration is the configuration object type for this Plugin
* @param TPlugin is the instance type of the Plugin object
*/
@Suppress("AddVarianceModifier")
public interface Plugin<
in TPipeline : Pipeline<*, ApplicationCall>,
out TConfiguration : Any,
TPlugin : Any
> {
/**
* A unique key that identifies a plugin.
*/
public val key: AttributeKey<TPlugin>
/**
* A plugin's installation script.
*/
public fun install(pipeline: TPipeline, configure: TConfiguration.() -> Unit): TPlugin
}
看完上面的程式,可能會讓人好奇,這樣的設計目的是什麼?又有什麼好處?
我們看看有哪些地方實作了 Plugin
這個介面
除了剛剛看過的 BaseApplicationPlugin
還會找到 BaseRouteScopedPlugin
public interface BaseRouteScopedPlugin<TConfiguration : Any, TPlugin : Any> :
Plugin<ApplicationCallPipeline, TConfiguration, TPlugin>
另外,實作 BaseApplicationPlugin
介面的地方
除了 ApplicationPlugin
還有 DataConversion
/**
* Object for installing [io.ktor.util.converters.DataConversion] plugin
*/
public object DataConversion :
BaseApplicationPlugin<ApplicationCallPipeline, DataConversion.Configuration, DataConversion> {
override fun install(
pipeline: ApplicationCallPipeline,
configure: DataConversion.Configuration.() -> Unit
): DataConversion {
val configuration = DataConversion.Configuration().apply(configure)
return DataConversion(configuration)
}
override val key: AttributeKey<DataConversion> = AttributeKey("DataConversion")
}
EnginePlugin
/**
* A plugin to install into an engine pipeline.
*/
public object EnginePlugin : BaseApplicationPlugin<EnginePipeline, Config, ShutDownUrl> {
override val key: AttributeKey<ShutDownUrl> = AttributeKey<ShutDownUrl>("shutdown.url")
override fun install(pipeline: EnginePipeline, configure: Config.() -> Unit): ShutDownUrl {
val config = Config()
configure(config)
val plugin = ShutDownUrl(config.shutDownUrl, config.exitCodeSupplier)
pipeline.intercept(EnginePipeline.Before) {
if (call.request.uri == plugin.url) {
plugin.doShutdown(call)
}
}
return plugin
}
}
等等地方。
也就是說,透過定義較單純的介面,並讓介面之間相互實作
我們可以讓介面的用途和範圍變得更明確
也不需要大量撰寫相同定義的小介面
這可以讓我們的程式碼更加的簡潔,同時不犧牲太多的可讀性。
接著我們回到 createApplicationPlugin.install
override fun install(
pipeline: Application,
configure: PluginConfigT.() -> Unit
): PluginInstance {
return createPluginInstance(pipeline, pipeline, body, createConfiguration, configure)
}
這邊的 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)
}
到這邊,我們終於看到 createConfiguration
被呼叫並回傳 PluginConfigT
的步驟。
並且由於所有的函數都可以使用 apply
這個 scope function
所以我們可以利用 apply
來加上 configure
的內容。
並且在 pluginBuilder
內加上我們處理好的 pluginConfig
。
到這邊,我們就看過了自定義套件加上設置套件參數的撰寫方式,以及框架內如何實作。