问题引入

2024 年 Q2,某电商公司的订单服务在容器化改造后,启动时间从 12 秒飙升到 38 秒。Kubernetes 的滚动更新策略要求 maxUnavailable=0,这意味着每次发布需要先启动新 Pod、通过健康检查、再下线旧 Pod。38 秒的启动时间让一次简单的发布要持续 15 分钟,开发团队苦不堪言。

排查过程充满了困惑:

  • 有人怀疑是容器资源限制,加了 CPU 和内存,启动时间只降了 2 秒
  • 有人怀疑是网络问题,检查了 DNS 和镜像拉取,都不是瓶颈
  • 有人在 @PostConstruct 里加了 System.currentTimeMillis() 打点,发现某个 Bean 的初始化占了 8 秒——但它明明只是从数据库加载了一些配置
  • 最终发现,自动配置加载了 147 个配置类,其中 30 多个根本不需要,只是因为在类路径上而已

SpringBoot 的 main 方法里只有一行代码:

java 复制代码
SpringApplication.run(Application.class, args);

但这行代码背后,SpringBoot 完成了应用类型推断、环境准备、容器创建、自动配置加载、单例 Bean 实例化、内嵌服务器启动等数十个步骤。面试追问下去:

  • SpringApplication.run() 到底做了什么?每一步的输入和输出是什么?
  • Environment 是怎么组装出来的?为什么命令行参数能覆盖 application.yml
  • 自动配置在启动的哪个阶段加载?内嵌 Tomcat 又是在哪个阶段启动的?
  • ApplicationRunnerCommandLineRunner 有什么区别?执行时机是什么时候?
  • 如果启动耗时过长,应该从哪里入手排查?

本文从 SpringApplication 的构造到 ApplicationReadyEvent 的发布,穿透启动流程的每一个环节。

核心概念

1. SpringApplication 的角色

SpringApplication 是 SpringBoot 应用的启动引导类,它不是 Spring Framework 的一部分,而是 SpringBoot 对 Spring 容器启动过程的封装和编排。

java 复制代码
// SpringApplication 的核心职责
public class SpringApplication {
    // 1. 推断应用类型(Servlet / Reactive / None)
    private WebApplicationType webApplicationType;

    // 2. 启动前的扩展点
    private List<ApplicationContextInitializer<?>> initializers;
    private List<ApplicationListener<?>> listeners;

    // 3. 主类(用于包扫描和配置推断)
    private Class<?> mainApplicationClass;

    // 4. 启动入口
    public ConfigurableApplicationContext run(String... args) {
        // ... 九步启动流程
    }
}

可以把 SpringApplication 理解为一场音乐会的指挥家——它自己不演奏乐器,但负责确定演出类型(交响乐团还是室内乐)、调音(准备环境)、安排乐手就位(创建容器)、指挥演奏(刷新上下文)。

2. 应用类型推断

SpringBoot 2.0 引入了 WebApplicationType 枚举,根据类路径推断应用类型:

java 复制代码
public enum WebApplicationType {
    NONE,       // 非 Web 应用(批处理、定时任务)
    SERVLET,    // 传统 Servlet Web 应用(Tomcat/Jetty/Undertow)
    REACTIVE    // 响应式 Web 应用(Netty)
}

推断逻辑(WebApplicationType.deduceFromClasspath()):

复制代码
类路径存在 reactor.core.publisher.Flux
    AND 存在 org.springframework.web.reactive.DispatcherHandler
    AND 不存在 org.springframework.web.servlet.DispatcherServlet
    → REACTIVE

类路径存在 javax.servlet.Servlet(或 jakarta.servlet.Servlet)
    AND 存在 org.springframework.web.context.ConfigurableWebApplicationContext
    → SERVLET

否则 → NONE

读图导引:推断顺序是先检查 Reactive 再检查 Servlet。如果类路径上同时有 Spring MVC 和 Spring WebFlux,SpringBoot 会优先判定为 SERVLET 类型(因为 DispatcherServlet 的存在),除非显式排除 spring-webmvc。

3. ApplicationContextInitializer 与 ApplicationListener

这是 SpringBoot 留给开发者的两个核心扩展点:

