从原理到实战:Spring IoC/DI 核心知识体系与高频面试题全解

前言

Spring 的 IoC(控制反转)DI(依赖注入) 是 Spring 框架最核心的基石。理解它们不仅是掌握 Spring 的前提,也是 Java 后端开发面试中的必考题。很多开发者对这两个概念的理解仅停留在"会用注解"的层面,一旦遇到循环依赖、AOP 失效或容器启动异常等问题便束手无策。


一、 核心概念解析

1. IoC (Inversion of Control) - 控制反转

IoC 是一种架构设计思想,而非具体的技术实现。

  • 什么是"控制"?
    在传统 Java SE 开发中,对象的创建、初始化、销毁以及对象间依赖关系的维护,都是由开发者在代码中通过 new 关键字主动控制的。这种"控制权"牢牢掌握在业务代码手中。
  • 什么是"反转"?
    Spring 将对象的创建权、生命周期管理权从开发者手中转移到了 IoC 容器 手中。开发者不再主动创建对象,而是被动地接收容器注入的对象。
  • 为什么要反转?
    • 解耦:类与类之间不再直接硬编码依赖,而是依赖于抽象(接口)和容器配置,修改实现类无需改动调用方代码。
    • 可测试性:单元测试时可以轻松替换 Mock 对象,无需修改业务代码,也无需启动完整容器。
    • 统一管理:集中管理对象的生命周期、事务、AOP 代理等横切关注点,避免资源泄漏和重复创建。

💡 核心总结

IoC 是目的,DI 是实现 IoC 的手段。没有 DI,IoC 只是一个空泛的概念;而除了 DI,IoC 理论上也可以通过服务定位器(Service Locator)模式实现,但 Spring 选择了更优雅的 DI,因为 DI 对单元测试更友好,且不引入对容器 API 的直接依赖。

2. DI (Dependency Injection) - 依赖注入

DI 是 IoC 的具体实现方式。它描述了容器如何将依赖关系"注入"到组件中。Spring 提供了三种主流注入方式,它们在工程实践中有着严格的适用边界,绝非可以随意互换:

注入方式 描述 优点 缺点 Spring 推荐度
构造器注入 通过构造函数参数注入依赖 ① 保证对象不可变且必需依赖不为 null② 方便单元测试(无需反射)③ 避免循环依赖暴露问题 参数过多时构造函数冗长 ⭐⭐⭐⭐⭐ (官方首推)
Setter 注入 通过 setter 方法注入依赖 灵活,适合可选依赖 ① 对象可能处于未完全初始化状态② 无法保证依赖不可变 ⭐⭐⭐ (仅用于可选依赖)
字段注入 直接在字段上使用 @Autowired 代码最简洁 ① 隐藏了依赖关系② 无法在单元测试中手动注入③ 容易忽略循环依赖风险 ⭐ (不推荐,但广泛存在)

⚠️ 为什么 Spring 官方强烈推荐构造器注入?

  1. 强制契约 :构造函数参数明确了该 Bean 正常工作所必须的依赖,缺少依赖时启动即报错,符合"快速失败"原则。
  2. Immutability :可以将依赖字段声明为 final,保证线程安全和状态稳定。
  3. 容器无关性 :即使脱离 Spring 容器,也可以通过 new Service(dep) 进行纯 Java 测试,无需任何 Mock 框架支持。

二、 Spring IoC 容器底层原理

1. 容器的两个核心接口

  • BeanFactory :IoC 容器的根接口,定义了最基本的 getBeancontainsBean 等方法。它是 Spring 的底层基础设施,采用懒加载策略(首次 getBean 时才创建),通常不直接使用。
  • ApplicationContext :BeanFactory 的子接口,是企业级开发的门面。它在 BeanFactory 基础上扩展了国际化、事件发布、资源访问、AOP 集成等企业级特性,并且默认预加载所有单例 Bean。预加载虽然增加了启动时间,但能在应用上线前暴露绝大多数配置错误和依赖缺失问题,这在生产环境中远比节省几秒启动时间重要。

2. BeanDefinition:一切配置的终极抽象

Spring 并不直接读取你的类文件来创建 Bean,而是先将所有配置源(XML <bean> 标签、@Component 扫描、@Bean 工厂方法)解析为 BeanDefinition 对象。

