人人都会AI编程

3.3 Bean 加载全流程:资源定位 → BeanDefinition 解析 → 实例化 → 属性注入 → 初始化 → 容器注册

更新时间:2026-07-10

IoC 容器掌控一切 Bean 的生命周期,但“掌控”并不是一个模糊的魔术,而是一系列精密的工序。当一个应用启动,Spring 是如何将一段配置、一行注解、一个普通 Java 类最终变成可工作的 Bean,并放入容器供整个系统调用的?这背后的流程可以清晰地拆解为六个核心阶段。

理解这个流程,不仅能帮助你解决各种“注入失败”“Bean 未找到”“初始化不生效”等实际问题,还能让你对 Spring 的设计精巧度产生新的认识。

3.3.1 资源定位(Resource Location)

一切从配置的读取开始。Spring 需要知道“哪些类应该被容器管理”。这些信息可能来自:

  • XML 配置文件<bean id="..." class="..."/>
  • 注解扫描@Component@Service@Repository@Controller 等标注的类
  • Java Config@Configuration 类中使用 @Bean 标注的方法
  • 其他来源:Groovy 脚本、Properties 文件,甚至自定义的元数据提供者

Spring 将配置源统一抽象为 Resource 接口,无论是 XML 文件、类路径下的包,还是一个 URL,全都视为 Resource。在此基础上,BeanDefinitionReaderClassPathBeanDefinitionScanner 负责读取这些资源,从中提取 Bean 的定义信息。

例如,当你在 Spring Boot 应用的主类上标注 @SpringBootApplication,它隐含的 @ComponentScan 就会触发类路径扫描。扫描器遍历指定包下的所有 .class 文件,寻找带有 @Component 注解的类,将其纳入后续的处理流程。

3.3.2 BeanDefinition 解析(BeanDefinition Parsing)

读取到的配置元数据并不会直接用于建对象,而是先被解析成一种统一的元数据描述对象—— BeanDefinition

可以把 BeanDefinition 看作 Bean 的“设计图纸”,它包含以下重要信息:

  • beanClassName:全限定类名
  • scope:作用域(singleton、prototype 等)
  • lazyInit:是否延迟初始化
  • dependsOn:强制依赖的 Bean 名称
  • constructorArgumentValues:构造器参数
  • propertyValues:属性值(setter 注入的值)
  • initMethodName / destroyMethodName:初始化与销毁回调方法名
  • factoryMethodName:工厂方法(如果通过工厂创建)

对于 XML 中的 <bean> 定义,XmlBeanDefinitionReader 会解析每一个元素生成对应的 BeanDefinition。对于注解扫描到的类,AnnotationConfigUtils 会提取注解中的属性填充到 BeanDefinition。对于 @Bean 方法,Spring 会将其标注的工厂方法解析为 BeanDefinition,并将工厂方法所在的配置类作为工厂。

最终,所有 BeanDefinition 被注册到 DefaultListableBeanFactory 内部维护的一个 ConcurrentHashMap<String, BeanDefinition> 中,键是 Bean 的名称(id 或默认生成的名称),值就是那张“图纸”。

这一步结束后,容器已经“知道”要创建哪些 Bean、每个 Bean 长什么样,但尚未实际创建任何对象。

3.3.3 实例化(Instantiation)

当容器进入“刷新”阶段(AbstractApplicationContext.refresh()),或者当一个 Bean 被首次获取时(对于非懒加载的单例 Bean,通常在刷新阶段完成),真正的对象创建开始了。

实例化阶段的任务是:根据 BeanDefinition 中的类名和构造信息,创建一个对象的原始实例。这里的关键是使用什么方式创建实例:

  • 构造器实例化:最常用方式,通过反射调用类的构造器。如果存在多个构造器,Spring 会根据参数数量、类型和可用度进行推断(结合 @Autowired 或参数匹配)。
  • 工厂方法实例化:如果 BeanDefinition 指定了 factoryMethodName,Spring 会调用该静态工厂方法或实例工厂方法来获得对象(常用于 @Bean 方法)。
  • CGLIB 增强:如果存在方法拦截(AOP),Spring 可能会在实例化阶段就生成 CGLIB 子类代理,或者在初始化阶段创建 JDK 动态代理(稍后详述)。

源码中,这一阶段的核心方法是 AbstractAutowireCapableBeanFactory.createBeanInstance(),它会执行以下逻辑:

  1. 检查是否有 Supplier 提供实例(极少使用)。
  2. 检查是否有工厂方法(@Bean 方法),有则调用工厂方法。
  3. 确定最终使用的构造器(如有 @Autowired 标注的构造器或根据匹配策略选择),解析构造器参数依赖。
  4. 调用选定构造器或工厂方法,拿到“原始对象”。

此时创建出的对象仅仅是一个空的实例,所有的属性都还是默认值,依赖尚未注入。

3.3.4 属性注入(Property Injection / 依赖注入)

有了原始对象后,第二大核心步骤就是填充依赖。容器会根据 BeanDefinition 中的属性值定义,以及 @Autowired@Value@Inject 等注解,给实例的字段、setter 方法或构造器注入依赖。