扩展点 触发时机 用途
ApplicationContextInitializer Context 创建后、刷新前 修改 Context 的初始状态(如注册自定义的 BeanDefinition、修改 Environment)
ApplicationListener 启动全生命周期 监听 Spring 事件(EnvironmentPreparedEvent、ContextRefreshedEvent 等)
java 复制代码
// 自定义 Initializer:在刷新前修改 Environment
public class CustomInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext context) {
        ConfigurableEnvironment env = context.getEnvironment();
        env.getPropertySources().addFirst(new MapPropertySource("custom",
            Collections.singletonMap("custom.key", "customValue")));
    }
}

注册方式:在 META-INF/spring.factoriesMETA-INF/spring/org.springframework.context.ApplicationContextInitializer.imports 中声明:

复制代码
org.springframework.context.ApplicationContextInitializer=\
com.example.CustomInitializer

4. Environment 与 PropertySource

Environment 是 Spring 的属性源抽象,内部包含多个 PropertySource,按优先级排列:

复制代码
PropertySources(按优先级从高到低)
├── ServletConfigPropertySource        // Servlet 配置
├── ServletContextPropertySource       // ServletContext 参数
├── JndiPropertySource                 // JNDI 属性
├── MapPropertySource (systemProperties) // System.getProperties()
├── SystemEnvironmentPropertySource    // System.getenv()
├── RandomValuePropertySource          // random.*
├── OriginTrackedMapPropertySource (application-dev.yml)
├── OriginTrackedMapPropertySource (application.yml)
└── MapPropertySource (defaultProperties)

读图导引:高优先级的 PropertySource 会覆盖低优先级的同名属性。PropertySourcesPropertyResolver 在解析属性时,按顺序遍历 PropertySource 列表,第一个匹配的键值即为结果。

5. SpringApplicationRunListener

这是 SpringBoot 特有的启动监听器接口,比 ApplicationListener 更贴近启动流程:

java 复制代码
public interface SpringApplicationRunListener {
    void starting();                    // 启动开始时
    void environmentPrepared(ConfigurableEnvironment environment);  // Environment 准备完成
    void contextPrepared(ConfigurableApplicationContext context);   // Context 创建完成
    void contextLoaded(ConfigurableApplicationContext context);     // Context 加载完成
    void started(ConfigurableApplicationContext context);           // 启动完成
    void running(ConfigurableApplicationContext context);           // 运行中
    void failed(ConfigurableApplicationContext context, Throwable exception); // 启动失败
}

事件序列:

复制代码
starting() ──▶ environmentPrepared() ──▶ contextPrepared() ──▶ contextLoaded() ──▶ started() ──▶ running()
     │                                                                    │
     └── 发布 ApplicationStartingEvent                          └── 发布 ApplicationStartedEvent
                                                                          └── 发布 ApplicationReadyEvent

读图导引:SpringApplicationRunListener 是 SpringBoot 启动的"里程碑"系统,每个方法对应一个确定的启动阶段。开发者可以在特定阶段插入自定义逻辑(如在 environmentPrepared 时动态修改配置)。

6. ApplicationRunner vs CommandLineRunner

两者都是在容器启动完成后执行的回调接口,用于执行初始化逻辑:

对比维度 ApplicationRunner CommandLineRunner
参数类型 ApplicationArguments(结构化参数) String... args(原始参数数组)
参数解析 自动解析 --key=value--flag 不解析,原始字符串
执行时机 都在 refreshContext 之后、running 之前 同上
排序方式 @OrderOrdered 接口 @OrderOrdered 接口
执行顺序 所有 Runner 按 @Order 排序后先执行 ApplicationRunner,再执行 CommandLineRunner 同上
java 复制代码
@Component
@Order(1)
public class CacheWarmUpRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
        // 解析 --warmup.cache=users,orders
        List<String> caches = args.getOptionValues("warmup.cache");
        // 预热缓存...
    }
}

原理分析

1. 启动全景:九步流程

