1. 完整回答
Spring Bean 的创建过程,就是 IoC 容器根据 BeanDefinition 创建对象,完成依赖注入、初始化和必要的代理包装,最终将 Bean 交给业务使用的过程。通过这套机制,Spring 可以统一管理对象的创建、依赖关系和生命周期。
首先,Spring 在容器启动时,会通过组件扫描、配置类、@Bean 等方式注册 BeanDefinition,里面保存了 Bean 的类型、作用域、依赖关系等信息。默认情况下,非懒加载的单例 Bean 会在容器启动阶段创建。
真正创建 Bean 时,Spring 会先检查单例缓存。如果已经存在,就直接返回;如果不存在,就根据 BeanDefinition 选择构造方法或工厂方法进行实例化。
实例化完成后,进入属性填充阶段,处理 @Autowired、@Resource 等依赖注入。接着执行 Aware 接口回调,让 Bean 感知容器提供的信息,然后通过 BeanPostProcessor 执行初始化前处理,调用 @PostConstruct、afterPropertiesSet 等初始化方法。
初始化完成后,还会执行 BeanPostProcessor 的初始化后处理。如果 Bean 需要 AOP 增强,通常会在这个阶段创建代理对象。最后将完整的单例 Bean 放入一级缓存,后续获取时可以直接复用。
另外,Spring 还提供了三级缓存机制,在满足条件时可以提前暴露尚未完成初始化的单例 Bean 引用,解决部分循环依赖问题。
这套流程在 Service、Repository、事务代理等组件中都会涉及。理解创建过程后,就能进一步分析依赖注入失败、循环依赖以及 AOP 代理相关的问题。
2. 核心原理与关键概念
2.1 核心原理
整体流程:
BeanDefinition 注册 → 获取 Bean → 实例化 → 属性填充 → Aware 回调 → 初始化前处理 → 初始化方法 → 初始化后处理 → 单例缓存
步骤 A:解析并注册 BeanDefinition
Spring 通过组件扫描、配置类、@Bean 等方式收集 Bean 定义。
BeanDefinition 相当于 Bean 的创建说明,记录类型、作用域、构造参数、初始化方法等元数据。
在创建普通 Bean 之前,容器还会执行 BeanFactoryPostProcessor,允许对 BeanDefinition 进行调整。
步骤 B:获取 Bean 并判断是否需要创建
调用 getBean() 时,Spring 会先尝试获取已有实例。
-
如果单例缓存中已有 Bean,直接返回。
-
如果没有实例,则根据 BeanDefinition 进入创建流程。
-
非懒加载的普通单例 Bean,通常会在容器刷新阶段提前创建。
步骤 C:实例化 Bean
Spring 根据 BeanDefinition 选择无参构造、有参构造或工厂方法创建对象。
如果使用构造器注入,需要先解析构造参数所依赖的 Bean,再完成当前对象的实例化。
此时对象通常还没有完成字段注入和初始化。
步骤 D:提前暴露单例引用
对于允许循环依赖、且正在创建的单例 Bean,Spring 会在实例化之后、属性填充之前,将一个用于获取早期引用的 ObjectFactory 放入三级缓存。
后续如果其他 Bean 需要引用当前 Bean,就有机会通过这个工厂获取早期引用,避免重复实例化。
步骤 E:属性填充
Spring 解析并注入 Bean 所依赖的对象和属性。
例如通过 @Autowired 注入字段或 Setter 方法依赖。这个过程中,如果依赖的 Bean 尚未创建,Spring 会继续触发对应 Bean 的创建。
步骤 F:初始化 Bean
主要执行顺序:
-
执行相关 Aware 回调,例如
BeanNameAware、BeanFactoryAware。 -
执行 BeanPostProcessor 初始化前处理,其中包括通过后处理器调用
@PostConstruct。 -
执行
InitializingBean.afterPropertiesSet()。 -
执行自定义初始化方法,例如
@Bean(initMethod = "init")。 -
执行 BeanPostProcessor 初始化后处理,按需完成 AOP 代理包装。
ApplicationContextAware 等部分 Aware 回调也通过初始化前处理器完成。
步骤 G:完成创建并缓存
对于普通单例 Bean,Spring 会将最终对外暴露的对象放入一级缓存,并清理对应的早期引用缓存。
对于原型 Bean,每次请求通常都会创建新实例,不进入单例缓存。
2.2 关键概念
| 概念 | 作用 |
|---|---|
| BeanDefinition | 描述 Bean 的创建方式、依赖、作用域和生命周期配置 |
| BeanFactory | 提供 Bean 创建、依赖解析和获取能力的基础容器接口 |
| ApplicationContext | 在 BeanFactory 基础上提供事件、资源访问等应用级能力 |
| BeanPostProcessor | 在 Bean 初始化前后执行扩展逻辑,也可参与代理包装 |
| Aware 接口 | 让 Bean 获取自身名称、容器等相关信息 |
| 三级缓存 | 管理单例的完整实例、早期引用和早期引用工厂 |
| AOP 代理 | 为符合条件的 Bean 提供事务、切面等方法增强能力 |
3. 高频面试追问
3.1 Spring 从 getBean 到创建 Bean,经过哪些核心方法?
核心流程是 doGetBean() → createBean() → doCreateBean(),分别负责获取与创建判断、创建前处理,以及实际创建过程。
getBean() 最终会进入 doGetBean(),先查找单例缓存。如果没有,就解析 BeanDefinition、处理依赖关系,再根据作用域决定如何创建 Bean。
createBean() 会先执行实例化前处理,允许某些后处理器提前返回代理对象。
如果没有提前返回,就进入 doCreateBean(),依次调用:
createBeanInstance() → populateBean() → initializeBean()
分别完成实例化、属性填充和初始化。普通单例 Bean 创建成功后,再注册到单例缓存中。
3.2 Bean 的实例化和初始化有什么区别?
实例化负责创建对象,初始化负责在依赖配置完成后执行生命周期回调。
实例化阶段主要通过构造方法或工厂方法得到对象。例如执行 new UserService(),此时对象已经存在,但字段依赖可能还没有注入。
属性填充完成后,Spring 才进入初始化阶段,执行 Aware 回调、BeanPostProcessor 和相关初始化方法。
如果同时配置了三种不同的初始化回调,执行顺序为:
@PostConstruct → afterPropertiesSet() → 自定义 initMethod
例如,一个 Service 的构造方法负责创建对象,依赖注入负责设置 Repository,而 @PostConstruct 可以在依赖完成后进行必要的状态检查或资源准备。
3.3 构造器注入和字段注入在创建过程中有什么区别?
核心区别是依赖注入的时机。
构造器注入发生在实例化过程中。Spring 必须先解析构造参数对应的依赖,才能调用构造方法创建当前 Bean。
字段注入和 Setter 注入发生在实例化之后的属性填充阶段,主要由相关 BeanPostProcessor 处理,例如 AutowiredAnnotationBeanPostProcessor。
构造器注入更适合必需依赖,能够保证对象创建时就具备这些依赖,也方便使用 final 字段。
字段注入虽然简洁,但对象在构造完成时可能还没有获得所需依赖,独立测试和显式管理依赖关系也相对困难。
3.4 BeanFactoryPostProcessor 和 BeanPostProcessor 有什么区别?
BeanFactoryPostProcessor 主要处理 BeanDefinition,BeanPostProcessor 主要处理 Bean 实例。
BeanFactoryPostProcessor 在普通 Bean 实例化前执行,可以修改 BeanDefinition,例如调整配置属性或者注册额外的 BeanDefinition。
BeanPostProcessor 则参与 Bean 的创建过程,能够在初始化前后处理对象,甚至返回包装后的代理对象。
例如,ConfigurationClassPostProcessor 负责解析配置类及相关 Bean 定义,属于 BeanFactoryPostProcessor 体系。
而 Spring AOP 使用的自动代理创建器属于 BeanPostProcessor 体系,可以在 Bean 创建过程中生成代理。
两者的区别主要是处理对象不同,执行阶段也不同。
3.5 Spring 的三级缓存是如何解决循环依赖的?
核心机制是提前暴露已经实例化、但尚未完成初始化的单例 Bean 引用。
三级缓存分别保存不同阶段的对象:
| 缓存 | 存储内容 | 主要作用 |
|---|---|---|
一级 singletonObjects |
完整单例 Bean | 对外提供最终的单例对象 |
二级 earlySingletonObjects |
提前暴露的 Bean 引用 | 复用早期引用 |
三级 singletonFactories |
ObjectFactory | 按需生成早期引用,必要时创建代理 |
假设 A 依赖 B,B 又依赖 A,且都采用字段注入:
-
Spring 实例化 A,将 A 的早期引用工厂放入三级缓存。
-
为 A 注入 B,触发 B 的创建。
-
B 实例化后需要注入 A。
-
Spring 发现 A 正在创建,从三级缓存取得 A 的早期引用,并放入二级缓存。
-
B 完成创建,再注入 A。
-
A 继续完成初始化,最终进入一级缓存。
这样就能在 A 尚未完成创建时,让 B 先获得 A 的引用,打破相互等待。
继续追问:提前暴露的 Bean 会不会一直处于未初始化状态?
正常情况下不会。提前暴露的是同一个目标对象的引用,或者与其关联的早期代理。随着后续属性填充和初始化完成,目标对象也会逐步完成配置。
不过,如果在初始化完成前就调用它的业务方法,仍然可能访问到尚未准备好的状态,因此循环依赖即使能够解决,也存在设计风险。
3.6 为什么需要三级缓存?两级缓存不够吗?
三级缓存的关键价值,是支持早期引用的按需创建,尤其是 AOP 代理场景。
如果只考虑普通对象的属性注入循环依赖,提前保存原始对象引用,通过两级缓存也可以实现类似效果。
但如果 Bean 需要 AOP 增强,其他 Bean 最终应该持有正确的代理引用。
三级缓存保存 ObjectFactory,在真正需要提前引用时,通过 getEarlyBeanReference() 获取原始对象或提前创建的代理。
生成的早期引用会放进二级缓存,避免重复生成不同的代理对象。
因此:
-
一级缓存管理创建完成的单例。
-
二级缓存复用已经生成的早期引用。
-
三级缓存提供按需获取早期引用的能力。
这样能够更好地协调循环依赖和 AOP 代理的一致性。
3.7 Spring 能解决所有循环依赖吗?
不能。三级缓存主要支持特定条件下的单例 Bean 属性注入循环依赖。
构造器循环依赖通常无法通过三级缓存解决。因为 A 创建时必须先获得 B,而 B 创建时又必须获得 A,此时 A 连实例化都没有完成,也就没有可以提前暴露的对象。
Prototype 作用域的循环依赖也无法依靠单例三级缓存机制解决。
另外,Spring Boot 默认禁止循环引用。即使是理论上可以提前暴露引用的单例属性注入循环依赖,也可能直接导致启动失败。
在项目中通常优先调整依赖关系,例如抽取公共服务、拆分职责。如果确实需要延迟获取依赖,可以结合 @Lazy 或 ObjectProvider,但要保证设计本身合理。
3.8 AOP 代理是在 Bean 创建的哪个阶段生成的?
通常在 Bean 初始化后处理阶段生成,由 BeanPostProcessor 负责。
Spring AOP 的自动代理创建器会判断当前 Bean 是否需要增强。如果符合切面条件,就创建代理对象,并将代理返回给容器。
后续其他 Bean 获取该 Bean 时,通常拿到的是代理对象。
如果存在循环依赖,Spring 也可能通过 getEarlyBeanReference() 提前创建代理,保证其他 Bean 注入的引用与最终暴露的引用保持一致。
例如 @Transactional,通常需要通过代理拦截方法调用才能开启事务。由于目标对象内部通过 this 调用其他方法时绕过了代理,所以同类方法自调用可能导致事务增强不生效。
继续追问:为什么在 @PostConstruct 中调用 @Transactional 方法通常不会开启事务?
因为 @PostConstruct 在初始化前处理阶段执行,此时常规 AOP 代理尚未完成最终包装。
而且初始化方法内部直接调用当前对象的方法,也没有经过事务代理。
因此,依赖事务执行的数据库初始化逻辑,更适合放到能够通过代理调用的独立服务中,并在合适的应用启动阶段触发。
3.9 Singleton、Prototype 和 @Lazy 对 Bean 创建有什么影响?
作用域决定实例的复用方式,懒加载决定是否延迟创建。
Singleton 是默认作用域,同一个容器中,同一 Bean 定义通常对应一个共享实例。非懒加载的单例 Bean 一般在容器刷新时创建。
Prototype 每次被请求时通常创建一个新对象,不使用单例缓存。Spring 会完成它的初始化,但默认不会在容器关闭时自动执行其销毁回调。
@Lazy 可以推迟单例 Bean 的预实例化。不过,如果懒加载 Bean 被非懒加载单例直接依赖,仍然可能在启动时被创建。
继续追问:单例 Bean 注入 Prototype Bean,每次调用都会得到新对象吗?
不会自动得到新对象。普通依赖注入只在单例创建时执行一次,因此后续一直使用当时注入的 Prototype 实例。
如果希望每次使用时获取新对象,可以注入 ObjectProvider,在业务调用时通过 getObject() 获取。
3.10 项目中 Bean 创建失败,通常怎么排查?
先根据异常类型确定失败阶段,再沿着 Bean 的依赖链定位根因。
如果出现 NoSuchBeanDefinitionException,通常先检查组件扫描范围、条件注册是否生效,以及需要的 Bean 是否已经注册。
如果出现 NoUniqueBeanDefinitionException,说明依赖解析存在多个无法确定唯一候选者的 Bean,可以使用 @Primary 或 @Qualifier 明确选择。
如果出现 BeanCurrentlyInCreationException,重点检查构造器循环依赖、原型循环依赖或不允许提前暴露引用的情况。
如果出现 BeanCreationException,还需要检查构造方法、属性填充和初始化回调是否抛出了异常。
例如,Service 注入支付接口时存在两个实现类,导致启动失败,就应该先检查候选 Bean,再明确指定需要的实现。
排查时会优先查看异常链中最底层的原因,以及日志中的 Bean 依赖关系,避免只根据最外层异常判断问题。
4. 易混淆概念对比
4.1 BeanDefinition 与 Bean
| 对比项 | BeanDefinition | Bean |
|---|---|---|
| 本质 | Bean 的定义元数据 | 容器管理的对象实例 |
| 产生阶段 | 定义解析与注册阶段 | 实例化阶段 |
| 主要内容 | 类型、作用域、依赖、生命周期配置 | 实际对象状态与行为 |
| 作用 | 指导容器创建 Bean | 提供具体业务能力 |
一个 BeanDefinition 可以根据作用域产生不同数量的对象实例。
4.2 实例化与初始化
| 对比项 | 实例化 | 初始化 |
|---|---|---|
| 发生时机 | 属性填充之前 | 属性填充之后 |
| 核心操作 | 构造方法、工厂方法 | 生命周期回调 |
| 主要作用 | 创建对象 | 完成初始化逻辑 |
| 典型方式 | 构造器调用 | @PostConstruct、afterPropertiesSet() |
4.3 BeanFactoryPostProcessor 与 BeanPostProcessor
| 对比项 | BeanFactoryPostProcessor | BeanPostProcessor |
|---|---|---|
| 处理对象 | BeanDefinition | Bean 实例 |
| 执行时机 | 普通 Bean 实例化前 | Bean 创建过程中 |
| 典型用途 | 修改 Bean 定义 | 初始化扩展、代理包装 |
| 代表实现 | ConfigurationClassPostProcessor | 自动代理创建器 |
4.4 构造器注入与字段、Setter 注入
| 对比项 | 构造器注入 | 字段、Setter 注入 |
|---|---|---|
| 注入时机 | 实例化过程中 | 实例化之后 |
| 依赖可用性 | 创建完成时已获得构造参数依赖 | 属性填充后才完整 |
| 循环依赖 | 普通相互构造依赖无法提前暴露 | 满足条件时可通过三级缓存解决 |
| 适用场景 | 必需依赖 | 可选依赖或特定框架场景 |
4.5 Singleton 与 Prototype
| 对比项 | Singleton | Prototype |
|---|---|---|
| 默认行为 | 同一容器、同一 Bean 定义共享实例 | 每次请求创建新实例 |
| 单例缓存 | 使用 | 不使用 |
| 创建时机 | 默认启动时预实例化 | 被请求时创建 |
| 初始化回调 | 支持 | 支持 |
| 销毁回调 | 容器通常负责管理 | 使用方负责清理 |
5. 总结
Spring Bean 的创建过程,核心就是 根据 BeanDefinition 完成实例化、依赖注入、初始化和必要的代理包装。容器先判断是否已有可复用的实例,再选择构造方法或工厂方法创建对象,随后填充属性,执行 Aware 回调和 BeanPostProcessor,完成初始化后将单例对象放入缓存。整个过程中,BeanPostProcessor 提供了生命周期扩展和 AOP 代理能力,三级缓存能够在满足条件时解决部分单例循环依赖。需要注意,构造器循环依赖通常无法通过提前暴露引用解决,而且 Spring Boot 默认禁止循环引用,因此实际项目中仍然应该优先保证依赖关系清晰。