1. 简介

依赖注入(Dependency Injection)是一种软件设计模式,核心思想是将对象的创建与其使用者解耦。 这样做的好处是让主业务代码更干净、职责更清晰,从而提升可维护性和可测试性。

本文将介绍 Injekt 框架——一个为 Kotlin 设计的轻量级依赖注入工具。

⚠️ 注意:Injekt 库目前已停止积极维护,官方推荐转向使用 Kodein 尽管如此,理解其设计思路对掌握 DI 原理仍有价值。


2. 什么是依赖注入?

依赖注入是现代应用开发中广泛采用的设计模式,用于解耦组件之间的创建与使用关系。

✅ 核心优势包括:

  • 解耦构造逻辑:对象不再自己 new 依赖,而是由外部传入。
  • 便于单元测试:可以轻松替换真实依赖为 Mock 对象,实现精准控制。
  • 灵活替换实现:比如将 JPA DAO 替换为 MongoDB 实现,只要接口一致,上层代码无需改动。

在 Java 生态中,Spring 是最知名的 DI 框架。但 Spring 功能庞大,有时显得“杀鸡用牛刀”。而像 Injekt 这类轻量方案,仅聚焦于“对象构造与使用的分离”,更适合小型项目或库的集成。


3. Maven 依赖

Injekt 已发布到 Maven Central,可直接引入项目:

<dependency>
    <groupId>uy.kohesive.injekt</groupId>
    <artifactId>injekt-core</artifactId>
    <version>1.16.1</version>
</dependency>

为了简化代码,建议使用通配符导入:

import uy.kohesive.injekt.*
import uy.kohesive.injekt.api.*

这样可以直接使用 Injekt.get()addSingleton 等便捷 API。


4. 基础组件装配

引入 Injekt 后,即可开始配置对象间的依赖关系。

4.1. 启动类定义

最简单的做法是继承 InjektMain,它提供了一个标准入口:

class SimpleApplication {
    companion object : InjektMain() {
        @JvmStatic fun main(args: Array<String>) {
            SimpleApplication().run()
        }

        override fun InjektRegistrar.registerInjectables() {
            addSingleton(Server())
        }
    }

    fun run() {
        val server = Injekt.get<Server>()
        server.start()
    }
}
  • registerInjectables() 中注册所有可注入对象。
  • run() 是实际业务入口,通过 Injekt.get<T>() 获取实例。

4.2. 单例对象注册

使用 addSingleton() 可注册单例对象。该对象会在容器初始化时创建并缓存。

⚠️ 踩坑提示:若单例构造依赖其他 Bean,此时容器尚未构建完成,可能导致 NPE。

解决方案:改用工厂方式延迟初始化:

class Server(private val config: Config) {
    private val LOG = LoggerFactory.getLogger(Server::class.java)
    fun start() {
        LOG.info("Starting server on ${config.port}")
    }
}
override fun InjektRegistrar.registerInjectables() {
    addSingleton(Config(port = 12345))
    addSingletonFactory { Server(Injekt.get()) }
}

addSingletonFactory 特点:

  • 实例按需创建(首次获取时)
  • 构造过程中可通过 Injekt.get() 获取其他 Bean
  • 类型自动推断,无需显式声明

4.3. 工厂对象(每次新建实例)

某些场景需要每次注入都返回新实例,例如网络客户端:

class Client(private val config: Config) {
    private val LOG = LoggerFactory.getLogger(Client::class.java)
    fun start() {
        LOG.info("Opening connection to on ${config.host}:${config.port}")
    }
}

使用 addFactory 实现:

override fun InjektRegistrar.registerInjectables() {
    addSingleton(Config(host = "example.com", port = 12345))
    addFactory { Client(Injekt.get()) }
}

✅ 效果:

  • 每次 Injekt.get<Client>() 都返回全新实例
  • 所有实例共享同一个 Config 单例

5. 对象获取方式

除了构造器注入,Injekt 支持多种获取方式,适应不同场景。

5.1. 直接从容器获取

可在任意位置调用 Injekt.get() 获取实例,尤其适合运行时动态创建对象:

class Notifier {
    fun sendMessage(msg: String) {
        val client: Client = Injekt.get()
        client.use {
            client.send(msg)
        }
    }
}

✅ 适用场景:

  • 工厂方法内部
  • Top-level 函数中
  • 需要按需创建实例的逻辑

5.2. 作为默认参数值

Kotlin 允许参数设置默认值,结合 Injekt 可实现“自动注入 or 手动覆盖”:

class Client(private val config: Config = Injekt.get()) {
    // ...
}

✅ 优势:

  • 主流程无需传参,自动从容器取
  • 单元测试时可传入 Mock 配置,便于隔离测试

该技巧适用于构造函数和普通方法参数。


5.3. 使用委托属性

Injekt 提供了两个内置委托,简化字段注入:

class Notifier {
    private val client: Client by injectLazy()
}
委托类型 行为说明
injectValue() 构造时立即解析并注入
injectLazy() 第一次访问时才解析,支持懒加载

✅ 推荐使用 injectLazy(),避免启动阶段不必要的初始化开销。


6. 高级对象构造策略

Injekt 还提供一些进阶特性,解决复杂场景下的对象生命周期管理。

6.1. 线程级单例(Per-Thread 实例)

直接使用 addFactory 虽然安全但性能差;全局单例又可能引发线程安全问题。

解决方案:addPerThreadFactory,为每个线程创建独立实例并缓存:

override fun InjektRegistrar.registerInjectables() {
    addPerThreadFactory { Client(Injekt.get()) }
}

✅ 效果:

  • 同一线程内多次获取返回同一实例
  • 不同线程间实例隔离
  • 避免竞争同时减少对象创建频率

⚠️ 注意:适用于线程数固定的环境(如线程池),否则可能内存泄漏。


6.2. 带键值的对象工厂(Keyed Objects)

有时需要根据上下文获取不同配置的同类实例,例如多个 OAuth 提供商:

override fun InjektRegistrar.registerInjectables() {
    addPerKeyFactory { provider: String ->
        OAuthClientDetails(
            clientId = System.getProperty("oauth.provider.${provider}.clientId"),
            clientSecret = System.getProperty("oauth.provider.${provider}.clientSecret")
        )
    }
}

✅ 使用方式:

val googleClient = Injekt.get<OAuthClientDetails>("google")
val twitterClient = Injekt.get<OAuthClientDetails>("twitter")
  • 相同 key 返回相同实例
  • 工厂函数可基于 key 定制行为
  • 适合多租户、多服务商等场景

7. 模块化应用构建

随着项目变大,将所有配置堆在一处会难以维护。

Injekt 支持模块化组织配置,提升可读性和复用性。

模块定义

模块是继承 InjektModule 的 Kotlin object:

object TwitterBotModule : InjektModule {
    override fun InjektRegistrar.registerInjectables() {
        addSingletonFactory { TwitterConfig(clientId = "someClientId", clientSecret = "someClientSecret") }
        addSingletonFactory { TwitterBot(Injekt.get()) }
    }
}

InjektMain 本质上就是 InjektModule 的子类。

模块导入

通过 importModule() 引入其他模块:

override fun InjektRegistrar.registerInjectables() {
    importModule(TwitterBotModule)
}

✅ 模块化优势:

  • 第三方库可自带 DI 配置
  • 团队分工明确,各负责一块
  • 易于启用/禁用功能模块

8. 总结

本文介绍了 Injekt 在 Kotlin 中实现依赖注入的基本用法和高级技巧,包括:

  • 单例、工厂、线程级实例管理
  • 多种依赖获取方式(构造注入、默认参数、委托)
  • 键值化对象与模块化配置

尽管 Injekt 已不再活跃维护,但其设计理念简洁直观,适合作为理解 DI 原理的学习材料。

📌 生产项目建议迁移到 Kodein 或 Koin 等活跃框架。

完整示例代码见 GitHub:https://github.com/Baeldung/kotlin-tutorials/tree/master/kotlin-libraries-2


原始标题:Dependency Injection for Kotlin with Injekt