
构造器注入必须写在类定义里,不是随便加个@Autowired就完事
Java中构造器注入的本质是让Spring在创建Bean时,通过构造函数参数把依赖“塞进去”。这要求你显式声明构造函数,且不能靠默认无参构造器蒙混过关——否则Spring会报NoSuchBeanDefinitionException或直接用反射调无参构造,导致字段为null。
常见错误现象:加了@Autowired但运行时报NullPointerException,尤其在单元测试里;或者IDE提示“Constructor injection is preferred”,但代码跑起来却没生效。
- 必须把依赖作为构造函数参数,而不是在字段上加
@Autowired(那是字段注入,违背构造器注入原则) - 如果类有多个构造函数,Spring 5.3+ 默认只认唯一一个(无论是否有
@Autowired),但更稳妥的做法是显式标注@Autowired在目标构造器上 - Lombok的
@RequiredArgsConstructor能自动生成含final字段的构造器,但要注意它不会包含非final字段,也不识别@NonNull以外的注解 - 构造器参数类型必须与容器中Bean类型严格匹配,泛型擦除后不一致(比如
List<String>vsArrayList<String>)可能引发NoUniqueBeanDefinitionException
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// ✅ 正确:显式构造器 + final字段 + @Autowired(Spring 5.2前必需,之后可省略但建议保留)
@Autowired
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}
字段必须声明为final,否则解耦效果打折扣
构造器注入配合final字段,才能真正锁死依赖不可变。如果不加final,虽然Spring仍能注入,但后续代码可能意外重赋值,破坏不可变性假设,也失去编译期防护——比如在测试中手动setXxx()绕过注入逻辑,掩盖真实问题。
使用场景很明确:只要这个依赖从创建起就不该变(99%的Service、Repository、Client类都属于这一类),就必须final。
立即学习“Java免费学习笔记(深入)”;
-
final字段在构造器执行完毕后无法再修改,强制依赖在初始化阶段就到位 - 没有
final,Lombok的@Setter或IDE生成的setter可能悄悄打开后门,让测试或业务代码绕过DI容器 - 某些静态分析工具(如SonarQube)会将非
final的构造器注入字段标为“可变状态风险” - 注意:
final字段不能是原始类型包装类的空值(如final Integer port = null;),否则构造器里没赋值会编译失败
多构造器或可选依赖?用@Nullable或@Autowired(required = false)
不是所有依赖都必须存在。比如日志上报Client、异步通知模块,在某些环境可能被关闭。这时硬塞进必填构造器会导致启动失败,但又不能退回到字段注入——得在保持构造器注入前提下处理可选性。
错误做法是搞两个构造器(一个带可选参数,一个不带),Spring会因歧义报错;或者把可选依赖设为null后在方法里判空,既丑又易漏。
- 推荐用
@Nullable(来自org.springframework.lang或javax.annotation)标注参数,配合final字段,Spring会注入null而非抛异常 - 或者用
@Autowired(required = false),语义更直白,但需确保参数类型在容器中至多只有一个候选Bean - 避免在构造器里对可选依赖做非空断言(如
Objects.requireNonNull(emailService)),否则失去“可选”意义 - 如果可选依赖本身有默认实现(如
NoOpEmailService),注册为@Primary比判空更干净
public UserService(UserRepository userRepository, @Nullable EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService; // 允许为null,业务方法里自行判断
}
测试时别手写new,用@ContextConfiguration或ApplicationContextRunner
构造器注入让类天然适合单元测试——你可以直接new UserService(new MockUserRepository(), new StubEmailService())。但一旦引入Spring上下文,很多人又倒回去写new,结果测的是“脱离容器的裸对象”,漏掉AOP、事务代理、条件化Bean等真实行为。
容易踩的坑:Mockito的@InjectMocks试图模拟构造器注入,但实际是字段反射赋值,绕过了构造器逻辑,还可能因Lombok干扰失效。
- 集成测试用
@SpringBootTest最省事,但启动慢;轻量级测试推荐ApplicationContextRunner(Spring Boot 2.1+) - 手动构建上下文时,确保构造器参数类型和
@Bean方法返回类型完全一致,别用接口当参数却注册了具体实现类而没设@Primary - 如果测试中需要替换某个依赖(比如用
H2代替PostgreSQL),优先用@TestConfiguration覆盖Bean,而不是在构造器里传Mock
final、构造器显式、可选依赖不硬编码判空、测试不绕过容器——漏掉任何一环,解耦就变成假象。










