KtorとKoinの組み合わでWebAPIを作る
はじめに
KotlinでWeb開発するときに、Springが選ばれることが多いと思うのですが、個人的な思いとしてはKotlin由来のライブラリーやフレームワークをなるべく使いたいという気持ちがあります。
KotlinでそのへんをやるにはKtorとWebフレームワークとKoinというDIコンテナを組み合わせて使うのが1つの大きな選択肢となると思います。KoinはKtorのサポートも行ってそうだったのでプロジェクトを作って簡単なWebアプリを作るまでをやってみようかと思います。
やってみる
環境
$ uname -srvmpio Linux 5.4.0-77-generic #86-Ubuntu SMP Thu Jun 17 02:35:03 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.2 LTS Release: 20.04 Codename: focal $ java --version openjdk 11.0.10 2021-01-19 OpenJDK Runtime Environment 18.9 (build 11.0.10+9) OpenJDK 64-Bit Server VM 18.9 (build 11.0.10+9, mixed mode) $ mvn -v Apache Maven 3.6.3 Maven home: /usr/share/maven Java version: 11.0.10, vendor: Oracle Corporation, runtime: /home/someone/.sdkman/candidates/java/11.0.10-open Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-77-generic", arch: "amd64", family: "unix"
プロジェクトを作成
プロジェクトはGenerate Ktor projectを用いて作成します。
今回はMavenを使ってプロジェクトを作成します。
依存としてはRoutingだけ入れています。
Koinの依存を追加する
IDEか何かで、プロジェクトを開いてKoinの依存を追加します。
Pomに以下の依存を付け加えます。
<dependency> <groupId>io.insert-koin</groupId> <artifactId>koin-ktor</artifactId> <version>3.1.2</version> </dependency>
これで、プロジェクトの準備はできました。
諸々の設定を行なう
まずは、Ktorがapplication.conf
を読み込むように修正します。
Application.kt
を以下のように書き換えます。
fun main(args: Array<String>) { embeddedServer(Netty, commandLineEnvironment(args)).start(wait = true) }
次にハンドラーを1つ追加します。
HelloHandler.kt
を作り以下のルーティングの定期を書きます。
import io.ktor.application.* import io.ktor.response.* import io.ktor.routing.* fun Route.hello() { get("/hello") { call.respond("Hello, Koin") } }
このハンドラーをRouteingとして登録します。
再びAplication.ktに戻り以下のように修正します。
import io.ktor.application.* import io.ktor.routing.* import io.ktor.server.engine.* import io.ktor.server.netty.* fun Application.main() { install(CallLogging) routing { hello() } } fun main(args: Array<String>) { embeddedServer(Netty, commandLineEnvironment(args)).start(wait = true) }
application.conf
を作成してハンドラーの設定をモジュールとして読み込むようにします。
あと、今回は必要ないものもありますが、もそもろの設定もしておきます。
ktor {
deployment {
port = 8081
port = ${?APP_PORT}
}
application {
modules = [
dev.hirooka.ApplicationKt.main,
]
}
environment = "test"
environment = ${?KTOR_ENV}
}
$ mvn compile exec:java $ curl localhost:8081/hello Hello, Koin
KoinでDIする
Koinで依存を定義しDIをやってみます。
まずは、以下のようなサービスクラスとデータクラスを作成します。
data class Name(val value: String = "Moheji") interface HelloService { fun greeting(): String } class HelloServiceImlp(private val name: Name) : HelloService { override fun greeting() = "Hello, ${name.value}" }
関係性としては、HelloService
インターフェースをHelloServiceImpl
が実装してName
データクラスに依存しています。
DIの設定を記述していきます。
Application.kt
を以下のように書き換えます。
import io.ktor.application.* import io.ktor.features.* import io.ktor.routing.* import io.ktor.server.engine.* import io.ktor.server.netty.* import org.koin.dsl.module import org.koin.ktor.ext.Koin fun Application.main() { install(DefaultHeaders) install(CallLogging) routing { hello() } } fun Application.koin() { install(Koin) { modules( module { single { Name() } single { HelloServiceImlp(get()) as HelloService } } ) } } fun main(args: Array<String>) { embeddedServer(Netty, commandLineEnvironment(args)).start(wait = true) }
Application.koin()
でモジュールを1つ追加しKoinの依存の設定を記述しています。Application.main()
モジュールで書いても問題はないのですが、設定を分けて置けるとあとから読みやすかったりするので分けました。
上記ではName
データクラスとHelloSerivceImpl
クラスをそれぞれコンテナに入れています、すでにコンテナに入っているものはget()
で取り出すことが可能で、HelloSerivceImpl
のインスタンスを生成する際のName
インスタンスをインジェクションする際に利用しています。
また、HelloSerivceImpl
をHelloSerivce
でキャストすることで利用時にHelloSerivce
方でのコンテナからの取り出しを行えます。
application.conf
を書き換えモジュールを読み込むように変更します。
ktor {
deployment {
port = 8081
port = ${?APP_PORT}
}
application {
modules = [
dev.hirooka.ApplicationKt.main,
dev.hirooka.ApplicationKt.koin
]
}
environment = "test"
environment = ${?KTOR_ENV}
}
それでは最後にDIコンテナに入れたHelloSerivceImpl
をハンドラーから利用します。
ハンドラーを以下のように書き換えます。
import io.ktor.application.* import io.ktor.response.* import io.ktor.routing.* import org.koin.ktor.ext.inject fun Route.hello() { val helloService by inject<HelloService>() get("/hello") { call.respond(helloService.greeting()) } }
コンテナからサービスを取り出す際にはinject
を利用します。
アプリケーションを起動し直して、アクセスします。
$ mvn compile exec:java $ curl localhost:8081/hello -v Hello, Moheji
一通りの使い方はこんな感じですね。