Spring Boot 核心运行原理与启动加载机制
Spring Boot 的诞生极大地简化了新 Spring 应用的初始搭建以及开发过程。它通过约定优于配置(Configuration Over Convention)的理念,实现了应用的快速启动(Out of the Box)。本文将深度剖析 Spring Boot 的核心运行原理,包含自动装配扩展机制、启动生命周期与事件监听器、内置 Web 容器的启动原理,以及 Loader 双亲委派双重机制。
一、 Spring Boot 启动流程全景剖析
Spring Boot 应用的入口是 SpringApplication.run(Application.class, args)。下面用一张时序图详细描述完整的启动生命周期。
1. 初始化阶段(实例化 SpringApplication)
在进入 run 方法之前,会首先实例化 SpringApplication 节点,在其构造函数中进行以下几件事:
-
推断 Web 应用类型:
通过类路径中是否存在特定的类来推断应用类型:
- 存在
org.springframework.web.reactive.DispatcherHandler且不存在Servlet相关的类,则为REACTIVE(WebFlux)。 - 不存在
javax.servlet.Servlet或jakarta.servlet.Servlet,则为NONE(纯后台/定时任务)。 - 否则,为
SERVLET(标准 Spring MVC)。
- 存在
-
加载 Initializer 与 Listener:
从类路径下的
META-INF/spring.factories或者 Spring 3.x 指定的配置文件中,读取所有的ApplicationContextInitializer和ApplicationListener,利用反射进行实例化并保存。 -
确定
main方法所在的类:通过遍历当前调用栈的堆栈跟踪信息(StackTrace),寻找方法名为
main的类,将其作为mainApplicationClass。
二、 自动装配的生命周期与初始化时机
在 ../cloud/15-springboot-springcloud.md 中,我们学习了自动装配的核心入口 @EnableAutoConfiguration。本节深入剖析它在 IoC 容器启动过程中的加载与初始化时机。
1. 自动装配在 refreshContext 的何处触发?
自动装配类(如 RedisAutoConfiguration)的加载并非发生在容器启动的最初期,而是在 AbstractApplicationContext.refresh() 方法的 invokeBeanFactoryPostProcessors 阶段完成的。
2. 深入:延迟导入选择器(DeferredImportSelector)
AutoConfigurationImportSelector 实现了 DeferredImportSelector 接口:
public interface DeferredImportSelector extends ImportSelector {
// 专门的分组机制,用于对自动配置类进行排序和过滤
Class<? extends Group> getImportGroup();
interface Group {
void process(AnnotationMetadata metadata, DeferredImportSelector selector);
Iterable<Entry> selectImports();
}
}
-
为什么不直接使用普通的
ImportSelector?如果是普通的
ImportSelector,它会在解析普通@Configuration类时被立即处理。而DeferredImportSelector会在所有其他常规@Configuration类都被解析、注册完毕之后,才开始执行其selectImports方法。 -
这样设计的核心目的:
确保用户的自定义配置(例如用户显式声明的
RedisTemplateBean)优先于自动配类导入。当自动配置类执行@ConditionalOnMissingBean检查时,能够准确发现用户已经声明的 Bean,从而实现用户自定义配置覆盖自动配置。
三、 内置 Web 容器启动原理
在传统的 Spring 应用中,我们需要将项目打包成 war 包,然后部署到外部的 Tomcat/Jetty 容器中。而在 Spring Boot 中,应用可以直接打包成 jar 包并运行,这是因为其内置了 Web 容器。
1. 核心接口及其抽象
Spring Boot 抽象了内置 Web 容器的生命周期,提供了 WebServer 接口:
public interface WebServer {
void start() throws WebServerException;
void stop() throws WebServerException;
int getPort();
}
其底层子类实现由于使用不同的服务器组件而有所不同,常见的实现包含 TomcatWebServer、JettyWebServer、UndertowWebServer。
2. 内置容器的创建与启动时机
内置 Tomcat 容器的创建并不是在应用最初期,而同样在 refreshContext 刷新上下文时:
-
onRefresh()阶段:AbstractApplicationContext的onRefresh()方法在基本的 BeanFactory 准备完毕后被调用。在 Web 环境下,子类ServletWebServerApplicationContext覆写了该方法:@Overrideprotected void onRefresh() {super.onRefresh();try {// 创建内置的 Web 服务器(如 Tomcat)createWebServer();} catch (Throwable ex) {throw new ApplicationContextException("Unable to start web server", ex);}} -
createWebServer()逻辑:- 从容器中获取
ServletWebServerFactory(默认是TomcatServletWebServerFactory)。 - 通过工厂类方法
getWebServer()获取WebServer实例。这期间会初始化 Tomcat 相关的组件:Engine、Host、Context以及Connector,并且将其绑定至对应的端口。
- 从容器中获取
-
finishRefresh()最终暴利激活:在
refresh流程的最后一步finishRefresh(),会调用WebServer.start()方法,此时 Tomcat 的工作线程池正式启动,开始接受外部的 HTTP 请求。
四、 Spring Boot 可执行 Jar 的加载与类加载机制
Spring Boot 提供了一种神奇的打包方式:通过 spring-boot-maven-plugin,可以将整个应用打包为一个可以直接通过 java -jar 运行的 Fat Jar。
然而,Java 虚拟机默认的类加载器只支持从目录或者标准的外部 Jar 包中加载类,无法直接加载嵌套在 Jar 包内部的 Jar 包(Nested Jars)的 class。Spring Boot 是如何打破这一限制的?
1. 归档包结构剖析 (Fat Jar 骨架)
一个典型的 Spring Boot Fat Jar 解压后结构如下:
my-app.jar
├── META-INF
│ └── MANIFEST.MF <- 关键元数据(定义 Main-Class 和 Start-Class)
├── org
│ └── springframework
│ └── boot
│ └── loader <- Spring Boot 自己的引导加载器代码 (真正的 Main-Class)
│ ├── JarLauncher.class
│ └── LaunchedURLClassLoader.class
└── BOOT-INF
├── classes <- 存放应用自身的业务代码编译点 (.class)
└── lib <- 存放应用依赖的所有第三方框架 Jar 包 (.jar)
查看 META-INF/MANIFEST.MF 内容:
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Main-Class:指向JarLauncher。这是 JVM 进程启动时真正调用的入口。Start-Class:指向开发者编写的@SpringBootApplication类。这是业务逻辑的起点。
2. LaunchedURLClassLoader 如何打破双亲委派?
JVM 默认的加载器在其类路径(Classpath)中找不到 BOOT-INF/lib/ 下嵌套包里的依赖。为了支持这种嵌套加载,Spring Boot 自定义了 LaunchedURLClassLoader。
双亲委派双重机制
-
Java 原生双亲委派:
当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,所有加载请求最终都应该传送到最顶层的启动类加载器中。
-
LaunchedURLClassLoader的自定义寻址:LaunchedURLClassLoader继承自URLClassLoader。它的核心变动在于重写了寻找资源的路径。JarLauncher启动时,会扫描BOOT-INF/classes/和BOOT-INF/lib/目录。- 将这些目录下的所有文件和嵌套 Jar 包,转化为自定义的
URL协议支持(利用Handler扩展支持jar:file:xx.jar!/BOOT-INF/lib/yy.jar!/的感叹号级联定位)。 - 创建
LaunchedURLClassLoader并把这些特殊的 URL 注入其中。、 - 当需要加载第三方依赖类时,
LaunchedURLClassLoader会先向上委托。若父加载器模块(如系统类加载器)在常规路径中找不到,它会在自己管理的BOOT-INF/lib/内部嵌套 Jar 包中进行递归搜索并成功读取字节码。
3. Launcher 引导启动的时序
通过这一层引导和类加载隔离,Spring Boot 既保留了 Java 标准的启动入口,又获得了极大的依赖打包高弹柔性,真正实现了“一次编译,直接处处执行(Fat Jar)”。