graph TD Start[main: SpringApplication.run] --> Step1[Step 1: 构造 SpringApplication] Step1 --> Step2[Step 2: 发布 starting 事件] Step2 --> Step3[Step 3: 准备 Environment] Step3 --> Step4[Step 4: 创建 ApplicationContext] Step4 --> Step5[Step 5: 准备 Context] Step5 --> Step6[Step 6: 刷新 Context] Step6 --> Step7[Step 7: 启动完成事件] Step7 --> Step8[Step 8: 执行 Runner] Step8 --> Step9[Step 9: 发布 running 事件] Step6 --> RefreshSub[refresh 内部子流程] RefreshSub --> RF1[invokeBeanFactoryPostProcessors<br/>加载自动配置] RefreshSub --> RF2[registerBeanPostProcessors] RefreshSub --> RF3[onRefresh<br/>启动内嵌 Web 服务器] RefreshSub --> RF4[finishBeanFactoryInitialization<br/>实例化单例 Bean] style Step6 fill:#f9f,stroke:#333,stroke-width:2px style RefreshSub fill:#f9f,stroke:#333,stroke-width:2px style RF3 fill:#ff9,stroke:#333,stroke-width:2px

读图导引:粉色节点是启动流程的核心——refresh Context 阶段承载了自动配置加载、Bean 实例化和内嵌服务器启动三大重任。黄色节点 onRefresh 是 Web 应用独有的扩展点,非 Web 应用跳过此步。

2. Step 1:构造 SpringApplication

java 复制代码
public SpringApplication(Class<?>... primarySources) {
    // 1. 保存主配置类
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

    // 2. 推断应用类型
    this.webApplicationType = WebApplicationType.deduceFromClasspath();

    // 3. 加载所有 ApplicationContextInitializer
    // 从 META-INF/spring.factories 读取 key=org.springframework.context.ApplicationContextInitializer
    setInitializers((Collection) getSpringFactoriesInstances(
        ApplicationContextInitializer.class));

    // 4. 加载所有 ApplicationListener
    // 从 META-INF/spring.factories 读取 key=org.springframework.context.ApplicationListener
    setListeners((Collection) getSpringFactoriesInstances(
        ApplicationListener.class));

    // 5. 推断主类(通过堆栈跟踪找到 main 方法所在的类)
    this.mainApplicationClass = deduceMainApplicationClass();
}

推断主类的技巧

java 复制代码
private Class<?> deduceMainApplicationClass() {
    try {
        StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
        for (StackTraceElement stackTraceElement : stackTrace) {
            if ("main".equals(stackTraceElement.getMethodName())) {
                return Class.forName(stackTraceElement.getClassName());
            }
        }
    } catch (ClassNotFoundException ex) {
        // ignore
    }
    return null;
}

SpringBoot 通过构造一个 RuntimeException 获取调用栈,然后找到方法名为 main 的栈帧——这个技巧确保了即使 SpringApplication.run() 被包装在另一个类的静态方法中调用,也能正确推断出用户的主类。

3. Step 2-3:准备 Environment

java 复制代码
ConfigurableEnvironment environment = prepareEnvironment(listeners, args);

prepareEnvironment 的内部流程:

graph LR A[准备 Environment] --> B[创建 Environment 实例] B --> C[配置 PropertySources] C --> D[发布 ApplicationEnvironmentPreparedEvent] D --> E[绑定 spring.main.* 到 SpringApplication] B --> B1[Web: StandardServletEnvironment] B --> B2[Reactive: StandardReactiveWebEnvironment] B --> B3[None: StandardEnvironment] C --> C1[添加 SimpleCommandLinePropertySource] C --> C2[添加系统属性 PropertySource] C --> C3[添加环境变量 PropertySource] C --> C4[加载 application.yml] C --> C5[激活 profile] style A fill:#f9f,stroke:#333,stroke-width:2px

读图导引:Environment 的准备是"层层叠加"的过程。每添加一个 PropertySource,都在列表的尾部追加,但由于解析时从前向后遍历,后添加的高优先级源会覆盖前面的同名属性。

配置文件加载与 profile 激活

java 复制代码
// ConfigFileApplicationListener 的处理逻辑
void load() {
    // 1. 加载默认配置文件:application.yml / application.properties
    // 2. 根据 spring.profiles.active 加载 profile 专属配置
    // 3. 高优先级文件覆盖低优先级文件
}

profile 的激活优先级:

  1. SpringApplication.setAdditionalProfiles()
  2. ConfigurableEnvironment.setActiveProfiles()
  3. JVM 参数 -Dspring.profiles.active=prod
  4. 环境变量 SPRING_PROFILES_ACTIVE=prod
  5. application.yml 中的 spring.profiles.active

