araki tech

for developers including me

KotlinのWebフレームワークKtorを触ってみる

KotlinのWebフレームワークKtorを触ってみる

Ktor – Kotlin用非同期Webフレームワーク

KotlinのWebフレームワークKtorを触ってみる

KtorはJetBrainsが開発している、Kotlin用のWebフレームワークです。

JetBrainsといえば、Kotlinの開発をしている会社であり、IntelliJ IDEAなどの優れたIDEを開発している会社でもあります。

Kotlin開発をしている会社が公式で提供しているWebフレームワークなので、とりあえず触ってみようというのがこの記事の目的です。

筆者の開発環境は IntelliJ IDEA 2021.3.1 (Ultimate Edition) です。

また、下記が関連リンクで、主に本記事で参考にしているページです。

  1. Ktor: Build Asynchronous Servers and Clients in Kotlin | Ktor Framework
    (公式ページ)
  2. Ktor – Kotlin用非同期Webフレームワーク
    (日本語にドキュメントを翻訳してくれているページ。Githubの更新を見る限りちょっと古いかもしれない。)

ちなみに、発音は公式が既に回答を出しており、カタカナなら「ケイター」と発音するのが良いでしょう。

英語的には「Kay – tor (keɪ tɚ)」。

クイックスタート

プロジェクトを作る

IntelliJ IDEAを使っているのであば、特別に何かする必要もなく、Ktorのプロジェクトを作成してくれる項目が存在します。

KotlinのWebフレームワークKtorを触ってみる

見ての通り、執筆当時の私の環境では、2.0.3 が最新なようです。

クイックスタートなのでプロジェクト名は「ktor-sample」のままにしておきますが、Websiteの項目は「araki.tech」にしました。

Gradleもせっかくなので、GroovyではなくKotlinで。

Nextを押すと下記のように、関連するプラグインを追加するか否かを問われます。

KotlinのWebフレームワークKtorを触ってみる

今は Routing のみ追加しておきます。

Finish でプロジェクトが出来上がり下記のようなディレクトリ構成で、サンプルコード付きで生成されます。

KotlinのWebフレームワークKtorを触ってみる

実行してみると、http://0.0.0.0:8080/ で「Hello World!」と表示されました。

コードを見てみる

生成されたコードには、下記の Application.kt と Routing.kt があります。

Application.kt

package tech.araki

import io.ktor.server.engine.*
import io.ktor.server.netty.*
import tech.araki.plugins.*

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configureRouting()
    }.start(wait = true)
}

ここでは、下記のことが行われています。

  • embeddedServer()
    • サーバを立ち上げるために必要な構成を引数で指定し、実際にサーバを立ち上げる。第一引数は、サーバのエンジンで今回は Netty と呼ばれるものを使う。
  • configureRouting()
    • これはこのあとの Routing.kt で定義されている、Application クラスの拡張関数。

Routing.kt

package tech.araki.plugins

import io.ktor.server.routing.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.request.*

fun Application.configureRouting() {

    routing {
        get("/") {
            call.respondText("Hello World!")
        }
    }
}

plugins パッケージ下に配置されたこのファイルでは下記のことをしています。

  • routing ブロック
    • Routingプラグインを実行またはインストールして、ブロック内で書かれた設定を反映させる。
    • get() は GETリクエストの意。

単体テスト

自動生成されたファイルの中に、ApplicationTest.kt もあるので、見てみます。

package tech.araki

import io.ktor.server.routing.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.request.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlin.test.*
import io.ktor.server.testing.*
import tech.araki.plugins.*

class ApplicationTest {
    @Test
    fun testRoot() = testApplication {
        application {
            configureRouting()
        }
        client.get("/").apply {
            assertEquals(HttpStatusCode.OK, status)
            assertEquals("Hello World!", bodyAsText())
        }
    }
}

見てみるとアプリケーションのテストをJUnitでテストできるようです。

Androidで同じようなことをするとInstrumentation Testと言って、実際にその動作環境が必要になるのですが、この場合はうまくアプリケーション自体をモックして単体テストとして実行できます。

