Springboot - Fat Jar示例详解
作者:光阴迷客 发布时间:2023-11-19 21:28:35
导读
Spring Boot应用可以使用spring-boot-maven-plugin
快速打包,构建一个可执行jar。Spring Boot内嵌容器,通过java -jar
命令便可以直接启动应用。
虽然是一个简单的启动命令,背后却藏着很多知识。今天带着大家探索FAT JAR启动的背后原理。本文主要包含以下几个部分:
JAR 是什么。首先需要了解jar是什么,才知道
java -jar
做了什么事情。FatJar 有什么不同。 Spring Boot提供的可执行jar与普通的jar有什么区别。
启动时的类加载原理。 启动过程中类加载器做了什么?Spring Boot又如何通过自定义类加载器解决内嵌包的加载问题。
启动的整个流程。最后整合前面三部分的内容,解析源码看如何完成启动。
JAR 是什么
JAR简介
JAR文件(Java归档,英语: Java ARchive)是一种软件包文件格式,通常用于将大量的Java类文件、相关的元数据和资源(文本、图片等)文件聚合到一个文件,以便分发Java平台应用软件或库。简单点理解其实就是一个压缩包,既然是压缩包那么为了提取JAR文件的内容,可以使用任何标准的unzip解压缩软件提取内容。或者使用Java虚拟机自带命令jar -xf foo.jar
来解压相应的jar文件。
JAR 可以简单分为两类:
非可执行JAR。打包时,不用指定
main-class
,也不可运行。普通jar包可以供其它项目进行依赖。可执行JAR。打jar包时,指定了
main-class
类,可以通过java -jar xxx.jar
命令,执行main-class
的main
方法,运行jar包。可运行jar包不可被其他项目进行依赖。
JAR结构
包结构
不管是非可行JAR还是可执行JAR解压后都包含两部分:META-INF
目录(元数据)和package
目录(编译后的class)。这种普通的jar不包含第三方依赖包,只包含应用自身的配置文件、class 等。
.
├── META-INF
│ ├── MANIFEST.MF #定义
└── org # 包路径(存放编译后的class)
└── springframework
描述文件MANIFEST.MF
JAR包的配置文件是META-INF
文件夹下的MANIFEST.MF
文件。主要配置信息如下:
Manifest-Version: 用来定义manifest文件的版本,例如:Manifest-Version: 1.0
Created-By: 声明该文件的生成者,一般该属性是由jar命令行工具生成的,例如:Created-By: Apache Ant 1.5.1
Signature-Version: 定义jar文件的签名版本
Class-Path: 应用程序或者类装载器使用该值来构建内部的类搜索路径,可执行jar包里需要设置这个。
上面是普通jar包的属性,可运行jar包的.MF文件中,还会有mian-class
或start-class
等属性。如果依赖了外部jar包,还会在MF文件中配置lib路径等信息。更多信息参见:maven为MANIFEST.MF文件添加内容的方法
至于可运行jar包和普通jar包的目录结构,没有什么特别固定的模式,总之,无论是什么结构,在.MF文件中,配置好jar包的信息,即可正常使用jar包了。
FatJar有什么不同
什么是FatJar?
普通的jar只包含当前 jar的信息,不含有第三方 jar。当内部依赖第三方jar时,直接运行则会报错,这时候需要将第三方jar内嵌到可执行jar里。将一个jar及其依赖的三方jar全部打到一个包中,这个包即为 FatJar。
SpringBoot FatJar解决方案
Spring Boot
为了解决内嵌jar问题,提供了一套FatJar解决方案,分别定义了jar目录结构和 MANIFEST.MF
。在编译生成可执行 jar 的基础上,使用spring-boot-maven-plugin
按Spring Boot 的可执行包标准repackage
,得到可执行的Spring Boot jar。根据可执行jar类型,分为两种:可执行Jar和可执行war。
spring-boot-maven-plugin打包过程
因为在新建的空的 SpringBoot 工程中并没有任何地方显示的引入或者编写相关的类。实际上,对于每个新建的 SpringBoot 工程,可以在其 pom.xml 文件中看到如下插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
这个是SpringBoot官方提供的用于打包FatJar的插件,org.springframework.boot.loader
下的类其实就是通过这个插件打进去的;
下面是此插件将 loader 相关类打入 FatJar 的一个执行流程:
org.springframework.boot.maven#execute->
org.springframework.boot.maven#repackage -> org.springframework.boot.loader.tools.Repackager#repackage->
org.springframework.boot.loader.tools.Repackager#writeLoaderClasses->
org.springframework.boot.loader.tools.JarWriter#writeLoaderClasses
最终的执行方法就是下面这个方法,通过注释可以看出,该方法的作用就是将 spring-boot-loader 的classes 写入到 FatJar 中。
/**
* Write the required spring-boot-loader classes to the JAR.
* @throws IOException if the classes cannot be written
*/
@Override
public void writeLoaderClasses() throws IOException {
writeLoaderClasses(NESTED_LOADER_JAR);
}
打包结果
Spring Boot项目被编译以后,在targert
目录下存在两个jar文件:一个是xxx.jar
和xxx.jar.original
。
其中
xxx.jar.original
是maven编译后的原始jar文件,即标准的java jar。该文件仅包含应用本地资源。 如果单纯使用这个jar,无法正常运行,因为缺少依赖的第三方资源。因此
spring-boot-maven-plugin
插件对这个xxx.jar.original
再做一层加工,引入第三方依赖的jar包等资源,将其"repackage"
为xxx.jar
。可执行Jar的文件结构如下图所示:
.
├── BOOT-INF
│ ├── classes
│ │ ├── application.properties # 用户-配置文件
│ │ └── com
│ │ └── glmapper
│ │ └── bridge
│ │ └── boot
│ │ └── BootStrap.class # 用户-启动类
│ └── lib
│ ├── jakarta.annotation-api-1.3.5.jar
│ ├── jul-to-slf4j-1.7.28.jar
│ ├── log4j-xxx.jar # 表示 log4j 相关的依赖简写
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.glmapper.bridge.boot
│ └── guides-for-jarlaunch
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── MainMethodRunner.class
├── PropertiesLauncher$1.class
├── PropertiesLauncher$ArchiveEntryFilter.class
├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
├── PropertiesLauncher.class
├── WarLauncher.class
├── archive
│ ├── # 省略
├── data
│ ├── # 省略
├── jar
│ ├── # 省略
└── util
└── SystemPropertyUtils.class
META-INF: 存放元数据。MANIFEST.MF 是 jar 规范,Spring Boot 为了便于加载第三方 jar 对内容做了修改;
org: 存放Spring Boot 相关类,比如启动时所需的 Launcher 等;
BOOT-INF/class: 存放应用编译后的 class 文件;
BOOT-INF/lib: 存放应用依赖的 JAR 包。
Spring Boot的MANIFEST.MF
和普通jar有些不同:
Spring-Boot-Version: 2.1.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.rock.springbootlearn.SpringbootLearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk: 1.8.0_131
Main-Class: 是java -jar
启动引导类,但这里不是项目中的类,而是Spring Boot内部的JarLauncher。
Start-Class: 这个才是正在要执行的应用内部主类
所以java -jar
启动的时候,加载运行的是JarLauncher。Spring Boot内部如何通过JarLauncher 加载Start-Class 执行呢?为了更清楚加载流程,我们先介绍下java -jar
是如何完成类加载逻辑的。
启动时的类加载原理
这里简单说下java -jar
启动时是如何完成记载类加载的。Java 采用了双亲委派机制,Java语言系统自带有三个类加载器:
Bootstrap CLassloder: 最顶层的加载类,主要加载核心类库
Extention ClassLoader: 扩展的类加载器,加载目录
%JRE_HOME%/lib/ext
目录下的jar包和class文件。 还可以加载-D java.ext.dirs选项指定的目录。AppClassLoader: 是应用加载器。
默认情况下通过java -classpath
,java -cp
,java -jar
使用的类加载器都是AppClassLoader。 普通可执行jar通过java -jar
启动后,使用AppClassLoader加载Main-class
类。 如果第三方jar不在AppClassLoader里,会导致启动时候会报ClassNotFoundException。
例如在Spring Boot可执行jar的解压目录下,执行应用的主函数,就直接报该错误:
Exception in thread "main" java.lang.NoClassDefFoundError: org/springframework/boot/SpringApplication
at com.glmapper.bridge.boot.BootStrap.main(BootStrap.java:13)
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
从异常堆栈来看,是因为找不到SpringApplication
这个类;这里其实还是比较好理解的,BootStrap
类中引入了SpringApplication
,但是这个类是在BOOT-INF/lib
下的,而java指令在启动时未指明classpath
,依赖的第三方jar无法被加载。
Spring Boot JarLauncher启动时,会将所有依赖的内嵌 jar (BOOT-INF/lib 目录下) 和class(BOOT-INF/classes 目录)都加入到自定义的类加载器LaunchedURLClassLoader中,并用这个ClassLoder去加载MANIFEST.MF配置Start-Class,则不会出现类找不到的错误。
LaunchedURLClassLoader是URLClassLoader的子类, URLClassLoader会通过URL[] 来搜索类所在的位置。Spring Boot 则将所需要的内嵌文档组装成URL[],最终构建LaunchedURLClassLoader类。
启动的整个流程
有了以上知识的铺垫,我们看下整个 FatJar 启动的过程会是怎样。为了以便查看源码和远程调试,可以在 pom.xml 引入下面的配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
</dependency>
简单概括起来可以分为几步:
java -jar 启动,AppClassLoader 则会加载 MANIFEST.MF 配置的Main-Class, JarLauncher。
JarLauncher启动时,注册URL关联协议。
获取所有内嵌的存档(内嵌jar和class)
根据存档的URL[]构建类加载器。
然后用这个类加载器加载Start-Class。 保证这些类都在同一个ClassLoader中。
参考资料
聊一聊 SpringBoot 中 FatJar 启动原理
Spring Boot 解析(二):FatJar 启动原理
Springboot - Fat Jar
来源:https://blog.csdn.net/changqing5818/article/details/128812135


