1. 完整回答
Spring IoC,也就是控制反转(Inversion of Control),是 Spring 框架的核心机制,主要解决对象创建、依赖管理和组件之间耦合度过高的问题。
传统开发中,一个对象通常需要主动创建它所依赖的其他对象,比如 Service 直接通过 new 创建 DAO。这样业务代码就会和具体实现绑定,后续替换实现、扩展功能或者单元测试都会比较麻烦。
引入 IoC 后,对象的创建和依赖关系交给 Spring 容器统一管理。实现控制反转的主要方式就是依赖注入(DI),由容器把对象所需的依赖提供给它。
Spring IoC 的核心实现是 BeanFactory 和 ApplicationContext。容器启动时,会读取注解或者配置文件,将 Bean 的类型、作用域、依赖关系等信息注册为 BeanDefinition。创建 Bean 时,容器根据这些定义完成对象实例化、依赖注入、初始化以及必要的后置处理,最终提供可使用的 Bean。
Spring 默认采用单例作用域,同时支持原型、请求、会话等作用域,还可以通过 BeanPostProcessor 扩展 Bean 的创建过程,例如生成 AOP 代理对象。
实际开发中,Controller、Service、Repository 等组件通常都交给 Spring 管理,通过构造器注入建立依赖关系。这样可以降低组件耦合度,提高代码的可维护性、可扩展性和可测试性。
2. 核心原理与关键概念
2.1 核心原理
Spring IoC 的核心是:利用 BeanDefinition 描述对象,通过容器统一创建和管理 Bean,再通过依赖注入建立对象之间的关系。
整体流程:
读取配置 → 注册 BeanDefinition → 执行容器扩展 → 创建 Bean → 注入依赖 → 初始化 Bean → 提供 Bean
-
读取配置:通过组件扫描、
@Configuration、@Bean或 XML 等方式获取 Bean 的定义信息。 -
注册 BeanDefinition:将 Bean 的类型、作用域、构造参数、依赖关系等信息保存到容器中,作为后续创建对象的依据。
-
执行容器扩展:通过
BeanFactoryPostProcessor等机制,对 BeanDefinition 进行补充或修改。 -
创建 Bean:根据 BeanDefinition 选择构造方法或者工厂方法,完成对象实例化。
-
依赖注入:解析 Bean 所依赖的其他对象,通过构造器、Setter 或字段完成注入。
-
初始化 Bean:执行
BeanPostProcessor、@PostConstruct等初始化相关逻辑,必要时生成代理对象。 -
提供 Bean:将完成初始化的 Bean 交给业务代码使用。单例 Bean 通常会被缓存,后续直接获取已有实例。
默认情况下,ApplicationContext 会在启动阶段预先创建非懒加载的单例 Bean;懒加载 Bean 和原型 Bean 则按各自规则创建。
2.2 关键概念
| 概念 | 核心作用 |
|---|---|
| IoC | 控制反转,将对象创建和依赖管理交给容器 |
| DI | 依赖注入,由容器为对象提供所需依赖 |
| Bean | 由 Spring 容器管理的对象 |
| BeanDefinition | 描述 Bean 如何创建、装配和管理的元数据 |
| BeanFactory | 提供 Bean 创建、获取和依赖管理等基础能力 |
| ApplicationContext | 在 BeanFactory 基础上增加事件、国际化及扩展机制等功能 |
| BeanPostProcessor | 在 Bean 初始化前后进行处理,支持代理对象创建等扩展 |
3. 高频面试追问
3.1 IoC 和 DI 有什么区别?
IoC 是设计思想,DI 是实现这种思想的主要方式。
IoC 强调控制权的转移,也就是对象的创建和依赖管理由容器负责。
DI 强调依赖关系如何建立。一个对象需要依赖其他对象时,由容器通过构造器、Setter 或字段把依赖注入进去。
例如,订单服务依赖支付服务,传统方式是订单服务主动创建支付服务;使用 DI 后,Spring 在创建订单服务时就会提供对应的支付服务实例。
所以,IoC 解决控制权归属问题,DI 解决依赖如何提供的问题。
3.2 Spring Bean 是如何注册到 IoC 容器中的?
Spring 主要通过组件扫描、配置类和 XML 等方式注册 Bean,最终统一转换为 BeanDefinition。
常见方式有三种:
-
组件扫描:通过
@Component、@Service、@Repository、@Controller等注解标记组件,由 Spring 扫描并注册。 -
Java 配置:通过
@Configuration配合@Bean,把方法返回的对象注册到容器。 -
XML 配置:通过
<bean>标签声明 Bean。
实际项目中,自定义业务组件一般采用组件扫描,第三方客户端或者需要复杂构造逻辑的对象通常通过 @Bean 注册。
继续追问:@Component 和 @Bean 有什么区别?
@Component 标记在类上,主要通过组件扫描注册 Bean;@Bean 标记在方法上,由方法负责创建并返回 Bean。
对于能够直接修改源码的业务类,通常使用 @Component 及其派生注解;对于第三方类或需要手动控制创建过程的对象,通常使用 @Bean。
3.3 Spring Bean 的生命周期是怎样的?
Spring Bean 的生命周期主要包括实例化、属性填充、初始化、使用和销毁。
典型流程如下:
实例化 → 依赖注入 → Aware 回调 → 初始化前置处理 → 初始化方法 → 初始化后置处理 → 使用 → 销毁
具体来说:
-
实例化:通过构造方法或工厂方法创建对象。
-
依赖注入:填充属性,建立 Bean 之间的依赖关系。
-
Aware 回调:如果实现了相关接口,可以获取 Bean 名称、BeanFactory 等容器信息。
-
初始化前置处理:执行
BeanPostProcessor的postProcessBeforeInitialization(),@PostConstruct通常在这一阶段被调用。 -
初始化方法:执行
InitializingBean.afterPropertiesSet()和自定义初始化方法。 -
初始化后置处理:执行
postProcessAfterInitialization(),AOP 代理对象可能在这一阶段产生。 -
使用和销毁:Bean 正常提供服务;容器关闭时,对符合条件的 Bean 执行
@PreDestroy等销毁回调。
需要注意,prototype Bean 通常只由 Spring 负责创建和初始化,销毁阶段由使用方管理。
继续追问:BeanPostProcessor 有什么作用?
BeanPostProcessor 是 Spring 的扩展接口,可以在 Bean 初始化前后执行自定义逻辑。
例如,Spring AOP 可以借助 BeanPostProcessor,在初始化过程中为符合条件的 Bean 创建代理对象,从而实现事务、日志等增强功能。
3.4 Spring 有哪些依赖注入方式?为什么推荐构造器注入?
Spring 常见的依赖注入方式有构造器注入、Setter 注入和字段注入,通常优先选择构造器注入。
-
构造器注入:创建对象时就提供依赖,适合必需的依赖。
-
Setter 注入:对象创建后再设置依赖,适合可选或者允许调整的依赖。
-
字段注入:直接通过
@Autowired注入字段,代码简洁,但依赖关系不够直观。
构造器注入的优势主要有三个:
第一,依赖关系明确,创建对象时就能发现缺失的必需依赖。
第二,依赖字段可以声明为 final,降低对象初始化后依赖被修改的风险。
第三,单元测试时可以直接通过构造方法传入模拟对象,减少对 Spring 容器的依赖。
另外,如果一个 Bean 只有一个构造方法,通常可以省略构造方法上的 @Autowired。
3.5 @Autowired 是如何完成自动注入的?存在多个同类型 Bean 怎么办?
@Autowired 主要根据类型寻找候选 Bean,再结合限定符、优先级等规则确定具体注入对象。
例如,一个支付接口有支付宝和微信支付两个实现,Spring 在注入支付接口时可能找到多个候选 Bean。
常见解决方式包括:
-
使用
@Qualifier明确指定候选 Bean。 -
使用
@Primary指定默认优先使用的实现。 -
在没有更高优先级匹配规则时,利用注入点名称与 Bean 名称进行匹配。
-
如果业务需要全部实现,可以注入
List<PaymentService>等集合。
如果最终无法确定唯一候选对象,就会出现依赖注入异常。
实际项目中,如果多个支付实现对应不同业务场景,通常通过 @Qualifier 明确区分,避免依赖隐式匹配规则。
3.6 Spring Bean 有哪些作用域?单例 Bean 是线程安全的吗?
Spring 默认使用 singleton 单例作用域,但单例 Bean 并不天然具备线程安全性。
常见作用域包括:
| 作用域 | 含义 |
|---|---|
| singleton | 同一个容器中,同一个 Bean 定义通常对应一个共享实例 |
| prototype | 每次向容器请求该 Bean 时创建新实例 |
| request | 每次 HTTP 请求对应一个实例 |
| session | 每个 HTTP Session 对应一个实例 |
Spring 的单例主要控制对象实例的数量,线程安全则取决于对象内部是否存在被并发访问的可变状态。
例如,一个单例 Service 只调用 Repository 并使用方法局部变量处理业务,通常比较容易保证线程安全。
如果在单例 Service 中定义一个共享的计数变量,多个请求同时修改它,就可能产生并发问题。
因此,业务 Service 通常设计为无状态单例组件。
继续追问:Spring 单例和设计模式中的单例有什么区别?
Spring 单例的范围是容器和 Bean 定义,同一个类在一个容器里也可以通过不同 Bean 定义产生不同实例。
传统单例模式通常由类自身控制实例数量,常见实现是在一个类加载器范围内维护唯一实例。
3.7 BeanFactory 和 ApplicationContext 有什么区别?
BeanFactory 是 Spring IoC 容器的基础接口,ApplicationContext 在它的基础上提供更完整的应用级功能。
BeanFactory 主要负责 Bean 的创建、获取和依赖管理。
ApplicationContext 继承 BeanFactory,并提供国际化、事件发布、环境配置、自动识别和注册容器扩展组件等能力。
另外,普通 BeanFactory 实现通常按需创建 Bean,而 ApplicationContext 默认会在启动时预先实例化非懒加载的单例 Bean,有利于尽早发现配置问题。
因此,实际 Spring 和 Spring Boot 应用通常使用 ApplicationContext。
3.8 Spring 如何解决循环依赖?为什么构造器循环依赖无法正常解决?
Spring 可以在特定条件下通过提前暴露单例对象引用来处理循环依赖,但构造器之间的直接循环依赖通常无法解决。
例如,A 依赖 B,B 又依赖 A。
对于支持提前暴露引用的单例 Bean,Spring 可以通过三级缓存机制处理部分 Setter 或字段注入形成的循环依赖:
-
一级缓存:保存已经完成创建的单例 Bean。
-
二级缓存:保存提前暴露的单例对象引用。
-
三级缓存:保存能够生成提前引用的对象工厂。
典型过程是:
实例化 A → 暴露 A 的对象工厂 → 创建 B → B 需要 A → 获取 A 的提前引用 → 完成 B → 完成 A
三级缓存中的对象工厂还可以配合 AOP 提前生成代理引用,使循环依赖场景下的对象引用尽量保持一致。
构造器循环依赖无法通过这种方式正常解决,因为 A 创建时必须先获得 B,而 B 创建时又必须先获得 A,双方都无法完成最初的实例化。
实际开发中更推荐调整组件职责,消除循环依赖。Spring Boot 默认也禁止自动解决循环引用。
3.9 singleton Bean 注入 prototype Bean,为什么不会每次使用都创建新对象?
因为依赖注入通常发生在单例 Bean 创建阶段,prototype Bean 只会在这次注入时创建并提供给单例对象。
例如,单例订单服务依赖一个 prototype 类型的任务对象。
Spring 创建订单服务时,会创建一个任务对象并注入进去。后续每次调用订单服务,如果一直使用这个字段,访问的仍然是最初注入的任务实例。
如果希望每次执行业务都获得新的 prototype Bean,可以注入 ObjectProvider,在每次使用时通过 getObject() 获取新实例。
这类方式适用于每次任务都需要独立状态的场景。
3.10 为什么类上加了 @Autowired,运行时却没有注入成功?
常见原因是对象没有被 Spring 容器管理,或者依赖没有找到符合条件的 Bean。
例如,某个工具类通过 new 创建,即使字段上添加了 @Autowired,Spring 通常也不会自动处理这个对象的依赖注入。
另外,还可能存在组件没有被扫描、缺少 Bean 定义、多个候选 Bean 无法确定等问题。
实际排查一般按照以下顺序:
确认对象由 Spring 管理 → 检查组件扫描范围 → 检查依赖 Bean 是否注册 → 检查自动装配条件
如果是业务组件,通常直接交给 Spring 容器创建,再通过构造器注入依赖。
这样也能减少后续事务代理、生命周期回调等功能因为绕过容器而失效的问题。
4. 易混淆概念对比
4.1 IoC 与 DI
| 对比维度 | IoC | DI |
|---|---|---|
| 含义 | 控制反转 | 依赖注入 |
| 关注点 | 对象创建和依赖管理由谁控制 | 对象依赖如何提供 |
| 核心作用 | 降低对象之间的耦合 | 建立对象之间的依赖关系 |
| 关系 | 设计思想 | IoC 的一种主要实现方式 |
4.2 BeanFactory 与 ApplicationContext
| 对比维度 | BeanFactory | ApplicationContext |
|---|---|---|
| 定位 | 基础容器接口 | 应用级容器接口 |
| Bean 管理 | 支持 | 支持 |
| 单例初始化 | 具体实现通常按需创建 | 默认预先创建非懒加载单例 |
| 扩展能力 | 基础能力,需要手动配置部分扩展 | 自动集成后置处理器、事件、国际化等功能 |
| 使用场景 | 底层容器及特殊场景 | 常规 Spring 应用 |
4.3 @Component 与 @Bean
| 对比维度 | @Component | @Bean |
|---|---|---|
| 标注位置 | 类 | 方法 |
| 注册方式 | 组件扫描 | 解析配置方法 |
| 创建控制 | 通常由 Spring 选择构造方式 | 开发者通过方法控制创建过程 |
| 使用场景 | 自定义业务组件 | 第三方组件、复杂对象配置 |
4.4 singleton 与 prototype
| 对比维度 | singleton | prototype |
|---|---|---|
| 实例数量 | 每个容器、每个 Bean 定义共享一个实例 | 每次向容器请求时创建新实例 |
| 创建时机 | 默认启动时创建,可配置懒加载 | 请求获取时创建 |
| 销毁管理 | 容器通常负责执行销毁回调 | 容器通常不负责销毁 |
| 适用场景 | 无状态 Service、Repository | 需要独立状态的任务对象 |
4.5 BeanFactoryPostProcessor 与 BeanPostProcessor
| 对比维度 | BeanFactoryPostProcessor | BeanPostProcessor |
|---|---|---|
| 处理对象 | BeanDefinition 等配置元数据 | Bean 实例 |
| 执行时机 | 常规 Bean 实例化之前 | Bean 初始化前后 |
| 核心作用 | 修改 Bean 的定义信息 | 扩展 Bean 的初始化过程 |
| 典型应用 | 解析配置、调整 BeanDefinition | 处理生命周期注解、创建 AOP 代理 |
两者最关键的区别是:BeanFactoryPostProcessor 处理 Bean 的定义,BeanPostProcessor 处理 Bean 的实例。
5. 总结
Spring IoC 是 Spring 的核心机制,主要通过控制反转和依赖注入,将对象创建、依赖管理和生命周期管理交给容器统一完成,从而降低组件之间的耦合。它的核心流程是读取配置并注册 BeanDefinition,再根据定义完成 Bean 的实例化、依赖注入、初始化以及后置处理。Spring 默认使用单例作用域,并通过 BeanPostProcessor 提供灵活的扩展能力,例如支持 AOP 代理。实际开发中通常使用 ApplicationContext 管理业务组件,优先通过构造器注入依赖。同时需要注意,单例 Bean 的线程安全由具体实现决定,循环依赖也存在限制,因此合理划分组件职责、避免循环依赖仍然非常重要。