1. 概述
本文将介绍在 Kotlin 环境下,Spring 提供的几种依赖注入(Dependency Injection)方式,并结合实际代码示例进行说明。
对于熟悉 Spring 的开发者来说,依赖注入是再常见不过的机制。但在 Kotlin 的语境下,由于其语言特性(如主构造函数、lateinit
、属性委托等),注入方式的选择和最佳实践会略有不同。本文重点不是讲 DI 原理,而是帮你避开 Kotlin + Spring 组合中常见的“坑”。
2. Spring 的 @Autowired 注解
Spring 自 2.5 版本起提供了 @Autowired
注解,用于实现基于注解的依赖注入。
✅ 核心机制:Spring 通过反射读取字节码中的元数据,自动完成 Bean 的装配。
这正是控制反转(IoC)的具体体现——容器负责管理对象生命周期和依赖关系。
⚠️ 在 Kotlin 中使用时需注意:虽然语法更简洁,但底层依然依赖 Java 反射,因此对构造函数、字段或方法的可见性有一定要求。
我们可以在以下位置使用 @Autowired
:
- 构造函数
- 字段
- 方法(包括 setter 或任意名称的方法)
接下来我们逐一分析不同场景下的用法。
3. 构造函数注入:隐式 @Autowired
从 Spring 4.3 开始,如果一个类只有一个构造函数,则无需显式添加 @Autowired
,Spring 会自动使用该构造函数进行依赖注入。
✅ 推荐做法:这是目前最推荐的注入方式,能保证 Bean 不可变(immutable),避免空指针风险。
单构造函数示例(无需 @Autowired)
@RestController
class PersonController(val personService: PersonService)
上述代码中,PersonController
只有一个主构造函数,Spring 会自动将 PersonService
注入进来,**不需要写 @Autowired
**。
多构造函数但含默认构造函数
@Service
class LocationService() {
var lat: Double = 109.344550
var lon: Double = 133.973849
constructor(lat: Double, lon: Double) : this() {
this.lat = lat
this.lon = lon
}
}
这种情况虽然有两个构造函数,但由于存在无参构造函数(默认构造函数),Spring 会选择它来创建实例,然后通过其他方式(如字段注入)填充依赖。⚠️ 注意:此时不会自动注入参数,除非有其他配置。
多个非默认构造函数 → 必须指定 @Autowired
@Service
class PersonService {
var person: Person
lateinit var address: Address
constructor(person: Person) {
this.person = person
}
@Autowired
constructor(person: Person, address: Address): this(person) {
this.address = address
}
}
这里有两个构造函数,且都没有被标记为 primary
。Spring 无法判断该用哪个,因此我们必须在期望使用的构造函数上加 @Autowired
,否则启动会报错 ❌。
💡 小贴士:Kotlin 的主构造函数对应 Java 的“唯一构造函数”,所以如果你只保留一个带参数的主构造函数,就能享受“免注解”的便利。
4. 构造函数注入:显式 @Autowired
尽管从 4.3 起可以省略 @Autowired
,但我们仍可以选择显式标注:
@Service
class AddressService @Autowired constructor(val address: Address)
这种写法更明确地表达了意图,尤其在团队协作或复杂项目中,有助于提升可读性。
📌 注意 Kotlin 的语法细节:构造函数上的注解需要使用 @Autowired constructor(...)
形式。
✅ 适用场景:
- 类有多个构造函数,想明确指定注入路径
- 团队规范要求显式标注
- 兼容老版本 Spring(<4.3)
5. 字段注入 + lateinit:谨慎使用 ❌
我们也可以直接在字段上使用 @Autowired
,配合 lateinit
实现延迟初始化:
@Service
class InventoryService @Autowired constructor(
val vehicleDao: VehicleDao
) {
@Autowired
private lateinit var dealerDao: DealerDao
// ...
}
上面的例子混合了两种注入方式:
vehicleDao
:通过构造函数注入 ✅dealerDao
:通过字段注入 ❌
⚠️ 为什么不推荐字段注入?
问题 | 说明 |
---|---|
❌ 不可变性丧失 | 对象创建后依赖仍可变,违反设计原则 |
❌ 依赖隐藏 | 外部无法通过构造函数看出依赖项 |
❌ 测试困难 | 单元测试中必须依赖 Spring 容器或反射才能设值 |
❌ 可能空指针 | 若未正确初始化,运行时报 UninitializedPropertyAccessException |
✅ 正确做法:优先使用构造函数注入,保持组件不可变。
📝 踩坑提醒:很多人图方便用字段注入,结果在写单元测试时不得不启动整个上下文,严重影响测试速度和隔离性。
6. 方法注入:灵活但少用
除了构造函数和字段,我们还可以在任意方法上使用 @Autowired
,让 Spring 在 Bean 初始化时调用该方法并传入所需依赖。
Setter 风格注入
@Component
class DealerDao {
@set: Autowired
lateinit var reviews: DealerReviewsDao
// ...
}
这里的 @set: Autowired
是 Kotlin 特有的语法,表示注解作用于 setter 方法而非字段本身。
自定义初始化方法
@Component
class VehicleDao {
lateinit var vehicleValueFinder: VehicleValueFinder
lateinit var vehicleReviewDao: VehicleReviewDao
@Autowired
fun initialize(
vehicleValueFinder: VehicleValueFinder,
vehicleReviewDao: VehicleReviewDao
) {
this.vehicleValueFinder = vehicleValueFinder
this.vehicleReviewDao = vehicleReviewDao
}
}
这个 initialize()
方法不是构造函数也不是 setter,但加上 @Autowired
后,Spring 会在 Bean 创建后自动调用它,并注入两个 DAO 实例。
✅ 适用场景:
- 需要执行一些初始化逻辑
- 依赖项较多,不想全塞进构造函数(但这通常意味着职责过重,应考虑拆分)
⚠️ 缺点同字段注入:破坏不可变性,增加测试复杂度。
7. 总结
注入方式 | 是否推荐 | 说明 |
---|---|---|
✅ 构造函数注入(隐式) | 强烈推荐 | Spring 4.3+ 默认行为,简洁安全 |
✅ 构造函数注入(显式) | 推荐 | 明确意图,兼容性好 |
❌ 字段注入 + lateinit | 不推荐 | 易出错,难测试 |
⚠️ 方法注入 | 视情况而定 | 适合初始化逻辑,慎用 |
📌 核心建议:
- 优先使用构造函数注入,尤其是在 Kotlin 中天然支持不可变属性的情况下。
- Spring 4.3+ 下,单构造函数无需加
@Autowired
。 - 避免字段注入,别为了省几行代码牺牲可维护性和可测试性。
- 方法注入可用于特殊场景,但非常规首选。
完整示例代码可在 GitHub 获取:https://github.com/baeldung/kotlin-tutorials/tree/master/spring-boot-kotlin-2