为什么 Spring 建议使用构造器注入?
Spring 官方文档里有一句话:
The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state.
翻译过来就是:Spring 团队推荐构造器注入,因为它能让组件变成不可变对象,保证必需依赖不为 null,并且返回给调用方的组件总是完全初始化好的。
下面从几个角度展开。
一、依赖不可变,用 final 修饰
构造器注入的字段可以声明为 final:
java
@Service
public class UserService {
private final UserDao userDao;
private final EmailService emailService;
public UserService(UserDao userDao, EmailService emailService) {
this.userDao = userDao;
this.emailService = emailService;
}
}
final 意味着依赖一旦注入就不能被修改,对象天生线程安全,也避免了后续代码意外替换依赖。字段注入和 Setter 注入做不到这一点,字段只能是可变的。
二、依赖完全就绪,避免空指针
构造器注入在对象创建时就把所有依赖传进来。对象一旦创建出来,依赖一定是非 null 的:
java
UserService service = new UserService(userDao, emailService);
service.register(user); // userDao 和 emailService 一定可用
字段注入则不同:
java
@Service
public class UserService {
@Autowired
private UserDao userDao; // 实例化时是 null,属性填充后才被注入
}
如果有人手动 new UserService(),userDao 就是 null,调用时直接空指针。构造器注入强制调用方提供依赖,从源头杜绝了这个问题。
三、便于单元测试
构造器注入让测试非常自然:
java
@Test
void testRegister() {
UserDao mockDao = mock(UserDao.class);
EmailService mockEmail = mock(EmailService.class);
// 直接传入 mock 对象
UserService service = new UserService(mockDao, mockEmail);
service.register(new User("张三"));
verify(mockDao).save(any(User.class));
}
字段注入的测试需要启动 Spring 容器,或者用 ReflectionTestUtils.setField() 反射赋值,麻烦且不直观。构造器注入不需要 Spring 容器,纯 Java 对象测试即可。
四、依赖关系清晰,暴露设计问题
构造方法参数列表直接反映了这个类需要哪些依赖:
java
public UserService(UserDao userDao, EmailService emailService,
SmsService smsService, LogService logService,
CacheService cacheService, ConfigService configService) {
// ...
}
如果参数超过 5 个,一眼就能看出这个类职责过重,需要拆分。字段注入没有这种视觉提示,依赖可以悄悄加到十几个而不被发现。
五、避免循环依赖被掩盖
构造器注入无法解决循环依赖,遇到循环依赖会直接启动失败:
java
@Service
public class AService {
public AService(BService bService) { }
}
@Service
public class BService {
public BService(AService aService) { }
}
启动报 BeanCurrentlyInCreationException。这看起来是缺点,实际上是设计上的保护。循环依赖通常意味着两个类的职责边界不清晰。构造器注入强制暴露这个问题,促使开发者重构,而不是靠三级缓存"兜底"掩盖。
字段注入的循环依赖可以通过三级缓存解决,但问题本身还在,只是被隐藏了。Spring Boot 2.6 之后默认禁止循环依赖,也是这个思路。
六、与 Spring 框架的集成
Spring 4.3 之后,如果一个类只有一个构造方法,@Autowired 可以省略:
java
@Service
public class UserService {
private final UserDao userDao;
public UserService(UserDao userDao) { // 不需要 @Autowired
this.userDao = userDao;
}
}
配合 Lombok 的 @RequiredArgsConstructor,代码非常简洁:
java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserDao userDao;
private final EmailService emailService;
}
编译后自动生成包含所有 final 字段的构造方法。
七、三种注入方式对比
| 对比维度 | 构造器注入 | Setter 注入 | 字段注入 |
|---|---|---|---|
| 依赖不可变 | ✅ final | ❌ | ❌ |
| 空指针风险 | 无 | 有 | 有 |
| 单元测试便利性 | 高 | 中 | 低 |
| 循环依赖支持 | 不支持 | 支持 | 支持 |
| 依赖过多时提示 | 明显 | 不明显 | 不明显 |
| Spring 官方推荐 | ✅ | ⚠️ 可选 | ❌ 不推荐 |
Setter 注入适合可选依赖:
java
@Autowired(required = false)
public void setCacheService(CacheService cacheService) {
this.cacheService = cacheService;
}
字段注入写起来最方便,但代价是牺牲了测试性、不可变性和设计清晰度。
八、官方文档的推荐
Spring 官方文档明确说:
- 构造器注入用于必需依赖。
- Setter 注入用于可选依赖。
- 字段注入不推荐,因为无法使用
final,不利于测试,且容易隐藏依赖过多的问题。
九、总结
| 原因 | 说明 |
|---|---|
| 依赖不可变 | 字段可以 final,线程安全 |
| 依赖完全就绪 | 对象创建后依赖一定非 null |
| 便于测试 | 直接传入 Mock,无需反射 |
| 关系清晰 | 构造方法参数暴露依赖数量 |
| 暴露设计问题 | 循环依赖直接失败,促使重构 |
| 官方推荐 | Spring 团队明确提倡 |
Spring 建议构造器注入,本质上是在引导开发者写出不可变、依赖明确、易于测试、设计清晰 的代码。字段注入虽然写起来方便,但牺牲了这些长期收益。如果一个类只有一个构造方法,@Autowired 都可以省掉,代码反而更简洁。