Ktorの面白い特徴のひとつとしては「Webアプリケーションに必要な機能を"install"していくスタイル」を採っていることだと思います。 その典型的な例はルーティングです。よく見るルーティング設定のコードは
routing {
get("/") {
call.respondText("Hello, world!")
}
}
のように routing を使うものが多いと思いますが、これは
install(Routing) {
get("/") {
call.respondText("Hello, world!")
}
}
と記述するのとだいたい同じです。
その他にも標準で提供されているFeatureはたくさんあって、例えばContentNegotiationやCompressionのようなものがあります。
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も併用しています。