Webアプリケーションってこういうものなのでしょうか?なかなか便利ですね。

ルーティングの追加

試しに、ルーティングの追加をしてみます。

fun Application.configureRouting() {

    routing {
        get("/") {
            call.respondText("Hello World!")
        }
        get("/ktor") {
            call.respondText("<h1>Hello Ktor!</h1>", ContentType.Text.Html)
        }
    }
}

これで問題なく、http://0.0.0.0:8080/ktor にアクセスすると、大きく「Hello Ktor!」と出てくることが確認できました。

ContentTypeの指定も簡単ですね。

ちょっとだけ中身を見てみる

Ktorを触ってみると、かなり簡潔に記述することができるので、どのように複雑な処理を隠蔽しているのかが気になるところです。

embeddedServer

public fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration>
embeddedServer(
    factory: ApplicationEngineFactory<TEngine, TConfiguration>,
    port: Int = 80,
    host: String = "0.0.0.0",
    watchPaths: List<String> = listOf(WORKING_DIRECTORY_PATH),
    configure: TConfiguration.() -> Unit = {},
    module: Application.() -> Unit
): TEngine = GlobalScope.embeddedServer(factory, port, host, watchPaths, EmptyCoroutineContext, configure, module)

中では、GlobalScopeを立ち上げて、別に定義されたembeddedServerに処理を渡していることがわかります。

GlobalScopeはよく「使うな」とされておりDelicateCroutinesApiでもあります。

が、今回のように閉じたCoroutineScopeではなく、アプリケーションの寿命に合致するものであればGlobalScopeは使ってもOKです。

GlobalScopeは無闇に使ってはいけないだけで、「絶対使うな」というわけでは無い、ということをJetBrainsが教えてくれています

さて、ちょっと話がずれましたが、このあと何度か引数が異なるembeddedServer()を経由します。

最終的には、下記のように第一引数で受け取ったfactoryに移譲しています。

public fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> embeddedServer(
    factory: ApplicationEngineFactory<TEngine, TConfiguration>,
    environment: ApplicationEngineEnvironment,
    configure: TConfiguration.() -> Unit = {}
): TEngine {
    return factory.create(environment, configure)
}

embeddedServer()の一連の役割は、「サーバを立ち上げるために必要な情報 (ポート、ホスト名、使うモジュール等) をまとめてfactoryに渡す」ということですね。

Netty

さて、先ほどのfactoryとして渡されていたのがNettyというオブジェクトです。

/**
 * An [ApplicationEngineFactory] providing a Netty-based [ApplicationEngine]
 */
public object Netty : ApplicationEngineFactory<NettyApplicationEngine, NettyApplicationEngine.Configuration> {
    override fun create(
        environment: ApplicationEngineEnvironment,
        configure: NettyApplicationEngine.Configuration.() -> Unit
    ): NettyApplicationEngine {
        return NettyApplicationEngine(environment, configure)
    }
}

NettyApplicationEngineFactoryというインターフェースを実装していて、先ほど呼ばれていたcreate()ApplicationEngineを返しています (Nettyの場合はNettyApplicationEngine)。

そして、そのNettyApplicationEngineに対してstart()をしているということですね。

fun main() {
    embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
        configureRouting()
    }.start(wait = true)
}

ちょっとこれ以上追うのは難しそうですが、公式のKotlinの書き方をみると学ぶことがたくさんありますね。

特に、DSL (ドメイン固有言語) の作り方は勉強になります。

いかに開発者に複雑な部分を隠蔽しつつ、柔軟性に富んだ実装を可能とさせるかは難しいところです。

終わりに

今回はKotlinの開発会社が作る KotinのWebフレームワーク Ktor を触ってみました。

本当はもっとアプリを作りたいのですが、それをこの記事でやると長くなるので別記事にします…。

あまりKtorの良さを書くことはできなかったですが、KotlinライクにWebアプリを書けることはちょっと伝わったかなと思います。

そしてこういったフレームワークの中身を見てみると面白いですね。

とても勉強になるので、みなさんもよく使っているフレームワークの中身を除いてみてください〜 (標準ライブラリでも面白いですよ)。