如何在ktor应用程序中注入依赖项

14
文档介绍了依赖注入,但没有真正展示如何实现。
文档还未完成,有许多占位符:http://ktor.io/getting-started.html 我尝试创建主函数使其接受参数(即我的依赖项),但在测试调用withTestApplication时失败了。 我查看了应用程序代码,并发现Application接受一个配置对象,但我不知道如何更改该配置对象以注入其中的某些依赖项。
package org.jetbrains.ktor.application

/**
 * Represents configured and running web application, capable of handling requests
 */
class Application(val environment: ApplicationEnvironment) : ApplicationCallPipeline() {
    /**
     * Called by host when [Application] is terminated
     */
    fun dispose() {
        uninstallAllFeatures()
    }
}

/**
 * Convenience property to access log from application
 */
val Application.log get() = environment.log

在使用withTestApplication的测试代码中,我有以下类似的内容:

@Test
internal fun myTest() = withTestApplication (Application::myMain)

如果我使用参数调用myMain(这些参数需要进行模拟和注入),那么上述的withTestApplication将会失败。

更新:

问题在于在我的请求处理中,我正在使用一个连接到其他外部网络服务并执行一些请求的依赖类。我需要一种方法来注入它,以便在测试中可以stub/mock并根据我的测试用例更改其行为。

4个回答

18

使用Koin进行简单示例。

1)首先,定义我们的生产和测试依赖项:

val prodModule = module {
    single<IFirstService> { RealFirstService() }
    single<ISecondService> { RealSecondService() }
}

val testModule = module {
    single<IFirstService> { FakeFirstService() }
    single<ISecondService> { FakeSecondService() }
}

2) 然后在应用程序启动之前添加 DI 初始化:

fun main(args: Array<String>) {
    startKoin(listOf(prodModule))
    embeddedServer(Netty, commandLineEnvironment(args)).start(true)
}

3) 在应用程序或路由中使用 inject:

fun Application.apiModule() {
    val firstService: IFirstService by inject()
    val secondService: ISecondService by inject()
    ...
    routing {
        someApi(inject(), inject())
    }
}

4) (可选) 对于测试,只需在运行测试之前在testModule中添加初始化即可:

fun testApp(test: TestApplicationEngine.() -> Unit) {
    withTestApplication({
        ... // configure your test app here

        stopKoin() // Need to stop koin and restart after other tests
        startKoin(listOf(testModule)) // Init with test DI

        apiModule() // Run you application
    })
}

// And run tests
@Test
fun `get events`() = testApp {
    // do tests
}

就这些了!


15

Ktor没有内置的依赖注入机制。如果您需要使用DI,您需要使用任何您喜欢的框架,例如Guice。它看起来会像这样:

fun Application.module() {
  Guice.createInjector(MainModule(this))
}

// Main module, binds application and routes
class MainModule(private val application: Application) : AbstractModule() {
    override fun configure() {
        bind(Application::class.java).toInstance(application)
        ... other bindings ...
    }
}

这样,您可以将应用程序组合委托给Guice,并像任何其他应用程序一样构建它。例如,您可以像这样组合应用程序的不同部分:

class Hello @Inject constructor(application: Application) {
  init {
    application.routing {
        get("/") {
            call.respondText("Hello")
        }
    }
  }
}

然后在主模块中绑定它:

bind(Hello::class.java).asEagerSingleton()

asEagerSingleton是必要的,这样Guice就会急切地创建它,因为没有其他服务会查询它。


1
谢谢您的回复。在我的情况下,我有一个与其他服务交互的类依赖关系,我该如何注入它(这样当我编写测试时,我只需存根/模拟该依赖项并传递一个模拟实例而不是真实实例)? - Mehdi Karamosly

3
在尝试了一下Koin、Kodein和Daggers之后,我们最终选择了与Ktor配合使用的spring-context。效果非常好。 步骤1:在我们的Gradle文件中:
implementation(group = "org.springframework", name = "spring-context", version = "5.3.5")

将其更改为您喜欢的任何spring-context版本。

步骤2:我们定义了我们的@Component@Configuration@Bean等,就像我们使用任何spring应用程序一样。

步骤3:在我们的main方法中,我们有一些粘合代码来显式初始化DI上下文并将其用于初始化Ktor:

    val ctx = AnnotationConfigApplicationContext("YOUR.ROOT.PACKAGE.GOES.HERE")
    val someBean = ctx.getBean(SomeBean::class.java) // you can get any bean you need to use in your top-level glue code

    // ... go ahead with KTOR configuration as usual. You can access any bean using the ctx variable.

当然,这种我们显式与Spring上下文交互的粘合代码仅需要执行一次。项目的其余部分由引用彼此并使用常规Spring上下文方式的组件组成。

1

Kodein DI在Ktor方面有很好的支持,更多详情请查看相应文档

文档中的示例代码:

fun main(args: Array<String>) {
    embeddedServer(Netty, port = 8080) {
        di { 
            bind<Random>() with singleton { SecureRandom() } 
        }

        routing {
            get("/") {
                val random by di().instance<Random>() 
                /* logic here */
            }
        }
   }.start(true)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接