KotlinのWebフレームワークKtorを触ってみる
Ktor – Kotlin用非同期Webフレームワーク
KtorはJetBrainsが開発している、Kotlin用のWebフレームワークです。
JetBrainsといえば、Kotlinの開発をしている会社であり、IntelliJ IDEAなどの優れたIDEを開発している会社でもあります。
Kotlin開発をしている会社が公式で提供しているWebフレームワークなので、とりあえず触ってみようというのがこの記事の目的です。
筆者の開発環境は IntelliJ IDEA 2021.3.1 (Ultimate Edition) です。
また、下記が関連リンクで、主に本記事で参考にしているページです。
- Ktor: Build Asynchronous Servers and Clients in Kotlin | Ktor Framework
(公式ページ) - Ktor – Kotlin用非同期Webフレームワーク
(日本語にドキュメントを翻訳してくれているページ。Githubの更新を見る限りちょっと古いかもしれない。)
ちなみに、発音は公式が既に回答を出しており、カタカナなら「ケイター」と発音するのが良いでしょう。
英語的には「Kay – tor (keɪ tɚ)」。
クイックスタート
プロジェクトを作る
IntelliJ IDEAを使っているのであば、特別に何かする必要もなく、Ktorのプロジェクトを作成してくれる項目が存在します。
見ての通り、執筆当時の私の環境では、2.0.3 が最新なようです。
クイックスタートなのでプロジェクト名は「ktor-sample」のままにしておきますが、Websiteの項目は「araki.tech」にしました。
Gradleもせっかくなので、GroovyではなくKotlinで。
Nextを押すと下記のように、関連するプラグインを追加するか否かを問われます。
今は Routing のみ追加しておきます。
Finish でプロジェクトが出来上がり下記のようなディレクトリ構成で、サンプルコード付きで生成されます。
実行してみると、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 で定義されている、
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)
}
}
Netty
はApplicationEngineFactory
というインターフェースを実装していて、先ほど呼ばれていた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アプリを書けることはちょっと伝わったかなと思います。
そしてこういったフレームワークの中身を見てみると面白いですね。
とても勉強になるので、みなさんもよく使っているフレームワークの中身を除いてみてください〜 (標準ライブラリでも面白いですよ)。