为什么 Spring 更推荐构造器注入,而不是字段 @Autowired
1. 两种写法
字段注入通常写成:
java
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void saveUser() {
redisTemplate.opsForValue().set("name", "Tom");
}
}
构造器注入写成:
java
@Service
public class UserService {
private final RedisTemplate<String, Object> redisTemplate;
public UserService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void saveUser() {
redisTemplate.opsForValue().set("name", "Tom");
}
}
这里需要准确区分:并不是"构造器注入不属于 @Autowired",而是当 Spring Bean 只有一个构造器时,Spring 可以自动使用该构造器完成依赖注入,因此通常不必再显式标注 @Autowired。
2. 字段注入的执行过程
使用字段注入时,Spring 大致会执行以下过程:
text
调用无参构造器创建 UserService
→ 此时 redisTemplate 为 null
→ Spring 发现字段上的 @Autowired
→ 从容器中查找 RedisTemplate Bean
→ 通过反射给字段赋值
因此,对象刚被创建出来时并不完整,它需要等待 Spring 后续填充字段。如果对象不是由 Spring 创建,而是开发者自己执行:
java
UserService userService = new UserService();
那么 Spring 不会参与这个对象的创建,也不会处理字段上的 @Autowired,此时 redisTemplate 仍然是 null。调用业务方法时可能出现:
text
java.lang.NullPointerException
3. 构造器注入的执行过程
使用构造器注入时,Spring 必须先找到构造器需要的依赖,再创建业务对象:
text
准备创建 UserService
→ 分析构造器参数
→ 从容器中找到 RedisTemplate Bean
→ 调用 new UserService(redisTemplate)
→ 得到依赖完整的 UserService
对象一旦创建完成,必需依赖就已经存在,不会先产生一个依赖为空的"半初始化对象"。
4. 推荐构造器注入的主要原因
4.1 保证必需依赖在创建时就存在
字段注入允许创建一个依赖尚未赋值的对象:
java
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
}
而构造器注入强制调用者提供依赖:
java
public UserService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
此时不能直接执行:
java
new UserService();
必须提供 RedisTemplate:
java
new UserService(redisTemplate);
这使无效对象更难被创建。
4.2 可以把依赖声明为 final
构造器注入允许使用:
java
private final RedisTemplate<String, Object> redisTemplate;
final 表示该引用必须在构造阶段完成初始化,并且之后不能被重新指向其他对象。这能更清楚地表达:RedisTemplate 是 UserService 的固定依赖,而不是运行过程中可以随意替换的普通字段。
字段注入通常不能把字段直接声明为由 Spring 后续赋值的普通 final 字段,因为 final 字段必须在构造阶段完成初始化。
4.3 依赖关系更加清晰
看到下面的构造器,就能直接知道 UserService 创建时依赖哪些对象:
java
public UserService(
UserRepository userRepository,
RedisTemplate<String, Object> redisTemplate) {
this.userRepository = userRepository;
this.redisTemplate = redisTemplate;
}
字段注入则把依赖分散在类的不同位置:
java
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
当一个类的依赖很多时,构造器会变得很长。这不完全是构造器注入的缺点,反而是在提醒开发者:这个类可能承担了过多职责,需要考虑拆分。
4.4 更方便进行单元测试
使用构造器注入时,可以在不启动 Spring 容器的情况下直接创建测试对象:
java
RedisTemplate<String, Object> mockTemplate = mock(RedisTemplate.class);
UserService userService = new UserService(mockTemplate);
测试代码能够明确提供模拟依赖。
字段注入没有公开的依赖入口:
java
UserService userService = new UserService();
此时 redisTemplate 是私有字段,测试往往需要启动 Spring 上下文、使用反射修改字段,或者使用额外的测试工具完成注入,测试成本更高。
4.5 减少对 Spring 容器的隐式依赖
字段注入写法只有在 Spring 处理对象后才正常工作:
java
@Autowired
private RedisTemplate<String, Object> redisTemplate;
单看 Java 类本身,并不能保证字段已经赋值。构造器注入则符合普通 Java 的对象创建规则,即使离开 Spring,也可以正常创建和测试对象:
java
UserService userService = new UserService(redisTemplate);
这使业务类与 Spring 的耦合更弱,类本身也更容易复用。
4.6 更容易发现循环依赖
假设 AService 依赖 BService,而 BService 又依赖 AService:
java
@Service
public class AService {
public AService(BService bService) {
}
}
@Service
public class BService {
public BService(AService aService) {
}
}
构造器注入会直接暴露循环关系,因为创建 AService 前需要 BService,创建 BService 前又需要 AService。这通常说明类之间的职责划分存在问题。
字段注入可能让循环关系在代码中不够明显。虽然 Spring 在某些情况下曾能通过提前暴露对象处理部分循环依赖,但依赖这种机制会增加设计复杂度。尽早发现并消除循环依赖通常更合理。
5. 为什么单个构造器可以省略 @Autowired
下面两种写法在只有一个构造器时通常效果相同:
java
@Service
public class UserService {
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public UserService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
java
@Service
public class UserService {
private final RedisTemplate<String, Object> redisTemplate;
public UserService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
Spring 发现这个类只有一个构造器后,会默认把它作为依赖注入入口,因此 @Autowired 可以省略。
如果类中存在多个构造器,Spring需要判断使用哪一个。这时可以显式标注需要用于注入的构造器,或者重新设计构造方式,避免产生歧义。
6. 多个同类型 Bean 时如何处理
构造器注入并不会消除同类型 Bean 的歧义。例如容器中存在两个 RedisTemplate Bean 时,可以使用 @Qualifier 指定名称:
java
@Service
public class UserService {
private final RedisTemplate<String, Object> redisTemplate;
public UserService(
@Qualifier("redisTemplate")
RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
也可以在某个 Bean 上使用 @Primary,将其设为默认候选对象。
7. 可选依赖怎么办
构造器注入更适合"没有它就无法正常工作"的必需依赖。如果某个依赖确实是可选的,可以使用 Optional、ObjectProvider,或者在合适场景下使用 Setter 注入。例如:
java
@Service
public class ReportService {
private final ObjectProvider<CacheService> cacheServiceProvider;
public ReportService(ObjectProvider<CacheService> cacheServiceProvider) {
this.cacheServiceProvider = cacheServiceProvider;
}
}
使用时再判断容器中是否存在对应 Bean:
java
CacheService cacheService = cacheServiceProvider.getIfAvailable();
不应为了省事,把所有依赖都改成可空字段,否则会把配置错误推迟到业务运行阶段。
8. Lombok 简化写法
当项目使用 Lombok 时,可以使用 @RequiredArgsConstructor 自动生成包含所有 final 字段的构造器:
java
@Service
@RequiredArgsConstructor
public class UserService {
private final RedisTemplate<String, Object> redisTemplate;
public void saveUser() {
redisTemplate.opsForValue().set("name", "Tom");
}
}
它大致等价于:
java
public UserService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
不过阅读代码时仍应理解真正完成注入的是生成后的构造器,而不是 Lombok 注解本身。
9. 三种注入方式对比
| 对比项 | 字段注入 | 构造器注入 | Setter 注入 |
|---|---|---|---|
| 必需依赖是否在创建时提供 | 否 | 是 | 否 |
是否可以使用 final |
通常不可以 | 可以 | 不可以 |
| 依赖是否清晰 | 一般 | 清晰 | 较清晰 |
| 单元测试是否方便 | 一般 | 方便 | 较方便 |
| 适合场景 | 简单示例、旧代码 | 必需依赖,推荐默认使用 | 可选或可替换依赖 |
10. 总结
Spring 更推荐构造器注入,并不是因为字段上的 @Autowired 无法工作,而是因为构造器注入能更好地保证对象完整性:
text
字段注入:
先创建对象
→ 对象中的依赖暂时为 null
→ Spring 再通过反射填充字段
构造器注入:
先找到全部必需依赖
→ 再创建完整对象
可以用一句话记住:
必需依赖使用构造器注入,可选依赖根据实际情况使用 Setter、
Optional或ObjectProvider。
因此,推荐写法是:
java
@Service
public class UserService {
private final RedisTemplate<String, Object> redisTemplate;
public UserService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
而不是默认使用:
java
@Autowired
private RedisTemplate<String, Object> redisTemplate;