spring boot jar 启动报错 Zip64 archives are not supported
- 原因、解决方案
- 问题
-
- [为什么 spring boot 不支持 zip64](#为什么 spring boot 不支持 zip64)
- [zip、zip64 功能上的区别](#zip、zip64 功能上的区别)
- [zip 的文件格式](#zip 的文件格式)
- [spring-boot-loader 是如何判断是否是 zip64 的?](#spring-boot-loader 是如何判断是否是 zip64 的?)
- 参考
spring boot 版本是 2.1.8.RELEASE,引入以下 phoenix 依赖之后启动报错。
xml
<dependency>
<groupId>org.apache.phoenix</groupId>
<artifactId>phoenix-client-hbase-2.4</artifactId>
<version>5.1.3</version>
</dependency>
错误日志:
bash
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.jar
at 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 supported
at 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
方法,依赖如下:
xml
<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 的?
java
// 从 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;
}
java
/**
* 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 最小长度,从表中可以看出在没有注释的情况下是 22
private static final int MINIMUM_SIZE = 22;
// 从表中可以看出注释长度为 2 字节,所有最大值是 65535
private 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 中"注释长度"字段的偏移量,从表中可以看出是 20
private 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 + comment
long 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 则为 Zip64
if (numberOfRecords == 0xFFFF) {
throw new IllegalStateException("Zip64 archives are not supported");
}
return (int) numberOfRecords;
}
}