Skip to content

カスタムサーバープラグイン

コード例: custom-plugin

v2.0.0以降、Ktorはカスタムプラグインを作成するための新しいAPIを提供しています。一般的に、このAPIではパイプラインやフェーズなどのKtor内部の概念を理解する必要はありません。代わりに、onCallonCallReceiveonCallRespondハンドラーを使用して、リクエストとレスポンスの処理のさまざまな段階にアクセスできます。

このトピックで説明されているAPIは、v2.0.0以降で有効です。古いバージョンについては、ベースAPIを使用できます。

最初のプラグインを作成してインストールする

このセクションでは、最初のプラグインを作成してインストールする方法を説明します。 Ktorプロジェクトの作成、開封、実行チュートリアルで作成したアプリケーションを開始プロジェクトとして使用できます。

  1. プラグインを作成するには、createApplicationPlugin関数を呼び出し、プラグイン名を渡します。

    kotlin
    import io.ktor.server.application.*
    
    val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
        println("SimplePlugin is installed!")
    }

    この関数は、次のステップでプラグインをインストールするために使用されるApplicationPluginインスタンスを返します。

    特定のルートにインストールできるプラグインを作成できるcreateRouteScopedPlugin関数もあります。

  2. プラグインをインストールするには、アプリケーションの初期化コードで作成したApplicationPluginインスタンスをinstall関数に渡します。

    kotlin
    fun Application.module() {
        install(SimplePlugin)
    }
  3. 最後に、アプリケーションを実行して、コンソール出力にプラグインの挨拶が表示されることを確認します。

    Bash
    2021-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を使用して受信リクエストをログに記録するカスタムプラグインを作成する方法を示しています。

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

このプラグインをインストールすると、アプリケーションはリクエストされたURLをコンソールに表示します。例:

Bash
Request URL: http://0.0.0.0:8080/
Request URL: http://0.0.0.0:8080/index

例2: カスタムヘッダー

この例では、各レスポンスにカスタムヘッダーを追加するプラグインを作成する方法を示します。

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

結果として、すべてのレスポンスにカスタムヘッダーが追加されます。

HTTP
HTTP/1.1 200 OK
X-Custom-Header: Hello, world!

このプラグインのカスタムヘッダー名と値はハードコードされていることに注意してください。必要なカスタムヘッダー名/値を渡すための設定を提供することで、このプラグインをより柔軟にすることができます。

onCallReceive

onCallReceiveハンドラーはtransformBody関数を提供し、クライアントから受信したデータを変換できるようにします。クライアントが、ボディにtext/plainとして10を含むサンプルのPOSTリクエストを行うと仮定します。

HTTP
POST http://localhost:8080/transform-data
Content-Type: text/plain

10

このボディを整数値として受信するには、POSTリクエスト用のルートハンドラーを作成し、Intパラメータを指定してcall.receiveを呼び出す必要があります。

kotlin
post("/transform-data") {
    val data = call.receive<Int>()
}

では、ボディを整数値として受け取り、それに1を加算するプラグインを作成してみましょう。これを行うには、次のようにonCallReceive内でtransformBodyを処理する必要があります。

kotlin
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は次のように動作します。

  1. TransformBodyContextは、現在のリクエストに関する型情報を含むラムダレシーバーです。上記の例では、TransformBodyContext.requestedTypeプロパティを使用して、要求されたデータ型を確認しています。
  2. dataは、リクエストボディをByteReadChannelとして受信し、必要な型に変換できるラムダ引数です。上記の例では、ByteReadChannel.readUTF8Lineを使用してリクエストボディを読み取っています。
  3. 最後に、データを変換して返す必要があります。この例では、受信した整数値に1が加算されます。

完全な例はこちらにあります: DataTransformationPlugin.kt

onCallRespond

onCallRespondtransformBodyハンドラーを提供し、クライアントに送信されるデータを変換できるようにします。このハンドラーは、ルートハンドラーでcall.respond関数が呼び出されたときに実行されます。 onCallReceiveの例の続きとして、POSTリクエストハンドラーで整数値が受信される場合を考えます。

kotlin
post("/transform-data") {
    val data = call.receive<Int>()
    call.respond(data)
}

call.respondを呼び出すとonCallRespondが呼び出され、クライアントに送信されるデータを変換できるようになります。例えば、以下のコードスニペットは初期値に1を加算する方法を示しています。

kotlin
onCallRespond { call ->
    transformBody { data ->
        if (data is Int) {
            (data + 1).toString()
        } else {
            data
        }
    }
}

完全な例はこちらにあります: DataTransformationPlugin.kt

その他の便利なハンドラー

onCallonCallReceiveonCallRespondハンドラーに加えて、Ktorはコールの他の段階を処理するのに役立つ一連の特定のフックを提供します。 これらのフックは、Hookをパラメータとして受け取るonハンドラーを使用して処理できます。 これらのフックには以下が含まれます。

  • CallSetup: コールの処理の最初のステップとして呼び出されます。
  • ResponseBodyReadyForSend: レスポンスボディがすべての変換を通過し、送信準備が整ったときに呼び出されます。
  • ResponseSent: レスポンスがクライアントに正常に送信されたときに呼び出されます。
  • CallFailed: コールが例外で失敗したときに呼び出されます。
  • AuthenticationChecked: 認証資格情報の確認後に実行されます。次の例は、このフックを使用して認機能を実装する方法を示しています: custom-plugin-authorization

以下の例は、CallSetupを処理する方法を示しています。

kotlin
on(CallSetup) { call->
    // ...
}

アプリケーションの起動や停止などのアプリケーションイベントを処理できるMonitoringEventフックもあります。

