🌊

Ktor v2 のPipeline構造

2023/01/28に公開

V2.0.0以降、Ktorのパイプライン構造などの内部実装はアプリケーションやプラグイン開発者が意識しないでよいものとする方向を目指しているようではありますが(参考)。でもね、ちょっと複雑なことをやろうとしてぼくは思いました。全然わからん。

というわけでがんばって調べた成果を共有しておきますね。

Ktorパイプラインの全体像

Ktorパイプラインの全体像

Pipeline クラスについて

基本的なことは↓の記事に詳しく書いていますのでよかったらどうぞ。

https://zenn.dev/xfan/articles/bd07f2f8a6ecc7

Routingプラグインについて

Routingは、プラグインと言っても、Webアプリケーションには必須ですし、上記のパイプラインに大きく関与しています。むしろ図に記載した処理フローのうち重要な部分はRoutingプラグインによって作られていると言ってもよいかもしれません。なので、この特殊なプラグインについてすこし補足をしておきます。

ルーティングそのものについては↓の記事に詳しく書いています。

https://zenn.dev/xfan/articles/a84b66373a2e40

ルーティングの実行

ルーティング、すなわち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