它是 Bean 的"元数据蓝图",包含了类名、作用域、是否懒加载、依赖列表、初始化/销毁方法等信息。容器后续的所有操作都是基于 BeanDefinition 进行的。这种设计实现了配置解析与 Bean 创建的彻底解耦,无论配置来源如何变化,容器内部的创建引擎只认 BeanDefinition,这也是 Spring 能够持续演进、支持多种配置范式的根本架构保障。

3. Bean 的生命周期(重中之重)

一个 Bean 从出生到死亡经历了极其复杂的流程,这也是面试考察的重灾区。标准单例 Bean 的生命周期如下:

  1. 实例化 (Instantiation):通过反射或 CGLIB 创建 Bean 的原始对象(此时属性为空)。
  2. 属性填充 (Populate Properties) :注入依赖(DI 发生在此阶段),处理 @Value@Autowired 等。这是循环依赖检测和处理的核心窗口期。
  3. Aware 接口回调 :如果 Bean 实现了 BeanNameAwareBeanClassLoaderAwareApplicationContextAware 等,容器会按固定顺序注入相应资源。
  4. BeanPostProcessor#postProcessBeforeInitialization :前置处理器,如 @PostConstruct 注解的处理就在此处。
  5. 初始化 (Initialization) :按优先级执行 @PostConstructInitializingBean#afterPropertiesSet → 自定义 init-method。
  6. BeanPostProcessor#postProcessAfterInitialization :后置处理器,AOP 代理通常在此阶段创建 。注意:你在初始化方法中看到的 this 是原始对象,而容器中最终存储和对外提供的可能是代理对象。
  7. 就绪/使用:Bean 放入一级缓存(单例池),供应用程序使用。
  8. 销毁 (Destruction) :容器关闭时,逆序执行 @PreDestroyDisposableBean#destroy → 自定义 destroy-method。

⚠️ 重要警告

Prototype Bean 不会被容器管理销毁,容器只负责创建,不负责回收。如果 Prototype Bean 持有需要释放的资源(如文件句柄、网络连接),必须自行实现清理逻辑或使用 DestructionAwareBeanPostProcessor 手动注册销毁回调。


三、 三级缓存解决循环依赖

Spring 通过三级缓存解决了单例 Bean 的 setter/字段注入循环依赖问题,这是一个在工程实用性与理论纯粹性之间做出的精妙妥协,也是面试必考的核心难点。

三级缓存的精确定义

  • 一级缓存 singletonObjects:ConcurrentHashMap,存放完全初始化好的成品 Bean,是对外提供服务的最终数据源。
  • 二级缓存 earlySingletonObjects:ConcurrentHashMap,存放提前暴露的半成品 Bean(已实例化但未填充属性),用于打破循环,保证同一个 Bean 的早期引用在容器中唯一。
  • 三级缓存 singletonFactories :ConcurrentHashMap>,存放 Bean 的工厂对象,主要用于处理 AOP 代理

为什么必须是三级?两级不行吗?

假设只有两级缓存,当 A 依赖 B、B 依赖 A,且 A 需要被 AOP 代理时,我们会陷入两难:

  • 方案一:在实例化后立即无条件创建代理放入二级缓存。后果是所有 Bean 都会在早期被代理,违背了"AOP 应在初始化后创建"的生命周期规范。
  • 方案二:放入原始对象。后果是 B 拿到的是原始 A,后续 A 被代理后,B 持有的引用与容器中的不一致,导致 AOP 增强失效。

三级缓存通过 ObjectFactory 延迟了代理创建决策:只有在真正发生循环依赖需要获取早期引用时,才触发工厂创建代理对象并放入二级缓存;如果没有循环依赖,AOP 代理依然在标准的后置处理阶段创建,保持了生命周期的正确性。

关键限制与破解之道

  • 构造器循环依赖无法解决 :因为实例化就需要依赖,无法提前暴露半成品。可通过 @Lazy 注入代理占位符破解,实际调用方法时才触发真实 Bean 的创建。
  • Prototype 循环依赖无法解决 :Spring 明确不支持,会抛出 BeanCurrentlyInCreationException。因为 Prototype 语义是"每次新建",缓存机制与其本质冲突。解决方案是重构设计,或使用 ObjectProvider<T> 将循环依赖转化为运行时的按需获取。

四、 高阶知识点补充

