1. 前言
在我们日常开发SpringBoot项目中,依赖注入(Dependency Injection, DI)是核心机制之一,最常见的写法有两种:
字段注入:在类的属性上直接使用 @Autowired 注解
构造函数注入 :通过构造函数参数注入依赖
相信不少小伙伴都喜欢在字段上直接使用@Autowired,因为字段注入用起来更简洁,但是Spring官方却明确推荐优先使用构造函数注入!这两种方式有何本质区别?为什么官方有明确的推荐?
本文博主将从代码对比、构造函数注入优点、实际单元测试场景 三个维度,带小伙伴们彻底搞懂其中的门道
2. 两种注入方式对比
假设我们有一个UserService,它依赖于UserMapper(数据访问层),我们需要在UserService中注入UserMapper
2.1 字段注入(@Autowired 在字段上)
这是很多小伙伴喜欢的方式,写法相对简洁直接:
@Service
public class UserService {
// 直接在字段上使用@Autowired注入
@Autowired
private UserMapper userMapper;
// 业务方法
public String getUser(Long id) {
return userMapper.findUserById(id);
}
}
AI写代码java
运行
123456789101112
2.2 构造函数注入(官方推荐)
通过类的构造方法注入依赖,无需在字段上添加注解:
@Service
public class UserService {
// 依赖被声明为final,确保不可变
private final UserMapper userMapper;
// 通过构造函数注入
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
// 业务方法
public String getUser(Long id) {
return userMapper.findUserById(id);
}
}
AI写代码java
运行
12345678910111213141516
使用Lombok 简化写法
如果你的项目中有使用了Lombok,那么构造函数注入的方式可以大大简化我们的写法,因为Lombok的@AllArgsConstructor注解会自动为类中的所有 final 或非 static 字段生成一个全参数构造函数
为了演示 :我们假设还有一个订单Mapper也需要注入
@Service
@AllArgsConstructor //Lombok注解
public class UserService {
// 依赖被声明为final,确保不可变
private final UserMapper userMapper;
private final OrderMapper orderMapper;
/**
* 无需手动编写构造函数,Lombok会在编译时生成:
public UserService(UserMapper userMapper, OrderMapper orderMapper) {
this.userMapper = userMapper;
this.orderMapper = orderMapper;
}
**/
// 业务方法
public String getUser(Long id) {
return userMapper.findUserById(id);
}
}
AI写代码java
运行
123456789101112131415161718192021
通过Lombok的注解大家会发现写法也简洁了很多!
3. 构造函数注入方式的优点
3.1 依赖不可变
通过上面的例子我们可以看出,使用构造函数注入可以声明把依赖声明为final
private final UserMapper userMapper;
AI写代码java
运行
1
使用了final,字段在对象创建时必须初始化,且初始化后无法修改,这就表示了:
- 依赖不会被意外篡改(线程安全)
- 强制依赖在对象创建时就必须注入(避免后续使用时为null)
@Autowired字段注入无法使用final(因为字段注入是在对象创建后通过反射赋值的),依赖可能被中途修改,存在安全隐患
3.2 依赖明确,避免空指针
还是上述示例代码
public UserService(UserMapper userMapper, OrderMapper orderMapper) {
this.userMapper = userMapper;
this.orderMapper = orderMapper;
}
AI写代码java
运行
1234
小伙伴们会发现构造函数注入时,Spring容器会在创建UserService对象时,通过构造方法传入所有依赖(UserMapper、OrderMapper)。
如果依赖缺失(比如UserMapper未被 Spring 管理时),容器启动时就会报错,那么在项目启动阶段就能发现问题。
而字段注入是 “隐式” 的:如果依赖缺失,Spring不会在启动时报错,而只有在调用UserMapper时才抛出NullPointerException,项目运行时才会出现异常,增加了我们调试难度
3.3 更利于测试
使用字段注入依赖于 Spring 容器,而单元测试的核心原则是 “隔离依赖”,但字段注入严重依赖 Spring 容器,导致测试困难
3.4 避免循环依赖隐患
在构造阶段就能发现依赖关系异常,而不是运行时才出现异常。
如果 A 依赖 B,B 又依赖 A(循环依赖):
构造函数注入时:Spring 容器启动阶段就会直接抛出BeanCurrentlyInCreationException,强制开发者解决循环依赖(比如通过拆分服务、引入中间层等),从根源上优化代码设计
字段注入时:Spring 会通过 “三级缓存” 暂时解决循环依赖,但仅仅是一种的解决的妥协,本质上是代码设计问题
3.5 总结对比
两种注入方式的对比
| 特性 | 构造函数注入 | 字段注入(@Autowired) |
|---|---|---|
| 依赖声明 | 显式(构造方法参数) | 隐式(类内部字段) |
| 依赖不可变 | 支持(final修饰) | 不支持(无法用final) |
| 依赖缺失检测 | 启动时检测(快速失败) | 运行时检测(隐藏风险) |
| 单元测试友好性 | 高(直接传参,无需容器) | 低(需反射或Spring容器) |
| 循环依赖处理 | 启动时报错(强制解决设计问题) | 允许存在(依赖容器妥协) |
| 与Spring耦合度 | 低(无注解也可工作) | 高(必须依赖@Autowired) |
| 代码可读性 | 高(依赖一目了然) | 低(需通读字段) |
4. 两种注入方式单元测试对比
上述讲解了两种注入方式的区别,我们用一个单元测试的示例来进行以下演示
4.1 字段注入的测试问题
假如我们用 字段注入,代码是这样的:
@Service
public class UserService {
@Autowired
private OrderService orderService;
public void createUser() {
orderService.createOrder();
System.out.println("用户创建成功");
}
}
AI写代码java
运行
12345678910
测试代码:
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
public class UserServiceFieldInjectionTest {
@Test
void testCreateUser() {
UserService userService = new UserService();
// 由于是 private 字段注入,无法直接传入 mock
// 只能用反射去强行设置
OrderService mockOrderService = Mockito.mock(OrderService.class);
try {
java.lang.reflect.Field field = UserService.class.getDeclaredField("orderService");
field.setAccessible(true);
field.set(userService, mockOrderService);
} catch (Exception e) {
throw new RuntimeException(e);
}
userService.createUser();
Mockito.verify(mockOrderService).createOrder();
}
}
AI写代码java
运行
12345678910111213141516171819202122232425
问题:
必须使用 反射 修改私有字段,测试代码繁琐、难维护
如果字段名修改了,测试就会失败
4.2 构造函数注入的测试优势
换成 构造函数注入 的写法:
@Service
public class UserService {
private final OrderService orderService;
// 构造函数注入
public UserService(OrderService orderService) {
this.orderService = orderService;
}
public void createUser() {
orderService.createOrder();
System.out.println("用户创建成功");
}
}
AI写代码java
运行
123456789101112131415
测试代码:
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
public class UserServiceConstructorInjectionTest {
@Test
void testCreateUser() {
// ✅ 直接通过构造函数传入 mock
OrderService mockOrderService = Mockito.mock(OrderService.class);
UserService userService = new UserService(mockOrderService);
userService.createUser();
Mockito.verify(mockOrderService).createOrder();
}
}
AI写代码java
运行
123456789101112131415
优势:
不需要反射,测试代码简洁。
依赖关系在构造函数中显式体现。
如果 UserService 有多个依赖,一眼就能看出它需要什么。
5. 题外话:字段注入真的就无用武之地?
并非所有场景都必须严格禁止字段注入。在一些 非核心业务类 (如配置类、工具类)中,若依赖简单且无需频繁测试,博主也推荐字段注入,例如:
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource; // 简单配置类,偶尔使用字段注入也可接受
// ...
}
AI写代码java
运行
123456
但核心业务逻辑类(Service、Mapper等),博主还是强烈建议使用构造函数注入!
6. 结语
本文详细介绍了Spring依赖注入的两种方式,从代码对比、构造函数注入优点、实际单元测试场景, 并给小伙伴们两种注入方式的使用建议,希望小伙伴们通过本文能测底理解Spring官方为什么推荐的构造函数注入。
总之下次写依赖注入时,不妨多敲几行构造函数 —— 未来的你会感谢这个决定。