Spring Boot · 第 01 章

容器与 Bean

起点页给了 refresh() 流水线的全景——这一章先定格其中的静态零件:容器里到底有什么。

本章你将建立的 schema

  • 容器 = ApplicationContext,本质是单例池 + BeanDefinition 元数据 + 后置处理器列表
  • BeanDefinition(蓝图)≠ Bean(实例)——蓝图先于实例存在,才能被 BFPP 改写
  • 两类后置处理器各司其职:BFPP 改蓝图、BPP 改实例,这一刀切开 Spring 大部分机制

1.1IoC 容器 = ApplicationContext

容器是管理对象创建与装配的运行时。

容器并不神秘。剥开注解语法糖,内部结构是三件东西:

  • 单例池——Map<String, Object> singletonObjects,存放已实例化的单例 Bean;
  • BeanDefinition 元数据——描述"怎么造这个 Bean"的蓝图集合;
  • 后置处理器列表——两类处理器(BeanFactoryPostProcessor 和 BeanPostProcessor)的有序队列,负责在蓝图阶段和实例阶段介入。

BeanFactory 是最基础的 DI 接口,只提供 getBean() 和注册/查询 BeanDefinition。ApplicationContext 继承 BeanFactory,并在其上叠加了五项能力:AOP 代理集成、应用事件发布/监听、i18n 消息资源、自动组件扫描、以及 Environment 抽象(统一管理 profiles 和属性源)。

为什么直接用 ApplicationContext 而非 BeanFactory

BeanFactory 在首次调用 getBean() 时才触发 BPP 注册,无法保证 BPP 在所有 Bean 实例化之前到位。ApplicationContext 在 refresh 阶段主动注册全部后置处理器,确保顺序正确。直接使用 BeanFactory 会导致代理、校验、事件等功能静默失效。

下面的示意代码展示注解只是配置容器的语法糖——AnnotationConfigApplicationContext 可以在不扫描任何包的情况下手动注册 BeanDefinition:

ContainerDemo.javaJava
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class AppConfig {
    @Bean  // 注解只是让容器知道"有这个蓝图",Bean 本身仍由容器实例化
    public OrderService orderService() {
        return new OrderService();
    }
}

public class ContainerDemo {
    public static void main(String[] args) {
        // 手动建容器,无需 Spring Boot — 验证容器本质
        var ctx = new AnnotationConfigApplicationContext(AppConfig.class);

        // getBean() 触发:若单例池已有则直接返回,否则按蓝图实例化后放入单例池
        OrderService svc = ctx.getBean(OrderService.class);
        System.out.println(svc == ctx.getBean(OrderService.class)); // true — 同一对象

        ctx.close();
    }
}
BeanFactory 最基础 DI 接口 单例池 singletonObjects BeanDefinition 元数据 后置处理器 列表 ApplicationContext 继承 BeanFactory,叠加五项能力 AOP 集成 事件发布 i18n 消息 组件扫描 Environment 继承
图 1.1BeanFactory 是内核,ApplicationContext 是外壳。注意:直接持有 BeanFactory 引用会绕过外壳注册的后置处理器,导致代理等功能静默失效。

1.2BeanDefinition(蓝图)vs Bean(实例)

BeanDefinition 是"怎么造这个 Bean"的元数据,不是 Bean 本身。

一个 BeanDefinition 持有的信息包括:目标 class、scope(singleton/prototype…)、构造参数、属性值、是否懒加载(lazyInit)、初始化方法名(initMethodName)、销毁方法名(destroyMethodName)。容器在 refresh 的前半段把所有 BeanDefinition 收齐,蓝图先于实例存在——这是 BeanFactoryPostProcessor 能够介入的前提:此时还没有任何 Bean 实例,BFPP 可以自由读写蓝图中的任何字段。

想一想

${server.port} 这种占位符替换,发生在改蓝图阶段还是改实例阶段?

展开答案(先停 10 秒)

改蓝图阶段。PropertySourcesPlaceholderConfigurer 是 BeanFactoryPostProcessor,在所有 Bean 实例化之前把 BeanDefinition 里的字符串占位符替换成真实值。如果放到实例阶段才替换,@Value 注入时拿到的就是未解析的字面量 ${server.port},而非端口数字。

元数据阶段 扫描 / 解析 / 收集 BeanDefinition BeanDefinition class · scope · initMethod · lazy constructorArgs · propertyValues ← BFPP 在此改蓝图 → 实例化分界 实例化阶段 按蓝图构造 → 注入 → 初始化 Bean 实例 存入 singletonObjects (单例池) ← BPP 在此改实例 → 按蓝图 实例化
图 1.2蓝图阶段与实例化阶段以一条分界线隔开。注意:BeanFactoryPostProcessor 只在分界线左侧生效;一旦进入实例化阶段,蓝图不再被修改。

