spring boot jar 启动报错 Zip64 archives are not supported

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;
	}

}

参考

相关推荐
不要飞升10 分钟前
百日筑基第十一天-看看SpringBoot
java·spring boot·后端·实习
ytgytg2825 分钟前
SpringBoot返回应答为String类型时,默认带双引号(““),取消双引号的方法
java·spring boot·后端
u0104058361 小时前
Spring Boot中的限流策略实现
spring boot·微服务·安全架构
missterzy1 小时前
Spring Boot 创建定时任务
java·数据库·spring boot·定时任务
德乐懿2 小时前
Spring Boot 2到3升级体验:解锁新特性与优势
java·spring boot·后端
基哥的奋斗历程2 小时前
springboot整合Camunda实现业务
java·spring boot·dubbo
hummhumm3 小时前
数据结构第08小节:双端队列
java·数据结构·spring boot·spring·java-ee·maven·intellij-idea
地瓜伯伯5 小时前
HandlerMethodArgumentResolver :深入spring mvc参数解析机制
大数据·人工智能·spring boot·spring·语言模型
亿只王菜菜6 小时前
WebRtc实现1V1音视频通话
spring boot·websocket·webrtc·实时音视频