Spring Boot 启动流程源码浅析:从 main 方法到容器初始化

引言

Spring Boot 凭借“一键启动”的特性极大简化了 Spring 应用的开发,但其背后隐藏着复杂的容器初始化逻辑。@SpringBootApplication 注解仅是入口,真正的启动流程涉及应用上下文创建、自动配置加载、Bean 实例化、嵌入式服务器启动等核心环节。本文将从 main 方法的 SpringApplication.run() 出发,逐行拆解源码,揭示 Spring Boot 如何从一行代码到完整运行的 Web 容器。

技术背景

传统 Spring 启动痛点

早期 Spring 应用需手动配置 web.xml 或 JavaConfig,通过 new ClassPathXmlApplicationContext("beans.xml") 初始化容器,步骤繁琐且易出错:

  • 需显式指定配置文件路径;
  • 依赖外部服务器(如 Tomcat)部署 WAR 包;
  • 组件扫描范围需手动控制,易遗漏或冗余。

Spring Boot 的革新

Spring Boot 提出“约定优于配置”,通过 SpringApplication 类封装启动逻辑,实现:

  • 自动推断应用类型(Servlet/Reactive);
  • 内嵌服务器(Tomcat/Jetty),无需外部部署;
  • 自动配置(基于类路径依赖动态加载 Bean);
  • 零 XML 配置,纯注解驱动。

应用使用场景

  1. 日常开发调试:理解启动流程可快速定位“Bean 未加载”“配置冲突”等问题。
  2. 性能优化:通过调整启动参数(如 @SpringBootApplication(scanBasePackages = "..."))减少扫描范围,提升启动速度。
  3. 定制化启动:修改 SpringApplication 行为(如自定义 Banner、环境变量)满足特殊需求。
  4. 框架扩展:开发 Spring Boot Starter 时需遵循启动流程规范(如 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件加载)。

不同场景下详细代码实现

场景 1:标准启动流程(最简形式)

启动类(SimpleApplication.java)

package com.example.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SimpleApplication {
    public static void main(String[] args) {
        // 核心:调用 SpringApplication.run() 启动容器
        SpringApplication.run(SimpleApplication.class, args);
    }
}

