Koinで始めるKtor DI
TL;DR
- サーバーサイド Kotlin フレームワーク・Ktor は
軽量フレームワーク
の触れ込みどおり、最低限の機能を提供している - そのため標準で DI の機能が提供されていないため、
Koin
という DI ライブラリを別途導入する -
Koin
の基本的な使い方について紹介する
メンバー募集中!
サーバーサイド Kotlin コミュニティを作りました!
Kotlin ユーザーはぜひご参加ください!!
環境
- IntelliJ IDEA
- macOS Monterey
Ktor Project の作成
以下の設定で雛形のプロジェクトを作成しました。
プラグインについては、今回は DI を試したいだけなので Routing だけ入れます。
作成したら Zip をダウンロードして展開し、IntelliJ で開きます。
Hello World!の確認
ダウンロードしたプロジェクトを IntelliJ で開くとビルドが始まるのでしばし待ちます。
ビルドが終わったら、Application.kt のエントリポイントを実行。
サーバーが立ち上がったらhttp://0.0.0.0:8080
にアクセスしてレスポンスを確認します。
以下のようなレスポンスが表示されれば OK です。
Koin のインストール
バージョンは IntelliJ が最新を教えてくれるので、適宜書き換えてください。
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 の準備
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
ではコンストラクタに二つのサービスが渡ってくる前提です。
package example.koin.service
class HelloService {
fun printsHello() {
println("Hello")
}
}
package example.koin.service
class WorldService {
fun printsWorld() {
println("World")
}
}
今回は簡易的に、それぞれHello
とWorld
を標準出力サービスを提供します。
Module ファイルに依存関係を登録する
以下のようにして、依存関係を列挙するだけで OK です。
余談ですがsingleOf()
ではなくsingle{...}
と記述した場合、シングルトンでインスタンスが作成されます。
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()
の中で実行します。
package example.koin
import io.ktor.server.application.*
import org.koin.ktor.plugin.Koin
fun Application.settingKoin() {
install(Koin){
modules(Module.koinPracticeModules)
}
}
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 がない場合のコードもコメントに記載しています。
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!")
}
}
}
動作確認
最後に一応実行できることを確認します。
以下のようにコンソール上にHello
とWorld
が出力されていれば OK です。
singleOf の中身を見てみる
そもそもsingleOf(::HelloService)
の::HelloService
の意味がわからなかったので println してみた。
fun `<init>`(): example.koin.service.HelloService
…けどよくわからんので singleOf をみると、
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 してるわけですね。
ただ根本的に依存関係をどう解釈しているのかもソース読んで理解したいかもしれない。
@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 ユーザーはぜひご参加ください!!
また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。
よろしければ Conpass からメンバー登録よろしくお願いいたします。
Discussion