暗面:profile 的继承关系容易让人困惑。application-prod.yml 不会继承 application.yml 的全部属性——如果 application.yml 定义了 server.port=8080,而 application-prod.yml 没有定义 server.port,最终生效的仍然是 8080。但如果 application-prod.yml 显式定义了 server.port=80,则覆盖默认值。这不是继承,而是"叠加后覆盖"。

4. Step 4-5:创建与准备 Context

java 复制代码
// Step 4: 创建 Context
context = createApplicationContext();

// Step 5: 准备 Context
prepareContext(context, environment, listeners, args, printedBanner);

Context 类型选择

应用类型 Context 类型
SERVLET AnnotationConfigServletWebServerApplicationContext
REACTIVE AnnotationConfigReactiveWebServerApplicationContext
NONE AnnotationConfigApplicationContext

prepareContext 的核心操作

java 复制代码
private void prepareContext(ConfigurableApplicationContext context,
        ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments, Banner printedBanner) {

    // 1. 将 Environment 关联到 Context
    context.setEnvironment(environment);

    // 2. 应用后处理(注册 BeanNameGenerator、ResourceLoader 等)
    postProcessApplicationContext(context);

    // 3. 执行所有 ApplicationContextInitializer
    applyInitializers(context);

    // 4. 发布 ApplicationContextInitializedEvent
    listeners.contextPrepared(context);

    // 5. 注册 SpringBoot 特殊 Bean
    //    - springApplicationArguments(命令行参数)
    //    - springBootBanner(Banner 对象)
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    if (printedBanner != null) {
        beanFactory.registerSingleton("springBootBanner", printedBanner);
    }

    // 6. 加载主类 BeanDefinition(后续 refresh 时实例化)
    Set<Object> sources = getAllSources();
    load(context, sources.toArray(new Object[0]));

    // 7. 发布 ApplicationPreparedEvent
    listeners.contextLoaded(context);
}

读图导引:prepareContext 是 Context 的"出厂设置"阶段。Initializer 在此阶段执行,可以对 Context 做最后的修改。主类的 BeanDefinition 在此注册,但尚未实例化——实例化要到 refresh() 阶段才发生。

5. Step 6:刷新 Context(核心)

java 复制代码
refreshContext(context);

这最终调用到 AbstractApplicationContext.refresh(),是 Spring Framework 的核心方法:

java 复制代码
@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // 1. 准备刷新(记录启动时间、初始化属性源)
        prepareRefresh();

        // 2. 获取/刷新 BeanFactory
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // 3. 准备 BeanFactory
        prepareBeanFactory(beanFactory);

        try {
            // 4. 子类扩展(Web 应用注册 ServletContextAwareProcessor)
            postProcessBeanFactory(beanFactory);

            // 5. 【关键】执行 BeanFactoryPostProcessor
            //    ConfigurationClassPostProcessor 解析 @Configuration 类
            //    AutoConfigurationImportSelector 加载自动配置类
            invokeBeanFactoryPostProcessors(beanFactory);

            // 6. 注册 BeanPostProcessor
            registerBeanPostProcessors(beanFactory);

            // 7. 初始化国际化和事件广播器
            initMessageSource();
            initApplicationEventMulticaster();

            // 8. 【关键】子类扩展:Web 应用在此创建并启动内嵌服务器
            onRefresh();

            // 9. 注册监听器
            registerListeners();

            // 10. 【关键】实例化所有非懒加载的单例 Bean
            finishBeanFactoryInitialization(beanFactory);

            // 11. 完成刷新,发布 ContextRefreshedEvent
            finishRefresh();
        }
        // ... catch and handle exceptions
    }
}

读图导引:三步"关键"调用构成了刷新阶段的核心骨架。第 5 步加载自动配置,第 8 步启动内嵌服务器,第 10 步实例化业务 Bean。这三步的顺序是严格设计的——自动配置必须在 Bean 实例化前完成,内嵌服务器必须在 Bean 实例化后启动(因为某些 Bean 的初始化可能依赖于服务器的就绪状态)。

自动配置在哪个阶段加载?

invokeBeanFactoryPostProcessors() 阶段,ConfigurationClassPostProcessor 会解析所有 @Configuration 类,包括通过 @Import(AutoConfigurationImportSelector.class) 引入的自动配置类。AutoConfigurationImportSelector 读取 AutoConfiguration.imports 文件,获取候选配置类列表,然后根据条件注解过滤,最终将生效的配置类注册为 BeanDefinition。

