Setter 注入是依赖注入的三种主要方式之一(另外两种是构造函数注入和字段注入)。虽然现在 Spring 官方和社区更推荐使用构造函数注入,但 Setter 注入在某些特定场景下依然非常有用,甚至更合适。
首先,什么是 Setter 注入?
Setter 注入是通过类的公开 setXXX() 方法来注入依赖。Spring IoC 容器会先调用类的无参构造函数创建一个实例,然后调用相应的 setter 方法将依赖关系注入进去。
代码示例:
@Service
public class ReportService {
private DataExporter dataExporter;
// Spring容器会调用这个方法来注入一个DataExporter类型的Bean
@Autowired
public void setDataExporter(DataExporter dataExporter) {
System.out.println("通过 setter 注入 DataExporter...");
this.dataExporter = dataExporter;
}
public void generateReport() {
if (dataExporter != null) {
dataExporter.export();
} else {
System.out.println("没有可用的导出器,生成默认报告。");
}
}
}
Setter 注入的核心适用场景
1. 注入可选的依赖(Optional Dependencies)
这是 Setter 注入最经典、最合理的应用场景。一个类可以正常工作,但如果提供了某个依赖,它就能获得额外的功能。
比喻:汽车和 GPS 导航系统。
- 一辆汽车必须有引擎才能行驶(强制依赖 -> 构造函数注入)。
- 但一辆汽车可以没有 GPS 导航系统,它仍然是一辆可以开的汽车。装上 GPS 后,它就获得了导航功能(可选依赖 -> Setter 注入)。
场景示例:一个 UserService,它有一个可选的 EventPublisher。如果配置了事件发布器,那么在用户注册后就发布一个事件;如果没有配置,程序也能正常运行,只是不发布事件。
@Service
public class UserService {
private final UserRepository userRepository; // 强制依赖
private EventPublisher eventPublisher; // 可选依赖
// 强制依赖通过构造函数注入
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 可选依赖通过setter注入
// 如果Spring容器中没有EventPublisher的Bean,这个方法就不会被调用(需要配合@Autowired(required = false))
@Autowired(required = false)
public void setEventPublisher(EventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void registerUser(String name) {
// ...保存用户到数据库...
userRepository.save(name);
// 如果可选的依赖被注入了,就使用它
if (eventPublisher != null) {
eventPublisher.publish("New user registered: " + name);
}
}
}
在这个例子中,UserService 的核心职责(用户管理)不依赖于 EventPublisher,所以将后者设为可选依赖并通过 Setter 注入是非常优雅的设计。
2. 解决循环依赖(Circular Dependencies)
这是一个非常著名但强烈不推荐依赖的场景。循环依赖通常是设计不良的信号。
- 循环依赖:A 依赖 B,同时 B 又依赖 A。
- 为什么构造函数注入会失败? 当 Spring 尝试创建 A 时,发现 A 的构造函数需要 B。于是它去创建 B,发现 B 的构造函数需要 A。这就陷入了一个死循环,Spring 会直接抛出
BeanCurrentlyInCreationException异常。 - 为什么 Setter 注入能解决?
- Spring 创建 A 的实例(调用无参构造函数)。此时 A 只是一个“半成品”,但已经存在于内存中。
- Spring 将这个“半成品”的 A 暴露出去(放入一个早期的缓存中)。
- Spring 准备对 A 进行属性注入,发现需要 B。
- Spring 去创建 B 的实例。在注入 B 的属性时,发现需要 A。
- Spring 从早期缓存中获取到了那个“半成品”的 A,并将其注入到 B 中。B 创建完成。
- Spring 回到 A 的创建流程,将已经创建好的 B 注入到 A 中。A 也创建完成。
虽然 Setter 注入可以“打破”这个循环,但这往往掩盖了更深层次的设计问题。最佳实践是重构代码以消除循环依赖,而不是依赖 Setter 注入来解决它。
3. 与需要无参构造函数的传统框架集成
某些旧的框架或规范(例如一些模板引擎、序列化库、或者遵循 JavaBean 规范的工具)要求类必须有一个公开的无参构造函数。在这种情况下,如果你想让 Spring 来管理这些类的实例,就只能使用 Setter 注入或字段注入,因为构造函数已经被占用了。
4. 在运行时动态地改变依赖
由于 Setter 方法是公开的,你可以在应用程序运行期间,通过 JMX(Java Management Extensions)或其他管理接口调用这个 setter 方法,来替换掉原有的依赖实现。这在需要动态切换策略(如不同的缓存策略、日志策略)的场景下可能有用。但这属于高级且罕见的用法。
总结:与构造函数注入的对比与选择
| 特性 | 构造函数注入 (Constructor Injection) | Setter 注入 (Setter Injection) |
|---|---|---|
| 依赖类型 | 强制性依赖 (Mandatory) | 可选性依赖 (Optional) |
| 不变性 | 可以将依赖声明为 final,确保不可变 | 依赖是可变的,可以在之后被修改 |
| 对象状态 | 构造完成后,对象处于完整可用状态 | 构造完成后,对象可能处于不完整状态,直到 setter 被调用 |
| 代码清晰度 | 依赖关系在构造函数签名中一目了然 | 依赖关系分散在多个 setter 方法中 |
| 循环依赖 | 无法处理,启动时快速失败 (Fail-fast) | 可以处理(但不推荐) |
实践和经验:
-
首选构造函数注入:对于所有必须存在的、核心的依赖,总是使用构造函数注入。这能保证你的组件在被创建出来的那一刻就是完整和可用的,并且代码更清晰、更易于测试。
-
谨慎使用 Setter 注入:只在你明确知道一个依赖是可选的,或者需要解决上述特定问题(如循环依赖、与旧框架集成)时,才使用 Setter 注入。
简单记:“必须的,走正门(构造函数);可选的,走侧门(Setter)。”

1万+

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