1.3两类后置处理器(本教程第一刀)

区分 BeanFactoryPostProcessor(BFPP)和 BeanPostProcessor(BPP)是理解 Spring 机制的第一刀。两者作用在不同的对象、不同的时机:

BFPP vs BPP 对比
维度 BeanFactoryPostProcessor(BFPP) BeanPostProcessor(BPP)
作用对象 BeanDefinition(蓝图) Bean 实例
触发时机 所有 Bean 实例化之前(refresh 第 5 阶段) 每个 Bean 实例化 + 依赖注入完成后(init 前后)
回调方法 postProcessBeanFactory(ConfigurableListableBeanFactory) postProcessBeforeInitialization(Object, String)
postProcessAfterInitialization(Object, String)
典型实现 ConfigurationClassPostProcessor(解析 @Configuration/@Bean/@Import,加载全部 BeanDefinition,含自动配置)
PropertySourcesPlaceholderConfigurer(占位符替换)
AutowiredAnnotationBeanPostProcessor(处理 @Autowired)
AbstractAutoProxyCreator(换代理,见第 4 章)
注入自身时机 容器直接调用,不经过普通 Bean 生命周期 必须先被实例化后才能拦截其他 Bean

ConfigurationClassPostProcessor 是最关键的 BFPP:它解析 @Configuration 类及其 @Bean、@Import、@ComponentScan,把应用中所有 BeanDefinition(包括自动配置产生的)全部加载进容器。自动配置的发现机制(AutoConfigurationImportSelector 读 .imports 文件)在这一步触发,详见 第 3 章。

时间 BFPP 阶段 改蓝图 / 加载 BeanDefinition ConfigurationClassPostProcessor PropertySourcesPlaceholderConfigurer 实例化开始 BPP 阶段 改实例 / 包代理 AutowiredAnnotationBeanPostProcessor AbstractAutoProxyCreator
图 1.3BFPP 在左(蓝图阶段),BPP 在右(实例阶段)。注意:BFPP 必须在 BPP 注册之前执行,才能确保 ConfigurationClassPostProcessor 加载完所有 BeanDefinition 后,BPP 才开始接管实例。

1.4Bean 生命周期 + 三级缓存与循环依赖

单例 Bean 的完整生命周期按以下顺序推进:

  1. 实例化——调用构造函数,对象分配内存(此时属性尚未注入);
  2. 属性注入——AutowiredAnnotationBeanPostProcessor 填充 @Autowired 依赖;
  3. Aware 回调——BeanNameAware、BeanFactoryAware、ApplicationContextAware 等;
  4. BPP before——postProcessBeforeInitialization();
  5. 初始化——@PostConstruct(JSR-250,jakarta 包)→ InitializingBean.afterPropertiesSet() → initMethod;
  6. BPP after——postProcessAfterInitialization()(代理在此替换原对象);
  7. 就绪——放入单例池,供 getBean() 返回;
  8. 销毁——容器关闭时:@PreDestroy → DisposableBean.destroy() → destroyMethod。

三级缓存与循环依赖

Spring 用三级缓存让 field/setter 注入的循环依赖能够成立:

  • singletonObjects——成品 Bean(完全初始化);
  • earlySingletonObjects——早期引用(已实例化但尚未注入完毕);
  • singletonFactories——工厂对象,按需产出早期引用(用于 AOP 场景,把半成品的代理暴露出去)。

循环依赖成立的过程:A 开始实例化 → 把 A 的工厂放入 singletonFactories → 注入 B → B 开始实例化 → 注入 A 时命中 singletonFactories,取出 A 的早期引用放入 earlySingletonObjects → B 完成初始化 → A 拿到 B 完成注入 → A 完成初始化 → A 进入 singletonObjects。整个过程不需要 A 已经完全就绪,只需要 A 的早期引用(内存地址)已固定。

构造器注入的环解不了:造 A 的构造函数需要 B 已存在,造 B 的构造函数需要 A 已存在——此时 A 连实例都还没有,无早期引用可暴露,Spring 直接抛出 BeanCurrentlyInCreationException。

Spring Boot 2.6+ 默认 spring.main.allow-circular-references=false,在启动时主动检测并拒绝所有循环依赖(包括 field/setter 注入的环),把隐式依赖变成显式构建错误。

陷阱 · 构造器循环依赖

症状:启动即抛 BeanCurrentlyInCreationException: Error creating bean with name 'A',堆栈里 A 和 B 互相出现。

根因:构造器注入的循环让容器无法产生任何一方的早期引用,三级缓存机制无法介入。

