Spring Boot 启动深度研究报告

java -jar 到应用就绪的全景解析

序章:神秘的 "Fat JAR"

一切始于一个普通的命令:java -jar my-app.jar。但 Spring Boot 的 JAR 文件并不普通,它被称为 "Fat JAR"(或可执行 JAR)。它的内部结构是精心设计的,这是它能够独立运行的秘密。

my-app.jar
├─ BOOT-INF/
│  ├─ classes/      <-- 你编写的业务代码 (.class文件)
│  │  └─ com/example/MyApplication.class
│  └─ lib/          <-- 所有的依赖JAR包 (如 tomcat-embed, spring-web, etc.)
│     ├─ tomcat-embed-core-9.0.58.jar
│     └─ spring-web-5.3.15.jar
├─ META-INF/
│  └─ MANIFEST.MF   <-- JVM的入口点清单文件
└─ org/springframework/boot/loader/...  <-- Spring Boot自己的加载器代码
核心解读:与传统JAR不同,它将所有依赖JAR包都“塞”了进去。普通的 java -jar 无法直接读取嵌套JAR中的类,因此 Spring Boot 需要一个特殊的“引导员”。

第一幕:JVM的唤醒与“引导员”登场

1. JVM 寻找入口

执行 java -jar 时,JVM会读取 JAR 包内 META-INF/MANIFEST.MF 文件,寻找 Main-Class 属性来确定程序的入口。

# 在 MANIFEST.MF 中:
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.MyApplication  <-- 你的主类
关键反转!JVM 的入口点并不是你写的 MyApplication,而是 Spring Boot 提供的一个特殊“引导员”:JarLauncher

2. 特殊的类加载器

JarLauncher 的首要任务是创建一个特殊的类加载器:LaunchedURLClassLoader

这个类加载器的“特异功能”就是能够读取并加载嵌套在 BOOT-INF/lib/ 目录下的 JAR 文件中的类。它打破了 Java 的常规限制。

核心解读:正是因为有了 LaunchedURLClassLoader,我们的应用才能在一个单一的 Fat JAR 文件中,无缝地使用所有依赖,而无需手动解压。

3. 启动真正的“主角”

JarLauncher 完成了环境准备后,会从 MANIFEST.MF 中读取 Start-Class 属性的值,找到你真正的应用主类(例如 com.example.MyApplication)。

然后,它使用刚刚创建的 LaunchedURLClassLoader 来加载这个主类,并调用其 main 方法。至此,程序的控制权正式交给了 Spring Boot 的核心启动逻辑。

引导员 JarLauncher 说:“舞台准备好了!”
主角 MyApplication.main(args) 登场!

第二幕:SpringApplication.run() 的宏大叙事

当你的 `main` 方法调用 `SpringApplication.run(MyApplication.class, args)` 时,Spring Boot 的核心启动流程正式拉开序幕。这个过程可以被看作是一系列的事件广播和响应。

4. 准备阶段:环境与监听器

Spring Boot 首先会创建一个 SpringApplication 实例。然后它会:

  • 判断应用类型(Servlet Web, Reactive Web, 或非 Web 应用)。
  • classpath 下的 META-INF/spring.factories 文件中加载并设置好各种 ApplicationContextInitializerApplicationListener
  • 广播第一个事件: ApplicationStartingEvent
核心解读:spring.factories 是 Spring Boot 自动配置的基石。这个阶段就像是电影开拍前,导演(SpringApplication)在召集所有工作人员(监听器和初始化器)。

5. 环境构建与“云”的介入

接下来,Spring Boot 会创建并准备 Environment 对象,它负责管理所有的配置属性。

  1. 加载所有来源的配置属性,并按优先级排序(命令行 > 系统变量 > application.properties 等)。
  2. 广播事件: ApplicationEnvironmentPreparedEvent

[Spring Cloud 特有流程]:如果这是一个 Spring Cloud 应用,BootstrapApplicationListener 会监听到这个事件,并在这里“暂停”主流程。它会创建一个临时的、父级的 "bootstrap" 上下文,专门用来连接配置中心(如 Nacos、Consul),拉取远程配置,并将其添加到 Environment 中。完成后,主流程继续。

核心解读:这是理解配置覆盖的关键。同时,Spring Cloud 通过监听这个事件,巧妙地将“获取远程配置”这个步骤无缝地插入到了启动流程中。

6. 创建 IoC 容器 (ApplicationContext)

万事俱备,Spring Boot 开始创建应用的核心——IoC容器 ApplicationContext

  • 根据应用类型选择合适的上下文实现(如 AnnotationConfigServletWebServerApplicationContext)。
  • 将前面准备好的 Environment 和所有 Bean 定义加载到上下文中。
  • 广播事件: ApplicationContextInitializedEvent

7. 刷新容器:Bean的实例化与Web服务器启动

这是整个启动流程中最核心、最复杂的一步:调用 context.refresh() 方法。这个方法会触发一系列的重量级操作:

  • Bean 定义扫描与处理:处理 @Configuration 类,扫描 @Component 等注解,将 Bean 定义注册到容器中。
  • Bean 实例化与依赖注入:实例化所有非懒加载的单例 Bean,并处理 @Autowired 等注解,完成依赖注入。
  • 启动内嵌 Web 服务器:如果是一个 Web 应用,此时会创建并启动内嵌的 Tomcat、Jetty 或 Undertow。

刷新完成后,广播事件: ApplicationPreparedEvent

核心解读:refresh() 是 Spring IoC 容器的“点火”开关。它完成了从“蓝图”(Bean 定义)到“实体建筑”(活生生的对象实例)的转变,并启动了对外服务的窗口(Web 服务器)。

第三幕:终章与就绪

8. 启动完成,执行自定义任务

容器已经完全准备就绪。此时 Spring Boot 会查找并调用所有实现了 ApplicationRunnerCommandLineRunner 接口的 Bean。这是留给开发者的一个钩子,可以在应用启动后立即执行一些自定义的初始化代码。

在 Runner 执行之后,广播最后一个关键事件: ApplicationReadyEvent

核心解读:当您看到这个事件的日志时,意味着您的应用已经完全启动成功,并且准备好接收外部请求了。

9. 应用运行中

main 方法此时已经执行完毕,但应用并不会退出。因为像 Tomcat 请求处理线程、数据库连接池线程等都是非守护(Daemon)线程,它们会阻止 JVM 的退出。

应用现在进入了稳定运行状态,静静地等待着网络请求的到来。

如果在启动过程中任何一步发生无法恢复的错误,Spring Boot 会广播 ApplicationFailedEvent,然后退出程序。