1. FactoryBean vs BeanFactory

  • BeanFactory:容器的顶层接口,管理所有 Bean。
  • FactoryBean :一个特殊的 Bean,实现了该接口的 Bean 本身是一个"工厂"。当你 getBean("myFactory") 时,返回的不是 FactoryBean 实例,而是其 getObject() 方法的返回值。常用于整合第三方框架(如 MyBatis 的 SqlSessionFactoryBean、Feign、Dubbo)。若要获取 FactoryBean 本身,需加 & 前缀:getBean("&myFactory")

2. BeanFactoryPostProcessor vs BeanPostProcessor

两者名称相似但职责截然不同,混用会导致严重问题:

  • BeanFactoryPostProcessor (BFPP) :作用于容器级别 ,在所有 Bean 实例化之前修改 BeanDefinition(如 PropertySourcesPlaceholderConfigurer 处理 ${} 占位符)。铁律:切勿在 BFPP 中尝试 getBean(),这会导致未预期的提前初始化,破坏生命周期顺序。
  • BeanPostProcessor (BPP) :作用于Bean 级别,在每个 Bean 初始化前后进行拦截处理,是实现自定义注解、AOP 代理、性能监控的标准扩展点。

3. Singleton 注入 Prototype 的陷阱

当一个 Singleton Bean 注入了 Prototype Bean 时,Prototype 只会在 Singleton 创建时被注入一次,后续调用得到的始终是同一个实例,违背了 Prototype "每次新建" 的语义。解决方案按推荐度排序:

  1. ObjectProvider(首选) :注入 ObjectProvider<PrototypeBean>,每次调用 getObject() 获取新实例,类型安全、无侵入、语义清晰。
  2. @Lookup 注解 :在 Singleton 中定义抽象方法并标注 @Lookup,Spring 通过 CGLIB 重写该方法返回新实例。
  3. ApplicationContext.getBean():手动从容器获取,简单但引入了容器耦合,仅建议在无法使用前两种方式时使用。

4. Spring 容器启动核心流程

核心入口是 AbstractApplicationContext#refresh() 方法,包含 12 个关键步骤,其中最核心的三个是:

  • invokeBeanFactoryPostProcessors():解析配置、处理占位符、注册 BeanDefinition。
  • registerBeanPostProcessors():注册所有 BPP,为后续 Bean 创建做准备。
  • finishBeanFactoryInitialization():实例化所有非懒加载单例 Bean,触发完整的生命周期流程。

五、 常见高频面试题

Q1: Spring 的 IoC 和 DI 有什么区别?

答:

  • IoC 是设计原则,强调将对象的控制权交给外部容器,目的是解耦。
  • DI 是实现模式,是 IoC 的一种具体落地方式,指容器在运行期间动态地将依赖关系注入到组件中。
  • 类比:IoC 像是"外包公司管理模式"(理念),DI 像是"外包公司给项目组派人的具体流程"(执行)。除了 DI,IoC 还可以通过服务定位器模式实现,但 Spring 选择了 DI,因为 DI 对单元测试更友好,且不引入对容器 API 的依赖。

Q2: @Autowired 和 @Resource 的区别?

答:

维度 @Autowired @Resource
来源 Spring 特有注解 JDK 标准注解 (JSR-250)
匹配规则 默认按类型匹配,多个候选时按名称 默认按名称匹配,找不到再按类型
必填属性 required=false 允许注入 null 无此属性,找不到直接报错
支持位置 构造器、Setter、字段 Setter、字段(不支持构造器)
性能 需要遍历容器查找类型,稍慢 直接按名查找,较快

最佳实践 :推荐使用 @Autowired + @Qualifier 组合,或者直接用构造器注入。若项目要求去 Spring 耦合,可用 @Resource

Q3: Spring 是如何解决循环依赖的?为什么需要三级缓存而不是两级?

答:

  • 解决思路:提前暴露半成品 Bean。当 A 依赖 B,B 又依赖 A 时,A 实例化后立即将自己放入缓存,B 注入 A 时从缓存获取半成品,待 B 完成后 A 再继续完成自身初始化。
  • 为什么需要第三级缓存?
    为了正确处理 AOP 代理 。如果只有两级缓存,当 A 需要被代理时,我们必须在属性填充前就决定是存原始对象还是代理对象。三级缓存通过 ObjectFactory 延迟了这个决策:只有在真正发生循环依赖需要获取早期引用时,才触发工厂创建代理对象并放入二级缓存。如果没有循环依赖,AOP 代理仍然在正常的 postProcessAfterInitialization 阶段创建,保持了生命周期的规范性。

Q4: Spring Bean 的作用域有哪些?

