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
も併用しています。