内嵌 Tomcat 在哪个阶段启动?

java 复制代码
// ServletWebServerApplicationContext.onRefresh()
@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        createWebServer();  // 创建并启动内嵌 Web 服务器
    } catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start web server", ex);
    }
}

onRefresh()finishBeanFactoryInitialization() 之前执行,这意味着内嵌服务器在业务 Bean 实例化前就已经启动。但有些 Bean 的初始化(如 @PostConstruct 方法)可能依赖于 ServletContext,因此 SpringBoot 在 createWebServer() 时通过 getSelfInitializer() 注册了一个 ServletContextInitializer,确保后续 Servlet 和 Filter 的注册能正确执行。

单例 Bean 的实例化

java 复制代码
// DefaultListableBeanFactory.preInstantiateSingletons()
@Override
public void preInstantiateSingletons() throws BeansException {
    // 1. 遍历所有 BeanDefinition 名称
    List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
    for (String beanName : beanNames) {
        // 2. 获取合并后的 BeanDefinition
        RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);

        // 3. 条件:非抽象、单例、非懒加载
        if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
            // 4. 如果是 FactoryBean,先获取 FactoryBean 本身
            if (isFactoryBean(beanName)) {
                // ...
            } else {
                // 5. 普通 Bean:走 getBean → createBean 流程
                getBean(beanName);
            }
        }
    }
}

这是 Spring IOC 容器的标准行为:按 BeanDefinition 的注册顺序,逐个实例化非懒加载的单例 Bean。如果 Bean A 依赖 Bean B,B 会先被实例化——这就是 Spring 的依赖注入保证。

6. Step 7-9:启动完成与 Runner 执行

java 复制代码
// SpringApplication.run() 的后半段

// Step 7: 启动完成
afterRefresh(context, applicationArguments);
listeners.started(context);  // 发布 ApplicationStartedEvent

// Step 8: 执行 Runner
callRunners(context, applicationArguments);

// Step 9: 运行中
listeners.running(context);  // 发布 ApplicationReadyEvent

// 返回 Context
return context;

事件发布的完整时序

sequenceDiagram participant SA as SpringApplication participant L as SpringApplicationRunListeners participant App as Application SA->>L: starting() Note over L: 发布 ApplicationStartingEvent SA->>L: environmentPrepared(environment) Note over L: 发布 ApplicationEnvironmentPreparedEvent SA->>L: contextPrepared(context) Note over L: 发布 ApplicationContextInitializedEvent SA->>L: contextLoaded(context) Note over L: 发布 ApplicationPreparedEvent SA->>L: started(context) Note over L: 发布 ApplicationStartedEvent SA->>SA: callRunners() Note over SA: 执行 ApplicationRunner<br/>和 CommandLineRunner SA->>L: running(context) Note over L: 发布 ApplicationReadyEvent App->>App: 应用就绪,开始处理请求

读图导引:纵向观察时间轴,事件从准备阶段一直贯穿到运行阶段。每个事件都是开发者可以介入的"钩子"。注意 callRunners()started() 之后、running() 之前——这意味着 Runner 执行时,容器已完全就绪但尚未发布最终的就绪事件。

7. 启动时间的暗面

SpringBoot 启动时间主要由以下几部分构成:

阶段 典型耗时 优化空间
构造 SpringApplication < 100ms
准备 Environment 100-500ms 中(减少配置文件解析)
加载 BeanDefinition 500-2000ms (减少扫描路径、排除不必要的自动配置)
自动配置条件评估 200-1000ms @EnableAutoConfiguration(exclude)
实例化单例 Bean 500-3000ms (懒初始化、减少 @PostConstruct 重逻辑)
内嵌服务器启动 300-1000ms 中(预编译 JSP、减少 Servlet 注册)
执行 Runner 视业务而定 大(异步化初始化逻辑)

排查启动瓶颈的工具

SpringBoot 2.4+ 提供了启动事件追踪:

yaml 复制代码
# application.yml
spring:
  output:
    ansi:
      enabled: always

以及 SpringApplicationStartup API(SpringBoot 2.4+ / Spring Framework 5.3+):

