2026年4月8日 08:00 北京时间
在Java后端开发体系中,Spring框架的地位早已毋庸置疑——它几乎是每一位Java开发者绕不开的核心基础设施。而支撑Spring实现代码松耦合、高可维护性的灵魂,正是依赖注入(Dependency Injection, DI) 。无数开发者每天熟练地使用@Autowired注解完成对象注入,但一旦被问到“Spring依赖注入的原理是什么”“构造器注入和字段注入有什么区别”,往往语焉不详。本文借助AI助手辅助与整理,系统梳理Spring依赖注入的核心概念、实现方式、底层原理与高频面试题,力求帮助技术学习者从“会用”进阶到“懂其所以然”。

一、痛点切入:为什么我们需要依赖注入?
先看一段传统开发代码:

public class OrderService { private PaymentService payment = new AlipayService(); public void pay() { payment.process(); } }
这段代码有什么问题?直观上看似乎没什么——代码能跑。但一旦需求变化,问题就暴露了:
耦合度过高。 OrderService直接new出了AlipayService,两者形成了强绑定关系。假如某天业务需要切换到微信支付,必须修改OrderService的源代码,重新编译部署-32。这违背了面向对象设计中的“开闭原则”——对扩展开放,对修改关闭。
单元测试困难。 想对OrderService进行单元测试时,无法模拟PaymentService的行为,因为代码里固定创建了真实的实现类-32。
依赖关系失控。 当对象A依赖对象B,对象B又依赖对象C时,开发者需要手动层层创建,代码像蜘蛛网一样缠绕,维护成本急剧攀升-49。
依赖注入正是为解决这些问题而生的设计模式。
二、核心概念讲解:依赖注入(DI)与控制反转(IoC)
依赖注入的定义
依赖注入(Dependency Injection,DI) 是一种设计模式:一个类所依赖的对象,不由该类自身创建,而是由外部容器创建并注入到该类中,以此实现类与类之间的解耦-32。
通俗地说,依赖注入的核心就是“你只管用,不用管它从哪里来” 。就像去餐厅吃饭:你不需要自己买菜、洗菜、炒菜,只需坐在那里,服务员(容器)会把做好的菜(依赖对象)送到你面前。
控制反转(IoC)的定义
控制反转(Inversion of Control,IoC) 是一种设计原则,它将对象的创建、依赖管理权从程序员转移给框架或容器-49。
IoC与DI的关系: IoC是“思想”,DI是“实现”。IoC定义了“控制权反转”的设计理念,而DI则是Spring落地这一理念的具体手段。可以这样记忆:
IoC:把对象的“创建权”交给容器,开发者不再手动
newDI:容器把依赖的对象“送”到需要它的地方
一句话概括:IoC是目标,DI是手段;IoC回答“谁控制谁”,DI回答“怎么控制” -30。
生活化类比:从自助修电脑到品牌整机
传统模式(高耦合) :修电脑需要自己买硬盘、内存条、CPU,一个一个组件手工组装。组件坏了,换起来麻烦不说,还得懂硬件兼容性-24。
依赖注入模式(松耦合) :直接买一台品牌整机,各个组件由厂家(容器)帮你配好、装好、测试好。你需要用电脑时,厂家直接“注入”给你。哪天想换块更大的硬盘,厂家帮你换,你不用自己动手。
这就是依赖注入的魅力——调用者只关心“用什么”,不关心“怎么造”。
三、关联概念讲解:依赖查找(DL)
依赖查找的定义
依赖查找(Dependency Lookup,DL) 是另一种获取依赖对象的方式:调用者主动从容器中“查找”并获取所需的对象实例。
DI与DL的区别
| 维度 | 依赖注入(DI) | 依赖查找(DL) |
|---|---|---|
| 依赖获取方式 | 容器主动推入,被动接收 | 调用者主动查找,主动获取 |
| 代码侵入性 | 低,只需声明依赖 | 高,需调用查找API |
| 与容器耦合度 | 低 | 较高 |
| Spring推荐程度 | 强烈推荐 | 仅限特殊场景 |
Spring同时支持DI和DL,但官方最佳实践明确推荐使用DI,因为DL要求代码主动与容器API交互,增加了耦合-49。
四、概念关系总结
理解IoC、DI、DL三者的关系,一张图就能说清:
IoC(设计思想)——“控制权交给容器” │ └── 实现方式 ├── 依赖注入(DI)——“容器把对象推给你”——推荐 └── 依赖查找(DL)——“你自己去容器里取”——不推荐
记忆口诀:IoC定目标,DI推上门,DL自己去取。
五、代码示例:Spring依赖注入的三种方式
Spring提供了三种主要的依赖注入方式,下面以用户服务层依赖数据访问层为例进行对比。
先定义依赖接口与实现
// 1. 定义接口——面向接口编程 public interface UserDao { User findById(Long id); } // 2. 实现类——可以有多个不同实现 @Repository public class UserDaoImpl implements UserDao { @Override public User findById(Long id) { // 模拟数据库查询 return new User(id, "张三"); } }
方式一:构造器注入(推荐)
@Service public class UserService { // final字段保证不可变 private final UserDao userDao; // 通过构造函数注入——Spring官方首选 public UserService(UserDao userDao) { this.userDao = userDao; } public User getUser(Long id) { return userDao.findById(id); } }
优点:
依赖关系明确,编译期即可见
支持
final字段,保证依赖不可变便于单元测试,直接
new即可传入Mock对象避免空指针异常(NPE),对象创建时所有依赖已就绪
方式二:Setter注入
@Service public class UserService { private UserDao userDao; @Autowired // 可选依赖使用Setter注入 public void setUserDao(UserDao userDao) { this.userDao = userDao; } }
适用场景: 可选依赖、需要在运行时重新注入的场景。
方式三:字段注入(不推荐)
@Service public class UserService { @Autowired // 直接在字段上注入——虽然方便但隐患多 private UserDao userDao; }
为什么不推荐?
违反单一职责原则:字段越来越多时,难以发现类已经“臃肿”
无法使用
final修饰脱离Spring容器后无法手动注入依赖,单元测试困难
容易引发循环依赖问题
对依赖关系“隐藏”,不像构造器注入那样显式暴露
三种方式对比总结
| 注入方式 | 可见性 | 不可变性 | 测试友好度 | 推荐度 |
|---|---|---|---|---|
| 构造器注入 | 显式 | 支持final | 极佳 | ⭐⭐⭐⭐⭐ |
| Setter注入 | 显式 | 不支持 | 良好 | ⭐⭐⭐ |
| 字段注入 | 隐式 | 不支持 | 较差 | ⭐ |
六、底层原理支撑
Spring依赖注入能够“神奇”地工作,背后依赖两大核心技术:
反射机制(Reflection)
Spring在运行时通过Java反射API读取类的信息:获取构造器参数列表、识别字段上的@Autowired注解、调用私有Setter方法进行赋值-。正是反射赋予了Spring“运行时感知代码结构”的能力。
动态代理(Dynamic Proxy)
当使用@Autowired注入时,Spring可能返回的不是原始对象,而是一个代理对象。例如AOP场景下,Spring通过JDK动态代理或CGLib创建代理,将横切逻辑(如事务、日志)织入其中-。
简要流程:
Spring启动时扫描所有带
@Component、@Service等注解的类解析类之间的依赖关系,生成Bean定义(BeanDefinition)
利用反射机制实例化Bean
识别注入点(构造器、Setter、字段),通过反射完成依赖赋值
对于需要增强的Bean,通过动态代理创建代理对象并注入
这两项技术是Spring DI能够“自动化”的底层保障。
七、高频面试题与参考答案
面试题1:依赖注入和控制反转有什么区别?
参考答案:
IoC是一种设计思想,指将对象的创建和管理权从程序内部“反转”给外部容器;DI是实现IoC的具体手段。IoC回答“谁来控制”,DI回答“如何实现控制”。Spring通过DI将依赖对象注入到目标类中,实现了解耦。
面试题2:Spring为什么推荐使用构造器注入?
参考答案:
支持
final字段,保证依赖不可变,增强线程安全依赖关系在编译期即可验证,避免空指针异常
单元测试时可以直接
new对象并传入Mock依赖,脱离Spring容器也能测试构造器参数列表明确暴露了类的依赖需求,符合单一职责原则
避免字段注入容易产生的循环依赖问题
面试题3:Spring依赖注入的三种方式是什么?各有什么优缺点?
参考答案:
构造器注入: 推荐方式,支持不可变字段,依赖显式,测试友好
Setter注入: 适用于可选依赖,支持运行时重新注入
字段注入: 代码最简洁但隐患多,违反单一职责,难以测试,不推荐
面试题4:Spring如何解决循环依赖问题?
参考答案:
Spring通过三级缓存机制解决单例模式下Setter注入产生的循环依赖:
一级缓存:存放完全初始化完成的单例Bean
二级缓存:存放早期暴露的、尚未完成依赖注入的Bean
三级缓存:存放Bean工厂,用于提前暴露对象引用
核心原理:在对象实例化之后、依赖注入之前,Spring将Bean的早期引用放入缓存,使另一个依赖方能够提前获取该引用,从而打破循环-39。
面试题5:字段注入有哪些具体风险?
参考答案:
无法使用
final关键字保证依赖不可变代码对依赖关系“隐藏”,不利于代码审查和维护
脱离Spring容器时无法手动注入,单元测试困难
随着字段增多,容易违反单一职责原则
构造器注入相比,更容易引发循环依赖问题
八、结尾总结
回顾全文,核心知识点梳理如下:
依赖注入(DI) 是Spring实现控制反转(IoC) 设计思想的核心手段,核心目的是解耦与提升可测试性
构造器注入是官方推荐的注入方式,支持不可变性、依赖显式、测试友好
字段注入虽然代码简洁,但存在明显隐患,生产环境应谨慎使用
Spring DI底层依赖反射机制和动态代理两项核心技术
理解IoC与DI的关系,是回答Spring面试题的基础得分点
本文配套完整代码已整理,可在公众号回复“Spring DI”获取。下一篇文章将深入讲解Spring AOP(面向切面编程)的原理与实战应用,敬请期待。