Spring 主要提供了三种依赖注入(DI)的方式,每种方式都有其优缺点和适用场景。
Spring 的三种主要依赖注入方式如下:
- 构造函数注入 (Constructor Injection) - 【官方推荐】
- Setter 方法注入 (Setter Injection)
- 字段注入 (Field Injection)
下面我们来详细解析。
1. 构造函数注入 (Constructor Injection)
这是 Spring 团队首选并推荐的方式。依赖通过类的构造函数进行传递和赋值。
示例代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository userRepository; // 依赖可以声明为 final,确保不可变
// 当类只有一个构造函数时,@Autowired 注解可以省略
// Spring 会自动使用这个构造函数进行注入
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void findUser() {
userRepository.getUser();
}
}
优点:
- 依赖不可变 (Immutability):可以将依赖声明为
final,确保它们在对象创建后不会被修改,增强了程序的健壮性和线程安全。 - 依赖关系明确:构造函数清晰地暴露了一个类需要哪些依赖才能工作。没有这些依赖,你甚至无法创建这个类的实例。这符合“高内聚”的设计原则。
- 保证依赖不为
null:对象在被创建时,其依赖必须已经被注入,否则无法完成实例化。这避免了在后续方法中遇到NullPointerException的风险。 - 更好的可测试性:在单元测试中,你可以非常轻松地
new一个该类的实例,并手动传入 Mock(伪造)的依赖对象,而无需启动整个 Spring 容器。 - 避免循环依赖:如果出现循环依赖(A 依赖 B,B 又依赖 A),Spring 在启动时会因为无法解决构造函数参数而直接抛出异常,让你能尽早发现设计问题。
缺点:
- 代码略显冗长:如果依赖项很多,构造函数的参数列表会变得很长,代码看起来不够简洁。(但这通常也暗示着这个类可能违反了“单一职责原则”,需要重构)。
2. Setter 方法注入 (Setter Injection)
通过公开的 setXxx() 方法来注入依赖。这是早期 Spring 版本中非常流行的方式。
示例代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private UserRepository userRepository;
@Autowired // @Autowired 标记在 setter 方法上
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void findUser() {
// 在调用此方法前,userRepository 必须已经被注入
userRepository.getUser();
}
}
优点:
- 灵活性高:允许在对象创建后,根据需要重新注入或更改依赖。
- 适用于可选依赖:对于那些不是必须的依赖,Setter 注入更加合适。如果没有提供依赖,对象依然可以被创建,只是部分功能可能无法使用。
缺点:
- 无法保证依赖的可用性:对象在创建时其依赖可以是
null。只有在 Setter 方法被调用后,依赖才可用。这可能导致在运行时抛出NullPointerException。 - 依赖可变:对象的状态可以在其生命周期内被任意改变,这可能会引入不确定性和潜在的 bug。
- 隐藏依赖关系:不像构造函数那样一目了然地展示所有必需的依赖。
3. 字段注入 (Field Injection)
直接在类的字段(成员变量)上使用 @Autowired 注解。这是最简洁的注入方式。
示例代码:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired // 直接在字段上注入
private UserRepository userRepository;
public void findUser() {
userRepository.getUser();
}
}
优点:
- 代码极其简洁:省去了编写构造函数或 Setter 方法的麻烦,是三种方式中最省事的。
缺点:
- 违反了依赖注入的设计原则:它隐藏了类的依赖关系,使得一个类看起来像是不需要任何外部依赖就能工作。
- 可测试性极差:在进行单元测试时,你无法在不使用 Spring 容器或复杂的反射工具(如 PowerMock)的情况下,为这个类设置 Mock 依赖。你不能简单地
new UserService()并传入一个假的UserRepository。 - 依赖不可变性差:无法使用
final关键字。 - 与容器强耦合:这种方式完全依赖于 Spring 容器来填充字段,使得组件在容器之外几乎无法使用。
总结与推荐
| 方式 | 优点 | 缺点 | 推荐级别 |
|---|---|---|---|
| 构造函数注入 | 依赖不可变、关系明确、保证非空、易于测试、避免循环依赖 | 代码稍多 | ⭐⭐⭐⭐⭐ (强烈推荐) |
| Setter 方法注入 | 灵活性高,适用于可选依赖 | 依赖可变,可能为null,隐藏必需依赖 | ⭐⭐☆☆☆ (仅用于可选依赖) |
| 字段注入 | 代码最简洁 | 可测试性差、隐藏依赖、与容器强耦合 | ⭐☆☆☆☆ (强烈不推荐用于业务代码) |
最终建议:
- 首选构造函数注入:对于所有必需的依赖,始终使用构造函数注入。这是一种更安全、更清晰、更符合设计原则的做法。
- 谨慎使用 Setter 注入:只在你需要注入一个可选的、或者需要在运行时动态改变的依赖时,才考虑使用 Setter 注入。
- 避免在业务代码中使用字段注入:尽管字段注入在很多教程和老项目中很常见,但由于其严重的缺点(尤其是可测试性差),在Spring 开发实践中强烈建议避免使用它。它可能在一些测试类(如
@SpringBootTest)或非常简单的场景下使用,但在核心业务组件中应被禁止。
304

被折叠的 条评论
为什么被折叠?