コールの状態を共有する

カスタムプラグインを使用すると、コールに関連する任意の値を共有できるため、そのコールを処理する任意のハンドラー内でこの値にアクセスできます。この値は、call.attributesコレクションに一意のキーを持つ属性として保存されます。以下の例は、属性を使用してリクエストの受信からボディの読み取りまでの時間を計算する方法を示しています。

kotlin
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リクエストを行うと、プラグインはコンソールに遅延を表示します。

Bash
Request URL: http://localhost:8080/transform-data
Read body delay (ms): 52

完全な例はこちらにあります: DataTransformationBenchmarkPlugin.kt

ルートハンドラー内でコールの属性にアクセスすることもできます。

アプリケーションイベントを処理する

onハンドラーは、MonitoringEventフックを使用してアプリケーションのライフサイクルに関連するイベントを処理する機能を提供します。 例えば、以下の事前定義されたイベントonハンドラーに渡すことができます。

  • ApplicationStarting
  • ApplicationStarted
  • ApplicationStopPreparing
  • ApplicationStopping
  • ApplicationStopped

以下のコードスニペットは、ApplicationStoppedを使用してアプリケーションのシャットダウンを処理する方法を示しています。

kotlin
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()

これはアプリケーションリソースを解放するのに役立ちます。

プラグインの設定を提供する

カスタムヘッダーの例では、各レスポンスに定義済みのカスタムヘッダーを追加するプラグインの作成方法を示しました。このプラグインをより便利にし、必要なカスタムヘッダー名/値を渡すための設定を提供してみましょう。

  1. まず、設定クラスを定義する必要があります。

    kotlin
    class PluginConfiguration {
        var headerName: String = "Custom-Header-Name"
        var headerValue: String = "Default value"
    }
  2. プラグインでこの設定を使用するには、設定クラスの参照をcreateApplicationPluginに渡します。

    kotlin
    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)
            }
        }
    }

    プラグイン設定フィールドは可変(mutable)であるため、ローカル変数に保存することをお勧めします。

  3. 最後に、次のようにプラグインをインストールして設定できます。

    kotlin
    install(CustomHeaderPlugin) {
        headerName = "X-Custom-Header"
        headerValue = "Hello, world!"
    }

完全な例はこちらにあります: CustomHeaderPlugin.kt

ファイルでの設定

Ktorでは、設定ファイルでプラグイン設定を指定できます。 CustomHeaderPluginでこれを実現する方法を見てみましょう。

  1. まず、application.confまたはapplication.yamlファイルにプラグイン設定を含む新しいグループを追加します。

    shell
    http {
        custom_header {
            header_name = X-Another-Custom-Header
            header_value = Some value
        }
    }
    yaml
    http:
      custom_header:
        header_name: X-Another-Custom-Header
        header_value: Some value

    この例では、プラグイン設定はhttp.custom_headerグループに保存されています。

  2. 設定ファイルのプロパティにアクセスするには、ApplicationConfigを設定クラスのコンストラクタに渡します。 tryGetString関数は、指定されたプロパティ値を返します。

    kotlin
    class CustomHeaderConfiguration(config: ApplicationConfig) {
        var headerName: String = config.tryGetString("header_name") ?: "Custom-Header-Name"
        var headerValue: String = config.tryGetString("header_value") ?: "Default value"
    }
  3. 最後に、createApplicationPlugin関数のconfigurationPathパラメータにhttp.custom_header値を割り当てます。

    kotlin
    val 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インスタンスを返します。以下の例は、サーバーで使用されているホストとポートを取得する方法を示しています。

kotlin
val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
   val host = applicationConfig?.host
   val port = applicationConfig?.port
   println("Listening on $host:$port")
}

環境

アプリケーションの環境にアクセスするには、environmentプロパティを使用します。例えば、このプロパティを使用すると、開発モードが有効かどうかを判断できます。

kotlin
val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
   val isDevMode = environment?.developmentMode
   onCall { call ->
      if (isDevMode == true) {
         println("handling request ${call.request.uri}")
      }
   }
}

その他

プラグインの状態を保存する

プラグインの状態を保存するために、ハンドラーラムダから任意の値をキャプチャできます。コンカレントデータ構造やアトミックデータ型を使用して、すべての状態値をスレッドセーフにすることをお勧めします。

kotlin
val SimplePlugin = createApplicationPlugin(name = "SimplePlugin") {
   val activeRequests = AtomicInteger(0)
   onCall {
      activeRequests.incrementAndGet()
   }
   onCallRespond {
      activeRequests.decrementAndGet()
   }
}

データベース

  • 中断可能な(suspendable)データベースでカスタムプラグインを使用できますか?

    はい。すべてのハンドラーは中断関数(suspending functions)であるため、プラグイン内で中断可能なデータベース操作を実行できます。ただし、特定のコール用のリソースの割り当て解除を忘れないでください(例えば、on(ResponseSent)を使用するなど)。

  • ブロッキングデータベースでカスタムプラグインを使用するにはどうすればよいですか?

    Ktorはコルーチンと中断関数を使用しているため、ブロッキングデータベースへのリクエストを行うのは危険です。ブロッキングコールを実行するコルーチンがブロックされ、そのまま永久に中断される可能性があるためです。これを防ぐには、別のCoroutineContextを作成する必要があります。

    kotlin
    val databaseContext = newSingleThreadContext("DatabaseThread")

    コンテキストを作成したら、データベースへの各呼び出しをwithContext呼び出しでラップします。

    kotlin
    onCall {
        withContext(databaseContext) {
            database.access(...) // データベースへの何らかの呼び出し
        }
    }