猜你喜欢
- 选择排序:(Selection sort)是一种简单直观的排序算法,也是一种不稳定的排序方法。选择排序的原理一组无序待排数组,做升序排序,我
- 通过微软的SAPI,不仅仅可以实现语音合成TTS,同样可以实现语音识别SR。下面我们就介绍并贴出相关代码。主要有两种方式:1、使用COM组件
- 在 APK 开发中,通过 Java 代码来打开系统的安装程序以安装 APK 并不是什么难事,一般的 Android 系统都有开放这一功能。但
- 本文实例为大家分享了Android ViewPager实现轮播图效果的具体代码,供大家参考,具体内容如下先上一张效果图:说到ViewPage
- 前提说明之前公司有一个项目是由androidstudio接入高德地图实现导航,定位等功能,然后还有一部分登陆页面和其他逻辑都放在Unity方
- Android Handler移除Message详解问题:1.removeMessage(what)函数是否只能移除对应what值的Mess
- 大家都知道protobuf好用,可是在网上找到的netty整合protobuf的文章都是千篇一律,自己编写proto文件然后使用工具转jav
- 本文实例讲述了Java Swing组件编程之JTable表格用法。分享给大家供大家参考,具体如下:表格是GUI编程中使用较多,但也是最麻烦的
- 一、申请你的AppIDhttp://open.weixin.qq.com/ 友情提示:推荐使用eclipse打包软件最后一步的M
- 本文实例为大家分享了java数独游戏的具体代码,供大家参考,具体内容如下自己写的数独游戏,共9关,代码如下:1、DoShudu类用于产生数独
- 很多人的程序在本地运行是好的,但是发布在服务器上后就会有各种各样的问题,但是服务器上又不能直接调试,所以直接读写本地日志成为解决问题的关键,
- 微信平台开放后倒是挺火的,许多第三方应用都想试下,毕竟可以利用微信建立起来的关系链来拓展自己的应用还是挺不错的,可以节约很多在社交方面的开销
- 一、 序列化和反序列化概念Serialization(序列化)是一种将对象以一连串的字节描述的过程;反序列化deserialization是
- 站点IP访问频率限制 针对单个站点using System;using System.Collections.Generic;u
- Java是怎么跨平台的我们上计算机课的时候老师讲过:"计算机只能识别0和1,所以我们写的程序要经过编译器翻译成0和1组成的二进制格
- 开发中经常需要将某个文件向另一个应用程序传递,如图片上传到另一个应用程序、文件在不同存储路径之间的复制粘贴等都需要共享文件,可以这样理解接收
- 前言使用SpringBoot来开发项目相对于传统模式,要快速优雅许多,相信目前国内绝大部分web项目的开发还没有使用SpringBoot来做
- 学习在于记录,把自己不懂得容易忘记得记录下,才是最好得选择。废话不多说,想要在Android开发中嵌入c/c++代码,直接开始如下步骤1、创
- 本文主要是自定义了EditText,当EditText有文本输入的时候会出现删除图标,点击删除图标实现文本的清空,其次对密码的返回做了处理,
- 一、内部类的概念在 Java 中,可以将一个类定义在另一个类或者一个方法的内部,前者称为内部类,后者称为外部类。内部类也是封装的一种体现。p