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

}

参考

相关推荐
世间万物皆对象28 分钟前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
qq_17448285752 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
代码小鑫4 小时前
A043-基于Spring Boot的秒杀系统设计与实现
java·开发语言·数据库·spring boot·后端·spring·毕业设计
真心喜欢你吖4 小时前
SpringBoot与MongoDB深度整合及应用案例
java·spring boot·后端·mongodb·spring
周全全4 小时前
Spring Boot + Vue 基于 RSA 的用户身份认证加密机制实现
java·vue.js·spring boot·安全·php
飞升不如收破烂~5 小时前
Spring boot常用注解和作用
java·spring boot·后端
计算机毕设源码qq-38365310416 小时前
(附项目源码)Java开发语言,215 springboot 大学生爱心互助代购网站,计算机毕设程序开发+文案(LW+PPT)
java·开发语言·spring boot·mysql·课程设计
岁岁岁平安6 小时前
springboot实战(15)(注解@JsonFormat(pattern=“?“)、@JsonIgnore)
java·spring boot·后端·idea
潜洋10 小时前
Spring Boot教程之五:在 IntelliJ IDEA 中运行第一个 Spring Boot 应用程序
java·spring boot·后端
灯雾️10 小时前
Spring Boot、Spring MVC和Spring间的区别
spring boot