Ktor v2 のPipeline構造
V2.0.0以降、Ktorのパイプライン構造などの内部実装はアプリケーションやプラグイン開発者が意識しないでよいものとする方向を目指しているようではありますが(参考)。でもね、ちょっと複雑なことをやろうとしてぼくは思いました。全然わからん。
というわけでがんばって調べた成果を共有しておきますね。
Ktorパイプラインの全体像
Pipeline
クラスについて
基本的なことは↓の記事に詳しく書いていますのでよかったらどうぞ。
Routingプラグインについて
Routingは、プラグインと言っても、Webアプリケーションには必須ですし、上記のパイプラインに大きく関与しています。むしろ図に記載した処理フローのうち重要な部分はRoutingプラグインによって作られていると言ってもよいかもしれません。なので、この特殊なプラグインについてすこし補足をしておきます。
ルーティングそのものについては↓の記事に詳しく書いています。
ルーティングの実行
ルーティング、すなわちKtorがリクエストを受け取ったときにルーティングツリーから該当するRouteを探す処理は、Application本体側パイプラインのCallフェーズにインターセプトされます。Callフェーズに入ること自体プラグインとしては例外的なのですが、というよりはむしろ、Routingプラグインのやっていること、Routeの検索および見つかったRouteのハンドラ実行こそがCallフェーズそのものなんでしょう。Routingプラグインの行う処理をApplicationのCallフェーズと呼んでいると言ってよいくらいです。
Routeレベルのパイプライン構築
シンプルなKtorパイプラインの図は他サイトでも目にしますが、ApplicationCallPipelineは実質2本あると考えたほうがよいです。Ktor本体だけであれば確かに1本なんですが、Routingプラグインがもう1本作ります。重要性を考えれば、通常その存在を考慮しないわけにもいかないでしょう。
Route
クラスは ApplicationCallPipeline
を継承しており、Route自体がひとつのパイプラインでもあります。特定のRouteにプラグインをインストールし、フェーズをインターセプトすることができます。
たとえば、認証機能を組み込むときに下記のような記述をしますが、これは、 /settings
のひとつ上位に、認証プラグインをインストールした Route
オブジェクトを配置することを意味しています。
routing {
get("/articles") { /* ... */ }
authenticate {
route("/settings") {
get("/profile") { /* ... */ }
}
}
}
ルーティング処理で、リクエストにもっともマッチするRouteが決定したら、マッチしたRouteとその上位にあるRouteすべてがパイプラインとしてマージされてひとつの ApplicationCallPipeline
になり、実行されます。
前述のようなルーティング定義があるとき、 GET /settings/profile
にアクセスがあると、ルート[root]のRoute・認証用Route・ /settings
のRoute・ /profile
のRoute、がこの順番でマージされます。順序は大事で、同じフェーズに設定されたインターセプタが複数ある場合、下位Routeのものが後に実行されることになります。
そして、マッチしたRoute自体がもっているリクエストハンドラ、今回の例でいうと get("/profile") { /* ... */ }
のラムダの部分ですが、これが、生成されたパイプラインのCallフェーズのインターセプタとして登録されます。これもルーティングの実行と同様で、RoutingプラグインがリクエストハンドラをCallフェーズにインターセプトしていることにより、RouteレベルパイプラインのCallフェーズはCallフェーズたらしめられている、と言えます。
リクエストハンドラの実行
Routeレベルのパイプラインにインターセプトされたリクエストハンドラが何を実行するかはアプリケーション開発者次第ですが、その中で receive
系の関数を利用してリクエストボディを要求したとき、リクエストボディは ApplicationReceivePipeline
によって処理されて手元に届きます。同様に respond
系関数を利用すると、出力するデータは ApplicationSendPipeline
によって処理され、HTTPレスポンスとなってクライアントに送り返されます。
Discussion