はじめに
こんにちは。リクルートライフスタイルでAndroidアプリを開発をしている@ykoyanoです。
KtorでCustom Featureを作ろうとした際、そもそもFeaturesの実行されるタイミングがどこで定義されているのかを把握したくてKtorの実装について調べたのですが、今回はそこで得たものをメモ代わりに少し整理して紹介したいと思います。
Ktor
KtorとはJetbrains社製のWebアプリケーションフレームワークです。
KtorはPrinciplesにもあるように、Unopinionated, Asynchronous, Testableを原則とした軽量かつ拡張性の高いフレームワークです1
KtorでどのようにWebアプリケーションが実装できるのか知りたい方は、以下のレポジトリを覗いてみると参考になるかもしれません。
この記事の目的
自分でKtor用に特定の機能を追加するライブラリなどを実装する場合、Custom Featureを実装することが多いと思うのですが、Custom Featureを実装する際には、普段Ktorを使う場合にはあまり意識しないようなPipelineの細かい実装などの知識も必要となる場合があります。
そこで本記事では、Featuresの実行されるタイミングがどう定義されているのかを、PipelinePhaseを通して理解し、Custom Feature実装に役立てたいと思います。
ちなみに、今回は触れる内容は、機能としてはserver, clientに限らないものですが、実際に例として取り上げるコードとはserverのものを中心としています。
Features
Ktorでは、AuthenticationやRoutingなど一般的なWebアプリケーションには必須な機能から、
Velocity TemplatesやWebjars supportといった拡張機能まで、アプリケーションの実行やリクエストの処理のために行う多くの機能が、プラガバブルなFeatureという機能単位で提供されています。
そして、上記の様な具体的なFeaturesの実装を、非同期処理アプリケーションフレームワーク本体の実装から分離することで、Ktorのコア部分の実装を軽量にしています。
ktor-serverでは、アプリケーションに導入するFeatureはApplicationFeatureを実装している必要があります。
interface ApplicationFeature<in TPipeline : Pipeline<*, ApplicationCall>, out TConfiguration : Any, TFeature : Any> {
...
fun install(pipeline: TPipeline, configure: TConfiguration.() -> Unit): TFeature
}
具体的なFeatureの例を見てみましょう。
ktor-serverにはDefaultHeadersというHTTP レスポンスのヘッダーに任意の値を付与することができる比較的シンプルな処理を行うFeatureが存在します。このFeatureをApplicationに導入してみましょう。
fun Application.main() {
/**
* これによって全てのレスポンスに X-Developer ヘッダーが付与される
* Application#install 内では DefaultHeaders#install が呼び出されている
*/
install(DefaultHeaders){
header("X-Developer", "John Doe")
}
}
このDefaultHeadersはどのように実装されているのでしょうか。
class DefaultHeaders(config: Configuration) {
... // DefaultHeadersの実装
// レスポンスヘッダーに値を付与する処理
private fun intercept(call: ApplicationCall) { ... }
companion object Feature : ApplicationFeature<Application, Configuration, DefaultHeaders> {
override val key = AttributeKey<DefaultHeaders>("Default Headers")
// Application#install 内で呼び出されるメソッド
override fun install(pipeline: Application, configure: Configuration.() -> Unit): DefaultHeaders {
... // DefaultHeaders の設定を行う
val feature = DefaultHeaders(config) // 設定を反映させた DefaultHeaders feature
pipeline.intercept(ApplicationCallPipeline.Features) { feature.intercept(call) } // pipelineにinterceptorを与える
return feature
}
}
}
さてここで、 pipeline.intercept(ApplicationCallPipeline.Features){...}
という処理が出てきました。
この呼び出しこそが、このFeatureがどのアプリケーションやリクエストのライフサイクルのどのタイミングにinterceptさせるかを指定している箇所になります。
ではこのFeatureをinterceptさせるには、どのような種類の実行タイミングを指定できるのでしょうか。
Pipeline
pipeline.intercept(ApplicationCallPipeline.Features){...}
にも出てくるPipelineとは、非同期の拡張可能な計算の実行パイプラインを表わすものです。
このPipelineがKtorの拡張性や非同期な動きを支える重要な要素となっています。
Ktorでは、リクエストを受け取ってからレスポンスのを返すまでに行う一連の様々な処理が、Pipeline上の非同期計算として行われます。Featureによって実行される計算もそのひとつです。
ktor-serverで既に定義されているPipelineには例えば以下のようなものがあります。
- ApplicationCallPipeline : リクエストを受け付けてからレスポンスを返すまでのパイプライン
- ApplicationReceivePipeline : 受信したデータを処理するためのパイプライン
- ApplicationSendPipeline : レスポンスを返すためのパイプライン
さらに、これらのPipelineが、パイプライン中での計算の実行順序を決定づけるためのPipelinePhaseという定義しています。
- Setup : Call Phaseに対する準備やAttributesの処理を行う
- Monitoring : Call PhaseをトレースするためのPhase。ロギング、メトリクス集計、エラーハンドリング時に有用なPhase
- Features : 認証を始め、多くのFeatureがこのPhaseにinterceptする
- Call :Routingを行い、レスポンスを返すPhase
- Fallback : 例外ハンドリング時などに呼び出されるPhase
といった5つのPipelinePhaseを持っています。それらは下図のように実行する順序が決められています。
ここでいったん、各PipelineがどんなPipelinePhaseを定義しているのかを整理してみます。
さて、ではここでDefaultHeadersの実装をもう一度見返してみましょう。
DefaultHeadersというFeatureをインストールする際に
pipeline.intercept(ApplicationCallPipeline.Features) {
feature.intercept(call)
}
という処理が呼び出されていました。
これによって
DefaultHeadersというFeatureで実行したい計算処理が
ApplicationCallPipelineのFeaturesというPipelinePhaseで実行されるようになります。
しかしPipelinePhaseの種類が多くてイメージがいまいちわかないので
標準に容易されている各FeatureがどのPipelinePhaseで
実行するようにinterceptされているのかまとめてみました。
(ここに載せられていないものもあります。)
これによって、各PipelinePhaseの使い分けがだいぶハッキリ見えてくるようになったと思います。
例えば自分でCustom Featureを作る場合も、実現したい計算はどのレイヤーのどのフェイズで行うのが適切なのかを判断する参考になりそうです。
まとめ
ざっくばらんではありましたが、Ktorにおいて、リクエストを受け付けてからレスポンスを返すまでの一連の流れに対して、FeaturesがどのようにPipelinePhaseに紐付けられているのか、の全体像を追ってみました。
もしコメントやツッコミなどありましたらぜひぜひお願いします〜!
また、ボリュームが多すぎてこの記事では紹介できなかったのですが
- 任意のPipelinePhaseの前後に、自分で定義した別のPipelinePhaseを追加する
- 異なるPipeline同士でmergeしてで、PipelinePhaseに紐づくinterceptを統合する
- 特定のRoutingのみPipelinePhaseに対してのみ処理をinterceptさせる
- PipelineTestにもあるような、複数のあるいはネストしたPipeline内では処理
- v1.0.1からのstructured concurrencyをサポートによる、複雑なパイプラインの分岐や制御
など、PipelinePhaseに関してはできることはまだまだたくさんあります!
Ktorはまだまだ発展途上でドキュメントや情報が少ないので、そういった情報もまとめられたらよいなと思っております!
ちなみに今回直接はふれなかったktor-clientに関しては、弊社アドベントカレンダーで@oxsoftがKtor on Androidでモックサーバアプリの作り方を紹介していますので、興味がある方はこちらもぜひ読んで見て下さい!