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 不推荐 易出错,难测试
⚠️ 方法注入 视情况而定 适合初始化逻辑,慎用

📌 核心建议:

  1. 优先使用构造函数注入,尤其是在 Kotlin 中天然支持不可变属性的情况下。
  2. Spring 4.3+ 下,单构造函数无需加 @Autowired
  3. 避免字段注入,别为了省几行代码牺牲可维护性和可测试性。
  4. 方法注入可用于特殊场景,但非常规首选。

完整示例代码可在 GitHub 获取:https://github.com/baeldung/kotlin-tutorials/tree/master/spring-boot-kotlin-2


原始标题:Spring Dependency Injection With Kotlin