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を用いて作成します。

f:id:yuya_hirooka:20210718001631p:plain

f:id:yuya_hirooka:20210718001737p:plain

今回は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インスタンスをインジェクションする際に利用しています。
また、HelloSerivceImplHelloSerivce でキャストすることで利用時に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

一通りの使い方はこんな感じですね。