spring boot jar 啟動報錯 Zip64 archives are not supported
- 原因、解決方案
- 問題
- 為什么 spring boot 不支持 zip64
- zip、zip64 功能上的區別
- zip 的文件格式
- spring-boot-loader 是如何判斷是否是 zip64 的?
- 參考
spring boot 版本是 2.1.8.RELEASE,引入以下 phoenix 依賴之后啟動報錯。
<dependency><groupId>org.apache.phoenix</groupId><artifactId>phoenix-client-hbase-2.4</artifactId><version>5.1.3</version>
</dependency>
錯誤日志:
PS D:\project\java\zip64\target> java -jar .\zip64-0.0.1-SNAPSHOT.jar
Exception in thread "main" java.lang.IllegalStateException: Failed to get nested archive for entry BOOT-INF/lib/phoenix-client-hbase-2.4-5.1.3.jarat org.springframework.boot.loader.archive.JarFileArchive.getNestedArchive(JarFileArchive.java:108)at org.springframework.boot.loader.archive.JarFileArchive.getNestedArchives(JarFileArchive.java:87)at org.springframework.boot.loader.ExecutableArchiveLauncher.getClassPathArchives(ExecutableArchiveLauncher.java:69)at org.springframework.boot.loader.Launcher.launch(Launcher.java:50)at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:52)
Caused by: java.io.IOException: Unable to open nested jar file 'BOOT-INF/lib/phoenix-client-hbase-2.4-5.1.3.jar'at org.springframework.boot.loader.jar.JarFile.getNestedJarFile(JarFile.java:258)at org.springframework.boot.loader.jar.JarFile.getNestedJarFile(JarFile.java:244)at org.springframework.boot.loader.archive.JarFileArchive.getNestedArchive(JarFileArchive.java:104)... 4 more
Caused by: java.lang.IllegalStateException: Zip64 archives are not supportedat org.springframework.boot.loader.jar.CentralDirectoryEndRecord.getNumberOfRecords(CentralDirectoryEndRecord.java:121)at org.springframework.boot.loader.jar.JarFileEntries.visitStart(JarFileEntries.java:117)at org.springframework.boot.loader.jar.CentralDirectoryParser.visitStart(CentralDirectoryParser.java:85)at org.springframework.boot.loader.jar.CentralDirectoryParser.parse(CentralDirectoryParser.java:56)at org.springframework.boot.loader.jar.JarFile.<init>(JarFile.java:125)at org.springframework.boot.loader.jar.JarFile.<init>(JarFile.java:112)at org.springframework.boot.loader.jar.JarFile.createJarFileFromFileEntry(JarFile.java:289)at org.springframework.boot.loader.jar.JarFile.createJarFileFromEntry(JarFile.java:266)at org.springframework.boot.loader.jar.JarFile.getNestedJarFile(JarFile.java:255)
原因、解決方案
Google 很快就找到了原因,stackoverflow 上有類似的問題 java - Add more than 65535 entries jar in Spring boot - Stack Overflow。
第一個回答給出了原因:spring boot 不支持一個 jar 文件中多于 65534(這里應該寫錯了,應該是 65535) 個文件,并附上了拋異常的代碼。
第二個回答是 spring boot 的 issues,有興趣的可以自己看一下 Support zip64 format executable archives · Issue #2895 · spring-projects/spring-boot (github.com)
第三個回答給出了解決辦法:升級到 2.2.x,也給出了支持 zip64 的提交記錄 Support zip64 jars by cvienot · Pull Request #16091 · spring-projects/spring-boot (github.com)。我升級成 2.2.0.RELEASE 確實解決了問題。
問題
回答一中的代碼來自 spring-boot-loader
子項目中的 org.springframework.boot.loader.jar.CentralDirectoryEndRecord#getNumberOfRecords
方法,依賴如下:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-loader</artifactId><version>2.1.8.RELEASE</version>
</dependency>
為什么 spring boot 不支持 zip64
從 ZIP (file format) - Wikipedia 中可以看出 zip、zip64 在文件的格式上是不同的。猜測應該是開發者沒想到 jar 包里的文件個數或 jar 包的大小會超過 65535,所以沒有實現 zip64 相關的。從以下提交中能看出一二。直到最后的兩個提交才有人實現了 zip64 的相關代碼。
zip、zip64 功能上的區別
zip64 格式是標準 zip 格式的擴展,實際上消除了 zip 存檔中文件大小和數量的限制。
每種格式允許的最大值總結如下:
Standard Format Zip64 Format Number of Files Inside an Archive 65,535 2^64 - 1 Size of a File Inside an Archive [bytes] 4,294,967,295 2^64 - 1 Size of an Archive [bytes] 4,294,967,295 2^64 - 1 Number of Segments in a Segmented Archive 999 (spanning) 65,535 (splitting) 4,294,967,295 - 1 Central Directory Size [bytes] 4,294,967,295 2^64 - 1
zip 的文件格式
zip格式壓縮包主要由三大部分組成:
數據區
、中央目錄記錄區(也有叫核心目錄記錄)
、中央目錄記錄尾部區
。數據區是由一系列
本地文件記錄
組成,本地文件記錄主要是記錄了壓縮前后文件的元數據
以及存放壓縮后的文件
。中央目錄記錄區是有一系列
中央目錄記錄
所組成,一條中央目錄記錄
對應數據區中的一個壓縮文件記錄
。中央目錄記錄尾部(End of central directory record)主要作用是用來定位
中央目錄記錄區的開始位置
,同時記錄壓縮包的注釋內容
。
End of central directory record (EOCD)
Offset | Bytes | Description[33] | 中文 |
---|---|---|---|
0 | 4 | End of central directory signature = 0x06054b50 | 簽名 |
4 | 2 | Number of this disk (or 0xffff for ZIP64) | |
6 | 2 | Disk where central directory starts (or 0xffff for ZIP64) | |
8 | 2 | Number of central directory records on this disk (or 0xffff for ZIP64) | |
10 | 2 | Total number of central directory records (or 0xffff for ZIP64) | 文件數量(ZIP64 為 0xffff ) |
12 | 4 | Size of central directory (bytes) (or 0xffffffff for ZIP64) | |
16 | 4 | Offset of start of central directory, relative to start of archive (or 0xffffffff for ZIP64) | |
20 | 2 | Comment length (n) | 注釋長度 |
22 | n | Comment |
spring-boot-loader 是如何判斷是否是 zip64 的?
// 從 bytes 的 offset 偏移量開始,以小端模式讀取 length 個字節
public static long littleEndianValue(byte[] bytes, int offset, int length) {long value = 0;for (int i = length - 1; i >= 0; i--) {value = ((value << 8) | (bytes[offset + i] & 0xFF));}return value;
}
/*** A ZIP File "End of central directory record" (EOCD).** @author Phillip Webb* @author Andy Wilkinson* @see <a href="https://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>*/
class CentralDirectoryEndRecord {// EOCD 最小長度,從表中可以看出在沒有注釋的情況下是 22private static final int MINIMUM_SIZE = 22;// 從表中可以看出注釋長度為 2 字節,所有最大值是 65535private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF;private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH;// EOCD 開始的標記private static final int SIGNATURE = 0x06054b50;// EOCD 中“注釋長度”字段的偏移量,從表中可以看出是 20private static final int COMMENT_LENGTH_OFFSET = 20;// 每次從文件尾部讀取 256 字節private static final int READ_BLOCK_SIZE = 256;// 最終是 EOCD 的字節數組private byte[] block;// EOCD 在 block 中的偏移量private int offset;// EOCD 的字節數private int size;/*** Create a new {@link CentralDirectoryEndRecord} instance from the specified* {@link RandomAccessData}, searching backwards from the end until a valid block is* located.* @param data the source data* @throws IOException in case of I/O errors*/CentralDirectoryEndRecord(RandomAccessData data) throws IOException {// 從文件尾部讀取 256 字節this.block = createBlockFromEndOfData(data, READ_BLOCK_SIZE);this.size = MINIMUM_SIZE;this.offset = this.block.length - this.size;// 嘗試找到 EOCD 的開頭while (!isValid()) {this.size++;if (this.size > this.block.length) {if (this.size >= MAXIMUM_SIZE || this.size > data.getSize()) {throw new IOException("Unable to find ZIP central directory " + "records after reading " + this.size + " bytes");}// 每次多讀 1 字節this.block = createBlockFromEndOfData(data, this.size + READ_BLOCK_SIZE);}// offset 每次向前移動 1 字節this.offset = this.block.length - this.size;}}private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException {int length = (int) Math.min(data.getSize(), size);return data.read(data.getSize() - length, length);}// 嘗試找到 EOCD 的開頭private boolean isValid() {// 長度小于 EOCD 的最小長度,肯定不符合if (this.block.length < MINIMUM_SIZE// 讀取 block 最開始的 4 個字節,與 EOCD 的標記進行比較,不符合則返回 false// 如果相等則找到了 EOCD 的開頭|| Bytes.littleEndianValue(this.block, this.offset + 0, 4) != SIGNATURE) {return false;}// 讀取注釋長度 2 字節// Total size must be the structure size + commentlong commentLength = Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2);// EOCD 的字節數肯定等于 EOCD 的最小長度 + 注釋內容的長度return this.size == MINIMUM_SIZE + commentLength;}/*** Return the number of ZIP entries in the file.* @return the number of records in the zip*/public int getNumberOfRecords() {// 讀取 block 偏移量文 10 的 2 個字節,即文件數量long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2);// 如果文件數量為 65535 則為 Zip64if (numberOfRecords == 0xFFFF) {throw new IllegalStateException("Zip64 archives are not supported");}return (int) numberOfRecords;}}
參考
- java - Add more than 65535 entries jar in Spring boot - Stack Overflow
- 壓縮包Zip格式詳析(全網最詳細)_zip格式詳解-CSDN博客
- ZIP文件格式分析 | Sp4n9x’s Blog
- ZIP (file format) - Wikipedia