Java JAR 檔案格式
Java Archive——把編譯後的 .class 檔與一份 manifest 打包起來的 ZIP。
- MIME 類型
- application/java-archive
- 副檔名
- .jar
- 分類
- 壓縮與封存
Magic number(檔案簽章)
| 0x00000000 | 504B0304 | PK·· | 'PK' — it is a ZIP |
ZIP 內含 'META-INF/MANIFEST.MF' 以及一個以上的 '.class' 項目。
關於 Java JAR 格式
JAR 是在 ZIP 之上疊了一層慣例:一個 META-INF 目錄,裡面的 MANIFEST.MF 描述這個壓縮檔——可執行 JAR 的主類別、class path、模組名稱,而在簽署過的 JAR 裡還有每個項目的摘要值。ZIP 本身沒有任何改變,所以任何壓縮軟體都打得開 JAR,這個格式也才會被 WAR、EAR、Spring Boot fat JAR,以及早年的 Android APK 重複拿去用。要注意的是這層慣例也包含項目順序:串流式讀取端只會在檔案最前面找 manifest,找不到就很快放棄。
檔頭結構,一個位元組一個位元組看
| 位移 | 長度 | 欄位 | 意義 |
|---|---|---|---|
| 0 | 4 | Local file header signature | 50 4B 03 04。JAR 逐位元組就是 ZIP,位移 0 完全沒有提到 Java。每個項目都以這種標頭開頭,只有第一個位在固定位置。 |
| 4 | 2 | Version needed to extract | 直接儲存的項目是 10,deflate 是 20——而 jar 工具寫出來的 JAR,第一個項目是 META-INF/ 這個目錄、採直接儲存,所以即使實際內容全是 deflate 過的,這個欄位常常讀到 10。 |
| 6 | 2 | General purpose bit flag | 0x0800(位元 11)標示項目名稱是 UTF-8,JDK 的 jar 工具會設。位元 3 是把大小推到資料後面的串流旗標,會出現在伺服器即時產生的 JAR 裡。 |
| 8 | 2 | Compression method | 0 是直接儲存、8 是 deflate。META-INF/ 這類目錄項目是以長度 0 直接儲存的,所以 JAR 第一個項目常常 CRC 和兩個大小都是 0,而這完全沒有問題。 |
| 10 | 4 | Last modified time and date | 位移 10 是 MS-DOS 打包的時間、位移 12 是日期:解析度兩秒、沒有時區、1980 年以前表達不出來。可重現建置會把它們釘成固定值,所以很多 JAR 都宣稱自己來自 1980-01-01 或 2010-01-01。 |
| 14 | 4 | CRC-32 | 未壓縮項目的 CRC-32,壓縮後大小在位移 18、未壓縮大小在位移 22。在簽署過的 JAR 裡,它們和 META-INF 裡的 SHA 摘要互不相干,所以一個項目可以 CRC 過關、簽章驗證卻失敗。 |
| 26 | 2 | File name length | 位移 30 那個名稱的位元組長度。jar 工具寫出來的 JAR 這裡是 9——META-INF/ 的長度——在 hex dump 裡是個雖不嚴謹但很快的線索。 |
| 30 | variable | First entry name | JAR 實務上宣告自己身分的地方:先是 META-INF/,接著 META-INF/MANIFEST.MF。這個順序不是裝飾,因為串流式讀取端只會在檔案前段找 manifest。 |
| -22 | 4 | End of central directory signature | 50 4B 05 06,沒有壓縮檔註解時剛好位於倒數第 22 個位元組。類別載入器從這裡讀中央目錄,所以它能從 60 MB 的 JAR 裡載入一個類別,而不必解壓縮其餘部分。 |
| -6 | 4 | Offset of the central directory | 中央目錄的起點,從檔案開頭起算。前面貼了一段 shell script 的自解壓縮或可執行 JAR 照樣能用,因為讀取端容忍這段位移,但這裡的數值已經和原始檔案配置對不上了。 |
怎麼跟長得像的格式分開
| 容易搞混的 | 怎麼分 |
|---|---|
| ZIP | 位元組層級完全相同。讓一個 ZIP 成為 JAR 的,是中央目錄裡的 META-INF/MANIFEST.MF,通常還加上 .class 項目。把 JAR 改名成 .zip 什麼都沒變;把 ZIP 改名成 .jar,則會得到一個被 java -jar 以「沒有 main manifest attribute」為由拒收的檔案。 |
| Android APK | APK 的出身就是 JAR,而且到現在還有 META-INF,所以兩者在壓縮軟體裡看起來很像。分界在內容:有 .dex 項目與 AndroidManifest.xml 就是 APK,只有 .class 項目與 MANIFEST.MF 就是 JAR,而且只有 APK 會在中央目錄前面帶著 'APK Sig Block 42' magic。 |
| war / ear | 同樣的格式、不同的必備配置:WAR 把類別放在 WEB-INF/classes、程式庫放在 WEB-INF/lib,EAR 則裝著 application.xml 加上其他壓縮檔。servlet 容器看的是配置而不是副檔名,所以副檔名標錯的 WAR 是卡在「找不到 WEB-INF」,而不是卡在 ZIP 本身。 |
| Spring Boot jar | 一個可執行的 JAR,自己的類別放在 BOOT-INF/classes,相依套件則是 BOOT-INF/lib 底下巢狀的 JAR,而 MANIFEST.MF 裡指名的 Main-Class 其實是一個啟動器。它用一般 JAR 的方式打得開,但應用程式類別不在類別載入器平常會找的位置,所以把它解開、直接跑主類別是行不通的。 |
常見的壞法
- manifest 不在最前面了。用通用的 ZIP 工具重新打包 JAR,每個位元組的內容都保住了,順序卻沒了,串流式讀取端接著就回報「完全沒有 manifest」。這件事用 JDK 自己的 API 實測過:jar 工具寫出來的 JAR(先 META-INF/、再 META-INF/MANIFEST.MF)讀得到 manifest,而把 manifest 放在某個 class 檔之後、當成第二和第三個項目的兩種壓縮檔,讀出來都是 null。java -jar 照樣跑得動,因為它讀的是中央目錄而不是串流——所以同一個檔案,這樣用會過、那樣用會掛。
- 尾巴被截斷。每個本地標頭都活著、從位移 0 看過去很健康,但 end of central directory 記錄不見了,而從檔尾起步的類別載入器連一個項目都列不出來。救援方式是往後掃 50 4B 03 04 重建目錄。
- 重新打包之後簽章壞掉。簽署過的 JAR 會在 META-INF/MANIFEST.MF 裡替每個項目存一份摘要,旁邊還有一個簽章檔,所以做 shading、重新壓縮或合併都會改變摘要,JVM 會以安全性例外拒收,而不是報損壞。
- 粗糙的合併製造出重複項目。把相依套件串成單一壓縮檔,可能產生兩個路徑相同的項目;ZIP 允許這件事,而類別載入器取第一個,所以哪一版的類別勝出取決於中央目錄裡的順序——一個檔案本身沒問題、缺陷卻看不見的狀況。
這個格式現在還在用嗎
JAR 是整個 JVM 生態系的散布單位:Maven Central 上的東西都是 JAR,建置工具解析出來的每個相依套件都是 JAR,每個伺服器端 Java 應用程式也都以它出貨——不管是單純的程式庫、給容器用的 WAR,還是自給自足的 Spring Boot 壓縮檔。沒有東西取代它——模組系統是在 JAR 裡加了一個 module-info.class,而不是換一個新容器;native-image 和 jlink 是從 JAR 產出執行檔,而不是在上游把它換掉。它失守的地方是用戶端交付:applet 沒了,Java Web Start 也跟著沒了,所以今天的 JAR 是伺服器端或建置期的產物。它歷久不衰的優勢是 ZIP 的中央目錄,讓類別載入器能從幾千個類別裡抽出一個,完全不碰其他部分。
在這個站上打開 Java JAR 檔
這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。
相關格式
容易和 Java JAR 搞混的格式,可以比對它們的開頭位元組。
| 格式 | 開頭位元組 | 副檔名 |
|---|---|---|
| ZIP | 50 4B 03 04 | .zip |
| Android APK | 50 4B 03 04 | .apk |
| Java class 檔 | CA FE BA BE | .class |