0%

为什么springboot可以直接执行

为什么 Spring Boot 项目可以直接执行?(从打包到启动的完整原理)

Spring Boot 项目能通过 java -jar xxx.jar 直接执行,核心原因是它打包生成的并非普通 JAR 文件,而是经过 spring-boot-maven-plugin 特殊处理的可执行 JAR(Executable JAR)—— 这种 JAR 内置了 “启动器” 和 “自定义类加载器”,能自动加载依赖、初始化 Spring 上下文,最终启动应用。从 “打包结构→启动器原理→类加载机制” 三个维度,彻底讲透 Spring Boot 直接执行的底层逻辑。

先明确:Spring Boot 打包的不是普通 JAR

普通 Java 项目打包的 JAR(如 Maven 的 jar 插件生成的)仅包含项目自身的 .class 文件,无法直接通过 java -jar 执行(会报 “没有主清单属性” 错误),因为它缺少两个关键要素:

  1. 明确的 “启动类” 配置(告诉 JVM 入口在哪);
  2. 依赖的第三方 JAR 包(普通 JAR 不会包含依赖,需手动指定类路径)。

而 Spring Boot 项目通过 spring-boot-maven-plugin 打包后,生成的 JAR 结构和配置完全不同 —— 它解决了 “依赖加载” 和 “启动入口” 两个核心问题

关键文件:MANIFEST.MF 中的启动配置

MANIFEST.MF 是 JAR 文件的 “清单文件”,位于 META-INF/ 目录下,用于描述 JAR 的元信息(如版本、主类)。Spring Boot 生成的 MANIFEST.MF 包含 3 个核心配置,直接决定了 “能否直接执行”:

1
2
3
4
5
6
7
# 1. JVM 实际执行的主类(Spring Boot 启动器)
Main-Class: org.springframework.boot.loader.JarLauncher
# 2. 我们自己写的 Spring Boot 应用主类(真正的业务入口)
Start-Class: com.zhanghe.exam.Application
# 3. 应用类和依赖的存放路径
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/

这三个配置的作用至关重要,我们逐一拆解:

1. Main-Class:JVM 的 “启动器”——JarLauncher

Main-Class 是 JVM 执行 java -jar 时的入口类(即 JVM 会先调用这个类的 main 方法)。
Spring Boot 没有直接将我们的 Application 类设为 Main-Class,而是用了 org.springframework.boot.loader.JarLauncher—— 它是 Spring Boot 内置的 “启动工具类”,核心职责是:

  • 加载 BOOT-INF/lib 目录下的第三方依赖 JAR(如 Spring 核心包、数据库驱动);
  • 加载 BOOT-INF/classes 目录下的项目自身类(我们写的 Controller、Service 等);
  • 最终通过反射调用 Start-Class(我们的 Application 类)的 main 方法,启动 Spring 应用。

2. Start-Class:我们的应用主类

Start-Class 配置的是真正的业务入口类(即标注 @SpringBootApplication 的类),例如该项目中的 com.zhanghe.exam.Application
JarLauncher 会在完成 “依赖加载” 和 “类加载器初始化” 后,通过反射调用这个类的 main 方法,触发 Spring Boot 的启动流程(初始化 Spring 上下文、自动配置等)。

3. Spring-Boot-Classes/Lib:资源存放路径

这两个配置指定了 “项目类” 和 “依赖包” 的位置,是 JarLauncher 加载资源的关键:

  • Spring-Boot-Classes: BOOT-INF/classes/:项目自身的 .class 文件、配置文件(如 application.yml)存放在这里;
  • Spring-Boot-Lib: BOOT-INF/lib/:项目依赖的所有第三方 JAR(如 spring-core.jarmybatis.jar)存放在这里。

普通 JAR 不会有 BOOT-INF 目录,依赖需要通过 -classpath 参数手动指定;而 Spring Boot 的 JarLauncher 会自动扫描这两个目录,无需手动配置类路径。

核心原理:JarLauncher 如何启动应用?

JarLauncher 是 Spring Boot 直接执行的 “核心引擎”,它的 main 方法执行流程可分为 3 步,本质是 “自定义类加载→找到应用入口→反射启动”:

步骤 1:初始化自定义类加载器(LaunchedURLClassLoader)

JVM 默认的类加载器(AppClassLoader)只能加载 “当前 JAR 根目录” 或 “-classpath 指定路径” 的类,无法识别 BOOT-INF/classesBOOT-INF/lib 目录下的资源。
JarLauncher 会创建一个自定义类加载器 LaunchedURLClassLoader,专门处理 Spring Boot 可执行 JAR 的特殊结构:

  1. 扫描 BOOT-INF/classes 目录,将其转换为 URL 格式(如 jar:file:/xxx/xxx.jar!/BOOT-INF/classes/);
  2. 扫描 BOOT-INF/lib 目录下的所有依赖 JAR,将每个 JAR 也转换为 URL(如 jar:file:/xxx/xxx.jar!/BOOT-INF/lib/spring-core.jar!/);
  3. 将这些 URL 作为 “类加载路径”,初始化 LaunchedURLClassLoader—— 后续所有类(包括 Spring 核心类、我们的业务类)都通过这个类加载器加载。

