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