[Day 8] 整合 Koin DI 实作 Ktor Plugin

Ktor Plugin & DSL

Ktor 的架构设计是让开发者透过实作 plugin,把 intercepting function 注册到 request pipeline,藉此增加功能或改变行为

我们要先使用 install function 来安装 plugin,然後在 trailing lambda 进行设定。例如 Ktor 的 CallLogging plugin,可以在 request 一进来的时候,就写入 log 记录资料. 我们可以实作 filter function 过滤 request,还有 format function 调整格式。

install(CallLogging) {
    filter { call ->
        call.request.path().startsWith("/api/v1")
    }
    format { call ->
        val status = call.response.status()
        val httpMethod = call.request.httpMethod.value
        val userAgent = call.request.headers["User-Agent"]
        "Status: $status, HTTP method: $httpMethod, User agent: $userAgent"
    }
}

Ktor Plugin & DI

Ktor 的设计原则是使用 DSL & Lambda 的写法撰写具有 declarative 风格的程序码,开发者可以从 main function 开始,依照自己的想法开发所有功能,所以开发者可以选择不使用 DI 开发,Ktor 自然也就没有内建 DI,这与 Spring Framework 要求实作特定介面或继承特定类别,再透过 DI 注入的方式有很大的不同。另一方面,Ktor 也很少定义介面要求我们实作,我目前也只有用到 Credential, Principal interface 而已,而且还是 marker interface。

目前官方对 Ktor 的定位是保持核心精简,所以官方现有的 Plugin 都是 HTTP Server 的相关功能。对於这种 Plugin 来说,开发者在安装设定之後,就几乎不必再对 Plugin 做什麽操作。但如果我们把 Plugin 的解释范围扩大为任何一个功能,或是整合其它函式库或框架,此时就需要在我们安装 Plugin 之後,从 Plugin 取得某个己初始化的物件进行操作。例如我们在安装 Redis Plugin 之後,需要取得 RedisClient 物件进行操作,此时我们必须思考要透过什麽方式拿到 RedisClient 物件,目前我认为使用 DI 取得物件是最低耦合的方式。

Koin DI

我曾使用过的 Spring 本身就是个 DI 为基础的框架,至於 Play Framework 则是使用 Google Guice,Guice 比较轻量,但两者都是 annotation based。对於 Ktor 来说,我想挑选「纯 kotlin」、「轻量」、「DSL风格」的 DI,最後我选择了 Koin,而且 Koin 已经支援 Ktor,有 Koin Plugin 可以直接使用。

启动 Ktor 一进入 main function 时,就要先安装 Koin Plugin,这样後续的 Plugin 才能使用 DI。接下来是设定 Koin Logging,才能知道 Koin 目前状态方便 debug。虽然 Koin 本身已内建 SLF4JLogger 实作,但因为我是使用 KotlinLogging library,所以必须要自己实作 Logger。最後再把设定档 appConfig 物件注入至 Koin,毕竟设定档物件在很多地方都会用到

fun Application.main() {
    val appConfig = ApplicationConfigLoader.load()

    install(Koin) {
        logger(KoinLogger(Level.INFO))

        modules(
            module(createdAtStart = true) {
                single { appConfig }
            },
            koinBaseModule(appConfig)
        )
    }
}
class KoinLogger(level: Level = Level.INFO) : Logger(level) {

    private val logger = KotlinLogging.logger("[Koin]")

    override fun log(level: Level, msg: MESSAGE) {
        when (level) {
            Level.DEBUG -> logger.debug { msg }
            Level.INFO -> logger.info { msg }
            Level.ERROR -> logger.error { msg }
            Level.NONE -> logger.trace { msg }
        }
    }
}

Ktor Plugin X Koin DI

初始化 Koin Plugin 之後,我们以 Redis Plugin 为例,来看如何使用 DI 初始化注入 Redis Client 物件。刚才有提到我们是呼叫 install function 来安装 plugin,所以在 install function 里面,透过 koin 内建的 Application.koin() extension function 来载入 koin module,module 里面使用 single function 将 RedisClient 物件载入至 Koin 里面。

override fun install(pipeline: Application, configure: Configuration.() -> Unit): RedisFeature {
    val configuration = Configuration().apply(configure)
    val feature = RedisFeature(configuration)

    val appConfig = pipeline.get<MyApplicationConfig>()
    config = appConfig.infra.redis ?: configuration.build()

    initClient(config)

    pipeline.koin {
        modules(
            module(createdAtStart = true) {
                single { client }
                KoinApplicationShutdownManager.register { shutdown() }
            }
        )
    }

    return feature
}

後续可以在其它 Plugin 的 install function 透过 pipeline.get<RedisClient>() 取得物件,如果是在 Ktor Routing function 可以使用 val redisClient by inject<RedisClient>() 取得。

在初始化 Plugin 的过程中,除了与 Koin DI 整合,读取 Ktor 外部设定档也是不可或缺的,毕竟许多 Plugin 的设定值是从外部来的,明天会再进一步说明如何读取设定档转换为自己定义的 appConfig 物件。


<<:  Day1 写程序的前置工作!

>>:  {DAY 1}开始吧!探索data世界

Day22 jQuery 基本教学(二)

selector 选取 JQ 的 DOM 存取方式是透过 selector 来达到索引目标,会先转换...

Render Element(Day3)

建立 React 应用程序最小的单位是 element。 —— React 文件 这句话好像可以有...

Day 10 : 案例分享(3.3) 会计模组-调节、立冲帐、应收付与收支款

案例说明及适用场景 当我们有某一个科目,需要管理他是否还有余额未被处理,这个科目就是所谓的 调节科目...

[Day 27] - 『转职工作的Lessons learned』 - Cube.js(III)

今天要说在後端的Cube.js Server连线DB後,对DB的请求需要做什麽样的设置,也就是如何建...

Day 04:金鱼记忆力太短暂,交给外挂记吧!autosuggestions 与 substring-search

我把从第一天到现在每天的 Home 目录都放上 GitHub 了,README.md 里面有说明 ...