カスタムサーバープラグイン
コード例: custom-plugin
v2.0.0以降、Ktorはカスタムプラグインを作成するための新しいAPIを提供しています。一般的に、このAPIではパイプラインやフェーズなどのKtor内部の概念を理解する必要はありません。代わりに、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を受け取ります。これにより、リクエスト/レスポンス情報にアクセスし、レスポンスパラメータを変更(カスタムヘッダーの追加など)できます。リクエスト/レスポンスのボディを変換する必要がある場合は、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関数を提供し、クライアントから受信したデータを変換できるようにします。クライアントが、ボディにtext/plainとして10を含むサンプルのPOSTリクエストを行うと仮定します。
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は、現在のリクエストに関する型情報を含むラムダレシーバーです。上記の例では、TransformBodyContext.requestedTypeプロパティを使用して、要求されたデータ型を確認しています。dataは、リクエストボディを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) } } }プラグイン設定フィールドは可変(mutable)であるため、ローカル変数に保存することをお勧めします。
最後に、次のようにプラグインをインストールして設定できます。
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" }最後に、
createApplicationPlugin関数のconfigurationPathパラメータにhttp.custom_header値を割り当てます。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}")
}
}
}その他
プラグインの状態を保存する
プラグインの状態を保存するために、ハンドラーラムダから任意の値をキャプチャできます。コンカレントデータ構造やアトミックデータ型を使用して、すべての状態値をスレッドセーフにすることをお勧めします。
val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
val activeRequests = AtomicInteger(0)
onCall {
activeRequests.incrementAndGet()
}
onCallRespond {
activeRequests.decrementAndGet()
}
}データベース
中断可能な(suspendable)データベースでカスタムプラグインを使用できますか?
はい。すべてのハンドラーは中断関数(suspending functions)であるため、プラグイン内で中断可能なデータベース操作を実行できます。ただし、特定のコール用のリソースの割り当て解除を忘れないでください(例えば、on(ResponseSent)を使用するなど)。
ブロッキングデータベースでカスタムプラグインを使用するにはどうすればよいですか?
Ktorはコルーチンと中断関数を使用しているため、ブロッキングデータベースへのリクエストを行うのは危険です。ブロッキングコールを実行するコルーチンがブロックされ、そのまま永久に中断される可能性があるためです。これを防ぐには、別のCoroutineContextを作成する必要があります。
kotlinval databaseContext = newSingleThreadContext("DatabaseThread")コンテキストを作成したら、データベースへの各呼び出しを
withContext呼び出しでラップします。kotlinonCall { withContext(databaseContext) { database.access(...) // データベースへの何らかの呼び出し } }
