聊一聊 JAR 檔案和 MANIFEST.MF

glmapper發表於2019-06-29

在 JAVA 語言這個圈子裡面摸爬滾打,除了對於語言層面和框架層面的學習之外,有一些東西它一直存在,但是確沒有對它們有足夠的重視,因為都覺得它是理所當然,比如 JAR 是個什麼?

提到 JAR,最先可能想到的就是依賴,比如 fastjson.jar ,它可以作為依賴在專案中來引用,但是不能通過 java -jar 來執行,這種就是非可執行的 JAR。另外一種,比如我們專案打包之後生成的 JAR (當然也可能是 war),我們可以通過 java -jar 來執行程式,我們把它稱之為可執行的 JAR。

JAR 作用大體可以分為以下幾種:

  • 用於釋出和使用類庫
  • 作為應用程式和擴充套件的構建單元
  • 作為元件、applet 或者外掛程式的部署單位
  • 用於打包與元件相關聯的輔助資源

基本概念

JAR 檔案是一種歸檔檔案,以 ZIP 格式構建,以 .jar 為副檔名。使用者可以使用 JDK 自帶的 jar 命令建立或提取 JAR 檔案。也可以使用其他 zip 壓縮工具,不過壓縮時 zip 檔案頭裡的條目順序很重要,因為 MANIFEST 檔案常需放在首位。JAR 檔案內的檔名是 Unicode 文字。

JAR 檔案(Java 歸檔,英語:Java Archive)是一種軟體包檔案格式,通常用於聚合大量的 Java 類檔案、相關的後設資料和資源(文字、圖片等)檔案到一個檔案,以便分發 Java 平臺應用軟體或庫。

以上來自維基百科

JAR 檔案格式提供了許多優勢和功能,其中很多是傳統的壓縮格式如 ZIP 或者 TAR 所沒有提供的。它們包括:

  • 安全性:可以對 JAR 檔案內容加上數字化簽名。這樣,能夠識別簽名的工具就可以有選擇地為您授予軟體安全特權,這是其他檔案做不到的,它還可以檢測程式碼是否被篡改過。
  • 減少下載時間:如果一個 applet 捆綁到一個 JAR 檔案中,那麼瀏覽器就可以在一個 HTTP 事務中下載這個 applet 的類檔案和相關的資源,而不是對每一個檔案開啟一個新連線。
  • 壓縮:JAR 格式允許您壓縮檔案以提高儲存效率。
  • 傳輸平臺擴充套件。Java 擴充套件框架 (Java Extensions Framework) 提供了向 Java 核心平臺新增功能的方法,這些擴充套件是用 JAR 檔案打包的 (Java 3D 和 JavaMail 就是由 Sun 開發的擴充套件例子 )。
  • 包密封:儲存在 JAR 檔案中的包可以選擇進行 密封,以增強版本一致性和安全性。密封一個包意味著包中的所有類都必須在同一 JAR 檔案中找到。
  • 包版本控制:一個 JAR 檔案可以包含有關它所包含的檔案的資料,如廠商和版本資訊。
  • 可移植性:處理 JAR 檔案的機制是 Java 平臺核心 API 的標準部分。

JAR 檔案格式

這裡分別給出兩個 JAR 的解壓之後的示例

普通的 JAR 解壓之後的檔案目錄

以 fastjson 為例:

.
├── META-INF
│   ├── LICENSE.txt
│   ├── MANIFEST.MF
│   ├── NOTICE.txt
│   ├── maven
│   │   └── com.alibaba
│   │       └── fastjson
│   │           ├── pom.properties
│   │           └── pom.xml
│   └── services
│       ├── javax.ws.rs.ext.MessageBodyReader
│       ├── javax.ws.rs.ext.MessageBodyWriter
│       ├── javax.ws.rs.ext.Providers
│       └── org.glassfish.jersey.internal.spi.AutoDiscoverable
└── com
    └── alibaba
        └── fastjson
            ├── JSON.class
            ├── JSONArray.class
            ├── JSONAware.class
            ├── JSONException.class
            ├── JSONObject.class
            ....省略
複製程式碼

可執行的 jar (以 SpringBoot 的 FAT JAR 為例)

這個 jar 是從 start.spring.io 上下載下來的一個最簡單的 demo 打包來的

