1. 概述

本文将介绍 Kodein —— 一个专为 Kotlin 设计的纯函数式依赖注入(DI)框架,并与其他主流 DI 框架进行对比。对于已经熟悉 Spring 或 Dagger 的开发者来说,Kodein 提供了一种更轻量、更符合 Kotlin 风格的替代方案,尤其适合 Android 和小型服务场景。

2. 依赖配置

在 Maven 项目中引入 Kodein 非常简单,只需添加以下依赖:

<dependency>
    <groupId>com.github.salomonbrys.kodein</groupId>
    <artifactId>kodein</artifactId>
    <version>4.1.0</version>
</dependency>

📌 建议始终查看 Maven Central 获取最新版本,避免踩坑旧版中的已知问题。

3. 示例模型

为了演示 Kodein 的配置方式,我们使用如下类结构:

class Controller(private val service : Service)

class Service(private val dao: Dao, private val tag: String)

interface Dao

class JdbcDao : Dao

class MongoDao : Dao

这些类将用于后续各种绑定方式的示例。

4. 绑定类型

Kodein 支持多种对象生命周期管理策略,即“绑定类型”。合理选择能有效控制内存和性能。

4.1. Singleton(单例)

  • 首次访问时懒加载
  • ✅ 后续获取返回同一实例
var created = false;
val kodein = Kodein {
    bind<Dao>() with singleton {
        created = true
        MongoDao()
    }
}

assertThat(created).isFalse() // 此时尚未创建

val dao1: Dao = kodein.instance()
assertThat(created).isTrue()   // 第一次获取触发初始化

val dao2: Dao = kodein.instance()
assertThat(dao1).isSameAs(dao2) // 是同一个实例

⚠️ 注意:instance() 是基于类型静态解析的,不支持运行时多态注入。

4.2. Eager Singleton(预加载单例)

singleton 类似,但容器构建时立即初始化,适用于必须提前加载的组件(如数据库连接池)。

var created = false;
val kodein = Kodein {
    bind<Dao>() with eagerSingleton {
        created = true
        MongoDao()
    }
}

assertThat(created).isTrue() // 容器创建后立刻执行

✅ 适合全局配置、缓存管理器等需要启动即就绪的 Bean。

4.3. Factory(工厂模式)

每次调用都返回新实例,且支持传参。常用于需要动态参数构造的服务。

val kodein = Kodein {
    bind<Dao>() with singleton { MongoDao() }
    bind<Service>() with factory { tag: String -> Service(instance(), tag) }
}

val service1: Service = kodein.with("myTag").instance()
val service2: Service = kodein.with("myTag").instance()

assertThat(service1).isNotSameAs(service2) // 不同实例

with(arg) 用于传递工厂参数,非常灵活。

4.4. Multiton(多例缓存)

类似 Factory,但对相同参数缓存结果,避免重复创建。

val kodein = Kodein {
    bind<Dao>() with singleton { MongoDao() }
    bind<Service>() with multiton { tag: String -> Service(instance(), tag) }
}

val service1: Service = kodein.with("myTag").instance()
val service2: Service = kodein.with("myTag").instance()

assertThat(service1).isSameAs(service2) // 相同参数 → 相同实例

✅ 适合按用户、租户等维度隔离的服务实例。

4.5. Provider(无参工厂)

相当于无参数的 factory,每次调用都新建实例。

val kodein = Kodein {
    bind<Dao>() with provider { MongoDao() }
}
val dao1: Dao = kodein.instance()
val dao2: Dao = kodein.instance()

assertThat(dao1).isNotSameAs(dao2)

✅ 适合轻量级、无状态对象。

4.6. Instance(实例注册)

直接注册已有对象,适用于第三方库或手动构建的 Bean。

val dao = MongoDao()
val kodein = Kodein {
    bind<Dao>() with instance(dao)
}
val fromContainer: Dao = kodein.instance()

assertThat(dao).isSameAs(fromContainer)

✅ 常用于集成外部 SDK 或测试桩。

4.7. Tagging(标签绑定)

允许同一类型注册多个实例,通过标签区分。

val kodein = Kodein {
    bind<Dao>("dao1") with singleton { MongoDao() }
    bind<Dao>("dao2") with singleton { JdbcDao() }
}
val dao1: Dao = kodein.instance("dao1")
val dao2: Dao = kodein.instance("dao2")

assertThat(dao1).isNotSameAs(dao2)

✅ 标签是字符串,建议统一定义常量避免拼写错误。

4.8. Constant(常量绑定)

专为配置项设计的语法糖,本质是带标签的简单类型绑定。

val kodein = Kodein {
    constant("magic") with 42
}
val fromContainer: Int = kodein.instance("magic")

assertThat(fromContainer).isEqualTo(42)

✅ 推荐用于注入超时时间、重试次数等配置值。

5. 绑定分离

大型项目应避免所有配置写在一起,Kodein 提供了模块化组织能力。

5.1. Modules(模块)

将相关组件分组,便于复用和维护。

