算譜王におれはなる!!!!

偏りはあると思うけど情報技術全般についてマイペースに書くよ。

KtorのカスタムFeatureをつくる

Ktorの面白い特徴のひとつとしては「Webアプリケーションに必要な機能を"install"していくスタイル」を採っていることだと思います。 その典型的な例はルーティングです。よく見るルーティング設定のコードは

routing {
    get("/") {
        call.respondText("Hello, world!")
    }
}

のように routing を使うものが多いと思いますが、これは

install(Routing) {
    get("/") {
        call.respondText("Hello, world!")
    }
}

と記述するのとだいたい同じです。

その他にも標準で提供されているFeatureはたくさんあって、例えばContentNegotiationCompressionのようなものがあります。

install(ContentNegotiation) {
    jackson()
}
install(Compression) {
    gzip {
        priority = 1.0
    }
}
install(Routing) {
    get("/") {
        call.respondText("Hello, world!")
    }
}

今回は、このFeatureを自作してみます。 有用なものを作って配布したら、いろんな人にinstallして使ってもらえるかもしれませんね。

参考

とは言っても役に立ちそうなアイデアがパッと浮かばないので、単純なロギングFeatureを実装したいと思います。 ポイントは

  • 大元となるFeatureクラスを定義する(今回はMyLoggingクラス)
  • そこに設定用クラスをネストする(名前は何でもいいけどConfigurationクラスとします)
  • ApplicationFeatureインタフェースを実装したcompanion objectを定義する

です。 必ずしもこれに従うことはないとは思いますが、上記参考URLの公式ドキュメントではこうなっていました。 ざっとこんな感じになります(importは割愛)。

class MyLogging(configuration: Configuration) {

    val decoration: String = configuration.decoration

    class Configuration {
        var decoration: String = "✨"
    }

    companion object : ApplicationFeature<ApplicationCallPipeline, Configuration, MyLogging> {
        override val key: AttributeKey<MyLogging> = AttributeKey("MyLogging")

        override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): MyLogging {
            val configuration = Configuration().apply(configure)
            val feature = MyLogging(configuration)
            /* TODO */
            return feature
        }
    }

    // あとで使う
    private fun log(message: String) {
        println("$decoration $message $decoration".trim())
    }
}

このコードで一旦はFeatureとしての体を成しています。が、現時点では何も仕事はしていません。 メソッドinstallの引数configureが、独自定義の設定用クラスConfigurationの拡張関数になっていることに注目してください。 このFeatureのユーザは、ラムダ式の中でConfigurationのプロパティにアクセスすることで設定を組み立てていきます。

install(MyLogging) {
    decoration = "🌟"
}
routing {
    get("/") {
        call.respondText("Hello, world!")
    }
}

さて、まだ何の面白いこともしていないMyLoggingですが、リクエストの前後でログを出力したいと思います。 2つ前のコードのメソッドinstallに再び注目してください。第1引数pipelineを扱います。 PipelineもKtorの面白い特徴のひとつなんですが、ドキュメントコメントの言葉を借りればこれは「非同期の拡張可能な計算のための実行パイプラインを表す」ものです。 Pipelineにはフェーズがあって、そのフェーズをインターセプトすることができます。

override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): MyLogging {
    val configuration = Configuration().apply(configure)
    val feature = MyLogging(configuration)

    pipeline.intercept(ApplicationCallPipeline.Features) {
        feature.log("${call.request.path()}へのリクエストが始まるよ!")
        proceed()
        feature.log("${call.request.path()}へのリクエストが終わったよ!")
    }
    return feature
}

余談ですが、メインのApplicationCallPipelineには5つのフェーズがあって、今回は"Features"というフェーズをインターセプトしました。 本来であれば"Monitoring"でやるべきだったのかなと思いつつCallLoggingという標準のロギングFeatureのコードを読んでみたら、新たに"Logging"というフェーズを作り、"Monitoring"の前に挿入していました。そういうプレイングもあるのか…。

なお、今回つくってみたMyLoggingの使ってみた結果はこんな感じです。 CallLoggingも併用しています。

f:id:ngsw_taro:20191217183951p:plain