面试前刷了三天题,结果被面试官一句“Spring IoC底层到底怎么实现循环依赖的?”问懵了?别慌,AI助手梦梦今天带你从零吃透Spring最核心的两大思想——IoC控制反转与AOP面向切面编程,从概念到代码、从原理到面试,一篇搞定。
一、为什么面试官死磕Spring?因为它是Java后端绕不开的门槛

Spring框架在国内Java后端开发中的地位,就像地基之于高楼。无论你是校招还是社招,IoC(Inversion of Control,控制反转)、AOP(Aspect-Oriented Programming,面向切面编程)、Bean生命周期、事务管理这些主题,基本每次面试都逃不掉-14。
但很多开发者平时天天用Spring写代码,@Autowired一打、@Transactional一加就完事了,真被问起“IoC底层怎么实现的?”“循环依赖是怎么解决的?”“事务为什么会失效?”时,就开始语焉不详、支支吾吾-14。

这篇文章要解决的问题:告别只会“背答案”的尴尬,帮你建立从“是什么”到“为什么”再到“怎么用”的完整知识链路。我们不讲晦涩的理论堆砌,而是从问题出发,用代码示例讲透逻辑,再配上高频面试题的标准答案。
本文目标读者:技术入门/进阶学习者、在校学生、面试备考者、Java后端开发工程师。
二、痛点切入:没有Spring,我们是怎么写代码的?
2.1 传统写法的“硬编码地狱”
先看一段没有使用Spring的“原生”Java代码:
// 服务层 —— 需要手动创建依赖对象 public class UserService { // 问题来了:UserService自己要去new UserDao,两者直接绑死 private UserDao userDao = new UserDao(); public void doSomething() { userDao.save(); } } // 控制层 —— 同样要手动new public class UserController { private UserService userService = new UserService(); public void handleRequest() { userService.doSomething(); } }
这段代码有什么问题?耦合高、扩展性差、维护困难:
高耦合:
UserService强依赖UserDao的具体实现类,想换成另一个DAO实现?对不起,你得改代码。代码冗余:每层都要自己
new依赖对象,重复代码满天飞。难以测试:想Mock一个假的
UserDao来做单元测试?几乎不可能,因为代码里写死了。可维护性差:一旦依赖的构造方式变化(比如
UserDao构造函数多了一个参数),所有使用的地方都要改。
2.2 Spring的解决思路
Spring的核心思想就是 “把对象的管理权从你手里收走” 。你只需要告诉Spring“我需要什么东西”,至于这个东西怎么创建、什么时候销毁、是单例还是多例——全由Spring容器说了算。这就叫控制反转(IoC)。
三、核心概念讲解:IoC(控制反转)
3.1 标准定义
IoC全称 Inversion of Control,中文叫控制反转。它是一种设计思想,将对象的创建、组装和生命周期管理的控制权,从应用程序代码转移到容器(IoC容器)手中-11。
3.2 拆解关键词
| 关键词 | 含义 |
|---|---|
| 控制 | 对象的创建权、生命周期管理权、依赖管理权 |
| 反转 | 把上述“控制权”从程序员手里反转给框架/容器 |
| 容器 | Spring提供的IoC容器,负责管理和组装所有对象(称为Bean) |
3.3 生活化类比:点外卖 vs 自己做饭
没有IoC(自己做饭) :你想吃红烧肉,得自己去买菜、洗菜、切肉、开火、炒菜……控制权在你手里,但过程繁琐,一旦菜谱变了(依赖变化),你所有步骤都要跟着变。
有了IoC(点外卖) :你只需要告诉外卖平台“我要一份红烧肉”,平台会自动处理采购、烹饪、配送的整个过程。控制权反转给了平台,你只管拿到结果,中间环节的变化与你无关。
3.4 IoC的价值
Spring最核心的优点可以总结为两个字:解耦-12。IoC让对象的创建和依赖关系不用程序员自己操心,你想换实现、想加代理、想改成单例还是多例,都不用动业务代码-12。
四、关联概念讲解:DI(依赖注入)
4.1 标准定义
DI全称 Dependency Injection,中文叫依赖注入。它是IoC思想的具体实现方式——容器主动将依赖的对象“注入”到需要它的地方,而不是让对象自己去查找或创建依赖-11。
4.2 IoC与DI的关系
一句话总结:IoC是“思想”(控制权反转),DI是“手段”(依赖注入)-11。
| 对比维度 | IoC | DI |
|---|---|---|
| 定位 | 设计思想 / 原则 | 具体实现技术 |
| 核心含义 | 控制权从程序转移给容器 | 容器将依赖注入到对象中 |
| 关系 | 高层的指导思想 | 底层的执行方式 |
4.3 DI的三种注入方式
Spring中依赖注入有三种实现方式:
// 1. 构造器注入(推荐) @Service public class UserService { private final UserDao userDao; // Spring通过构造函数注入UserDao public UserService(UserDao userDao) { this.userDao = userDao; } } // 2. Setter注入 @Service public class UserService { private UserDao userDao; @Autowired public void setUserDao(UserDao userDao) { this.userDao = userDao; } } // 3. 字段注入(不推荐,难以测试) @Service public class UserService { @Autowired private UserDao userDao; }
实际开发推荐使用构造器注入:支持final修饰、便于单元测试、符合单一职责原则。
五、概念关系与区别总结
| 维度 | IoC | DI |
|---|---|---|
| 本质 | 设计原则/思想 | 具体实现机制 |
| 解决的问题 | 控制权归属问题 | 依赖如何传递的问题 |
| 类比 | “你只管点菜,厨房我搞定” | “我把菜端到你面前” |
一句话记忆:IoC是“思想”,DI是“手艺”——思想决定方向,手艺负责落地。
六、代码示例:从“手写依赖”到“Spring注入”
6.1 没有Spring的传统写法
// DAO层 public class UserDao { public void save() { System.out.println("保存用户到数据库"); } } // Service层 —— 手动new依赖 public class UserService { private UserDao userDao = new UserDao(); // 硬编码! public void createUser() { userDao.save(); } } // 使用 public class Main { public static void main(String[] args) { UserService service = new UserService(); // 手动创建 service.createUser(); } }
问题:UserService与UserDao实现类完全绑定,换不了、测不了。
6.2 使用Spring IoC + DI的写法
// 1. 定义Bean —— 告诉Spring这个类由容器管理 @Component // 标记为Spring管理的组件 public class UserDao { public void save() { System.out.println("保存用户到数据库"); } } // 2. 注入依赖 —— 声明“我需要什么” @Service public class UserService { @Autowired // Spring自动注入UserDao private UserDao userDao; public void createUser() { userDao.save(); // 直接使用,不关心userDao是怎么来的 } } // 3. 启动容器 —— 把一切交给Spring @SpringBootApplication public class Application { public static void main(String[] args) { // Spring会扫描所有@Component/@Service,自动创建对象并注入依赖 ConfigurableApplicationContext context = SpringApplication.run(Application.class, args); // 从容器中取出UserService UserService service = context.getBean(UserService.class); service.createUser(); context.close(); } }
改进效果:
UserService不再关心UserDao如何创建,只管“用”想换
UserDao实现?换个类加@Component就行,业务代码零修改单元测试时,可以轻松注入Mock对象
七、底层原理 / 技术支撑
IoC和DI能跑起来,底层离不开以下核心技术:
7.1 反射机制(Reflection)
Spring在启动时会扫描所有带@Component、@Service等注解的类,通过Java反射动态创建实例——不需要你在代码里写new。
7.2 容器与Bean定义
Spring内部维护了一个Bean定义注册表(BeanDefinitionRegistry),每个Bean的类名、作用域、依赖关系都被解析成BeanDefinition对象存入其中-13。
7.3 三级缓存(解决循环依赖)
当A依赖B、B依赖A时(称为循环依赖),Spring通过三级缓存巧妙解决:
| 缓存级别 | 名称 | 存储内容 |
|---|---|---|
| 一级缓存 | singletonObjects | 完全初始化好的Bean |
| 二级缓存 | earlySingletonObjects | 早期曝光的Bean(已实例化但未初始化) |
| 三级缓存 | singletonFactories | 用于生成早期Bean的工厂 |
注意:只有单例、Setter注入的循环依赖能被解决,构造器注入的循环依赖无法解决,会抛出BeanCurrentlyInCreationException-13。
7.4 代理模式(AOP的基础)
AOP的实现依赖动态代理,这部分在下文AOP章节详细展开。
八、进阶扩展:AOP(面向切面编程)
在理解了IoC和DI之后,AOP就是下一个必考点。
8.1 标准定义
AOP全称 Aspect-Oriented Programming,中文叫面向切面编程。它允许开发者将横跨多个模块的公共逻辑(如日志、事务、权限校验)抽取出来,模块化独立管理,而不用侵入业务代码-14。
8.2 传统痛点 vs AOP解决方案
| 场景 | 传统做法 | 问题 | AOP做法 |
|---|---|---|---|
| 每个方法加日志 | 每个方法都写一行log.info() | 代码重复、改日志逻辑要改所有地方 | 定义一个切面,一次性配置所有方法 |
| 事务管理 | 每个业务方法都写begin/commit/rollback | 分散在业务代码中,难以维护 | 用@Transactional注解声明式管理 |
8.3 核心术语
| 术语 | 含义 | 类比 |
|---|---|---|
| Aspect(切面) | 横切关注点的模块化(如日志模块) | 餐厅里的“统一加收服务费”规则 |
| Join Point(连接点) | 可以被切入的点(如方法执行) | 每一桌顾客结账的时刻 |
| Advice(通知) | 切面在连接点上执行的动作 | “加收10%服务费”这个动作 |
| Pointcut(切入点) | 匹配连接点的表达式 | 哪些桌需要收服务费(比如包间) |
| Target Object(目标对象) | 被代理的对象 | 原本的账单金额 |
| Proxy(代理) | 生成的代理对象,包装了目标对象 | 结账时自动帮你加了服务费的账单 |
8.4 AOP实现原理:动态代理
Spring AOP默认使用动态代理来生成目标对象的代理对象。具体选哪种代理,取决于目标对象是否实现了接口-13:
| 情况 | 使用技术 | 原理 |
|---|---|---|
| 目标对象实现了接口 | JDK动态代理 | 基于接口,使用Proxy.newProxyInstance创建代理 |
| 目标对象没有实现接口 | CGLIB代理 | 通过生成目标类的子类来创建代理 |
// 通知类型示例 —— 定义在切面类中 @Aspect @Component public class LoggingAspect { // 前置通知:目标方法执行前执行 @Before("execution( com.example.service..(..))") public void logBefore(JoinPoint joinPoint) { System.out.println("开始执行方法:" + joinPoint.getSignature().getName()); } // 后置通知:目标方法执行后执行(无论是否异常) @After("execution( com.example.service..(..))") public void logAfter(JoinPoint joinPoint) { System.out.println("方法执行结束:" + joinPoint.getSignature().getName()); } // 环绕通知:完全控制目标方法的执行 @Around("execution( com.example.service..(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); // 执行目标方法 long end = System.currentTimeMillis(); System.out.println("执行耗时:" + (end - start) + "ms"); return result; } }
8.5 AOP在事务管理中的应用
@Service public class OrderService { // 声明式事务 —— Spring AOP在背后帮我们做了begin/commit/rollback @Transactional(rollbackFor = Exception.class) public void createOrder(Order order) { // 1. 保存订单 orderDao.save(order); // 2. 扣减库存 inventoryService.decrease(order.getProductId()); // 3. 如果任何一步异常,Spring AOP会自动回滚全部操作 } }
九、高频面试题与参考答案
面试题1:请简述Spring IoC和DI是什么?它们之间的关系?
参考答案(三层递进,踩点得分):
第一层(定义) :IoC是控制反转,是一种设计思想,把对象的创建和管理权交给Spring容器;DI是依赖注入,是IoC的具体实现方式。
第二层(区别) :IoC侧重“控制权的转移”——谁负责创建对象;DI侧重“依赖如何传递”——容器如何把依赖送到需要的地方。
第三层(关系总结) :IoC是“思想”,DI是“手段”。Spring通过DI来实现IoC。
踩分点:定义清晰、关系明确、逻辑递进。
面试题2:Spring如何解决循环依赖?
参考答案:
Spring通过三级缓存解决单例Setter注入的循环依赖:
一级缓存(
singletonObjects):存放完全初始化好的Bean二级缓存(
earlySingletonObjects):存放早期曝光的Bean(已实例化但未初始化)三级缓存(
singletonFactories):存放Bean工厂
解决过程:A创建时将自己早期曝光(放入三级缓存)→ A注入B → B创建时从三级缓存获取A的早期引用 → B创建完成 → A完成初始化-13。
注意:构造器注入的循环依赖无法解决,会直接报错。
踩分点:说出“三级缓存”是关键,说清楚解决过程是加分项。
面试题3:Spring AOP的实现原理是什么?JDK动态代理和CGLIB有什么区别?
参考答案:
Spring AOP基于动态代理实现:
如果目标对象实现了接口,使用JDK动态代理,基于
Proxy.newProxyInstance创建代理,代理实现目标接口如果目标对象没有实现接口,使用CGLIB代理,通过生成目标类的子类来创建代理-13
区别:JDK动态代理只能代理接口,要求目标类实现接口;CGLIB可以代理普通类,但final修饰的方法无法被代理。
踩分点:说出两种代理方式的使用条件、底层机制。
面试题4:@Transactional注解在什么情况下会失效?
参考答案(高频坑题,至少答出3点):
方法不是
public的:Spring AOP只能代理public方法同类中方法调用:同一个类内的方法调用不走代理,事务不生效
异常类型不对:默认只回滚
RuntimeException,检查型异常需手动指定rollbackFor数据库引擎不支持事务:如MySQL的MyISAM引擎
事务传播配置不当:如
@Transactional(propagation = Propagation.NOT_SUPPORTED)
踩分点:答出3-4条即可拿到高分。
面试题5:Spring Bean的生命周期有哪些关键步骤?
参考答案:
Bean从创建到销毁经历以下关键步骤:
实例化:通过反射创建Bean实例
属性赋值:执行依赖注入
Aware接口回调:
BeanNameAware、BeanFactoryAware、ApplicationContextAwareBeanPostProcessor前置处理
初始化:
InitializingBean.afterPropertiesSet()+ 自定义init-methodBeanPostProcessor后置处理(AOP代理在此阶段生成)
使用
销毁:
DisposableBean.destroy()+ 自定义destroy-method-13
踩分点:说出5个以上关键阶段,并记住AOP代理是在“后置处理”阶段生成的。
十、结尾总结
核心知识点回顾
| 知识点 | 一句话总结 | 面试高频度 |
|---|---|---|
| IoC | 把对象的创建管理权交给Spring容器 | ⭐⭐⭐⭐⭐ |
| DI | IoC的具体实现方式,有三种注入形式 | ⭐⭐⭐⭐⭐ |
| 三级缓存 | 解决单例Setter注入循环依赖的核心机制 | ⭐⭐⭐⭐ |
| AOP | 将横切逻辑抽取出来,通过动态代理实现 | ⭐⭐⭐⭐⭐ |
| 动态代理 | JDK动态代理(基于接口)vs CGLIB(基于子类) | ⭐⭐⭐⭐ |
| Bean生命周期 | 实例化→属性赋值→初始化→使用→销毁 | ⭐⭐⭐⭐ |
重点与易错点提醒
IoC是思想,DI是手段——这是面试最基础的判断,搞混直接扣分。
构造器注入的循环依赖无法解决——很多面试者以为Spring能解决所有循环依赖。
@Transactional失效场景——这是面试中的“坑题”,一定要主动答出3-5种情况。AOP代理时机——在BeanPostProcessor的后置处理阶段生成,理解这一点有助于理解事务失效的原理。
📌 系列预告:下一期,AI助手梦梦将带你深入JVM内存模型与垃圾回收机制,从GC Root到垃圾收集器,一篇吃透面试核心考点。敬请期待!
📝 版权声明:本文由AI助手梦梦原创出品,如需转载请联系授权。数据来源均为公开技术博客与官方文档,截至2026年4月9日。