依赖配置(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.0</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>boot-startup-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <dependencies>
        <!-- Web  starter:自动引入 Tomcat、Spring MVC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

场景 2:定制化 SpringApplication(动态调整启动行为)

定制启动类(CustomSpringApplication.java)

package com.example.boot;

import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.core.env.ConfigurableEnvironment;

@SpringBootApplication
public class CustomSpringApplication {
    public static void main(String[] args) {
        // 方式 1:通过 SpringApplication 构造器定制
        SpringApplication app = new SpringApplication(CustomSpringApplication.class);
        app.setBannerMode(Banner.Mode.OFF); // 关闭 Banner
        app.setAdditionalProfiles("dev"); // 激活 dev 环境
        ConfigurableEnvironment env = app.run(args).getEnvironment();
        printEnvInfo(env);

        // 方式 2:通过 SpringApplicationBuilder 流式 API 定制(更简洁)
        new SpringApplicationBuilder(CustomSpringApplication.class)
            .bannerMode(Banner.Mode.CONSOLE)
            .profiles("prod")
            .run(args);
    }

    private static void printEnvInfo(ConfigurableEnvironment env) {
        System.out.println("\n=== 环境变量 ===");
        System.out.println("激活环境:" + env.getProperty("spring.profiles.active"));
        System.out.println("服务器端口:" + env.getProperty("server.port", "8080"));
    }
}

场景 3:非 Web 应用启动(如批处理任务)

批处理启动类(BatchApplication.java)

package com.example.batch;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class BatchApplication {
    public static void main(String[] args) {
        // 强制指定为非 Web 应用(即使依赖了 web starter)
        SpringApplication app = new SpringApplication(BatchApplication.class);
        app.setWebApplicationType(WebApplicationType.NONE); // 关键:禁用 Web 环境
        ConfigurableApplicationContext context = app.run(args);
        
        // 执行批处理逻辑
        BatchTask task = context.getBean(BatchTask.class);
        task.execute();
        context.close(); // 非 Web 应用需手动关闭上下文
    }
}

// 批处理任务组件
@Component
public class BatchTask {
    public void execute() {
        System.out.println("执行批处理任务...");
    }
}

原理解释

Spring Boot 启动核心阶段

SpringApplication.run() 方法可拆分为两大阶段:初始化 SpringApplication 实例执行 run 方法启动容器

阶段 1:初始化 SpringApplication(构造器逻辑)

源码位置:org.springframework.boot.SpringApplication#SpringApplication(Class<?>... primarySources)
核心步骤:

  1. 推断应用类型:通过类路径是否存在 Servlet/Reactive 相关类,判断是 SERVLET(传统 Web)、REACTIVE(响应式 Web)还是 NONE(非 Web)。
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    
  2. 加载初始化器和监听器:从 META-INF/spring.factoriesMETA-INF/spring/org.springframework.boot.SpringApplicationRunListener.imports 加载 SpringApplicationRunListenerApplicationContextInitializer
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    
  3. 推断主启动类:通过堆栈跟踪找到包含 main 方法的类(即标注 @SpringBootApplication 的类)。
    this.mainApplicationClass = deduceMainApplicationClass();
    
阶段 2:执行 run 方法(容器启动核心)

源码位置:org.springframework.boot.SpringApplication#run(String... args)
核心步骤(对应流程图):

步骤方法作用
1prepareEnvironment准备环境:加载配置文件(application.yml)、激活 Profile、设置系统属性。
2createApplicationContext创建应用上下文:根据 Web 应用类型选择 AnnotationConfigServletWebServerApplicationContext(Servlet)或 AnnotationConfigReactiveWebServerApplicationContext(Reactive)。
3prepareContext预处理上下文:注册主启动类为配置源、应用初始化器(ApplicationContextInitializer)。
4refreshContext刷新上下文:核心!触发 Spring 容器的 AbstractApplicationContext.refresh(),完成 Bean 定义加载、自动配置、Bean 实例化、嵌入式服务器启动。
5afterRefresh刷新后钩子:空方法,供子类扩展(如执行自定义初始化逻辑)。
6callRunners执行 CommandLineRunner/ApplicationRunner 接口的 Bean(如启动时执行数据初始化)。

关键源码片段解析

1. 环境准备(prepareEnvironment
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
        DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
    // 1. 创建环境(根据 Web 类型选择 StandardServletEnvironment 或 StandardEnvironment)
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 2. 配置环境:绑定命令行参数(如 --server.port=8081)、激活 Profile
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 3. 通知监听器环境准备完成(如打印激活的 Profile)
    ConfigurationPropertySources.attach(environment);
    listeners.environmentPrepared(bootstrapContext, environment);
    return environment;
}
2. 上下文刷新(refreshContext

最终调用 AbstractApplicationContext.refresh(),其中与 Spring Boot 强相关的步骤:

  • invokeBeanFactoryPostProcessors:执行 BeanFactoryPostProcessor,包括 ConfigurationClassPostProcessor(解析 @Configuration 类,触发 @ComponentScan@EnableAutoConfiguration)。
  • onRefresh:在 ServletWebServerApplicationContext 中重写此方法,调用 createWebServer() 启动嵌入式 Tomcat。

核心特性

特性说明
自动推断应用类型无需手动指定 Web 环境,通过类路径智能判断(如存在 DispatcherServlet 则为 SERVLET)。
事件驱动架构通过 SpringApplicationRunListener 发布启动阶段事件(如 ApplicationStartingEventApplicationReadyEvent),支持自定义监听器扩展。
环境隔离支持多 Profile 配置(application-dev.ymlapplication-prod.yml),通过 --spring.profiles.active 切换。
嵌入式服务器内嵌 Tomcat/Jetty,无需外部部署,通过 ServerProperties 自动配置端口、上下文路径。

原理流程图

渲染错误: Mermaid 渲染失败: Parse error on line 2: ...pringApplication.run()] --> B[初始化 Spring -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

环境准备

开发环境

  • JDK 17+、Maven 3.8+、IntelliJ IDEA
  • 依赖:spring-boot-starter-web(自动引入 Tomcat、Spring MVC)

实际详细应用代码示例实现

示例 1:监听启动事件(自定义监听器)

自定义监听器(StartupListener.java)

package com.example.boot.listener;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

import java.util.HashMap;
import java.util.Map;

// 需通过 META-INF/spring/org.springframework.boot.SpringApplicationRunListener.imports 注册
public class StartupListener implements SpringApplicationRunListener {

    // 构造器必须接收 SpringApplication 和 String[] args(Spring Boot 反射调用)
    public StartupListener(SpringApplication application, String[] args) {}

    // 环境准备完成后触发
    @Override
    public void environmentPrepared(ConfigurableEnvironment environment, 
                                   org.springframework.boot.DefaultBootstrapContext bootstrapContext) {
        System.out.println("\n=== 自定义监听器:环境准备完成 ===");
        // 动态添加环境变量(优先级高于配置文件)
        Map<String, Object> customProps = new HashMap<>();
        customProps.put("app.custom.key", "custom-value-from-listener");
        environment.getPropertySources().addLast(new MapPropertySource("customProps", customProps));
    }

    // 上下文准备完成后触发
    @Override
    public void contextPrepared(ConfigurableApplicationContext context, 
                                org.springframework.boot.DefaultBootstrapContext bootstrapContext) {
        System.out.println("=== 自定义监听器:上下文准备完成 ===");
    }
}

注册监听器(META-INF/spring/org.springframework.boot.SpringApplicationRunListener.imports)
src/main/resources/META-INF/spring/ 目录下创建文件 org.springframework.boot.SpringApplicationRunListener.imports,内容为:

com.example.boot.listener.StartupListener

测试控制器(TestController.java)

package com.example.boot.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    @Value("${app.custom.key:default}")
    private String customKey;

    @GetMapping("/custom-key")
    public String getCustomKey() {
        return "自定义环境变量值:" + customKey; // 输出 "custom-value-from-listener"
    }
}

示例 2:自定义 CommandLineRunner(启动时执行逻辑)

CommandLineRunner 实现类(InitRunner.java)

package com.example.boot.runner;

import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
@Order(1) // 执行顺序(值越小越先执行)
public class InitRunner implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        System.out.println("\n=== CommandLineRunner 执行:初始化数据 ===");
        System.out.println("命令行参数:" + Arrays.toString(args));
        // 模拟初始化逻辑(如加载缓存、创建初始用户)
    }
}