java 复制代码
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Application.class);
        // 使用 BufferingApplicationStartup 记录启动步骤
        app.setApplicationStartup(new BufferingApplicationStartup(10000));
        app.run(args);
    }
}

访问 /actuator/startup(需暴露该端点)可查看详细的启动步骤耗时。

懒初始化:SpringBoot 2.2+ 支持全局懒初始化:

yaml 复制代码
spring:
  main:
    lazy-initialization: true

开启后,所有单例 Bean 不会在启动时实例化,而是在第一次被依赖时创建。这可以显著缩短启动时间,但代价是第一次请求的延迟会增加("冷启动"效应)。生产环境通常不建议全局开启,而是对特定的重 Bean 使用 @Lazy 注解。

实战/源码

1. 源码走读:SpringApplication.run() 主流程

java 复制代码
public ConfigurableApplicationContext run(String... args) {
    // 1. 启动计时器
    long startTime = System.nanoTime();

    // 2. 创建 BootstrapContext(SpringBoot 2.4+ 的启动上下文)
    DefaultBootstrapContext bootstrapContext = createBootstrapContext();

    ConfigurableApplicationContext context = null;
    configureHeadlessProperty();

    // 3. 获取 SpringApplicationRunListeners
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);

    try {
        // 4. 包装命令行参数
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

        // 5. 准备 Environment
        ConfigurableEnvironment environment = prepareEnvironment(
            listeners, bootstrapContext, applicationArguments);
        configureIgnoreBeanInfo(environment);

        // 6. 打印 Banner
        Banner printedBanner = printBanner(environment);

        // 7. 创建 ApplicationContext
        context = createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);

        // 8. 准备 Context
        prepareContext(bootstrapContext, context, environment,
            listeners, applicationArguments, printedBanner);

        // 9. 【核心】刷新 Context
        refreshContext(context);

        // 10. 刷新后处理
        afterRefresh(context, applicationArguments);

        // 11. 记录启动时间
        Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                .logStarted(getApplicationLog(), timeTakenToStartup);
        }

        // 12. 发布 started 事件
        listeners.started(context, timeTakenToStartup);

        // 13. 执行 Runner
        callRunners(context, applicationArguments);
    } catch (Throwable ex) {
        // 14. 异常处理
        handleRunFailure(context, ex, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        // 15. 发布 running 事件
        Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
        listeners.ready(context, timeTakenToReady);
    } catch (Throwable ex) {
        // ...
    }

    return context;
}

2. 自定义启动监听器

场景:在应用启动完成后发送告警通知或注册服务发现。

java 复制代码
@Component
public class StartupEventListener {

    @EventListener(ApplicationReadyEvent.class)
    public void onApplicationReady(ApplicationReadyEvent event) {
        // 应用完全就绪后执行
        ConfigurableApplicationContext context = event.getApplicationContext();
        String appName = context.getEnvironment().getProperty("spring.application.name");
        System.out.println("应用 " + appName + " 启动完成,准备注册到服务发现...");
    }

    @EventListener(ApplicationFailedEvent.class)
    public void onApplicationFailed(ApplicationFailedEvent event) {
        // 启动失败时执行
        Throwable exception = event.getException();
        System.err.println("应用启动失败: " + exception.getMessage());
        // 发送告警...
    }
}

更细粒度的 SpringApplicationRunListener

java 复制代码
public class CustomRunListener implements SpringApplicationRunListener {

    public CustomRunListener(SpringApplication application, String[] args) {
        // 构造方法必须有两个参数
    }

    @Override
    public void starting(ConfigurableBootstrapContext bootstrapContext) {
        System.out.println("[启动] 应用即将启动...");
    }

    @Override
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
            ConfigurableEnvironment environment) {
        System.out.println("[启动] Environment 准备完成,active profiles: " +
            Arrays.toString(environment.getActiveProfiles()));
    }

    @Override
    public void started(ConfigurableApplicationContext context, Duration timeTaken) {
        System.out.println("[启动] 应用启动耗时: " + timeTaken.toMillis() + "ms");
    }
}

注册到 META-INF/spring.factories

复制代码
org.springframework.boot.SpringApplicationRunListener=\
com.example.CustomRunListener

3. 启动耗时分析实战

方案一:Spring Boot Actuator 启动端点(2.4+)

yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: startup,metrics

