KtorのApplication.install周りの処理についてコードを読んでみた

[前回](https://qiita.com/doyaaaaaken/items/46b5c57205bdc24c4178)の続き。
Kotlin製webフレームワークKtorのコードを読んで整理してみた話。
[Ktor公式ドキュメント](https://ktor.io/)
[Ktor日本語版ドキュメント](https://jp.ktor.work/)

versionは1.1.3。

パッケージ構成の次は、主要な処理である`Application.install`の呼び出し内部を調べてみた。

# そもそもApplication.installメソッドとは？

Ktorでは認証、ルーティング、ヘッダー付与、Cookie、テンプレートエンジン等の機能は全て、`Feature`と呼ばれるプラグインになっており、`Application.install`メソッドでそれらを利用可能にする。

例えば以下のようなコードで、Sessionを利用したり、HttpへのリクエストをHttpsへリダイレクトしたりできるようになる。

```kotlin

data class MySession(val username: String)
data class IndexData(val items: List<Int>)

fun Application.module() {
    //Application.installメソッド
    install(HttpsRedirect) {
        sslPort = 443
    }
    install(Sessions) {
        cookie<MySession>("SESSION")
    }
    routing {
        get("/") {
            val session = call.sessions.get<MySession>()
            if (session != null) {
                call.respondText("User is logged")
            } else {
                call.respond(FreeMarkerContent("index.ftl", mapOf("data" to IndexData(listOf(1, 2, 3))), ""))
            }
        }
    }
} 
```

# Applicationクラスとは

まずは役割について。[公式](https://ktor.io/servers/application.html#application)には以下のように記載があった。

>An Application instance is the main unit of a Ktor Application. When a request comes in (a request can be HTTP, HTTP/2 or WebSocket requests), it is converted to an ApplicationCall and goes through a pipeline which is owned by the Application. The pipeline consists of one or more interceptors that are previously installed, providing certain functionality such as routing, compression, etc. that ends handling the request.

> Normally, a Ktor program configures the Application pipeline through modules that install and configure features.

つまりは以下のような存在になる。

- 起動しているWebアプリ本体そのものを指す
- Pipelineとみなせ、処理を予めApplicationに登録しておくことで、受け取ったリクエスト、レスポンスに対し処理を行える

継承ツリーをたどると下のクラス図のようにApplicationクラスはPipelineクラスを継承していることがわかります。
また、下図を見るとApplicationはPipelineであるという以外にも、以下のようなこともわかります。

- Pipelineには`addPhase`, `insertPhaseXXX`メソッドなどでPipelinePhaseを登録可能（PipelinePhaseは振る舞いをほぼ持たない値オブジェクトに近いもの。定義はほぼ`class PipelinePhase(val name: String)` だけ。）
- ApplicationはWebアプリのCoroutineのスコープを表すCoroutineContextを持つ（Coroutineに詳しくないのでちょっと自信なし）
- Applicationはそれ自体がPipelineである他にも、ApplicationCallPipelineの2つの変数(receivePipeline, sendPipeline)によって、受信したコンテンツ・送信するコンテンツへの処理（Pipeline）も持つ。

※ 矢印は継承関係を表す
![Screen Shot 2019-04-07 at 15.42.11.png](https://qiita-image-store.s3.amazonaws.com/0/47429/29d3bf9d-0eec-1432-4e4a-5bf76499fc30.png)


また、上図のようにPipelineクラスのコンストラクタはPipelinePhaseを複数受け取るようになっていますが、
ここには記載しませんがコードを読むと以下のこともわかります。

- ApplicationCallPipeline
    - `PipelinePhase("Setup")`,`PipelinePhase("Monitoring")`,`PipelinePhase("Features")`,`PipelinePhase("Call")`,`PipelinePhase("Fallback")`をPipelineコンストラクタに渡しインスタンス生成している
- ApplicationReceivePipeline
    - `PipelinePhase("Before")`,`PipelinePhase("Transform")`,`PipelinePhase("After")`をPipelineコンストラクタに渡しインスタンス生成している
- ApplicationSendPipeline
    - `PipelinePhase("Before")`,`PipelinePhase("Transform")`,`PipelinePhase("Render")`等7つをPipelineコンストラクタに渡しインスタンス生成している

Pipelineに関する具体的な制御処理は、基底クラスであるPipelineに集まっており子クラスにはほとんどないようです。

**<話の重要な部分まとめ>**

- **起動したWebアプリそのものを表すApplicationクラスはPipelineである**
- **ApplicationはPipelineでありつつも、他に2つPipelineを持つ（sendPipeline, receivePipeline）**
- **PipelineにはPipelinePhaseを複数設定できる。**
- **PipelinePhaseは実質ただの文字列値であり振る舞いは持たない**

ここまでで説明していないPipelineクラスの変数`attributes: Attributes`の話は後述します。

# Application.installメソッドの中身

本題に移ります。簡略化した形で持ってくると以下のようになります。

```kotlin
fun <P : Pipeline<*, ApplicationCall>, B : Any, F : Any> P.install(
    feature: ApplicationFeature<P, B, F>,
    configure: B.() -> Unit = {}
): F {
    //ここの`attributes`はApplicationクラスの変数（Attributes型）
    val registry: Attributes = attributes.computeIfAbsent(AttributeKey("ApplicationFeatureRegistry")) { 
        Attributes(true)
    }
    val installedFeature: F? = registry.getOrNull(feature.key)
    when (installedFeature) {
        null -> {
            try {
                val installed: F = feature.install(this, configure)
                registry.put(feature.key, installed)
                return installed
            } catch (t: Throwable) {
                throw t
            }
        }
        feature -> { return installedFeature }
        else -> { throw DuplicateApplicationFeatureException("") }
    }
}
```

定義は`P.install` メソッド（拡張関数）であり、Pは`Pipeline<*, ApplicationCall>`型を指すので、
実際の定義は`Application.install`ではなく`Pipeline<*, ApplicationCall>.install`メソッドであることがわかります。

なお、BとFについては、FがFeature（実際にInstallする機能）クラスであり、BがFeatureのConfigurationになります。

さて、`attributes.computeIfAbsent`でattributesという変数が出てきますが、これはApplicationクラスの変数です。
Attributes型であり、Attributesクラスの中身は、ただのMapのデータ構造です。

つまり最初の二行

```kotlin
val registry: Attributes = attributes.computeIfAbsent(AttributeKey("ApplicationFeatureRegistry")) { 
        Attributes(true)
}
val installedFeature: F? = registry.getOrNull(feature.key)
```
はApplicationクラスの持つMap型変数にインストールしたいFeatureが登録されているかどうかを確認する処理になります。
そしてもし未登録だった場合は以下の2行でApplicationFeature.installメソッドを呼び出した後、そのFeatureをApplication.attributesに登録してます。

```kotlin
val installed: F = feature.install(this, configure)
registry.put(feature.key, installed)
```

**<話の重要な部分まとめ>**

- **Applicationクラスの持つ変数attributesはFeatureを登録するのに使われていた**
- **Feature登録時にApplicationFeature.installメソッドを呼び、実際にPipelineにFeatureの処理登録を行う。**

というわけで`ApplicationFeature.install`について見ていきます。

# ApplicationFeature.installについて

ApplicationFeatureはinterfaceであり、installメソッドは以下のようなシグニチャになります。

```kotlin
//TConfiguration, TFeatureはAny型
fun install(pipeline: Pipeline<*, ApplicationCall>, configure: TConfiguration.() -> Unit): TFeature
```

具体的に何をしているのかは各具象クラスの実装を見ないとわからないので、`io.ktor.features.HttpsRedirect`の中身を見てみます。
`HttpsRedirect`クラスはktorがデフォルトで用意しているFeatureです。
HTTPリクエストをHTTPSリクエストにリダイレクトするという内容になります。
（HttpsRedirectをApplicationにinstallするコードは冒頭にて紹介しています。）

以下が簡略化したコードになります。

```kotlin
class HttpsRedirect(config: Configuration) {
    val redirectPort: Int = config.sslPort
    val permanent: Boolean = config.permanentRedirect

    class Configuration {
        var sslPort: Int = URLProtocol.HTTPS.defaultPort
        var permanentRedirect: Boolean = true
    }

    companion object Feature : ApplicationFeature<ApplicationCallPipeline, Configuration, HttpsRedirect> {
        override val key = AttributeKey<HttpsRedirect>("HttpsRedirect")
        //ApplicationFeature.installメソッド
        override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): HttpsRedirect {
            val feature = HttpsRedirect(Configuration().apply(configure))
            pipeline.intercept(ApplicationCallPipeline.Features) {
                if (call.request.origin.scheme == "http") {
                    val redirectUrl = call.url { protocol = URLProtocol.HTTPS; port = feature.redirectPort }
                    call.respondRedirect(redirectUrl, feature.permanent)
                    finish()
                }
            }
            return feature
        }
    }
}
```

ApplicationFeature.installメソッド内で何をしているのかを見ると、

- 1行目: ユーザがApplication.installメソッド呼び出し時に書いたconfigureを適用する

```kotlin
val feature = HttpsRedirect(Configuration().apply(configure))
```

- 2行目: `pipeline.intercept`でPipelineの途中に処理を挟む。第一引数にPipelinePhase指定しどの時点での処理かを定義。第二引数に実際に挟む処理内容を記述する。

```kotlin
pipeline.intercept(ApplicationCallPipeline.Features) {
  //...
}
```

**<話の重要な部分まとめ>**

- **`Pipeline.intercept`メソッドを呼び、実際にPipelineに処理を登録するのは、`ApplicationFeature.install`内。**
- **ユーザが`Application.install`でFeatureをinstallする時に書いた設定コードは`ApplicationFeature.install`内で実行される**