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檔(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解壓縮後都包含兩部分:META-INF
目錄(元資料)和package
目錄(編譯後的class)。這種普通的jar不包含第三方依賴包,只包含應用自身的設定檔、class 等。
. ├── META-INF │ ├── MANIFEST.MF #定义 └── org # 包路径(存放编译后的class) └── springframework
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包了。
普通的jar只包含目前 jar的訊息,不含第三方 jar。當內部依賴第三方jar時,直接運作則會報錯,這時候需要將第三方jar內嵌到可執行jar裡。 將一個jar及其依賴的三方jar全部打到一個包中,這個包即為 FatJar。
Spring Boot
為了解決內嵌jar問題,提供了一套FatJar解決方案,分別定義了jar目錄結構和MANIFEST.MF
。在編譯生成可執行 jar 的基礎上,使用spring-boot-maven-plugin
按Spring Boot 的可執行套件標準repackage
,得到可執行的Spring Boot jar。 根據可執行jar類型,分為兩種:可執行Jar和可執行war。
因為在新建的空的 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中文網其他相關文章!