POST /actuator/startup 获取启动步骤详情:

json 复制代码
{
  "timeline": {
    "startTime": "2024-06-01T10:00:00.000Z",
    "events": [
      {
        "endTime": "2024-06-01T10:00:01.234Z",
        "duration": "1.234s",
        "startupStep": {
          "name": "spring.beans.instantiate",
          "description": "Instantiating bean: dataSource"
        }
      }
    ]
  }
}

方案二:自定义启动计时

java 复制代码
@Component
public class StartupTimeLogger implements ApplicationListener<ApplicationReadyEvent> {

    @Autowired
    private ApplicationContext context;

    @Override
    public void onApplicationReady(ApplicationReadyEvent event) {
        // 获取 BeanFactory 中的启动信息
        // 或使用自定义的 StopWatch 在关键步骤打点
    }
}

方案三:JVM 参数查看类加载耗时

bash 复制代码
java -verbose:class -jar app.jar | grep -E "Loaded|Unloaded"

类加载是启动耗时的隐形杀手。SpringBoot 应用通常要加载 8000-15000 个类,每个类加载涉及磁盘 I/O、字节码验证、解析,累积起来可达数秒。SpringBoot 3.x 的 AOT 编译可以将启动时的类加载开销前置到编译期。

4. 启动优化实践

优化一:排除不必要的自动配置

java 复制代码
@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,
    HibernateJpaAutoConfiguration.class,
    JacksonAutoConfiguration.class
})
public class Application { }

如果应用是纯 API 网关或消息消费者,不需要数据源和 JPA,排除这些配置可以节省数百毫秒的自动配置评估时间。

优化二:懒初始化 + 关键 Bean 预热

yaml 复制代码
spring:
  main:
    lazy-initialization: true
java 复制代码
@Component
@Lazy(false)  // 这个 Bean 仍然启动时实例化
public class CriticalService {
    // 关键服务需要预热
}

优化三:异步化初始化逻辑

java 复制代码
@Component
public class AsyncInitializer {

    @Async
    @EventListener(ApplicationReadyEvent.class)
    public void init() {
        // 耗时操作(如缓存预热、配置加载)异步执行
        // 不阻塞主启动流程
    }
}

优化四:SpringBoot 3.x AOT 编译

SpringBoot 3.x 支持 AOT(Ahead-of-Time)编译,配合 GraalVM 原生镜像:

xml 复制代码
<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
</plugin>

AOT 编译的优势:

  • 启动时无需类路径扫描和反射解析,BeanDefinition 在编译期确定
  • GraalVM 原生镜像启动时间从秒级降到毫秒级(典型值 50-200ms)
  • 内存占用降低 50%+

代价:

  • 失去运行时的动态性(如基于反射的自动配置、CGlib 动态代理受限)
  • 需要显式配置反射清单、资源清单、动态代理清单
  • 编译时间大幅增加(可达数分钟)
  • 部分库(如某些序列化框架)不完全兼容

常见问题

Q1:SpringApplication.run() 和 new AnnotationConfigApplicationContext() 有什么区别?

AnnotationConfigApplicationContext 是 Spring Framework 的原生 API,需要手动注册配置类、手动刷新:

java 复制代码
AnnotationConfigApplicationContext ctx =
    new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();

SpringApplication.run() 是 SpringBoot 的高级封装,额外做了:推断应用类型、准备 Environment、加载 Initializer/Listener、打印 Banner、自动配置加载、内嵌服务器启动、执行 Runner。可以理解为 SpringApplication.run() = new AnnotationConfigApplicationContext() + SpringBoot 的"启动编排"。

Q2:内嵌 Tomcat 在启动的哪个阶段启动?为什么启动后立刻能接收请求?

内嵌 Tomcat 在 refreshContext()onRefresh() 子阶段启动(第 8 步),位于 finishBeanFactoryInitialization()(第 10 步,实例化单例 Bean)之前。Tomcat 启动后会立即开始监听端口,但此时 Spring 容器尚未完成 Bean 实例化。SpringBoot 通过 TomcatStarter(一个 ServletContainerInitializer)确保 Tomcat 在接收第一个请求前,Spring 容器已经完成刷新。具体而言,TomcatStarter.onStartup() 会等待 Context 刷新完成才注册 DispatcherServlet。

