主题
容器启动流程:从 refresh() 到 BeanDefinition
本文是 Spring 系统学习系列的 L2 核心篇。前置:Spring Boot 快速上手:约定优于配置与 Starter。学完可以配合面试题食用:Spring IoC 容器初始化流程与 BeanFactory/ApplicationContext 区别
一行 new 背后发生了什么
写 Spring 测试代码时经常有这一行:
java
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
UserService u = ctx.getBean(UserService.class);第一行看着轻,实际干了两件大事:先把 AppConfig 这个配置类登记进容器,然后调用 refresh() 把容器启动走完。Spring Boot 的 SpringApplication.run() 前面绕了一圈(准备环境、决定应用类型、创建 ApplicationContext),最后一步 refreshContext 还是落到 refresh() 上。所以搞懂容器启动,本质就是搞懂 refresh() 每一步在干什么,以及在那之前 BeanDefinition 是怎么进容器的。
BeanDefinition:先收图纸,再开工
Spring 不会一上来就反射创建所有对象。第一步是收集"图纸":每个受管 Bean 对应一个 BeanDefinition,记录类名、scope、是否懒加载、initMethod、destroyMethod、构造参数和属性值。真正的实例在这张图纸登记之后才可能被创建。
java
BeanDefinition bd = BeanDefinitionBuilder.genericBeanDefinition(PaymentClient.class)
.addPropertyValue("endpoint", "https://api.pay.example.com")
.setInitMethodName("init")
.getBeanDefinition();
// 执行完这行,容器里多了一张图纸,PaymentClient 的实例还不存在
registry.registerBeanDefinition("paymentClient", bd);图纸有三条来源:注解扫描(@Component 及其派生注解,最终由 ConfigurationClassPostProcessor 解析)、XML 配置(<bean> 标签,由 XmlBeanDefinitionReader 解析)、编程式注册(registerBeanDefinition 或 BeanDefinitionRegistryPostProcessor)。所有图纸最终存进 DefaultListableBeanFactory 的 beanDefinitionMap,key 是 Bean 名,value 就是 BeanDefinition。分清"图纸登记"和"实例创建"两个阶段,后面 refresh() 的步骤才看得懂。
refresh() 的 12 步:盯住 6 个节点
AbstractApplicationContext#refresh() 源码里是 12 个固定步骤,全部在一把锁里顺序执行,任何一步抛异常整个启动失败。不必背全 12 个,记住 6 个关键节点就够画出全景:
- obtainFreshBeanFactory:拿到内部的 BeanFactory(DefaultListableBeanFactory 在更早的构造器里已创建),此时里面只有寥寥几张"种子图纸"(比如 AppConfig 自身)
- invokeBeanFactoryPostProcessors:执行 BeanFactoryPostProcessor,允许改图纸、加图纸,组件扫描就发生在这里(下一节展开)
- registerBeanPostProcessors:登记 BeanPostProcessor,它们是"加工工人",会在每个 Bean 实例化前后的各个节点插手(@Autowired 注入就靠这批工人)
- onRefresh:留给子类的钩子,默认空实现,Web 场景这里是重头戏
- finishBeanFactoryInitialization:预实例化所有非懒加载的单例 Bean,业务类大多在这一步被真正 new 出来
- finishRefresh:发布 ContextRefreshedEvent,宣告容器就绪
mermaid
sequenceDiagram
participant M as main / SpringApplication
participant C as AnnotationConfigApplicationContext
participant F as DefaultListableBeanFactory
participant P as ConfigurationClassPostProcessor
M->>C: new + register(AppConfig)
C->>F: 登记 AppConfig 自身的 BeanDefinition
M->>C: refresh()
C->>F: invokeBeanFactoryPostProcessors
F->>P: 触发 ConfigurationClassPostProcessor
P->>F: 解析 @ComponentScan/@Import/@Bean,批量登记图纸
C->>F: registerBeanPostProcessors(登记加工工人)
C->>C: onRefresh(Boot 在这里创建内嵌 Tomcat)
C->>F: finishBeanFactoryInitialization(预实例化单例)
C->>C: finishRefresh(发布 ContextRefreshedEvent)事件机制嵌在启动流程里的位置由此明确:ContextRefreshedEvent 在 finishRefresh() 里发布,晚于所有单例的实例化。老式 Spring MVC 的父子容器(root + servlet 两个 context)各 refresh 一次,同一个监听器会被触发两次,去重的惯用写法是判断 event.getApplicationContext().getParent() == null。Boot 默认单容器,没这个问题。
两步关键戏份:扫描与内嵌 Tomcat
invokeBeanFactoryPostProcessors 是注解模式的分水岭。 这一步优先执行 BeanDefinitionRegistryPostProcessor(BeanFactoryPostProcessor 的加强版,能增删图纸),其中最重要的角色是 ConfigurationClassPostProcessor。它解析 AppConfig 上的 @ComponentScan,用 ClassPathBeanDefinitionScanner 扫包:读字节码元数据,把带 @Component/@Service/@Repository 的类逐个做成 ScannedGenericBeanDefinition 登记进容器;顺带处理 @Import、@Bean 方法、@PropertySource。也就是说,new 容器的构造器只登记了配置类一张图纸,业务类的图纸全部在这一步批量进来,而且此时还没有任何业务 Bean 被实例化,改图纸来得及。
onRefresh 是 Boot 内嵌 Tomcat 的挂载点。 Boot 的 ServletWebServerApplicationContext 重写了 onRefresh,在这里创建 WebServer(TomcatServletWebServerFactory 产出 Tomcat 实例并绑定端口),再配合 finishRefresh 之后的生命周期回调把服务完全拉起。所以"TOMCAT 是在 main 里启动的"这个印象不准确,main 只是触发了 refresh(),Tomcat 是 refresh 中段的一环。
BeanFactory 与 ApplicationContext 的分工
跑完启动流程后回头看两个核心接口就清晰了。BeanFactory 是容器的基础契约:getBean、containsBean、类型查询,DefaultListableBeanFactory 是它的标准实现,一手持有 beanDefinitionMap(图纸库),一手持有 singletonObjects(成品仓库),Bean 的创建、注入、生命周期回调全由它执行。ApplicationContext 在 BeanFactory 之上加了四样东西:事件发布(ApplicationEventPublisher)、国际化(MessageSource)、资源加载(ResourcePatternResolver)、自动注册 BeanPostProcessor。它自己是门面,脏活委托给内部的 DefaultListableBeanFactory。面试里那句"ApplicationContext 是 BeanFactory 的超集"对应的就是代码里的继承关系加组合关系。
动手实操:动态注册 Bean
需求场景:第三方 jar 里的类没加 @Component,扫描扫不到,又不想为它写 @Bean 方法。用 BeanDefinitionRegistryPostProcessor 在图纸阶段手工登记:
java
@Configuration
@ComponentScan("com.example")
public class AppConfig {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
PaymentClient client = ctx.getBean("paymentClient", PaymentClient.class);
client.pay("order-1001"); // 输出:支付请求已发往 https://api.pay.example.com:order-1001
ctx.close();
}
// 它在 refresh() 第 5 步被触发,早于所有普通 Bean 的实例化
@Component
static class DynamicBeanRegistrar implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
registry.registerBeanDefinition("paymentClient",
BeanDefinitionBuilder.genericBeanDefinition(PaymentClient.class)
.addPropertyValue("endpoint", "https://api.pay.example.com")
.setInitMethodName("init")
.getBeanDefinition());
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// 第二个回调:所有图纸已登记、实例化尚未开始,适合改属性
beanFactory.getBeanDefinition("paymentClient")
.getPropertyValues().add("timeout", "3000");
}
}
}
// 注意:没有 @Component,靠常规扫描进不了容器
public class PaymentClient {
private String endpoint;
private int timeout;
public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
public void setTimeout(int timeout) { this.timeout = timeout; }
public void init() {
System.out.println("连接 " + endpoint + ",超时 " + timeout + "ms");
}
public void pay(String orderId) {
System.out.println("支付请求已发往 " + endpoint + ":" + orderId);
}
}跑一遍 main 能验证三件事:动态注册的 Bean 和正常扫描的待遇完全一样(属性注入、initMethod 都生效);postProcessBeanDefinitionRegistry 先于 postProcessBeanFactory 执行;两个回调都跑在任何业务 Bean 实例化之前,init() 打印的 timeout 是 3000,说明改图纸确实赶上了。
常见误区与小结
- BeanDefinition 等于 Bean 实例:图纸登记不等于开工,实例化发生在 finishBeanFactoryInitialization 或首次 getBean,两个阶段隔着大半个 refresh()
- 组件扫描发生在构造器里:AnnotationConfigApplicationContext 的构造器只登记配置类自身,@ComponentScan 的批量扫描在 refresh() 第 5 步
- 启动完成时所有 Bean 都创建了:@Lazy 单例和 prototype 不参与预实例化,后者每次 getBean 都新建
- Boot 的 Tomcat 在 main 里启动:Tomcat 挂在 refresh() 的 onRefresh 钩子里,main 只是发起方
- ContextRefreshedEvent 只发一次:父子容器场景(老式 MVC)会发两次,监听器里要判空 parent 去重
小结:本文把容器启动拆成两阶段看,先收集 BeanDefinition 图纸(扫描、解析、登记),再由 refresh() 12 步把容器拉起来,其中 invokeBeanFactoryPostProcessors 决定图纸长什么样,finishBeanFactoryInitialization 决定什么时候开工。它是上一篇 Boot 快速上手的下沉,也是下一篇的前置:实例化之后每个 Bean 还要经历初始化前后的一串回调节点,下一篇讲 Bean 生命周期的扩展点。
参考
参考:Spring Framework 源码 AbstractApplicationContext#refresh()(spring-context 模块);ConfigurationClassPostProcessor 与 BeanDefinitionRegistryPostProcessor 的 Javadoc;Spring 官方文档 Core - The IoC Container 的 Bean Definition Overview 一节