《Spring Boot 高级》1.1 ApplicationContext 启动流程

从 SpringApplication.run() 追到 ApplicationContext 可用:拆开 refresh() 的十二个步骤各自做了什么,标出 BeanFactoryPostProcessor 与 BeanPostProcessor 的介入时机,把本机实测启动日志逐行对回框架阶段,并给出断点位置与扩展点清单。

本节目标:把 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,发出 startinggetRunListeners / listeners.starting
2准备 ConfigurableEnvironment,发出 environmentPreparedprepareEnvironment
3打印 bannerprintBanner
4按 WebApplicationType 创建上下文createApplicationContext
5上下文准备:设环境、应用 Initializer、加载主配置类prepareContext
6触发 refresh()refreshContext
7刷新后回调、执行 Runner、发出 readyafterRefresh / 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 是「环境」与「容器」的交接点。它依次做四件事:

  1. context.setEnvironment(environment):把刚准备好的环境灌进上下文;
  2. postProcessApplicationContext(context):注册 BeanNameGenerator、ConversionService 等;
  3. applyInitializers(context):执行所有 ApplicationContextInitializer<C>.initialize(C),这是「上下文已创建、refresh 尚未开始」的唯一扩展点;
  4. 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() 是整条链路的骨架。它的方法序列固定为十二步,每步职责如下:

#方法做什么为什么放在这个位置
1prepareRefresh()记录启动时间、初始化 property source、校验必需属性后续所有步骤都可能读环境,必须先就绪
2obtainFreshBeanFactory()拿到 ConfigurableListableBeanFactoryGenericApplicationContext 里工厂早已存在,这里是取引用
3prepareBeanFactory(beanFactory)装配 ClassLoader、SpEL 解析器、ApplicationContextAwareProcessor、注册可解析依赖让后续 bean 能注入 BeanFactory/ApplicationContext 等基础设施
4postProcessBeanFactory(beanFactory)子类钩子servlet 上下文在此注册 request/session 作用域
5invokeBeanFactoryPostProcessors(beanFactory)调用全部 BeanFactoryPostProcessor定义尚未实例化,正是改定义的最后时机
6registerBeanPostProcessors(beanFactory)注册全部 BeanPostProcessor必须早于单例实例化,否则拦截不到
7initMessageSource()初始化国际化消息源依赖前面的后处理器已就绪
8initApplicationEventMulticaster()初始化事件多播器后面要发事件
9onRefresh()子类钩子ServletWebServerApplicationContext 在此启动 Tomcat
10registerListeners()注册监听器、发布早期事件多播器已就绪
11finishBeanFactoryInitialization(beanFactory)冻结定义、实例化全部非懒加载单例定义已定稿、后处理器已就位
12finishRefresh()启动 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 的分组顺序不是随便定的,它决定多个后处理器叠加时的先后:

分组判据典型例子
第一组实现 PriorityOrderedAutowiredAnnotationBeanPostProcessor
第二组实现 OrderedCommonAnnotationBeanPostProcessor(若显式排序)
第三组无序普通自定义后处理器

注意 @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.1run() 的 logStartupInfo,环境已就绪、上下文尚未创建
No active profile set, falling back to 1 default profileconfigureProfiles,发生在 prepareEnvironment 内
Root WebApplicationContext: initialization completed in 589 msprepareContext 收尾,主类定义已加载、环境已注入
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 secondsrefresh() 返回后,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 定义注册与合并 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计