iT邦幫忙

2023 iThome 鐵人賽

DAY 28
0

昨天我們試著寫了一個 SimplePlugin

import io.ktor.server.application.*

val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
    println("SimplePlugin is installed!")
}

除了印文字之外,自定義套件當然還可以有其他的功能

我們今天來看看其中的幾項

首先呼叫時的行為,可以在 onCall 定義

val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
    onCall { call ->
        call.request.origin.apply {
            println("Request URL: $scheme://$localHost:$localPort$uri")
        }
    }
}

onCall 的實作如下

/**
 * Specifies the [block] handler for every incoming [ApplicationCall].
 *
 * This block is invoked for every incoming call even if the call is already handled by other handler.
 * There you can handle the call in a way you want: add headers, change the response status, etc. You can also
 * access the external state to calculate stats.
 *
 * This example demonstrates how to create a plugin that appends a custom header to each response:
 * ```kotlin
 * val CustomHeaderPlugin = createApplicationPlugin(name = "CustomHeaderPlugin") {
 *     onCall { call ->
 *         call.response.headers.append("X-Custom-Header", "Hello, world!")
 *     }
 * }
 * ```
 *
 * @see [createApplicationPlugin]
 *
 * @param block An action that needs to be executed when your application receives an HTTP call.
 **/
public fun onCall(block: suspend OnCallContext<PluginConfig>.(call: ApplicationCall) -> Unit) {
	onDefaultPhase(
		callInterceptions,
		ApplicationCallPipeline.Plugins,
		PHASE_ON_CALL,
		::OnCallContext
	) { call, _ ->
		block(call)
	}
}

可以看到 call 的型態是 ApplicationCall

繼續往下追的話,可以看到 call.request 的型態是 ApplicationRequest

call.request.origin 的型態則是 RequestConnectionPoint

/**  
* Represents request address information is used to make a call.  
* There are at least two possible instances: "local" is how we see request at the server application and  
* "actual" is what we can recover from proxy provided headers.  
*/  
public interface RequestConnectionPoint

繼續往下看 onDefaultPhase 的定義如下

private fun <T : Any, ContextT : CallContext<PluginConfig>> onDefaultPhase(
	interceptions: MutableList<Interception<T>>,
	phase: PipelinePhase,
	handlerName: String,
	contextInit: (pluginConfig: PluginConfig, PipelineContext<T, ApplicationCall>) -> ContextT,
	block: suspend ContextT.(call: ApplicationCall, body: T) -> Unit
) {
	onDefaultPhaseWithMessage(interceptions, phase, handlerName, contextInit) { call, body -> block(call, body) }
}

onDefaultPhaseWithMessage 則是

private fun <T : Any, ContextT : CallContext<PluginConfig>> onDefaultPhaseWithMessage(
	interceptions: MutableList<Interception<T>>,
	phase: PipelinePhase,
	handlerName: String,
	contextInit: (pluginConfig: PluginConfig, PipelineContext<T, ApplicationCall>) -> ContextT,
	block: suspend ContextT.(ApplicationCall, T) -> Unit
) {
	interceptions.add(
		Interception(
			phase,
			action = { pipeline ->
				pipeline.intercept(phase) {
					// Information about the plugin name is needed for the Intellij Idea debugger.
					val key = this@PluginBuilder.key
					val pluginConfig = this@PluginBuilder.pluginConfig
					addToContextInDebugMode(key.name) {
						ijDebugReportHandlerStarted(pluginName = key.name, handler = handlerName)

						// Perform current plugin's handler
						contextInit(pluginConfig, this@intercept).block(call, subject)

						ijDebugReportHandlerFinished(pluginName = key.name, handler = handlerName)
					}
				}
			}
		)
	)
}

除了程式所需的邏輯,我們還可以看到專門提供 IDE 除錯用的

ijDebugReportHandlerStartedijDebugReportHandlerFinished

能取得 call.request,那麼照道理說,應該也能取得 call.response

寫法如下

val CustomHeaderPlugin = createApplicationPlugin(name = "CustomHeaderPlugin") {
    onCall { call ->
        call.response.headers.append("X-Custom-Header", "Hello, world!")
    }
}

除了 onCall  以外,還有 onCallReceive

/**  
* Specifies the [block] handler that allows you to obtain and transform data received from the client.* This [block] is invoked for every attempt to receive the request body.* @see [createApplicationPlugin]  
*  
* @param block An action that needs to be executed when your application receives data from a client.  
**/  
public fun onCallReceive(  
block: suspend OnCallReceiveContext<PluginConfig>.(call: ApplicationCall) -> Unit  
) {  
onCallReceive { call, _ -> block(call) }  
}
/**
 * Specifies the [block] handler that allows you to obtain and transform data received from the client.
 * This [block] is invoked for every attempt to receive the request body.
 * @see [createApplicationPlugin]
 *
 * @param block An action that needs to be executed when your application receives data from a client.
 **/
public fun onCallReceive(
	block: suspend OnCallReceiveContext<PluginConfig>.(call: ApplicationCall, body: Any) -> Unit
) {
	onDefaultPhase(
		onReceiveInterceptions,
		ApplicationReceivePipeline.Transform,
		PHASE_ON_CALL_RECEIVE,
		::OnCallReceiveContext,
	) { call, body: Any -> block(call, body) }
}

onCallRespond

/**
 * Specifies the [block] handler that allows you to transform data before sending it to the client.
 * This handler is executed when the `call.respond` function is invoked in a route handler.
 * @see [createApplicationPlugin]
 *
 * @param block An action that needs to be executed when your server is sending a response to a client.
 **/
public fun onCallRespond(
	block: suspend OnCallRespondContext<PluginConfig>.(call: ApplicationCall) -> Unit
) {
	onCallRespond { call, _ -> block(call) }
}
/**
 * Specifies the [block] handler that allows you to transform data before sending it to the client.
 * This handler is executed when the `call.respond` function is invoked in a route handler.
 * @see [createApplicationPlugin]
 *
 * @param block An action that needs to be executed when your server is sending a response to a client.
 **/
public fun onCallRespond(
	block: suspend OnCallRespondContext<PluginConfig>.(call: ApplicationCall, body: Any) -> Unit
) {
	onDefaultPhase(
		onResponseInterceptions,
		ApplicationSendPipeline.Transform,
		PHASE_ON_CALL_RESPOND,
		::OnCallRespondContext,
		block
	)
}

可以看出來,基本上邏輯都跟剛剛所說的 onCall 一樣,都是利用 onDefaultPhase 來協助接收開發者撰寫的內容。

不過由於階段的不同,以及處理對象的不同,所以在參數上需要調整一下收到的內容。

這邊我們就可以看到,這裡整理了接收請求以及送出回應的流程,將這兩種流程合併成 pipeline 的定義,這樣就可以共用這一段邏輯。我們只需要調整處理的對象,以及該對象面對的 pipeline 即可。

今天針對自定義的流程控制,我們就先看到這邊。


上一篇
Day 27:利用 createApplicationPlugin 定義客製化套件
下一篇
Day 29:利用 createConfiguration 設置自定義套件參數
系列文
深入解析 Kotlin 專案 Ktor 的程式碼,探索 Ktor 的強大功能30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言