Spring 提供 4 种核心注入方式:构造函数注入、Setter 注入、字段注入、方法注入 。每种方式在强制性、可测试性、不变性等方面差异巨大
一、构造函数注入(Constructor Injection)
java
@Service
public class UserService {
private final UserRepository userRepository; // final 修饰
private final EmailService emailService;
// Spring 4.3+:单构造函数可省略 @Autowired
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}
优点
1.强制依赖完整性 :必须提供所有依赖,对象创建后处于完整状态
2.不可变性(Immutability) :字段可声明为 final,线程安全
3.易于单元测试 :无需反射,直接 new UserService(mockRepo, mockEmail) 即可测试
4.清晰的依赖声明 :构造函数参数即依赖清单,一目了然
5.循环依赖检测 :Spring 启动时即报错,避免运行时 NPE
缺点
1.参数过多时臃肿 :依赖超过 5 个时构造函数过长(设计信号:违反单一职责)
2.循环依赖无法解决 :构造函数间的循环依赖会导致 Spring 启动失败
3.稍显冗长:需要显式编写构造器和赋值语句
适用场景
1.必选依赖 :Bean 没有这些依赖无法正常工作
2.追求不变性 :需要线程安全或不可变对象
3.团队协作:代码可读性优先
二、Setter 注入(Setter Injection)
java
@Service
public class UserService {
private UserRepository userRepository;
private EmailService emailService;
private Optional<SmsService> smsService = Optional.empty(); // 可选依赖
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
@Autowired(required = false) // 可选依赖
public void setSmsService(SmsService smsService) {
this.smsService = Optional.ofNullable(smsService);
}
}
优点
1.依赖可选性 :通过 required = false 支持可选依赖
2.灵活性高 :可在对象创建后动态修改依赖(不推荐,但可行)
3.解决循环依赖 :结合三级缓存可解决 Setter 循环依赖
4.符合 JavaBean 规范:对旧框架兼容性好
缺点
1.对象状态不完整 :构造后可能缺少依赖,导致运行时 NPE
2.无法使用 final :字段可被修改,失去不可变性保证
3.测试依赖反射 :需要 Mockito 等工具注入 mock 对象
4.依赖关系不清晰 :无法直观看出哪些是必选依赖
5.多次调用风险:可能被意外多次调用,需手动防御
适用场景
1.可选依赖 :如插件式功能、@Autowired(required = false)
2.配置类 :需要动态设置策略的场景
3.遗留系统兼容:老代码迁移的过渡方案
三、字段注入(Field Injection)
java
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // 非 final
@Autowired
private EmailService emailService;
}
优点
1.代码简洁 :无需构造器或 Setter,注解直接打在字段上
2.编写快速 :开发效率最高
3.可读性尚可:依赖一目了然(类级别)
缺点
1.无法使用 final:失去不可变性,线程不安全
2.强依赖反射:单元测试必须启动 Spring 容器或使用 @InjectMocks
java
// 测试必须这样
@InjectMocks
private UserService userService;
3.隐藏依赖 :通过反射注入,IDE 无法识别为依赖项
4.循环依赖难检测 :可能延迟到运行时才能发现
5.难以初始化 :无法在构造函数中使用依赖,导致 NPE 风险
6.静态分析困难 :工具无法分析依赖关系(影响架构治理)
致命问题:如果你将项目升级到 Spring Boot 2.2+,会发现 IDEA 会提示 "Field injection is not recommended"。
适用场景
1.快速原型开发 :不关心测试的临时代码
2.集成测试 :测试类本身可以使用
3.不推荐在生产代码中使用
四、方法注入(Method Injection)
1.普通方法注入
java
@Service
public class UserService {
private UserRepository userRepository;
@Autowired // 可注入任意方法
public void prepare(UserRepository userRepository) {
this.userRepository = userRepository;
// 可执行额外初始化逻辑
userRepository.initCache();
}
}
特点:与 Setter 类似,但方法名不要求 setXxx
2. Lookup 方法注入(解决原型 Bean 依赖)
java
@Service
@Scope("singleton")
public abstract class UserService {
// Spring 会动态实现此方法,每次返回新的 Prototype Bean
@Lookup
public abstract PrototypeBean getPrototypeBean();
public void process() {
PrototypeBean bean = getPrototypeBean(); // 每次都获取新实例
bean.doWork();
}
}
用途:Singleton Bean 依赖 Prototype Bean 时,保证每次获取新实例
对比总结
| 维度 | 构造函数注入 | Setter 注入 | 字段注入 | 方法注入 |
|---|---|---|---|---|
| 强制性 | ✅ 必选 | ⚠️ 可选 | ❌ 隐藏 | ⚠️ 可选 |
| final 字段 | ✅ 支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 |
| 不可变性 | ✅ 线程安全 | ❌ 可变 | ❌ 可变 | ❌ 可变 |
| 测试难度 | ✅ 简单 | ⚠️ 需反射 | ❌ 需容器 | ⚠️ 需反射 |
| 循环依赖 | ❌ 无法解决 | ✅ 可解决 | ⚠️ 可解决但有风险 | ✅ 可解决 |
| 依赖清晰度 | ✅ 极高 | ⚠️ 中等 | ❌ 隐藏 | ⚠️ 中等 |
| IDE 支持 | ✅ 完美 | ⚠️ 一般 | ❌ 困难 | ⚠️ 一般 |
| Spring 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐(不推荐) | ⭐⭐ |