构造注入详解:Spring 依赖注入的首选方式
一、什么是构造注入?
构造注入(Constructor Injection)是 Spring 依赖注入的一种方式,它通过类的构造方法将依赖对象传递给目标类。
java
@Service
public class UserService {
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
Spring 在创建 UserService 实例时,会从容器中查找 UserDao 类型的 Bean,作为参数调用这个构造方法。依赖在对象创建的一刻就已经就绪。
二、基本用法
2.1 单个构造方法
如果类只有一个构造方法,Spring 4.3 之后不需要显式加 @Autowired,Spring 会自动使用它进行注入。
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;
}
}
2.2 多个构造方法
如果类有多个构造方法,需要标注 @Autowired 告诉 Spring 使用哪一个。
java
@Service
public class UserService {
private final UserDao userDao;
private final EmailService emailService;
@Autowired
public UserService(UserDao userDao, EmailService emailService) {
this.userDao = userDao;
this.emailService = emailService;
}
public UserService(UserDao userDao) {
this(userDao, null);
}
}
2.3 配合 Lombok
使用 Lombok 的 @RequiredArgsConstructor 可以自动生成包含所有 final 字段的构造方法,代码更简洁。
java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserDao userDao;
private final EmailService emailService;
}
编译后等价于手写了带这两个参数的构造方法。
三、构造注入的核心优势
3.1 依赖不可变
构造注入的字段可以用 final 修饰,对象一旦创建,依赖就不可更改。这保证了线程安全和状态一致性。
java
private final UserDao userDao; // 不可变
3.2 依赖完全就绪
对象创建完成后,所有依赖都已经注入完毕。不会出现字段注入那种"对象创建了但依赖还是 null"的中间状态。
java
UserService service = new UserService(userDao); // 创建即就绪
service.getUser(1L); // 安全,userDao 一定不为 null
3.3 便于单元测试
测试时直接通过构造方法传入 Mock 对象,不需要启动 Spring 容器,也不需要反射工具。
java
@Test
void testGetUser() {
UserDao mockDao = mock(UserDao.class);
when(mockDao.findById(1L)).thenReturn(new User(1L, "张三"));
UserService service = new UserService(mockDao); // 直接传入 mock
User user = service.getUser(1L);
assertEquals("张三", user.getName());
}
3.4 明确依赖关系
构造方法的参数列表直接反映了类的依赖。如果参数过多,说明类承担了过多职责,需要拆分------这是一个早期设计信号。
3.5 避免循环依赖
构造注入无法解决循环依赖。两个类互相通过构造方法注入对方时,Spring 会直接抛出 BeanCurrentlyInCreationException。
java
@Service
public class AService {
private final BService bService;
public AService(BService bService) { this.bService = bService; }
}
@Service
public class BService {
private final AService aService;
public BService(AService aService) { this.aService = aService; }
}
启动时会报错,因为创建 A 需要 B,创建 B 需要 A,互相等待,无法完成。
这看似是缺点,但实际上是一个设计上的保护。循环依赖通常意味着两个类的职责边界不清晰,构造注入强制暴露这个问题,促使开发者重构。而字段注入和 Setter 注入可以通过三级缓存"绕过"循环依赖,但可能掩盖了设计缺陷。
四、三种注入方式对比
| 对比维度 | 构造注入 | Setter 注入 | 字段注入 |
|---|---|---|---|
| 依赖可变性 | 不可变(final) |
可变 | 可变 |
| 空指针风险 | 无 | 有 | 有 |
| 单元测试便利性 | 高 | 中 | 低 |
| 循环依赖支持 | 不支持 | 支持 | 支持 |
| 依赖过多时提示 | 明显(参数列表长) | 不明显 | 不明显 |
| Spring 官方推荐 | ✅ 推荐 | ⚠️ 可选 | ❌ 不推荐 |
五、底层原理
构造注入的处理入口是 AbstractAutowireCapableBeanFactory.createBeanInstance()。当 Bean 定义中指定了构造参数,或类只有一个有参构造方法时,Spring 会走 autowireConstructor() 方法。
简化流程:
- 获取候选构造方法列表(
determineConstructorsFromBeanPostProcessors和getDeclaredConstructors) - 按参数数量排序,优先匹配参数最多的构造方法
- 对每个构造方法参数,调用
beanFactory.resolveDependency()查找匹配的 Bean - 解析出所有参数后,通过反射调用
Constructor.newInstance(args)创建实例 - 如果找不到匹配的 Bean,根据
required决定是否跳过或抛异常
核心类 ConstructorResolver 负责解析构造方法参数、匹配依赖、处理 @Autowired(required=false) 等逻辑。
六、构造注入的常见问题
参数过多
当一个类的构造方法参数超过 5 个,通常意味着类承担了太多职责,应考虑拆分。构造注入强制暴露这个问题,是一种"设计预警"。
可选依赖
如果某个依赖是可选的,构造注入不太适合,需要配合 ObjectProvider 或 @Nullable。
java
@Service
public class UserService {
private final UserDao userDao;
private final Optional<CacheService> cacheService;
public UserService(UserDao userDao,
ObjectProvider<CacheService> cacheServiceProvider) {
this.userDao = userDao;
this.cacheService = Optional.ofNullable(cacheServiceProvider.getIfAvailable());
}
}
多个同类型 Bean
当容器中有多个同类型的 Bean 时,构造参数需要配合 @Qualifier 指定具体名称。
java
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(@Qualifier("aliPayService") PaymentService paymentService) {
this.paymentService = paymentService;
}
}
七、最佳实践
优先使用构造注入。 Spring 官方从 4.3 开始就推荐构造注入,它能保证依赖完整性和不可变性。
用 final 修饰所有通过构造注入的字段。 表达依赖的不可变性,也防止意外修改。
依赖过多时拆分职责。 如果一个类的构造方法参数超过 5 个,通常说明它做了太多事情。考虑拆分为多个类,或者提取一个聚合依赖对象。
避免构造注入的循环依赖。 如果出现循环依赖,不要试图用 @Lazy 或改成字段注入来"绕过",而应该分析两个类之间的关系,提取公共依赖或调整职责边界。
配合 Lombok 使用 @RequiredArgsConstructor。 让代码保持简洁,同时保留构造注入的所有优点。
八、总结
构造注入是 Spring 依赖注入中最推荐的方式。它通过构造方法将依赖在对象创建时一次性注入,保证了依赖的不可变性和完整性,同时便于单元测试和设计审查。
与字段注入和 Setter 注入相比,构造注入在依赖明确性、测试友好性和线程安全性上都更优。它无法解决循环依赖,但这恰恰是它的一个设计优势------强制暴露不健康的依赖关系。
在 Spring Boot 项目中,结合 Lombok 的 @RequiredArgsConstructor,可以以最简洁的方式实现构造注入,同时保持代码的清晰和健壮。