聊一聊JAR文件和MANIFEST.MF

NO IMAGE

在 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

參考

相關文章

SpringBoot實踐Filter中的異常處理和Controller中的異常處理

聊一聊SpringBoot中配置加載優先級

SpringBoot系列啟動過程

shell腳本簡單歸納和實踐