@Component
@Order(2)
public class AnotherRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("=== AnotherRunner 执行:发送启动通知 ===");
    }
}

运行结果

示例 1 运行日志

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.1.0)

=== 自定义监听器:环境准备完成 ===
=== 自定义监听器:上下文准备完成 ===
2023-07-20T15:00:00.000+08:00  INFO 12345 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
...(省略 Tomcat 启动日志)
=== CommandLineRunner 执行:初始化数据 ===
命令行参数:[]
=== AnotherRunner 执行:发送启动通知 ===
2023-07-20T15:00:05.000+08:00  INFO 12345 --- [           main] c.e.boot.SimpleApplication               : Started SimpleApplication in 5.678 seconds (process running for 6.012)

示例 2 访问 /custom-key 接口

curl http://localhost:8080/custom-key
# 响应:自定义环境变量值:custom-value-from-listener

测试步骤以及详细代码

测试 1:验证启动事件触发顺序

测试类(StartupListenerTest.java)

package com.example.boot.listener;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@ActiveProfiles("test")
class StartupListenerTest {

    @Test
    void testListenerTriggered() {
        // 测试通过日志观察事件触发顺序(实际开发中可通过日志断言或 Mock 监听器验证)
        System.out.println("测试启动事件触发顺序");
    }
}

