首頁  >  文章  >  Java  >  Springboot中FatJar和Jar是什麼

Springboot中FatJar和Jar是什麼

WBOY
WBOY轉載
2023-05-11 09:58:131832瀏覽

    導讀

    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-classmain方法,運行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- classstart-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.jarxxx.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 -classpathjava -cpjava -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中文網其他相關文章!

    陳述:
    本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除