val jdbcModule = Kodein.Module {
    bind<Dao>() with singleton { JdbcDao() }
}
val kodein = Kodein {
    import(jdbcModule)
    bind<Controller>() with singleton { Controller(instance()) }
    bind<Service>() with singleton { Service(instance(), "myService") }
}

val dao: Dao = kodein.instance()
assertThat(dao).isInstanceOf(JdbcDao::class.java)

⚠️ 注意:每个 Kodein 实例导入模块都会重新执行绑定逻辑,不会共享状态。

5.2. Composition(组合)

通过 extend 复用已有容器,实现继承式配置。

val persistenceContainer = Kodein {
    bind<Dao>() with singleton { MongoDao() }
}
val serviceContainer = Kodein {
    extend(persistenceContainer)
    bind<Service>() with singleton { Service(instance(), "myService") }
}

val fromPersistence: Dao = persistenceContainer.instance()
val fromService: Dao = serviceContainer.instance()

assertThat(fromPersistence).isSameAs(fromService)

✅ 适合多环境配置(如 dev → test 扩展)。

5.3. Overriding(覆盖绑定)

允许替换已有绑定,非常适合测试替换成 Mock 对象

class InMemoryDao : Dao

val commonModule = Kodein.Module {
    bind<Dao>() with singleton { MongoDao() }
    bind<Service>() with singleton { Service(instance(), "myService") }
}
val testContainer = Kodein {
    import(commonModule)
    bind<Dao>(overrides = true) with singleton { InMemoryDao() }
}
val dao: Dao = testContainer.instance()

assertThat(dao).isInstanceOf(InMemoryDao::class.java)

✅ 测试利器,无需修改主配置。

6. 多绑定(Multi-Bindings)

将多个实现注入到一个集合中,适用于插件式架构。

val kodein = Kodein {
    bind() from setBinding<Dao>()
    bind<Dao>().inSet() with singleton { MongoDao() }
    bind<Dao>().inSet() with singleton { JdbcDao() }
}
val daos: Set<Dao> = kodein.instance()

assertThat(daos.map {it.javaClass as Class<*>})
  .containsOnly(MongoDao::class.java, JdbcDao::class.java)

setBinding<T>() 声明集合注入点,.inSet() 添加成员。

7. Injector(注入器)

前面的例子中业务代码完全解耦于 Kodein,但有时需要延迟注入或 Android 场景下更灵活的方式。

Injector 允许类通过委托属性声明依赖,并在运行时绑定容器。

class Controller2 {
    private val injector = KodeinInjector()
    val service: Service by injector.instance()
    fun injectDependencies(kodein: Kodein) = injector.inject(kodein)
}

val kodein = Kodein {
    bind<Dao>() with singleton { MongoDao() }
    bind<Service>() with singleton { Service(instance(), "myService") }
}

val controller = Controller2()
controller.injectDependencies(kodein)

assertThat(controller.service).isNotNull

✅ 特别适合 Android 中 Activity/Fragment 的按需注入。

8. 在 Android 中使用 Kodein

Android 推荐在自定义 Application 中初始化 Kodein 容器,并通过 Context 共享。

组件继承 KodeinActivity 等基类即可使用注入功能:

class MyActivity : Activity(), KodeinInjected {
    override val injector = KodeinInjector()

    val random: Random by instance()

    override fun onCreate(savedInstanceState: Bundle?) {
        inject(appKodein())
    }
}

appKodein() 是约定方法,需在 Application 中实现。

📌 实际项目中建议封装 BaseApplication 和 BaseActivity 减少模板代码。

9. 对比分析

9.1. vs Spring Framework

特性 Spring Kodein
组件扫描 ✅ 自动扫描 @Component ❌ 手动配置
扩展点 BeanPostProcessor 等强大机制 ❌ 无
生态 ✅ AOP、事务、测试等完整生态 ❌ 仅核心 DI
适用场景 服务端大型应用 轻量级应用、Android

✅ 如果你已经在用 Spring Boot,没必要切换;但如果追求简洁,Kodein 更清爽。

9.2. vs Dagger 2

特性 Dagger 2 Kodein
注入方式 编译期生成代码 运行时反射+委托
性能 ✅ 极致高效 ⚠️ 有轻微开销
错误检查 ✅ 编译时报错 ❌ 运行时报错
方法数 约 1.3k ~1.3k(含 kotlin-stdlib)
上手难度 高(注解多) 低(DSL 清晰)

📊 方法数对比图:

Kodein: kodein

Dagger 2: dagger2

注:Kodein 的方法数主要来自 kotlin-stdlib,剥离后约为 1282 methods / 244 KB。

Dagger 更适合性能敏感的 Android App;Kodein 更适合快速原型或非核心模块。

10. 总结

Kodein 以极简 API 和 Kotlin DSL 提供了优雅的依赖注入方案,特别适合:

  • Android 应用开发
  • 小型微服务或工具类项目
  • 希望避免 Spring 重型配置的场景

虽然缺乏编译期校验和自动扫描,但其清晰的代码结构和低学习成本让它成为 Dagger 和 Spring 之外的一个优秀选择。

示例代码详见 GitHub 仓库


原始标题:Kotlin Dependency Injection with Kodein