導讀
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和Jar是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

javaispopularforcross-platformdesktopapplicationsduetoits“ writeonce,runany where”哲學。 1)itusesbytiesebyTecodeThatrunsonAnyJvm-備用Platform.2)librarieslikeslikeslikeswingingandjavafxhelpcreatenative-lookingenative-lookinguisis.3)

在Java中編寫平台特定代碼的原因包括訪問特定操作系統功能、與特定硬件交互和優化性能。 1)使用JNA或JNI訪問Windows註冊表;2)通過JNI與Linux特定硬件驅動程序交互;3)通過JNI使用Metal優化macOS上的遊戲性能。儘管如此,編寫平台特定代碼會影響代碼的可移植性、增加複雜性、可能帶來性能開銷和安全風險。

Java將通過雲原生應用、多平台部署和跨語言互操作進一步提昇平台獨立性。 1)雲原生應用將使用GraalVM和Quarkus提升啟動速度。 2)Java將擴展到嵌入式設備、移動設備和量子計算機。 3)通過GraalVM,Java將與Python、JavaScript等語言無縫集成,增強跨語言互操作性。

Java的強類型系統通過類型安全、統一的類型轉換和多態性確保了平台獨立性。 1)類型安全在編譯時進行類型檢查,避免運行時錯誤;2)統一的類型轉換規則在所有平台上一致;3)多態性和接口機制使代碼在不同平台上行為一致。

JNI會破壞Java的平台獨立性。 1)JNI需要特定平台的本地庫,2)本地代碼需在目標平台編譯和鏈接,3)不同版本的操作系統或JVM可能需要不同的本地庫版本,4)本地代碼可能引入安全漏洞或導致程序崩潰。

新興技術對Java的平台獨立性既有威脅也有增強。 1)雲計算和容器化技術如Docker增強了Java的平台獨立性,但需要優化以適應不同雲環境。 2)WebAssembly通過GraalVM編譯Java代碼,擴展了其平台獨立性,但需與其他語言競爭性能。

不同JVM實現都能提供平台獨立性,但表現略有不同。 1.OracleHotSpot和OpenJDKJVM在平台獨立性上表現相似,但OpenJDK可能需額外配置。 2.IBMJ9JVM在特定操作系統上表現優化。 3.GraalVM支持多語言,需額外配置。 4.AzulZingJVM需特定平台調整。

平台獨立性通過在多種操作系統上運行同一套代碼,降低開發成本和縮短開發時間。具體表現為:1.減少開發時間,只需維護一套代碼;2.降低維護成本,統一測試流程;3.快速迭代和團隊協作,簡化部署過程。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

WebStorm Mac版
好用的JavaScript開發工具

Dreamweaver CS6
視覺化網頁開發工具

SAP NetWeaver Server Adapter for Eclipse
將Eclipse與SAP NetWeaver應用伺服器整合。

EditPlus 中文破解版
體積小,語法高亮,不支援程式碼提示功能

Safe Exam Browser
Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。