步骤 2:找到 Start-Class 的全类名

JarLauncher 会读取 MANIFEST.MF 文件中的 Start-Class 配置,获取我们的应用主类全类名(如 com.zhanghe.exam.Application)。

步骤 3:反射调用 Start-Class 的 main 方法

通过 LaunchedURLClassLoader 加载 Start-Class 对应的 Class 对象,然后通过反射调用其 main 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// JarLauncher 核心逻辑简化
public class JarLauncher extends Launcher {
public static void main(String[] args) throws Exception {
// 1. 初始化自定义类加载器
ClassLoader classLoader = createClassLoader();
// 2. 获取 Start-Class 全类名
String startClass = getStartClass();
// 3. 加载 Start-Class 并反射调用 main 方法
Class<?> startClass = classLoader.loadClass(startClass);
Method mainMethod = startClass.getMethod("main", String[].class);
mainMethod.invoke(null, (Object) args);
}
}

这一步之后,就会进入我们熟悉的 Spring Boot 启动流程:SpringApplication.run(Application.class, args) → 初始化 Spring 上下文 → 自动配置 → 启动嵌入式容器(如 Tomcat)。

打包插件:spring-boot-maven-plugin 的关键作用

所有上述 “特殊结构” 和 “配置”,都由 spring-boot-maven-plugin 插件在打包时自动完成 —— 它是 Spring Boot 能直接执行的 “幕后推手”,核心功能有 3 个:

1. 构建特殊的 JAR 目录结构

插件会将项目编译后的 .class 文件和配置文件,移动到 BOOT-INF/classes 目录;将所有依赖的第三方 JAR,复制到 BOOT-INF/lib 目录。最终生成的 JAR 结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xxx.jar
├── META-INF/
│ └── MANIFEST.MF # 包含 Main-Class、Start-Class 等配置
├── BOOT-INF/
│ ├── classes/ # 项目自身的类和配置
│ │ ├── com/zhanghe/exam/Application.class
│ │ └── application.yml
│ └── lib/ # 第三方依赖 JAR
│ ├── spring-core-5.1.8.RELEASE.jar
│ ├── mybatis-3.5.3.jar
│ └── ...
└── org/ # Spring Boot 启动器相关类(JarLauncher 等)
└── springframework/boot/loader/
├── JarLauncher.class
└── LaunchedURLClassLoader.class

2. 生成正确的 MANIFEST.MF

插件会自动在 META-INF/ 目录下生成 MANIFEST.MF,并填充关键配置:

  • Main-Class 固定为 org.springframework.boot.loader.JarLauncher(或 WarLauncher,若打包为 WAR);
  • Start-Class 自动读取项目的 “主类”(即包含 main 方法且标注 @SpringBootApplication 的类);
  • Spring-Boot-ClassesSpring-Boot-Lib 固定指向 BOOT-INF/classes/BOOT-INF/lib/

3. 注入启动器类(JarLauncher 等)

插件会将 Spring Boot 启动器相关的类(如 JarLauncherLaunchedURLClassLoader)打包到 JAR 的根目录下(org/springframework/boot/loader/),确保 JVM 能找到并执行 Main-Class

对比:普通 JAR vs Spring Boot 可执行 JAR

为了更直观理解,我们对比两种 JAR 的核心差异:

对比维度 普通 JAR(Maven jar 插件生成) Spring Boot 可执行 JAR(spring-boot-maven-plugin 生成)
依赖存放 不包含依赖,需手动指定 -classpath 依赖存放在 BOOT-INF/lib,自动加载
MANIFEST.MF 配置 Main-Class 或仅指向普通类 Main-Class(JarLauncher)和 Start-Class(应用主类)
类加载器 使用 JVM 默认类加载器(AppClassLoader) 使用自定义 LaunchedURLClassLoader,支持 BOOT-INF 结构
执行方式 无法直接 java -jar(需 -classpath 可直接 java -jar xxx.jar 执行
核心组件 仅包含项目自身类 包含项目类、依赖、Spring Boot 启动器

总结:Spring Boot 直接执行的完整流程

最后,我们用一张流程图总结 Spring Boot 从 “打包” 到 “启动” 的完整链路:

  1. 打包阶段
    开发者执行 mvn clean packagespring-boot-maven-plugin 插件工作:
    • 构建 BOOT-INF/classes(项目类)和 BOOT-INF/lib(依赖);
    • 生成 MANIFEST.MF,配置 Main-Class=JarLauncherStart-Class=应用主类
    • 注入启动器类(JarLauncher 等)。
  2. 执行阶段
    开发者执行 java -jar xxx.jar → JVM 和 JarLauncher 协同工作:
    • JVM 读取 MANIFEST.MF,执行 JarLaunchermain 方法;
    • JarLauncher 创建 LaunchedURLClassLoader,加载 BOOT-INF 下的类和依赖;
    • JarLauncher 反射调用 Start-Class(应用主类)的 main 方法;
    • 触发 SpringApplication.run(),启动 Spring Boot 应用

欢迎关注我的其它发布渠道

表情 | 预览
快来做第一个评论的人吧~
Powered By Valine
v1.3.10