别再乱用 @Autowired!Spring官方推荐的构造函数注入详解
🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用
🍎 《前端技术》专栏以实战为主介绍日常开发中前端应用的一些功能以及技巧,均附有完整的代码示例
✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
👍《Spring Security》专栏中我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~
别再乱用 @Autowired!Spring官方推荐的构造函数注入详解
1. 前言
在我们日常开发SpringBoot项目中,依赖注入(Dependency Injection, DI)是核心机制之一,最常见的写法有两种:
字段注入:在类的属性上直接使用 @Autowired 注解
构造函数注入:通过构造函数参数注入依赖
相信不少小伙伴都喜欢在字段上直接使用@Autowired,因为字段注入用起来更简洁,但是Spring官方却明确推荐优先使用构造函数注入!这两种方式有何本质区别?为什么官方有明确的推荐?
本文博主将从代码对比、构造函数注入优点、实际单元测试场景 三个维度,带小伙伴们彻底搞懂其中的门道

2. 两种注入方式对比
假设我们有一个UserService,它依赖于UserMapper(数据访问层),我们需要在UserService中注入UserMapper
2.1 字段注入(@Autowired 在字段上)
这是很多小伙伴喜欢的方式,写法相对简洁直接:
@ServicepublicclassUserService{// 直接在字段上使用@Autowired注入@AutowiredprivateUserMapper userMapper;// 业务方法publicStringgetUser(Long id){return userMapper.findUserById(id);}}2.2 构造函数注入(官方推荐)
通过类的构造方法注入依赖,无需在字段上添加注解:
@ServicepublicclassUserService{// 依赖被声明为final,确保不可变privatefinalUserMapper userMapper;// 通过构造函数注入publicUserService(UserMapper userMapper){this.userMapper = userMapper;}// 业务方法publicStringgetUser(Long id){return userMapper.findUserById(id);}}使用Lombok 简化写法
如果你的项目中有使用了Lombok,那么构造函数注入的方式可以大大简化我们的写法,因为Lombok的@AllArgsConstructor注解会自动为类中的所有 final 或非 static 字段生成一个全参数构造函数

为了演示 :我们假设还有一个订单Mapper也需要注入
@Service@AllArgsConstructor//Lombok注解publicclassUserService{// 依赖被声明为final,确保不可变privatefinalUserMapper userMapper;privatefinalOrderMapper orderMapper;/** * 无需手动编写构造函数,Lombok会在编译时生成: public UserService(UserMapper userMapper, OrderMapper orderMapper) { this.userMapper = userMapper; this.orderMapper = orderMapper; } **/// 业务方法publicStringgetUser(Long id){return userMapper.findUserById(id);}}通过Lombok的注解大家会发现写法也简洁了很多!
3. 构造函数注入方式的优点
3.1 依赖不可变
通过上面的例子我们可以看出,使用构造函数注入可以声明把依赖声明为final
privatefinalUserMapper userMapper;使用了final,字段在对象创建时必须初始化,且初始化后无法修改,这就表示了:
- 依赖不会被意外篡改(线程安全)
- 强制依赖在对象创建时就必须注入(避免后续使用时为null)
@Autowired字段注入无法使用final(因为字段注入是在对象创建后通过反射赋值的),依赖可能被中途修改,存在安全隐患
3.2 依赖明确,避免空指针
还是上述示例代码
publicUserService(UserMapper userMapper,OrderMapper orderMapper){this.userMapper = userMapper;this.orderMapper = orderMapper;}小伙伴们会发现构造函数注入时,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 字段注入的测试问题
假如我们用 字段注入,代码是这样的:
@ServicepublicclassUserService{@AutowiredprivateOrderService orderService;publicvoidcreateUser(){ orderService.createOrder();System.out.println("用户创建成功");}}测试代码:
importorg.junit.jupiter.api.Test;importorg.mockito.Mockito;publicclassUserServiceFieldInjectionTest{@TestvoidtestCreateUser(){UserService userService =newUserService();// 由于是 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){thrownewRuntimeException(e);} userService.createUser();Mockito.verify(mockOrderService).createOrder();}}问题:
必须使用 反射 修改私有字段,测试代码繁琐、难维护
如果字段名修改了,测试就会失败
4.2 构造函数注入的测试优势
换成 构造函数注入 的写法:
@ServicepublicclassUserService{privatefinalOrderService orderService;// 构造函数注入publicUserService(OrderService orderService){this.orderService = orderService;}publicvoidcreateUser(){ orderService.createOrder();System.out.println("用户创建成功");}}测试代码:
importorg.junit.jupiter.api.Test;importorg.mockito.Mockito;publicclassUserServiceConstructorInjectionTest{@TestvoidtestCreateUser(){// ✅ 直接通过构造函数传入 mockOrderService mockOrderService =Mockito.mock(OrderService.class);UserService userService =newUserService(mockOrderService); userService.createUser();Mockito.verify(mockOrderService).createOrder();}}优势:
不需要反射,测试代码简洁。
依赖关系在构造函数中显式体现。
如果 UserService 有多个依赖,一眼就能看出它需要什么。
5. 题外话:字段注入真的就无用武之地?
并非所有场景都必须严格禁止字段注入。在一些非核心业务类(如配置类、工具类)中,若依赖简单且无需频繁测试,博主也推荐字段注入,例如:
@ConfigurationpublicclassAppConfig{@AutowiredprivateDataSource dataSource;// 简单配置类,偶尔使用字段注入也可接受// ...}但核心业务逻辑类(Service、Mapper等),博主还是强烈建议使用构造函数注入!
6. 结语
本文详细介绍了Spring依赖注入的两种方式,从代码对比、构造函数注入优点、实际单元测试场景, 并给小伙伴们两种注入方式的使用建议,希望小伙伴们通过本文能测底理解Spring官方为什么推荐的构造函数注入。
总之下次写依赖注入时,不妨多敲几行构造函数 —— 未来的你会感谢这个决定。
如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言,最后希望大家一键三连给博主一点点鼓励!
专栏最新回顾
【01】ThreadLocal的原理以及实际应用技巧详解 - 如何在身份认证场景Token中传递获取用户信息
【02】基于MyBatis-Plus Dynamic-Datasource实现 SaaS 系统动态租户数据源管理
【03】基于nacos实现动态线程池设计与实践:告别固定配置,拥抱弹性调度
【04】Java常用加密算法详解与实战代码 - 附可直接运行的测试示例
【05】Java synchronized 锁机制深度解析与实战指南 - 银行转账案例
【06】还在为线上BUG苦苦找寻?试试IntelliJ IDEA远程调试线上Java程序
【07】使用 Apache Commons Exec 自动化脚本执行实现 MySQL 数据库备份
【08】JAVA开发中几个常用的lambda表达式!记得收藏起来哦~
【09】看完!我不允许你还不知道 Spring Boot如何读取Resource目录文件
【10】分词搜索必须上Elasticsearch?试试MySQL分词查询,轻松满足大多数搜索场景的需求
【11】Java 状态机详解 - 三种状态机实现方式优雅消灭 if-else 嵌套
【12】别再踩坑!Spring事务@Transactional失效?一文读懂参数与8大失效场景