🦜

Koinで始めるKtor DI

2023/08/18に公開

TL;DR

  • サーバーサイド Kotlin フレームワーク・Ktor は軽量フレームワークの触れ込みどおり、最低限の機能を提供している
  • そのため標準で DI の機能が提供されていないため、Koinという DI ライブラリを別途導入する
  • Koinの基本的な使い方について紹介する

メンバー募集中!

サーバーサイド Kotlin コミュニティを作りました!

Kotlin ユーザーはぜひご参加ください!!

https://serverside-kt.connpass.com/

環境

  • IntelliJ IDEA
  • macOS Monterey

Ktor Project の作成

https://start.ktor.io/#/settings?name=koin-practice&website=example.com&artifact=com.example.koin-practice&kotlinVersion=1.9.0&ktorVersion=2.3.3&buildSystem=GRADLE_KTS&engine=TOMCAT&configurationIn=CODE&addSampleCode=true&plugins=

以下の設定で雛形のプロジェクトを作成しました。

プラグインについては、今回は DI を試したいだけなので Routing だけ入れます。

作成したら Zip をダウンロードして展開し、IntelliJ で開きます。

Hello World!の確認

ダウンロードしたプロジェクトを IntelliJ で開くとビルドが始まるのでしばし待ちます。

ビルドが終わったら、Application.kt のエントリポイントを実行。

サーバーが立ち上がったらhttp://0.0.0.0:8080にアクセスしてレスポンスを確認します。

以下のようなレスポンスが表示されれば OK です。

Koin のインストール

バージョンは IntelliJ が最新を教えてくれるので、適宜書き換えてください。

build.gradle.kts
dependencies {
    implementation("io.ktor:ktor-server-core-jvm")
    implementation("io.ktor:ktor-server-tomcat-jvm")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    implementation("io.ktor:ktor-server-config-yaml:2.3.3")
    implementation("io.insert-koin:koin-core:3.3.3") //これを追加
    implementation("io.insert-koin:koin-ktor:3.4.3") //これを追加
    testImplementation("io.ktor:ktor-server-tests-jvm")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

最終的なプロジェクトの構成

以下のようになります。

ここから Koin の使い方について説明していきます。

DI 対象とする Controller,Service の準備

HelloController
package example.koin.controller

import example.koin.service.HelloService
import example.koin.service.WorldService

class HelloController(
    private val helloService: HelloService,
    private val worldService: WorldService
) {
    fun printsHello() {
        helloService.printsHello()
        worldService.printsWorld()
    }
}

このHelloControllerではコンストラクタに二つのサービスが渡ってくる前提です。

HelloService.kt
package example.koin.service

class HelloService {
    fun printsHello() {
        println("Hello")
    }
}
WorldService.kt
package example.koin.service

class WorldService {
    fun printsWorld() {
        println("World")
    }
}

今回は簡易的に、それぞれHelloWorldを標準出力サービスを提供します。

Module ファイルに依存関係を登録する

以下のようにして、依存関係を列挙するだけで OK です。

余談ですがsingleOf()ではなくsingle{...}と記述した場合、シングルトンでインスタンスが作成されます。

Module.kt
package example.koin

import example.koin.controller.HelloController
import example.koin.service.HelloService
import example.koin.service.WorldService
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module

object Module {
    val koinPracticeModules = module {
        //Service
        singleOf(::HelloService)
        singleOf(::WorldService)

        //Controller
        singleOf(::HelloController)
    }
}

アプリケーション起動時に Koin の設定を読み込む

Module ファイルに依存関係を登録しただけでは、依存性注入は行われません。

アプリケーションの起動時に、Module ファイルに登録した変数(この例だとkoinPracticeModules)を読み込ませる必要があります。

ここでは settingKoin を定義し、Application.module()の中で実行します。

SettingKoin.kt
package example.koin

import io.ktor.server.application.*
import org.koin.ktor.plugin.Koin

fun Application.settingKoin() {
    install(Koin){
        modules(Module.koinPracticeModules)
    }
}
Application.kt
package example.koin

import io.ktor.server.application.*

fun main(args: Array<String>) {
    io.ktor.server.tomcat.EngineMain.main(args)
}

fun Application.module() {
    settingKoin()
    configureRouting()
}

これで Koin による依存性の注入が可能になりました。

Koin による依存性注入を利用する

利用方法は、val helloController by inject<HelloController>()と書くだけです。

Koin がない場合のコードもコメントに記載しています。

Routing.kt
package example.koin

import example.koin.controller.HelloController
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.inject

fun Application.configureRouting() {
    // Koinがないとこうなる。コントローラー1個ならいいかもだが、増えていくと…
    //    val helloController = HelloController(
    //        HelloService(),
    //        WorldService()
    //    )
    val helloController by inject<HelloController>()

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

動作確認

最後に一応実行できることを確認します。

以下のようにコンソール上にHelloWorldが出力されていれば OK です。

singleOf の中身を見てみる

そもそもsingleOf(::HelloService)::HelloServiceの意味がわからなかったので println してみた。

fun `<init>`(): example.koin.service.HelloService

…けどよくわからんので singleOf をみると、

SingleOf.kt
inline fun <reified R> Module.singleOf(
    crossinline constructor: () -> R,
    noinline options: DefinitionOptions<R>? = null,
): KoinDefinition<R> = single { new(constructor) }.onOptions(options)

ということで、fun<init>(): example.koin.service.HelloServiceというのはとあるクラスのプライマリコンストラクタですよという意味みたい。

それを渡して new してるわけですね。

ただ根本的に依存関係をどう解釈しているのかもソース読んで理解したいかもしれない。

OptionDSL.kt
@OptionDslMarker
inline infix fun <T> KoinDefinition<T>.withOptions(
    options: DefinitionOptions<T>
): KoinDefinition<T> {
    val def = factory.beanDefinition
    val primary = def.qualifier
    def.also(options)
    if (def.qualifier != primary) {
        module.indexPrimaryType(factory)
    }
    if (def.secondaryTypes.isNotEmpty()) {
        module.indexSecondaryTypes(factory)
    }
    if (def._createdAtStart && factory is SingleInstanceFactory<*>) {
        module.prepareForCreationAtStart(factory)
    }
    return this
}

おわりに

メンバー募集中!

サーバーサイド Kotlin コミュニティを作りました!

Kotlin ユーザーはぜひご参加ください!!

https://serverside-kt.connpass.com/

また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。

よろしければ Conpass からメンバー登録よろしくお願いいたします。

https://blessingsoftware.connpass.com/

Discussion