测试 2:验证 CommandLineRunner 执行

测试类(CommandLineRunnerTest.java)

package com.example.boot.runner;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;

import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest
class CommandLineRunnerTest {

    @Autowired
    private ApplicationContext context;

    @Test
    void testRunnerBeansExist() {
        // 验证 CommandLineRunner Bean 被 Spring 管理
        InitRunner initRunner = context.getBean(InitRunner.class);
        AnotherRunner anotherRunner = context.getBean(AnotherRunner.class);
        assertTrue(initRunner != null && anotherRunner != null);
    }
}

部署场景

1. 本地开发部署

  • IDE 直接运行 main 方法,或使用 mvn spring-boot:run

2. JAR 包部署(生产环境)

# 打包(跳过测试)
mvn clean package -DskipTests

# 运行(指定生产环境和端口)
java -jar target/boot-startup-demo-0.0.1-SNAPSHOT.jar \
  --spring.profiles.active=prod \
  --server.port=80

3. Docker 部署

Dockerfile

FROM eclipse-temurin:17-jre-alpine
VOLUME /tmp
COPY target/boot-startup-demo-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar", "--spring.profiles.active=prod"]

构建与运行

docker build -t boot-startup-demo .
docker run -d -p 80:80 --name boot-app boot-startup-demo

疑难解答

问题 1:启动时报 ApplicationContextInitializer 类找不到

  • 原因:自定义的 ApplicationContextInitializer 未在 META-INF/spring/org.springframework.boot.context.initializer.Initializer.imports 中注册。
  • 解决:在该文件中添加全类名(Spring Boot 2.7+ 推荐格式,替代旧版 spring.factories)。

问题 2:嵌入式 Tomcat 端口被占用

  • 现象:启动时报 Port 8080 is already in use
  • 解决
    • 临时修改端口:--server.port=8081 或配置文件 server.port=8081
    • 杀死占用进程:lsof -i:8080kill -9 <PID>

问题 3:非 Web 应用启动后仍尝试启动 Tomcat

  • 原因:未通过 setWebApplicationType(WebApplicationType.NONE) 禁用 Web 环境,或依赖了 spring-boot-starter-web 但未排除 Tomcat。
  • 解决:显式设置 Web 类型,或在依赖中排除 Tomcat:
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    

未来展望

技术趋势

  • AOT 编译与 GraalVM 原生镜像:Spring Boot 3.x 支持 AOT 编译,将启动时的反射逻辑提前处理,生成原生可执行文件,启动时间从秒级降至毫秒级(如从 2s → 50ms)。
  • 虚拟线程(Project Loom):JDK 21+ 虚拟线程可优化 Spring Boot 的并发处理能力,减少线程上下文切换开销,提升高并发场景下的吞吐量。
  • 配置外部化与动态刷新:结合 Spring Cloud Config 或 Nacos,实现配置实时推送,无需重启应用。

挑战

  • 启动性能与复杂度平衡:AOT 编译虽提升速度,但增加了构建复杂度(需处理反射、资源文件的静态分析)。
  • 云原生适配:Serverless 场景要求应用“冷启动”极快,需进一步优化容器初始化逻辑(如按需加载自动配置类)。
  • 多运行时支持:Spring Boot 需兼容 Quarkus、Micronaut 等轻量级框架的生态,同时保持自身“约定优于配置”的优势。

总结

Spring Boot 的启动流程是“初始化 SpringApplication → 准备环境 → 创建上下文 → 刷新容器 → 执行 Runner”的闭环,核心通过 SpringApplication.run() 串联起事件驱动、自动配置、嵌入式服务器等关键能力。理解这一流程,不仅能快速定位启动问题,更能为定制化开发(如 Starter 编写、性能优化)提供理论支撑。未来,随着 AOT、虚拟线程等技术的成熟,Spring Boot 的启动效率与应用性能将迎来新一轮突破,但其“简化开发”的核心价值将始终引领 Java 生态。

更多推荐