Q3:ApplicationRunner 和 CommandLineRunner 的执行顺序?

两者都在容器刷新完成后执行,但 ApplicationRunner 优先于 CommandLineRunner。同一类型的 Runner 按 @Order 排序。如果 Runner 抛出异常,SpringBoot 会将应用状态标记为失败,但已启动的 Web 服务器不会自动关闭——这是生产环境中需要注意的边界。

Q4:如何在启动过程中获取 Environment?

三种方式:

  1. 实现 EnvironmentAware 接口(在 Bean 实例化后注入)
  2. 监听 ApplicationEnvironmentPreparedEvent(最早可获取时机)
  3. ApplicationContextInitializer 中通过 context.getEnvironment() 获取

最早的方式是在 SpringApplicationRunListener.environmentPrepared()ApplicationEnvironmentPreparedEvent 监听器中获取,此时配置文件已加载、profile 已激活。

Q5:启动慢怎么系统排查?

排查清单:

  1. 开启 debug: true 查看自动配置报告,排除不必要的自动配置
  2. 使用 BufferingApplicationStartup + /actuator/startup 定位耗时步骤
  3. 检查 @PostConstructInitializingBean.afterPropertiesSet() 中是否有阻塞操作
  4. 检查类路径扫描范围是否过大(@ComponentScan 是否扫到了不必要的包)
  5. 使用 -verbose:class 查看类加载耗时,考虑使用 CDS(Class Data Sharing)或 AOT
  6. 检查 JVM 参数:-Xms-Xmx 设置是否合理,避免启动时频繁 GC

Q6:SpringBoot 3.x 的 AOT 编译和传统的 JIT 有什么区别?

JIT(Just-In-Time)是运行时编译,字节码在 JVM 运行过程中逐步编译为机器码。AOT(Ahead-of-Time)是编译期编译,在构建时将 Java 字节码编译为原生机器码(通过 GraalVM Native Image)。

SpringBoot 3.x 的 AOT 处理不仅仅是 GraalVM 原生镜像,还包括:

  • 在构建期生成 BeanDefinition 的静态元数据,替代运行时的组件扫描
  • 生成 reflect-config.jsonproxy-config.json 等 GraalVM 配置
  • 生成优化的 ApplicationContextInitializer,替代运行时的条件评估

AOT 模式适合: Serverless(冷启动敏感)、CLI 工具、边缘计算。不适合:需要动态代理频繁变化、大量使用反射和字节码增强的场景。

Q7:为什么有些 Bean 在启动时没有被创建?

可能原因:

  1. 标注了 @Lazy,需要首次被依赖时才创建
  2. 作用域是 prototype,每次获取时才创建
  3. 条件注解不匹配(如 @ConditionalOnProperty 条件不满足)
  4. @Profile 限制,当前未激活对应 profile
  5. 被其他自动配置的 @ConditionalOnMissingBean 覆盖

总结

SpringBoot 的启动流程是一出精心编排的交响乐:

  1. 前奏(构造阶段):推断应用类型、加载扩展点、确定主类——为演出选定乐器和指挥
  2. 调音(Environment 准备):层层叠加 PropertySource,命令行参数覆盖配置文件——调好每一个音准
  3. 就位(Context 创建与准备):创建舞台、Initializer 做最后调整、主类注册到节目单
  4. 演奏(refresh 核心):自动配置加载(总谱分发)、BeanPostProcessor 就位(乐手准备)、内嵌服务器启动(舞台灯光打开)、单例 Bean 实例化(正式演奏)
  5. 谢幕(Runner 执行与事件发布):安可曲目(初始化逻辑)、观众掌声(ApplicationReadyEvent)

理解启动流程的关键在于把握三个"里程碑":invokeBeanFactoryPostProcessors(自动配置加载)、onRefresh(内嵌服务器启动)、finishBeanFactoryInitialization(单例 Bean 实例化)。这三个步骤的顺序是严格设计的,任何一步的提前或延后都会导致启动失败或行为异常。

生产环境中的启动优化,本质是减少"不必要的工作":排除不需要的自动配置、推迟非关键 Bean 的初始化、将阻塞操作移出主线程。SpringBoot 3.x 的 AOT 编译则是一条更激进的优化路径——用编译期的确定性替代运行时的动态性,代价是牺牲部分灵活性。