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 和属性源)。
BeanFactory 在首次调用 getBean() 时才触发 BPP 注册,无法保证 BPP 在所有 Bean 实例化之前到位。ApplicationContext 在 refresh 阶段主动注册全部后置处理器,确保顺序正确。直接使用 BeanFactory 会导致代理、校验、事件等功能静默失效。
下面的示意代码展示注解只是配置容器的语法糖——AnnotationConfigApplicationContext 可以在不扫描任何包的情况下手动注册 BeanDefinition:
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();
}
}
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},而非端口数字。
1.3两类后置处理器(本教程第一刀)
区分 BeanFactoryPostProcessor(BFPP)和 BeanPostProcessor(BPP)是理解 Spring 机制的第一刀。两者作用在不同的对象、不同的时机:
| 维度 | 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 章。
1.4Bean 生命周期 + 三级缓存与循环依赖
单例 Bean 的完整生命周期按以下顺序推进:
- 实例化——调用构造函数,对象分配内存(此时属性尚未注入);
- 属性注入——
AutowiredAnnotationBeanPostProcessor填充@Autowired依赖; - Aware 回调——
BeanNameAware、BeanFactoryAware、ApplicationContextAware等; - BPP before——
postProcessBeforeInitialization(); - 初始化——
@PostConstruct(JSR-250,jakarta 包)→InitializingBean.afterPropertiesSet()→initMethod; - BPP after——
postProcessAfterInitialization()(代理在此替换原对象); - 就绪——放入单例池,供
getBean()返回; - 销毁——容器关闭时:
@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 是逃生舱,不是设计。
初始化回调(@PostConstruct)来自 Jakarta EE,Spring Boot 4.0 / Jakarta EE 11 下使用 jakarta.annotation.PostConstruct,不再是旧的 javax.annotation.PostConstruct。
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
先合上教程,把答案写下来再展开对照。
- BeanFactory 与 ApplicationContext 的区别是什么?哪些功能在 BeanFactory 上不存在?
- BeanDefinition 和 Bean 的区别用一句话说清楚——为什么蓝图必须先于实例存在?
- 为什么构造器注入的循环依赖解不了、而 field 注入却能?请从三级缓存机制角度解释。
答案(先做完再展开)
- BeanFactory 是最基础的 DI 接口,提供
getBean()和 BeanDefinition 注册/查询,不含 AOP 集成、事件发布、i18n、自动扫描、Environment。ApplicationContext 继承 BeanFactory,叠加上述五项能力,并在 refresh 时主动注册全部 BPP,确保实例化顺序正确。直接持有 BeanFactory 引用会导致代理等功能静默失效。 - BeanDefinition 是描述"如何造这个 Bean"的元数据(class、scope、initMethod 等),不是 Bean 实例本身。蓝图必须先存在,才能在实例化前被 BeanFactoryPostProcessor 读写(如
PropertySourcesPlaceholderConfigurer替换占位符);若蓝图与实例同时产生,BFPP 介入时机就丢了。 - 三级缓存的关键是
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)。