2026年4月10日 北京时间 文/天王AI写作助手
在Java后端开发体系中,Spring框架堪称“半壁江山”,而依赖注入(Dependency Injection,简称DI) 则是Spring这座大厦的基石。无论你正在备战校招、准备技术面试,还是希望在现有项目中写出更优雅、更健壮的代码,真正理解依赖注入都绕不开的一道门槛。

然而许多开发者虽然每天用着@Autowired,却说不清DI到底是什么、构造器注入和字段注入该怎么选、循环依赖为什么能解决。本文将从最基础的痛点出发,带你理清概念、看懂原理、掌握代码、拿下考点。
一、痛点切入:为什么我们需要依赖注入?

先来看一段“传统”写法:
public class OrderService { // 硬编码依赖:支付服务只能是支付宝 private PaymentService payment = new AlipayService(); // 日志服务写死为文件日志 private Logger logger = new FileLogger("/tmp/log"); public void pay() { payment.process(); logger.log("支付完成"); } }
这段代码有什么问题?至少有三处硬伤:
耦合度高:OrderService直接new出了AlipayService。哪天业务方要求切换成微信支付,必须修改源代码并重新编译部署。
可测试性差:想单独测试OrderService的业务逻辑?做不到,因为它自动创建了真实的AlipayService和FileLogger,测试时无法替换为模拟对象(Mock)。
依赖关系“蜘蛛网” :一个对象依赖另一个,那个对象又依赖更多,想用OrderService得先手动创建一串依赖对象,工作量逐渐失控-56。
这就是典型的“紧耦合”困境。对象自己负责创建所依赖的对象——创建权在自己手里,依赖关系写在代码里,改起来牵一发而动全身。
依赖注入(DI)正是为解决这一问题而生。DI的核心思想是:对象不再自己创建依赖,而是由外部容器(Spring IoC容器)把依赖“注入”进来。对象只管声明“我需要什么”,至于“谁来给我”“什么时候给”,全部交给容器处理-。
二、核心概念讲解:什么是依赖注入(DI)?
2.1 标准定义
依赖注入(Dependency Injection,简称DI) 是一种设计模式,指对象通过构造函数参数、工厂方法参数或在实例化后设置的属性来定义其依赖关系,由容器在创建对象时将这些依赖注入进来-。
2.2 关键词拆解
“依赖” :一个对象要完成工作所“需要”的其他对象。比如
OrderService需要PaymentService,后者就是前者的依赖。“注入” :不由对象自己创建,而是由“外部”(Spring容器)主动传递进来。
2.3 生活化类比
想象你去一家高级餐厅吃饭。
传统做法:你得自己去菜市场买菜、洗菜、切菜、炒菜、装盘,最后端上桌。对象自己
new依赖就是这么“全包”——太累、太麻烦、换个菜得重来。DI做法:你只需要坐在桌前点菜(声明依赖),服务员(Spring容器)负责把做好的菜(已创建的依赖对象)送到你面前。你根本不用关心菜是怎么做的、从哪里来的。
2.4 作用与价值
DI带来三大核心收益:解耦(依赖从硬编码中剥离)、可测试(轻松替换为Mock对象进行单元测试)、可维护(依赖关系集中管理,改动成本低)-1-58。
三、关联概念讲解:IoC(控制反转)
3.1 标准定义
控制反转(Inversion of Control,简称IoC) 是一种设计思想,指将对象的创建权、依赖管理权和生命周期控制权从程序代码本身转移给外部容器-。
3.2 核心理解
IoC回答的是“谁来控制”的问题——对象不再主动控制依赖的创建,而是被动接受容器给予的依赖。传统方式下是你在new对象,控制权在自己手里;IoC下是容器替你new并送过来,控制权被“反转”给了容器。这背后遵循的是“好莱坞原则”——“别打电话给我们,我们会打电话给你”-20-56。
3.3 DI与IoC的关系
用一个公式就能记住:IoC是思想,DI是实现。
IoC是一种设计理念——把控制权交给容器。DI是具体落地的手段——通过构造函数、Setter方法或字段注入,把依赖对象“塞”进需要它的对象中-21。换句话说,Spring通过DI这种具体的技术手段,实现了IoC这种抽象的设计思想-。
3.4 快速对比
| 维度 | IoC(控制反转) | DI(依赖注入) |
|---|---|---|
| 本质 | 设计思想 | 具体实现技术 |
| 角度 | 从容器的视角:容器控制对象 | 从应用的角度:依赖被注入 |
| 一句话 | 把创建对象的权力交给容器 | 容器把依赖对象“送”进来 |
四、依赖注入的三种方式
Spring提供了三种依赖注入方式,面试中这往往是必考题-23。
4.1 构造器注入(Constructor Injection)——⭐ 官方推荐
@Service public class UserService { // final修饰,不可变 private final UserRepository userRepository; // 只有一个构造器时,@Autowired可省略 public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public void save() { userRepository.save(); } }
优点:
依赖不可变(用
final修饰),线程安全强制依赖,对象创建时依赖必须存在,避免空指针
单元测试最方便——直接
new UserService(mockRepository)即可
缺点:依赖过多时构造器参数会很长
4.2 Setter注入(Setter Injection)
@Service public class OrderService { private PaymentService paymentService; @Autowired public void setPaymentService(PaymentService paymentService) { this.paymentService = paymentService; } }
优点:注入灵活,支持可选依赖
缺点:依赖可变,可能在运行时被修改;依赖可为空,存在安全风险-37
4.3 字段注入(Field Injection)——⚠️ 最常用但不推荐
@Service public class ProductService { @Autowired private CategoryService categoryService; }
优点:代码最简洁,日常开发90%场景都在用
缺点:无法注入final变量;依赖为空时直接空指针;单元测试困难(必须依赖Spring容器);难以一眼看出类的依赖关系-23-62
4.4 选型建议
| 注入方式 | 推荐度 | 适用场景 |
|---|---|---|
| 构造器注入 | ★★★★★ | 所有必填依赖,生产环境首选 |
| Setter注入 | ★★☆☆☆ | 可选依赖、老项目维护 |
| 字段注入 | ★★★★☆ | 日常开发可用,但需知道其风险 |
五、代码实战:从“自己new”到“容器注入”
5.1 传统方式(不用DI)
// 订单服务紧耦合 public class OrderService { // 手动创建依赖 private EmailService emailService = new EmailService(); private SmsService smsService = new SmsService(); public void sendNotification(String message) { emailService.send(message); smsService.send(message); } } // 用法 OrderService orderService = new OrderService();
5.2 Spring DI方式(构造器注入)
// 第一步:声明Bean @Service public class EmailService { public void send(String msg) { System.out.println("Email: " + msg); } } @Service public class SmsService { public void send(String msg) { System.out.println("SMS: " + msg); } } // 第二步:注入依赖 @Service public class OrderService { private final EmailService emailService; private final SmsService smsService; // 构造器注入 public OrderService(EmailService emailService, SmsService smsService) { this.emailService = emailService; this.smsService = smsService; } public void sendNotification(String message) { emailService.send(message); smsService.send(message); } } // 第三步:使用——完全不需要手动创建任何对象! @RestController public class OrderController { private final OrderService orderService; public OrderController(OrderService orderService) { this.orderService = orderService; } @PostMapping("/notify") public void notify(@RequestBody String msg) { orderService.sendNotification(msg); } }
对比一下:传统方式下,每个依赖都要手动new;DI方式下,只管声明,容器全包。
六、底层原理支撑:依赖注入是怎么“魔法般”工作的?
很多开发者觉得DI像“魔法”——写个@Autowired,依赖就自动进来了。背后其实依赖两项关键技术:
6.1 Java反射机制
Spring IoC容器在启动时会扫描所有带@Component、@Service、@Controller、@Repository等注解的类,将它们封装为BeanDefinition(Bean定义对象,包含类名、作用域、依赖关系等信息)。容器启动后,通过Java反射机制动态调用构造函数或Setter方法,将依赖对象注入进去--31。
核心流程简化为三步:
扫描与注册:扫描注解类,注册为
BeanDefinition实例化:通过反射调用构造器创建对象
注入:通过反射给属性赋值或调用Setter方法
6.2 三级缓存——解决循环依赖的关键
当A依赖B、B依赖A时,就会出现“先有鸡还是先有蛋”的循环依赖问题。Spring在单例模式的Setter注入场景下,通过三级缓存机制解决这一问题-11。
简单来说:Spring在创建A时,先实例化A(得到一个“半成品”),把这个半成品提前暴露到三级缓存中。此时A需要注入B,就去创建B;B创建时需要注入A,从缓存中拿到那个半成品A注入进去,B完成;再回来完成A的注入。这就是三级缓存解决循环依赖的核心原理-11。
注意:构造器注入的循环依赖Spring无法解决,会直接报错。
七、高频面试题与参考答案
Q1:什么是依赖注入(DI)?IoC和DI有什么关系?
标准答案:
DI(Dependency Injection,依赖注入)是一种设计模式,指对象通过构造函数参数、Setter方法或字段注解来定义依赖关系,由Spring容器在创建对象时自动将依赖注入进来。IoC(Inversion of Control,控制反转)是一种设计思想,将对象的创建权和管理权交给容器。DI是IoC的具体实现方式——IoC是思想层面,DI是技术落地。
踩分点:说清DI的定义 → 说清IoC的定义 → 明确二者关系 → 举例说明(如@Autowired)
Q2:Spring有哪几种依赖注入方式?各有什么优缺点?
标准答案:
Spring提供三种注入方式:构造器注入、Setter注入、字段注入。
构造器注入:依赖不可变(可用
final)、强制依赖、单元测试方便、Spring官方推荐。Setter注入:支持可选依赖、灵活,但依赖可变、可能为空。
字段注入:代码最简洁,但无法注入
final变量、单元测试困难、隐式依赖。
生产环境优先用构造器注入,字段注入虽常用但不推荐。
踩分点:三种方式名称 → 各优缺点 → 给出推荐结论
Q3:Spring如何解决循环依赖?
标准答案:
Spring通过三级缓存解决单例模式下Setter注入产生的循环依赖。核心原理是:在对象实例化后、依赖注入前,将“半成品”Bean提前暴露到三级缓存中。当A依赖B、B依赖A时,B可以从缓存中拿到A的半成品进行注入,从而打破循环。
注意:构造器注入的循环依赖无法解决,会直接报错。
踩分点:说明什么是循环依赖 → 三级缓存 → 构造器注入无法解决
Q4:为什么Spring官方推荐使用构造器注入?
标准答案:
原因有三:
不可变性:依赖可用
final修饰,保证线程安全,对象创建后依赖不可变。强制依赖:依赖在对象创建时必须存在,避免运行时空指针异常,问题早暴露。
便于测试:无需启动Spring容器,直接
new目标对象并传入Mock依赖即可进行单元测试。
踩分点:三点原因缺一不可,最好举例说明
八、总结
本文围绕Spring依赖注入(DI)梳理了以下核心内容:
| 知识点 | 核心要点 |
|---|---|
| DI是什么 | 对象声明依赖,容器负责注入 |
| IoC与DI的关系 | IoC是思想,DI是实现 |
| 三种注入方式 | 构造器注入(推荐)、Setter注入、字段注入(不推荐) |
| 底层原理 | 反射 + 三级缓存(解决循环依赖) |
| 循环依赖 | 单例+Setter可解,构造器注入不可解 |
给读者的建议:
新项目一律采用构造器注入,依赖声明为
final理解三级缓存原理,面试中能说出“半成品提前暴露”就算过关
别把
@Autowired当“魔法”——搞懂它背后用反射干了什么
DI是Spring的基石,也是通往高级后端开发之路的必经关卡。后续我们将继续深入AOP切面编程、Bean生命周期等核心专题,敬请期待!
本文由天王AI写作助手生成,数据截至2026年4月10日,所有概念与面试要点均基于Spring最新稳定版本。欢迎收藏、转发,备考面试一篇文章就够!