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


    spring boot 版本是 2.1.8.RELEASE,引入以下 phoenix 依赖之后启动报错。

    <dependency>
        <groupId>org.apache.phoenixgroupId>
        <artifactId>phoenix-client-hbase-2.4artifactId>
        <version>5.1.3version>
    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.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 确实解决了问题。

    image-20240625151243205

    问题

    回答一中的代码来自 spring-boot-loader 子项目中的 org.springframework.boot.loader.jar.CentralDirectoryEndRecord#getNumberOfRecords 方法,依赖如下:

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-loaderartifactId>
        <version>2.1.8.RELEASEversion>
    dependency>
    

    为什么 spring boot 不支持 zip64

    ZIP (file format) - Wikipedia 中可以看出 zip、zip64 在文件的格式上是不同的。猜测应该是开发者没想到 jar 包里的文件个数或 jar 包的大小会超过 65535,所以没有实现 zip64 相关的。从以下提交中能看出一二。直到最后的两个提交才有人实现了 zip64 的相关代码。

    image-20240625173000853

    zip、zip64 功能上的区别

    zip64 格式是标准 zip 格式的扩展,实际上消除了 zip 存档中文件大小和数量的限制。

    每种格式允许的最大值总结如下:

    Standard FormatZip64 Format
    Number of Files Inside an Archive65,5352^64 - 1
    Size of a File Inside an Archive [bytes]4,294,967,2952^64 - 1
    Size of an Archive [bytes]4,294,967,2952^64 - 1
    Number of Segments in a Segmented Archive999 (spanning) 65,535 (splitting)4,294,967,295 - 1
    Central Directory Size [bytes]4,294,967,2952^64 - 1

    zip 的文件格式

    zip格式压缩包主要由三大部分组成:数据区中央目录记录区(也有叫核心目录记录)中央目录记录尾部区

    数据区是由一系列本地文件记录组成,本地文件记录主要是记录了压缩前后文件的元数据以及存放压缩后的文件

    中央目录记录区是有一系列中央目录记录所组成,一条中央目录记录对应数据区中的一个压缩文件记录

    中央目录记录尾部(End of central directory record)主要作用是用来定位中央目录记录区的开始位置,同时记录压缩包的注释内容

    End of central directory record (EOCD)

    OffsetBytesDescription[33]中文
    04End of central directory signature = 0x06054b50签名
    42Number of this disk (or 0xffff for ZIP64)
    62Disk where central directory starts (or 0xffff for ZIP64)
    82Number of central directory records on this disk (or 0xffff for ZIP64)
    102Total number of central directory records (or 0xffff for ZIP64)文件数量(ZIP64 为 0xffff )
    124Size of central directory (bytes) (or 0xffffffff for ZIP64)
    164Offset of start of central directory, relative to start of archive (or 0xffffffff for ZIP64)
    202Comment length (n)注释长度
    22nComment

    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 Zip File Format
     */
    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;
    	}
    
    }
    

    参考

  • 相关阅读:
    什么是链动2+1模式?如何打造高活跃的商业生态圈?
    PYTHON-使用正则表达式进行模式匹配
    k8s之pod流量分析
    俄罗斯方块
    vue3 keepalive跳转页面保存页面状态
    C++ 学习宝藏网站分享
    自用软件分享,快来看看有没有同款
    maven项目正在idea的创建和配置
    自动驾驶技术
    【算法训练-动态规划 一】【应用DP问题】零钱兑换、爬楼梯、买卖股票的最佳时机I、打家劫舍
  • 原文地址:https://blog.csdn.net/qq_20919883/article/details/139966141