修复:① 重构:提取公共依赖到第三个 Bean,打破环;② 将其中一侧改为 setter 注入(需评估是否合理);③ 在其中一侧加 @Lazy,让容器注入一个延迟代理而非真实实例(代理在首次调用时才触发实例化)。首选重构,@Lazy 是逃生舱,不是设计。

Bean 生命周期 ① 实例化(构造函数) ② 属性注入(@Autowired) ③ Aware 回调 ④ BPP before ⑤ 初始化 @PostConstruct / afterPropertiesSet ⑥ BPP after(代理替换) ⑦ 就绪 → singletonObjects 三级缓存 L1: singletonObjects 成品 Bean(完全初始化) L2: earlySingletonObjects 早期引用(已实例化,未初始化完) L3: singletonFactories 工厂 → 按需产出早期引用 field/setter 环:A→B→A A 实例化后先入 L3 B 注入 A 时从 L3 取早期引用 构造器环:L3 无法介入 → 报错
图 1.4左侧是 Bean 生命周期顺序,右侧是三级缓存结构。注意:代理替换发生在第 6 步 BPP after,容器注册给单例池的是代理对象,不是原始实例。

初始化回调(@PostConstruct)来自 Jakarta EE,Spring Boot 4.0 / Jakarta EE 11 下使用 jakarta.annotation.PostConstruct,不再是旧的 javax.annotation.PostConstruct。

CacheWarmupService.javaJava
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.stereotype.Service;

@Service
public class CacheWarmupService implements BeanNameAware {

    private String beanName;

    // ③ Aware 回调 — 容器注入 Bean 名称
    @Override
    public void setBeanName(String name) {
        this.beanName = name;
    }

    // ⑤ 初始化回调 — 此时依赖已注入完毕
    @PostConstruct
    public void warmup() {
        // 容器保证:走到这里时所有 @Autowired 字段已就绪
        System.out.println(beanName + " warmup done");
    }

    // ⑧ 销毁回调 — 容器关闭时触发
    @PreDestroy
    public void cleanup() {
        System.out.println(beanName + " cleanup");
    }
}

§本章 self-check

先合上教程,把答案写下来再展开对照。

  1. BeanFactory 与 ApplicationContext 的区别是什么?哪些功能在 BeanFactory 上不存在?
  2. BeanDefinition 和 Bean 的区别用一句话说清楚——为什么蓝图必须先于实例存在?
  3. 为什么构造器注入的循环依赖解不了、而 field 注入却能?请从三级缓存机制角度解释。
答案(先做完再展开)
  1. BeanFactory 是最基础的 DI 接口,提供 getBean() 和 BeanDefinition 注册/查询,不含 AOP 集成、事件发布、i18n、自动扫描、Environment。ApplicationContext 继承 BeanFactory,叠加上述五项能力,并在 refresh 时主动注册全部 BPP,确保实例化顺序正确。直接持有 BeanFactory 引用会导致代理等功能静默失效。
  2. BeanDefinition 是描述"如何造这个 Bean"的元数据(class、scope、initMethod 等),不是 Bean 实例本身。蓝图必须先存在,才能在实例化前被 BeanFactoryPostProcessor 读写(如 PropertySourcesPlaceholderConfigurer 替换占位符);若蓝图与实例同时产生,BFPP 介入时机就丢了。
  3. 三级缓存的关键是 singletonFactories:A 实例化后(但未注入完毕)会将自己的工厂对象放入 L3,供其他 Bean 提前获取 A 的早期引用(内存地址已固定)。构造器注入的环:A 的构造函数还需要 B,此时 A 连 new 都还没执行完,无法往 L3 里放任何工厂——B 找 A 时 L3 里没有,循环无法解开,容器抛出 BeanCurrentlyInCreationException。
进阶挑战 · 刚好够不着

写一个 BeanFactoryPostProcessor,强制所有 BeanDefinition 的 lazy-init 为 false

实现一个 BeanFactoryPostProcessor,在容器启动时遍历所有已注册的 BeanDefinition,把 lazyInit 强制设为 false。该实现哪个接口?为什么这件事必须是 BFPP 而不能是 BPP?

提示(卡住再展开)

lazyInit 是 BeanDefinition 上的字段,属于蓝图属性。BPP 工作时实例已经在按原有 lazy 标记决定是否预实例化——蓝图不再被读取。因此只有在蓝图阶段(BFPP)才来得及修改。实现 BeanFactoryPostProcessor.postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory),通过 beanFactory.getBeanDefinitionNames() 遍历,对每个 beanFactory.getBeanDefinition(name) 调用 setLazyInit(false)。