Skip to content

Spring Boot 打成的 jar 和普通的 jar 有什么区别 ? ​

  • 标签:Spring Boot、后端

🏛 业务架构师 ​

两个核心区别:

  1. 结构与依赖组织:Boot 的 jar 是 FAT jar(可执行 jar)——依赖打在 BOOT-INF/lib,业务类在 BOOT-INF/classes,入口由 Main-Class: JarLauncher 引导,java -jar 直接运行。普通 jar 是给别的项目当依赖用的:类文件在包根部、无依赖打包、无 Main 引导;
  2. 可否当依赖引用:Boot jar 不适合被别的模块当依赖引用(类在 BOOT-INF 下,常规路径找不到),所以多模块工程里业务库模块仍打普通 jar,只有最终应用打可执行 jar。

由此产生的工程问题(架构师必须处理):

  • 同依赖多版本冲突:BOOT-INF/lib 里出现同一 artifact 多版本时启动直接报错——治理靠 BOM 统一版本 + dependency convergence 检查前置到 CI;
  • 配置文件不打包:生产配置外置(配置中心/K8s ConfigMap),jar 内只留默认值——配置变更不发版,这是发布效率的关键决策;
  • 分层打包(layertools):Boot 支持 jarmode=layoff 分层(依赖层/应用层分离),Docker 构建时依赖层缓存复用,镜像构建从分钟级降到秒级,CI/CD 流水线提速立竿见影。

一句话:可执行 jar 是"交付物",普通 jar 是"积木"——区分两种角色,是多模块工程和流水线设计的基本功。

🏗 系统架构师 ​

结构差异:Boot repackage 后的 jar = 顶层 JarLauncher 启动器 + BOOT-INF/classes(业务类)+ BOOT-INF/lib(依赖,嵌套 jar)+ META-INF。普通 jar 的类平铺在根目录,不含依赖。

后果:

  • Boot jar 可 java -jar 直接执行(Launcher 用自定义 ClassLoader 支持加载嵌套 jar);
  • 不能被当依赖引用——类不在 classpath 根,别的项目引它拿不到类。可复用模块要打普通 jar(不引 boot 的 repackage,或用 classifier 出第二个包);
  • 工程优化点:容器分层(layertools 把依赖与类分层,镜像构建只重打变化层)、CDS/AppCDS 加速启动。

架构视角:fat jar 的本质是自包含部署单元——依赖完整、版本闭合、启动即服务,是「制品即交付物」的物理实现,也是不可变基础设施的 Java 答案。

我踩过的坑也直说:把 Boot jar 当二方包发布的依赖地狱。团队规约必须写明「可复用库不引入 boot 插件 repackage」。

💻 全栈开发专家 ​

Boot 的可执行 jar(fat jar)不是标准 Java 结构:

text
demo.jar
├── META-INF/MANIFEST.MF        # Main-Class: JarLauncher
├── BOOT-INF/
│   ├── classes/                 # 我们自己的代码和资源
│   └── lib/                     # 全部依赖 jar 原样打进来
└── org/springframework/boot/loader/...

java -jar 时执行的是 JarLauncher:构建自定义类加载器,把 BOOT-INF/classes 和 lib 下所有 jar 加载进来,再调真实 main 类——所以一个 jar 自包含可运行。

普通 jar 的 classes 在根目录,供别人 import 依赖;fat jar 的类藏在 BOOT-INF 下,依赖它时什么都引不到——最经典的坑:多模块项目 B 依赖 A 的 boot jar,编译期看得到类,运行时 ClassNotFoundException。

解法三种:

  1. 拆模块:api 模块(普通 jar)+ boot 模块(只负责打包)——最干净
  2. 配 <classifier>exec</classifier>:同一构件出两种 jar,依赖方引普通版
  3. Docker 构建用 layertools 分层提取(依赖层在前、代码层在后),改代码只推薄层,镜像构建和推送明显提速——CI 优化的常规动作

一句话:可执行 jar 是"应用",普通 jar 是"库",两者的类路径结构决定了不能混用。