2. 编程语言-Java-Spring Boot-JAR

文章目录

  • 前言
  • [一、普通 JAR 为什么"不能直接跑"](#一、普通 JAR 为什么“不能直接跑”)
  • [二、Spring Boot 的 Fat JAR:把整个世界塞进一个文件](#二、Spring Boot 的 Fat JAR:把整个世界塞进一个文件)
  • [三、MANIFEST.MF:真正启动的不是你的 Application](#三、MANIFEST.MF:真正启动的不是你的 Application)
  • [四、JarLauncher 在干什么:自举类加载环境](#四、JarLauncher 在干什么:自举类加载环境)
  • [五、最精妙的一点:嵌套 JAR 怎么加载?](#五、最精妙的一点:嵌套 JAR 怎么加载?)
  • [六、内嵌 Tomcat 为什么能跑起来](#六、内嵌 Tomcat 为什么能跑起来)
  • 七、串起来看全貌

前言

当你改完最后一个 bug,敲下 mvn package,然后 java -jar app.jar,服务起来了。

你突然想到:普通 JAR 为什么不能这么跑?Spring Boot 的 JAR 里到底塞了什么?

这篇从 JAR 结构 → MANIFEST → 启动器 → 类加载器 → 内嵌容器 五个层面,把这件事讲透。本文以 **Spring Boot 3.5.16. ** 为例。


一、普通 JAR 为什么"不能直接跑"

普通 JAR 解出来大概长这样:

它本质上就是"编译后的 .class 文件压缩包":

•第三方依赖(Spring、MyBatis、Tomcat)不会自动打进来

•MANIFEST.MF 没写 Main-Class,JVM 不知道从哪个类的 main 开始执行

•就算写了 Main-Class,标准 JVM 也不会去加载"JAR 里的 JAR"------它只能识别平铺的 .class 文件

所以普通 JAR 不是不能跑,而是不具备自包含依赖 + 嵌套 JAR 加载的能力。

运行时你得自己用 -cp 把一堆依赖 JAR 拼上去,或者丢进 lib/ 目录再手动配 Class-Path 属性。这对部署来说是噩梦。


二、Spring Boot 的 Fat JAR:把整个世界塞进一个文件

spring-boot-maven-plugin 重新打包后,结构完全变了:

复制代码
example.jar

几个关键目录各司其职:

•BOOT-INF/classes/:项目编译后的 .class 文件

•BOOT-INF/lib/:所有依赖 JAR,包括内嵌 Tomcat

•org/springframework/boot/loader/launch:Spring Boot 自己的启动引导类(3.1之前的版本的 在org/springframework/boot/loader)

普通 JAR 是"类文件压缩包",Fat JAR 是"自包含的运行时"------一个文件包含运行所需的一切。


三、MANIFEST.MF:真正启动的不是你的 Application

java -jar 启动时,JVM 只认 META-INF/MANIFEST.MF 里的 Main-Class 属性。

Spring Boot 的 MANIFEST.MF 关键内容大致长这样:

两个属性各管各的:

•Main-Class:JarLauncher------Spring Boot 的启动引导类,不是你的代码

•Start-Class:com.example.MyApplication------你项目里带 main 方法的启动类

所以 java -jar 实际启动的是 JarLauncher,它负责搭好类加载环境之后,再去调用你的 Start-Class.main()。

一句话:JVM 启动引导类,引导类启动你的应用。


四、JarLauncher 在干什么:自举类加载环境

JarLauncher.main() 的执行过程简化如下:

  1. 创建自定义类加载器

    构建一个 LaunchedURLClassLoader,核心能力是把 Fat JAR 里的 BOOT-INF/classes/ 和 BOOT-INF/lib/*.jar 映射成可加载的 URL 资源。

  2. 设置线程上下文类加载器

    把刚创建的加载器设为当前线程的 contextClassLoader,这样后续 Spring 内部、反射加载、SPI 发现都会走这个加载器。

  3. 读取 Start-Class

    从 MANIFEST.MF 中解析 Start-Class 属性,拿到真正的启动类名。

  1. 加载并执行启动类
    用 LaunchedURLClassLoader 加载 Start-Class,通过反射找到 main(String[]) 方法,传入参数并执行。
  1. 进入 Spring 启动流程
    你的 main 里通常就是 SpringApplication.run(MyApplication.class, args),从此开始上下文初始化、自动配置、Bean 注册......

整个过程就像火箭发射:JarLauncher 是一级火箭,负责把"类加载环境"送入轨道;然后二级火箭(你的启动类)点火,正式进入 Spring 的世界。


五、最精妙的一点:嵌套 JAR 怎么加载?

这是整个机制里最值得讲的部分。

标准 URLClassLoader 只能加载这种路径:

bash 复制代码
file:/path/to/some.jar

但 Spring Boot 要加载的是:

bash 复制代码
file:app.jar!/BOOT-INF/lib/spring-core-6.0.0.jar

JVM 原生不支持"JAR 里的 JAR"。如果强行解压嵌套 JAR 到临时目录,启动慢、还占磁盘。

Spring Boot 的解法是自己造轮子:

•实现了一套自定义的 JarFile,能识别 Fat JAR 中每个嵌套条目的名称和在 outer JAR 中的字节范围

•配合自定义的 URLStreamHandler 和 JarURLConnection,用类似 jar:file:app.jar!/BOOT-INF/lib/xxx.jar!/ 的 URL 协议读取内容

•不需要把嵌套 JAR 解压到磁盘------按字节范围在内存中切片读取

LaunchedClassLoader 继承自 JarUrlClassLoader,后者专门负责处理这类嵌套 JAR URL。然后 LaunchedClassLoader 内部维护的 URL 列表长这样:

bash 复制代码
jar:file:/path/to/app.jar!/BOOT-INF/classes/

类加载时按这个顺序查找:

1先找 BOOT-INF/classes/(你的类)

2再逐个扫描 BOOT-INF/lib/*.jar(依赖)

3具体扫描顺序受 BOOT-INF/classpath.idx 或 JAR entry 顺序影响

注:Spring Boot 3.2+ 后 loader 类包名从 org.springframework.boot.loader 迁移到了 org.springframework.boot.loader.launch,但核心原理不变。


六、内嵌 Tomcat 为什么能跑起来

spring-boot-starter-web 间接引入了 tomcat-embed-core、tomcat-embed-websocket 等 JAR,它们同样躺在 BOOT-INF/lib/ 里,由同一个 LaunchedURLClassLoader 加载。

SpringApplication.run() 执行到 refreshContext() 阶段时:

1类路径上能找到 org.apache.catalina.startup.Tomcat 等类

2ServletWebServerFactoryAutoConfiguration 自动配置触发

3创建内嵌 Tomcat 实例(本质就是 new Tomcat() + 配置 Connector/Engine)

4Spring 上下文 refresh 过程中启动该实例

这个 Tomcat:

•不是系统安装的

•不是独立进程

•就是"跑在你应用进程里的一组对象"

你的 Web 应用直接注册到这个内嵌容器中,端口默认 8080,对外提供服务。


七、串起来看全貌

bash 复制代码
mvn package

Spring Boot 的本质不是黑魔法,而是:

用自定义类加载器 + 约定目录结构,把"应用服务器 + 依赖 + 业务代码"压进一个 JAR,再用启动器把运行环境还原出来。

下次再敲 java -jar 的时候,你心里应该清楚这条命令背后跑了多少东西。


你第一次看到 Main-Class 是 JarLauncher 而不是自己的启动类时,有没有懵过?

这篇文章帮你把这条链路理清了,转给正在用 Spring Boot 的同事。


本文的引用仅限自我学习如有侵权,请联系作者删除。

参考知识

你天天敲 java -jar,但你知道 Spring Boot 的 JAR 里到底塞了什么吗?


相关推荐
acd120091 小时前
域名解析缓存带来的隐性故障,很多排查都漏掉了这一层
java·运维·nginx
wuminyu1 小时前
JVM执行引擎学习总结:热点字节码编译成本地机器码
java·linux·c语言·jvm·c++
Jack_hsfs1 小时前
第6章Spring 管理的是对象,还是对象的说明书?
java·后端·spring
十二同学啊1 小时前
Python 工程化详解:项目结构、依赖管理、代码质量、测试与部署
java·开发语言·python
YIN_尹1 小时前
【git原理与使用】git基本操作及底层原理(一)
java·git·elasticsearch
程序员飞哥2 小时前
AI 编程额度怎么用才不浪费?
java·前端·后端
yh弓长2 小时前
Spring MVC 的请求链路
java·spring·mvc
拆房老料2 小时前
【第四部分:Agentic 智能体设计模式】17. Agent 路由模式详解:从意图识别到动态模型选择
java·设计模式·ai编程·策略模式
樱花落木兰2 小时前
Redis 消息队列的可靠性演进:从 List、Pub/Sub 到 Stream 消费者组,以及 8.x 版本改变了什么
java·开发语言·经验分享·redis·分布式·python·软件工程