本节目标:把
SpringApplication.run()到ApplicationContext可用的完整链路拆开,讲清refresh()每一步做了什么、两类后处理器在哪里介入,以及出问题时该把断点下在哪。
适用版本:Spring Boot 4.1.x(Java 21)
1.1 ApplicationContext 启动流程
实战卷解决的是「怎么配」——多模块怎么分、配置怎么外置、优雅停机怎么开。本节解决「为什么这样配」:一次 SpringApplication.run(...) 之后,容器到底按什么顺序做了哪些事,为什么有的扩展点必须在 bean 实例化之前、有的必须在之后。
很多「莫名奇妙」的现象,根源都在启动顺序上:为什么 @PostConstruct 里拿不到还没初始化的 bean、为什么在 BeanFactoryPostProcessor 里 getBean() 会出问题、为什么自定义 BeanPostProcessor 没生效。把顺序搞清,这些问题都能定位到具体一步。
本节围绕一个最小应用 LibraryApplication 演进,它注册了 BookRepository、LoanService 两个 bean,外加一个自定义 BeanPostProcessor。后面两节会继续用这套对象讲定义注册与依赖解析。
@SpringBootApplication
public class LibraryApplication {
public static void main(String[] args) {
try (ConfigurableApplicationContext ctx =
SpringApplication.run(LibraryApplication.class, args)) {
// 到这里 refresh() 已返回,容器可用
}
}
}
1.1.1 启动其实是两段:SpringApplication 与 refresh()
SpringApplication.run(String...) 返回 ConfigurableApplicationContext,它本身只做「环境准备 + 上下文创建」,真正的容器装配全部交给 AbstractApplicationContext.refresh()。把 run() 的方法序列拆开是这样的(方法名均已从本机 spring-boot-4.1.1.jar 核实):
| 顺序 | 关键动作 | 对应方法 |
|---|---|---|
| 1 | 创建 SpringApplicationRunListeners,发出 starting | getRunListeners / listeners.starting |
| 2 | 准备 ConfigurableEnvironment,发出 environmentPrepared | prepareEnvironment |
| 3 | 打印 banner | printBanner |
| 4 | 按 WebApplicationType 创建上下文 | createApplicationContext |
| 5 | 上下文准备:设环境、应用 Initializer、加载主配置类 | prepareContext |
| 6 | 触发 refresh() | refreshContext |
| 7 | 刷新后回调、执行 Runner、发出 ready | afterRefresh / callRunners |
第 4 步的 WebApplicationType 是个枚举,取值 NONE / SERVLET / REACTIVE(已核实)。SpringApplication 在构造阶段通过 WebApplicationType.deduce() 推断类型:classpath 上有 Spring MVC 就 SERVLET,只有 WebFlux 就 REACTIVE,都没有就 NONE。它决定了 DefaultApplicationContextFactory.create(...) 返回哪种上下文——本机是 servlet 应用,因此拿到的是 AnnotationConfigServletWebServerApplicationContext。
注意 4.x 的包路径变化:servlet 上下文从 3.x 的 org.springframework.boot.web.servlet.context.* 移到了 org.springframework.boot.web.server.servlet.context.*。这是模块化重构把 Web 服务器相关类抽到 spring-boot-web-server 模块的结果,升级时自定义过上下文类型的人会踩到。
1.1.2 prepareContext:把主类变成一个定义
prepareContext 是「环境」与「容器」的交接点。它依次做四件事:
context.setEnvironment(environment):把刚准备好的环境灌进上下文;postProcessApplicationContext(context):注册BeanNameGenerator、ConversionService等;applyInitializers(context):执行所有ApplicationContextInitializer<C>.initialize(C),这是「上下文已创建、refresh 尚未开始」的唯一扩展点;listeners.contextPrepared(context)与load(context, sources):后者用BeanDefinitionLoader把主类注册成一个 bean 定义。
ApplicationContextInitializer 是 org.springframework.context 包里的接口(已核实):
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
void initialize(C applicationContext);
}
Boot 自己注册了一批实现,例如 org.springframework.boot.context.ContextIdApplicationContextInitializer。另有一个名字相似但职责不同的 org.springframework.boot.web.context.servlet.WebApplicationContextInitializer(它处理 ServletContext,不是 ApplicationContextInitializer 的子类型),实测日志里 Root WebApplicationContext: initialization completed in 589 ms 那行正出自它的 logger。
关键点:prepareContext 结束时,主类还只是一个定义,@ComponentScan 尚未解析。真正的扫描发生在下一步 refresh() 的第 5 步。
1.1.3 refresh() 的十二个步骤
AbstractApplicationContext.refresh() 是整条链路的骨架。它的方法序列固定为十二步,每步职责如下:
| # | 方法 | 做什么 | 为什么放在这个位置 |
|---|---|---|---|
| 1 | prepareRefresh() | 记录启动时间、初始化 property source、校验必需属性 | 后续所有步骤都可能读环境,必须先就绪 |
| 2 | obtainFreshBeanFactory() | 拿到 ConfigurableListableBeanFactory | GenericApplicationContext 里工厂早已存在,这里是取引用 |
| 3 | prepareBeanFactory(beanFactory) | 装配 ClassLoader、SpEL 解析器、ApplicationContextAwareProcessor、注册可解析依赖 | 让后续 bean 能注入 BeanFactory/ApplicationContext 等基础设施 |
| 4 | postProcessBeanFactory(beanFactory) | 子类钩子 | servlet 上下文在此注册 request/session 作用域 |
| 5 | invokeBeanFactoryPostProcessors(beanFactory) | 调用全部 BeanFactoryPostProcessor | 定义尚未实例化,正是改定义的最后时机 |
| 6 | registerBeanPostProcessors(beanFactory) | 注册全部 BeanPostProcessor | 必须早于单例实例化,否则拦截不到 |
| 7 | initMessageSource() | 初始化国际化消息源 | 依赖前面的后处理器已就绪 |
| 8 | initApplicationEventMulticaster() | 初始化事件多播器 | 后面要发事件 |
| 9 | onRefresh() | 子类钩子 | ServletWebServerApplicationContext 在此启动 Tomcat |
| 10 | registerListeners() | 注册监听器、发布早期事件 | 多播器已就绪 |
| 11 | finishBeanFactoryInitialization(beanFactory) | 冻结定义、实例化全部非懒加载单例 | 定义已定稿、后处理器已就位 |
| 12 | finishRefresh() | 启动 LifecycleProcessor、发布 ContextRefreshedEvent | 容器可用了,对外宣告 |
第 11 步是耗时大头。它内部先 freezeConfiguration() 冻结定义、设置 ConversionService、注册 LoadTimeWeaverAware 相关后处理器,最后调用 preInstantiateSingletons() 遍历所有 bean 名逐个触发 getBean(),最终走到 AbstractAutowireCapableBeanFactory.doCreateBean()。本节 1.1.5 的断点观察就落在这里。
1.1.4 BeanFactoryPostProcessor 介入在第 5 步
BeanFactoryPostProcessor 只有一个抽象方法:
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
它的子接口 BeanDefinitionRegistryPostProcessor 多一个方法,且先于 postProcessBeanFactory 执行:
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
第 5 步的内部顺序是三段:先执行所有 BeanDefinitionRegistryPostProcessor(其中就包括解析 @Configuration 的 ConfigurationClassPostProcessor),再执行实现了 PriorityOrdered/Ordered 的普通后处理器,最后执行无序的。ConfigurationClassPostProcessor 实现的是 BeanDefinitionRegistryPostProcessor + PriorityOrdered,所以它一定排在绝大多数后处理器之前——这是「自动配置和组件扫描的定义必须先产出来」的前提。
关键点:这一步只操作「定义」,不创建 bean 实例。此时 getBean() 一个业务 bean 往往拿不到(定义可能还没产出,或依赖还没解析)。自定义后处理器若需要在实例化前改定义(例如动态改 scope、加 depends-on),写在这里才对。下面是一个把 LoanService 改成延迟初始化的例子:
@Component
public class LazyLoanServicePostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (registry.containsBeanDefinition("loanService")) {
registry.getBeanDefinition("loanService").setLazyInit(true);
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// 定义阶段无需额外处理
}
}
1.1.5 BeanPostProcessor 介入在第 6 步与第 11 步
BeanPostProcessor 的两个方法都是 default,通常只覆写其中一个:
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
第 6 步 registerBeanPostProcessors 只做「注册」——把它们实例化并按 PriorityOrdered → Ordered → 无序分组放进工厂。真正「拦截」发生在第 11 步:单例实例化时,doCreateBean() → initializeBean() 依次调用 applyBeanPostProcessorsBeforeInitialization 与 applyBeanPostProcessorsAfterInitialization。
这就是 AOP 代理能生效的原因:AbstractAutoProxyCreator 是一个 BeanPostProcessor,它在 postProcessAfterInitialization 里把原始 bean 换成代理对象。顺序上「代理在初始化之后」,所以切面能包住 @PostConstruct 之外的业务方法调用。
@Autowired 的注入则更早:AutowiredAnnotationBeanPostProcessor 同时实现 MergedBeanDefinitionPostProcessor 与 SmartInstantiationAwareBeanPostProcessor,它的 determineCandidateConstructors 在实例化阶段就介入(决定用哪个构造器),postProcessProperties 在 populateBean 阶段完成字段/方法注入。也就是说,注入发生在 initializeBean 之前。
1.1.6 后处理器的排序规则
registerBeanPostProcessors 的分组顺序不是随便定的,它决定多个后处理器叠加时的先后:
| 分组 | 判据 | 典型例子 |
|---|---|---|
| 第一组 | 实现 PriorityOrdered | AutowiredAnnotationBeanPostProcessor |
| 第二组 | 实现 Ordered | CommonAnnotationBeanPostProcessor(若显式排序) |
| 第三组 | 无序 | 普通自定义后处理器 |
注意 @Order 注解对 BeanPostProcessor 不生效——排序读的是 Ordered 接口的 getOrder(),不是注解。自定义后处理器想控制顺序,必须实现 Ordered 或 PriorityOrdered。这也是「我加了 @Order 但顺序没变」的常见原因。
1.1.7 把实测日志对回阶段
下面是一台本机 Spring Boot 4.1.1 应用的真实启动日志(Temurin 21.0.12.1,Tomcat 11.0.24),把每行标注到对应阶段:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v4.1.1)
2026-10-09T15:42:06.435+08:00 INFO 43496 --- [ main] com.example.probe.ProbeApplication : Starting ProbeApplication v0.0.1-SNAPSHOT using Java 21.0.12.1 with PID 43496
2026-10-09T15:42:06.436+08:00 INFO 43496 --- [ main] com.example.probe.ProbeApplication : No active profile set, falling back to 1 default profile: "default"
2026-10-09T15:42:07.027+08:00 INFO 43496 --- [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2026-10-09T15:42:07.039+08:00 INFO 43496 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-10-09T15:42:07.039+08:00 INFO 43496 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.24]
2026-10-09T15:42:07.059+08:00 INFO 43496 --- [ main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 589 ms
2026-10-09T15:42:07.285+08:00 INFO 43496 --- [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-10-09T15:42:07.421+08:00 INFO 43496 --- [ main] com.example.probe.ProbeApplication : Started ProbeApplication in 1.101 seconds (process running for 1.749)
2026-10-09T15:42:07.840+08:00 INFO 43496 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
对照关系:
| 日志行 | 阶段 |
|---|---|
Starting ProbeApplication ... using Java 21.0.12.1 | run() 的 logStartupInfo,环境已就绪、上下文尚未创建 |
No active profile set, falling back to 1 default profile | configureProfiles,发生在 prepareEnvironment 内 |
Root WebApplicationContext: initialization completed in 589 ms | prepareContext 收尾,主类定义已加载、环境已注入 |
Tomcat initialized with port 8080 | 第 9 步 onRefresh() → createWebServer() |
Starting service [Tomcat] / Starting Servlet engine | 同一步内,Tomcat 开始监听 |
Tomcat started on port 8080 | 第 12 步 finishRefresh(),Web 服务器已可用 |
Started ProbeApplication in 1.101 seconds | refresh() 返回后,run() 打印总耗时 |
Completed initialization in 0 ms | 首个请求到达时 DispatcherServlet 初始化 |
注意 4.x 的包名变化:Tomcat 相关日志来自 o.s.boot.tomcat.TomcatWebServer,3.x 是 o.s.b.w.embedded.tomcat.TomcatWebServer;优雅停机同理,4.x 是 o.s.boot.tomcat.GracefulShutdown。这是 4.0 模块化重构的直接证据。
1.1.8 用 ApplicationStartup 观测启动耗时
除了读日志,还可以让容器自己记录每一步的耗时。AbstractApplicationContext 提供了 setApplicationStartup(ApplicationStartup),默认实现是 DefaultApplicationStartup(空操作)。把 JDK 的 JFR 版本接上去,就能用飞行记录仪看启动各阶段:
SpringApplication app = new SpringApplication(LibraryApplication.class);
app.setApplicationStartup(new FlightRecorderApplicationStartup());
app.run(args);
org.springframework.core.metrics.jfr.FlightRecorderApplicationStartup 位于 spring-core(已核实)。它把 refresh() 各步记成 JFR 事件,可用 jfr print 导出。本机未接 JFR 后端,下面是示例输出格式:
jdk.ExecutionSample { ... }
spring.context.refresh { startTime = 15:42:06.44, duration = 612 ms }
更细的每步事件需要 JFR 录制配置支持,本节只给到接口与用法,具体事件名以你所在 Spring Framework 版本的 StartupStep 打点为准。
1.1.9 知道之后能做什么
排障断点。 想看清「某 bean 到底在哪一步被创建」,在 AbstractApplicationContext.refresh() 的十二个方法上逐个下断点,或在 AbstractAutowireCapableBeanFactory.doCreateBean 首行下断点,观察调用栈落在哪一步。
扩展点选择。 需要改定义(改 scope、加依赖)用 BeanDefinitionRegistryPostProcessor;需要包一层对象(代理、装饰)用 BeanPostProcessor.postProcessAfterInitialization;需要拿到「所有单例都就绪」的时点用 SmartInitializingSingleton.afterSingletonsInstantiated,它在 preInstantiateSingletons 末尾被调用;需要在 refresh 之前调整环境用 ApplicationContextInitializer。
验证顺序。 写一个 @Component 实现 ApplicationContextAware 与 SmartInitializingSingleton,在两个回调里各打印一次 beanFactory.getBeanDefinitionCount(),会看到后者远大于前者——因为 BeanFactoryPostProcessor 已经把扫描与自动配置的定义都补进去了。
小结
- 启动是「
SpringApplication.run()准备环境与上下文」+「refresh()装配容器」两段,扫描与自动配置的定义产出都在refresh()内。 refresh()的十二步顺序不可乱:定义改在第 5 步、后处理器注册在第 6 步、实例化在第 11 步。BeanFactoryPostProcessor只碰定义,BeanPostProcessor才碰实例;@Autowired注入早于initializeBean,AOP 代理晚于它。- 后处理器排序看
Ordered/PriorityOrdered接口,@Order注解在这里无效。 - 实测日志的每一行都能映射到具体阶段,出问题时先看日志停在哪一行,再决定断点位置。
下一节进入定义层:这些 bean 在内存里到底是什么结构,扫描器与配置类解析器如何把注解变成 BeanDefinition,父子定义又是怎么合并的。
阅读导航:上一节:目录 · 下一节:1.2 Bean 定义注册与合并 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。