├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   └── com
│   │       └── example   # 應用的.class 檔案目錄
│   │           └── demo
│   │               └── DemoApplication.class
│   └── lib # 這裡存放的是應用的 Maven 依賴的jar包檔案
│       ├── javax.annotation-api-1.3.2.jar
│       ├── jul-to-slf4j-1.7.26.jar
│       ├── log4j-api-2.11.2.jar
│       ├── log4j-to-slf4j-2.11.2.jar
│       ├── logback-classic-1.2.3.jar
│       ├── logback-core-1.2.3.jar
│       ├── slf4j-api-1.7.26.jar
│       ├── snakeyaml-1.23.jar
│       ├── spring-aop-5.1.8.RELEASE.jar
│       ├── spring-beans-5.1.8.RELEASE.jar
│       ├── spring-boot-2.1.6.RELEASE.jar
│       ├── spring-boot-autoconfigure-2.1.6.RELEASE.jar
│       ├── spring-boot-starter-2.1.6.RELEASE.jar
│       ├── spring-boot-starter-logging-2.1.6.RELEASE.jar
│       ├── spring-context-5.1.8.RELEASE.jar
│       ├── spring-core-5.1.8.RELEASE.jar
│       ├── spring-expression-5.1.8.RELEASE.jar
│       └── spring-jcl-5.1.8.RELEASE.jar
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.example
│           └── demo
│               ├── pom.properties
│               └── pom.xml
└── org
    └── springframework
        └── boot
            └── loader #存放的是 Spring boot loader 的 class 檔案
                ├── 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
                │   ├── Archive$Entry.class
                │   ├── ...
                ├── data
                │   ├── RandomAccessData.class
                │   ├── ...
                ├── jar
                │   ├── AsciiBytes.class
                │   ├── ...
                └── util
                    └── SystemPropertyUtils.class
複製程式碼

META-INF

大多數 JAR 檔案包含一個 META-INF 目錄,它用於儲存包和擴充套件的配置資料,如安全性和版本資訊。Java 2 平臺(標準版【J2SE】)識別並解釋 META-INF 目錄中的下述檔案和目錄,以便配置應用程式、擴充套件和類裝載器:

  • MANIFEST.MF:這個 manifest 檔案定義了與擴充套件和包相關的資料。
  • 通過 MAVEN 外掛打包進來的檔案比如:
    • maven
    • services : 儲存所有服務提供程式配置檔案
  • 其他的還有一些不常看到的:
    • INDEX.LIST :這個檔案由 jar工具的新選項 -i生成,它包含在應用程式或者擴充套件中定義的包的位置資訊。它是 JarIndex 實現的一部分,並由類裝載器用於加速類裝載過程。
    • .SF:這是 JAR 檔案的簽名檔案
    • .DSA:與簽名檔案相關聯的簽名程式塊檔案,它儲存了用於簽名 JAR 檔案的公共簽名。
    • LICENSE.txt :證照資訊
    • NOTICE.txt : 公告資訊

可執行的 JAR

可以執行的 JAR 與 普通的 JAR 最直接的區別就是能否通過 java -jar 來執行。

一個 可執行的 jar檔案是一個自包含的 Java 應用程式,它儲存在特別配置的 JAR 檔案中,可以由 JVM 直接執行它而無需事先提取檔案或者設定類路徑。要執行儲存在非可執行的 JAR 中的應用程式,必須將它加入到您的類路徑中,並用名字呼叫應用程式的主類。但是使用可執行的 JAR 檔案,我們可以不用提取它或者知道主要入口點就可以執行一個應用程式。可執行 JAR 有助於方便釋出和執行 Java 應用程式

一個可執行的 JAR 必須通過 menifest 檔案的頭引用它所需要的所有其他從屬 JAR。如果使用了 -jar選項,那麼環境變數 CLASSPATH 和在命令列中指定的所有類路徑都被 JVM 所忽略。

MANIFEST.MF 檔案

當我們用 JAR 命令打完包後,會在根目錄下面建立 META-INF 目錄,該目錄下面會有一些對該 JAR 包資訊的描述,其中肯定會有一個 MANIFEST.MF 檔案,該檔案包含了該 JAR 包的版本、建立人和類搜尋路徑等資訊。

  • FASTJSON jar 中的 MANIFEST.MF 檔案

    Manifest-Version: 1.0              # 用來定義manifest檔案的版本
    Archiver-Version: Plexus Archiver  # 詳見 http://codehaus-plexus.github.io/plexus-archiver/
    Built-By: wenshao                  # 構建者
    Created-By: Apache Maven 3.5.0  #  # 宣告該檔案的生成者,一般該屬性是由 jar 命令列工具生成的
    Build-Jdk: 1.8.0_162               # 基於構建的 JDK 版本
    複製程式碼
  • SpringBoot demo 的 MANIFEST.MF 檔案

    Manifest-Version: 1.0
    Implementation-Title: demo                     # 定義了擴充套件實現的標題
    Implementation-Version: 0.0.1-SNAPSHOT         # 定義擴充套件實現的版本
    Start-Class: com.example.demo.DemoApplication  # 啟動類
    Spring-Boot-Classes: BOOT-INF/classes/         # 編譯之後的 class 檔案目錄
    Spring-Boot-Lib: BOOT-INF/lib/                 # 當前工程依賴的 jar 包目錄
    Build-Jdk-Spec: 1.8                            # 指定的 JDK 版本
    Spring-Boot-Version: 2.1.6.RELEASE             # SpringBoot 版本
    Created-By: Maven Archiver 3.4.0             
    Main-Class: org.springframework.boot.loader.JarLauncher  # Main 函式
    複製程式碼