这一阶段由 AbstractAutowireCapableBeanFactory.populateBean() 负责,具体处理以下情况:

  • 通过 InstantiationAwareBeanPostProcessor 的后处理器:这是 AOP、依赖注入等的关键拦截点。AutowiredAnnotationBeanPostProcessor 会处理 @Autowired@Value 标注的字段、方法、构造器;CommonAnnotationBeanPostProcessor 处理 @ResourceRequiredAnnotationBeanPostProcessor 处理 @Required(已废弃)。
  • 显式属性值:来自 XML 的 <property>BeanDefinition 中通过 propertyValues 设置的属性,会通过 setter 方法注入。
  • 自动装配模式:如 autowire="byName"autowire="byType"(现在已很少使用,被注解取代)。

属性注入期间,容器需要解析依赖。例如,一个 UserService 的属性注入了 UserRepository,容器会查看 UserRepository 是否已经存在单例实例。如果尚未创建,则会递归触发 UserRepository 的完整加载流程(即“依赖的 Bean 先创建”)。这是解决 Bean 依赖层次的基础机制,也是循环依赖产生的根源。

3.3.5 初始化(Initialization)

当所有依赖注入完成,对象已经功能完备,但有时还需要执行一些自定义的初始化逻辑,比如打开资源、建立连接、校验关键配置、注册 MBean 等。Spring 为这一步骤提供了三层精细的回调:

1. @PostConstruct 标注的方法

这是 Java 标准(JSR-250)的方式,Spring 通过 CommonAnnotationBeanPostProcessor 查找并调用标注了 @PostConstruct 的方法。它是早期执行、依赖注入后立即触发的初始化。

2. InitializingBean 接口的 afterPropertiesSet()

如果 Bean 实现了 InitializingBean 接口,Spring 在调用完所有 BeanPostProcessor 之后(但仍在初始化阶段内),会调用此方法。这种方式与 Spring 耦合较强,实际开发中较少使用。

3. 自定义 init-method@Bean(initMethod = "xxx")

可以在 BeanDefinition 中指定初始化方法名,或者通过 @Bean 注解的 initMethod 属性声明。Spring 最后会通过反射调用该方法。这种方式最灵活,不会对业务代码产生框架侵入。

除了这些主动回调,BeanPostProcessorpostProcessBeforeInitializationpostProcessAfterInitialization 分别在初始化之前和之后执行,是 AOP 代理创建的关键位置。例如,AbstractAutoProxyCreatorpostProcessAfterInitialization 中检查是否需要为 Bean 创建 JDK 或 CGLIB 代理,如果需要,则创建代理对象并返回,后续容器中持有的将是该代理。

因此,初始化的结果可能是一个被代理增强后的对象,而不是原始实例。

3.3.6 容器注册(Container Registration & 可用化)

经历实例化、注入和初始化后,Bean 对象已经完全就绪。对于单例作用域的 Bean,最后一步是将它注册到容器的单例缓存中,以便后续大量快速的获取。

DefaultSingletonBeanRegistry 维护了一个核心的 ConcurrentHashMap<String, Object>——singletonObjects,即一级缓存。经过完整周期的单例 Bean 会被存入此 Map,键为 Bean 名称,值为最终的 Bean 实例(可能为代理)。

// 源码中简化后的逻辑
singletonObjects.put(beanName, singletonObject);

一旦放入缓存,这个 Bean 就正式向整个系统“注册”了。无论是其他地方通过 @Autowired 注入,还是直接调用 applicationContext.getBean(),获取到的都是这个共享的实例。

对于原型(prototype)作用域的 Bean,每次获取都会执行整个流程,不会被缓存。

3.3.7 完整流程的统一视角

将六个阶段串联起来,可以形象地概括为:

配置资源 ──► BeanDefinition 图纸 ──► 建造空房屋(实例化)
   └─► 水电气入户(属性注入) ──► 精装验收(初始化) ──► 交付入住(容器注册)

在实际的 ApplicationContext 刷新过程中,这一切都在 finishBeanFactoryInitialization() 方法中,通过调用 DefaultListableBeanFactory.preInstantiateSingletons() 触发所有非懒加载单例 Bean 的创建。每一个单例 Bean 都经历上述流水线,最终整齐地排列在缓存中。

实用提示

  • 当你遇到 NoSuchBeanDefinitionException,问题大多出在资源定位或 BeanDefinition 解析阶段——类未被扫描到、配置缺失或名称不匹配。
  • UnsatisfiedDependencyException 或注入失败通常发生在属性注入阶段,需要检查构造器或字段依赖是否可解析。
  • @PostConstruct 未执行?检查是否被 Spring 容器管理(是否在扫描路径内),以及是否有 CommonAnnotationBeanPostProcessor 生效(Spring Boot 自动配置已包含)。

理解这六个阶段,Spring 容器的行为就不再是一个黑盒。任何关于“这个 Bean 什么时候创建”“为什么代理没有生效”“循环依赖怎么解”的问题,都可以从这条流水线中找到清晰的答案。