答:

  • singleton(默认):整个容器只有一个实例,线程不安全需注意。
  • prototype:每次请求都创建新实例,容器不负责销毁。
  • request:Web 环境有效,每个 HTTP 请求一个实例。
  • session:Web 环境有效,每个 HTTP Session 一个实例。
  • application:ServletContext 级别共享。
  • websocket:WebSocket 会话级别。

注意 :Singleton Bean 注入 Prototype Bean 时,Prototype 不会每次都新建。解决方案:使用 @Lookup 注解、ObjectProvider<T>、或 ApplicationContext#getBean() 手动获取。

Q5: Spring 容器启动过程是怎样的?

答:

核心入口是 AbstractApplicationContext#refresh() 方法,包含 12 个关键步骤:

  1. 准备刷新 (prepareRefresh)
  2. 获取 BeanFactory (obtainFreshBeanFactory)
  3. 准备 BeanFactory(设置类加载器、表达式解析器等)
  4. 后处理 BeanFactory (postProcessBeanFactory)
  5. 调用 BeanFactoryPostProcessor(如 PropertySourcesPlaceholderConfigurer 处理占位符)
  6. 注册 BeanPostProcessor
  7. 初始化消息源、事件广播器
  8. onRefresh()(子类扩展点,如 Spring Boot 在此启动内嵌 Tomcat)
  9. 注册事件监听器
  10. finishBeanFactoryInitialization()(核心:实例化所有非懒加载单例 Bean)
  11. finishRefresh()(发布 ContextRefreshedEvent)
  12. 注册 MBean / 完成刷新

Q6: 如何优雅地处理 Bean 的初始化和销毁?

答: 优先级从高到低:

  1. JSR-250 注解@PostConstruct / @PreDestroy(推荐,标准且简洁)
  2. 接口实现InitializingBean / DisposableBean(Spring 特有,侵入性强)
  3. XML/注解指定init-method / destroy-method
  4. BeanPostProcessor:全局拦截,适合框架级处理

执行顺序@PostConstructafterPropertiesSetinit-method

Q7: Spring 的自动装配模式有哪些?

答:

在 XML 配置时代,Spring 提供了以下自动装配模式(autowire 属性):

  • no:不自动装配,需显式 ref(默认值)。
  • byName:根据属性名匹配 Bean ID,找不到则不注入。
  • byType:根据属性类型匹配,多个候选则报错,零个则不注入。
  • constructor:类似 byType,应用于构造函数参数。
  • autodetect:已废弃,优先 constructor,回退 byType。

现代实践说明 :在现代 Spring 开发中,几乎全部使用注解驱动的自动装配(@Autowired@Resource),XML 时代的这些模式了解即可,面试时能说出历史演变和现代替代方案即可体现知识广度。


六、 学习建议

  1. 不要只背八股文:尝试自己手写一个简易 IoC 容器(支持注解扫描、Bean 创建、简单 DI、三级缓存),这是理解 Spring 最快的方式。
  2. 阅读源码技巧 :从 AnnotationConfigApplicationContext 入手,断点打在 refresh() 方法,跟踪单例 Bean 的创建全流程。重点关注 doCreateBeangetSingletonpopulateBean 三个方法。
  3. 关注版本差异:Spring 5 到 6 变化巨大,面试时注意区分版本特性,尤其是 Spring Boot 3 要求的 JDK 17+ 和 Jakarta EE 迁移。
相关推荐
张不才1 小时前
一个静默吞数据的时间戳陷阱
后端
ServBay1 小时前
ServBay 1.30.0 更新:双平台引入 MCP 服务,AI 编程助手成为全栈本地运维
后端·ai编程
飞天狗1111 小时前
零基础JavaWeb入门——第五课第二小节:九大内置对象 · 第2个:response(响应对象)
java·开发语言
张不才1 小时前
分页查出来的数据总少几条?可能是 MyBatis 后置过滤的坑
后端
Windeal1 小时前
Agent ToolCall 循环怎么定制?PI Extension 与 DeepAgents Middleware 两条岔路深度对比
后端·openai
鱼人1 小时前
targets 包实战:R 语言数据分析流水线自动化管理方案
后端
时雨__1 小时前
一文搞懂 Python 并发:GIL、多线程/多进程/协程怎么选
后端
Anson4321 小时前
Dubbo架构深度分析
后端
许彰午1 小时前
39_Java单元测试JUnit入门
java·junit·单元测试