在 Java 平臺中, MANIFEST 檔案是 JAR 歸檔中所包含的特殊檔案,MANIFEST 檔案被用來定義擴充套件或檔案打包相關資料。

MANIFEST 檔案作為一個後設資料檔案,它包含了不同部分中的 k-v 對資料。

如果一個 JAR 檔案被當作可執行檔案,則其中的 MANIFEST 檔案需要指出該程式的主類檔案,如上面案例中的 SpringBoot demo 的那個 jar 中的MANIFEST 檔案所示

MANIFEST 作用

從 MANIFEST 檔案中提供的資訊大概可以瞭解到其基本作用

  • JAR 包基本資訊描述
  • Main-Class 指定程式的入口,這樣可以直接用java -jar xxx.jar來執行程式
  • Class-Path 指定jar包的依賴關係,class loader會依據這個路徑來搜尋class

獲取 MANIFEST.MF

JDK 中提供了可以獲取 jar 包中 MANIFEST.MF 檔案資訊的工具,可以通過 java.util.jar 這個類庫來獲取。

JarFile jar = new JarFile(new File("/Users/glmapper/Documents/test/demo/target/demo-0.0.1-SNAPSHOT.jar"));
Manifest manifest = jar.getManifest();
Attributes mainAttributes = manifest.getMainAttributes();
for(Map.Entry<Object, Object> attrEntry : mainAttributes.entrySet()){
    System.out.println("main\t"+attrEntry.getKey()+":"+attrEntry.getValue());
}
Map<String, Attributes> entries = manifest.getEntries();
for(Map.Entry<String, Attributes> entry : entries.entrySet()) {
    Attributes values = entry.getValue();
    for (Map.Entry<Object, Object> attrEntry : values.entrySet()) {
        System.out.println(attrEntry.getKey() + ":" + attrEntry.getValue());
    }
}
複製程式碼

執行結果為:

main	Implementation-Title:demo
main	Implementation-Version:0.0.1-SNAPSHOT
main	Start-Class:com.example.demo.DemoApplication
main	Spring-Boot-Classes:BOOT-INF/classes/
main	Spring-Boot-Lib:BOOT-INF/lib/
main	Build-Jdk-Spec:1.8
main	Spring-Boot-Version:2.1.6.RELEASE
main	Created-By:Maven Archiver 3.4.0
main	Manifest-Version:1.0
main	Main-Class:org.springframework.boot.loader.JarLauncher

複製程式碼

Jar 檔案和 Manifest 在 java 中的定義

下面為 JarFile 的定義,從程式碼就可以看出,前面我們所介紹的 Jar 是以 ZIP 格式構建一種歸檔檔案,因為它是 ZipFile 的子類。

public class JarFile extends ZipFile {
    private SoftReference<Manifest> manRef;
    private JarEntry manEntry;
    private JarVerifier jv;
    private boolean jvInitialized;
    private boolean verify;
    //指示是否存在Class-Path屬性(僅當hasCheckedSpecialAttributes為true時才有效)
    private boolean hasClassPathAttribute;
    // 如果清單檢查特殊屬性,則為 true
    private volatile boolean hasCheckedSpecialAttributes;
    // 在SharedSecrets中設定JavaUtilJarAccess
    static {
        SharedSecrets.setJavaUtilJarAccess(new JavaUtilJarAccessImpl());
    }
    /**
     * The JAR manifest file name.(JAR清單檔名)
     */
    public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
    // 省略其他
}
複製程式碼

下面是 Manifest 類的定義,用來描述 JAR 的 清單檔案。從其屬性中也很好的觀察到,其儲存的就是 K-V 鍵值對資料。

public class Manifest implements Cloneable {
    // manifest main attributes
    private Attributes attr = new Attributes();
    // manifest entries
    private Map<String, Attributes> entries = new HashMap<>();
    // 省略其他
}
複製程式碼

小結

JAR 格式遠遠超出了一種壓縮格式,它有許多可以改進效率、安全性和組織 Java 應用程式的功能。因為這些功能已經建立在核心平臺 -- 包括編譯器和類裝載器 -- 中了,所以開發人員可以利用 JAR 檔案格式的能力簡化和改進開發和部署過程。

附:常見的 jar工具用法

功能 命令
用一個單獨的檔案建立一個 JAR 檔案 jar cf jar-file input-file...
用一個目錄建立一個 JAR 檔案 jar cf jar-file dir-name
建立一個未壓縮的 JAR 檔案 jar cf0 jar-file dir-name
更新一個 JAR 檔案 jar uf jar-file input-file...
檢視一個 JAR 檔案的內容 jar tf jar-file
提取一個 JAR 檔案的內容 jar xf jar-file
從一個 JAR 檔案中提取特定的檔案 jar xf jar-file archived-file...
執行一個打包為可執行 JAR 檔案的應用程式 java -jar app.jar

參考

相關文章