You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ZipCompressor工具类偶发生成无效Zip归档问题求助

偶发无效Zip归档问题排查与解决

问题概述

自行实现的ZipCompressor工具类的compress()方法偶发产生无效Zip归档,首个条目压缩错误,无法被Windows、Java 11正常打开,7zip打开时也报错。该问题为偶发,重复创建相同归档时不一定复现,但重新运行生成脚本耗时约1小时。

现有工具类代码

package lib.util.compressors.zip;

import java.util.Enumeration;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import lib.util.compressors.Compressor;
import lib.util.compressors.CompressorException;
import lib.util.compressors.Entry;

import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import java.util.zip.ZipFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipInputStream;

/**
 * The ZipCompressor class supplies a simple way of writing zip files. The default zip level is set
 * to maximum compression.
 * 
 * @author REDACTED
 */
public class ZipCompressor extends Compressor {

    private int zipLevel = 9;

    /**
     * @see lib.util.compressors.Compressor#compress
     */
    public void compress(String fileName, String dirName) throws CompressorException {

        File zipFile = new File(fileName);
        try(FileOutputStream fos = new FileOutputStream(fileName);
                ZipOutputStream zos = new ZipOutputStream(fos);
                ) {

            // Open the file and set the compression level
            zos.setLevel(zipLevel);

            // Zip the directory
            File dir = new File(dirName);
            compress(zipFile, zos, dir, dir);
        } catch (FileNotFoundException fnfe) {
            throw new CompressorException(fnfe);
        } catch (IOException ioe) {
            throw new CompressorException(ioe);
        }
    }

    /**
     * @see lib.util.compressors.Compressor#uncompress
     */
    public void uncompress(String fileName, String dirName) throws CompressorException {

        try(// Open the zipfile
            ZipFile zipFile = new ZipFile(fileName);
                // Open streams
                FileInputStream fis = new FileInputStream(zipFile.getName());
                BufferedInputStream bis = new BufferedInputStream(fis);
                ZipInputStream zis = new ZipInputStream(bis);) {

            
            // Get the size of each entry
            Map<String, Integer> zipEntrySizes = new HashMap<String, Integer> ();
            Enumeration<? extends ZipEntry> e = zipFile.entries();
            while (e.hasMoreElements()) {
                ZipEntry zipEntry = (ZipEntry) e.nextElement();
                zipEntrySizes.put(zipEntry.getName(), Integer.valueOf((int) zipEntry.getSize()));
            }

            
            // Start reading zipentries
            ZipEntry zipEntry = null;
            while ((zipEntry = zis.getNextEntry()) != null) {
                
                // Zipentry is a file
                if (!zipEntry.isDirectory()) {

                    // Get the size
                    int size = (int) zipEntry.getSize();
                    if (size == -1) {
                        size = ((Integer) zipEntrySizes.get(zipEntry.getName())).intValue();
                    }

                    // Get the content
                    byte[] buffer = new byte[size];
                    int bytesInBuffer = 0;
                    int bytesRead = 0;
                    while (((int) size - bytesInBuffer) > 0) {
                        bytesRead = zis.read(buffer, bytesInBuffer, size - bytesInBuffer);
                        if (bytesRead == -1) {
                            break;
                        }
                        bytesInBuffer += bytesRead;
                    }

                    String zipEntryName = zipEntry.getName();
                    // replace all "\" with "/"
                    zipEntryName = zipEntryName.replace('\\', '/');

                    // Get the full path name
                    File file = new File(dirName, zipEntryName);

                    // Create the parent directory
                    if (!file.getParentFile().exists()) {
                        file.getParentFile().mkdirs();
                    }

                    // Save file
                    try (FileOutputStream fos = new FileOutputStream(file.getPath());) {
                        fos.write(buffer, 0, bytesInBuffer);
                    }

                    if (zipEntry.getTime() >= 0L) {
                        // Set modification date to the date in the zipEntry
                        file.setLastModified(zipEntry.getTime());
                    }
                }
                // Zipentry is a directory
                else {

                    String zipEntryName = zipEntry.getName();
                    // replace all "\" with "/"
                    zipEntryName = zipEntryName.replace('\\', '/');

                    // Create the directory
                    File dir = new File(dirName, zipEntryName);
                    dir.setLastModified(zipEntry.getTime());
                    if (!dir.exists()) {
                        dir.mkdirs();
                    }
                }
            }
        } catch (IOException ioe) {
            throw new CompressorException(ioe);
        }
    }

    /**
     * @see lib.util.compressors.Compressor#getEntries
     */
    public List<Entry> getEntries(String fileName, boolean calculateCrc) throws CompressorException {

        // List to return all entries
        List<Entry> entries = new ArrayList<Entry>();
        
        try( // Open the zipfile
                ZipFile zipFile = new ZipFile(fileName);) {

           
            // Get the size of each entry
            Enumeration<? extends ZipEntry> e = zipFile.entries();
            while (e.hasMoreElements()) {
                ZipEntry zipEntry = (ZipEntry) e.nextElement();
                Entry entry = new Entry();
                entry.setName(zipEntry.getName());
                if (calculateCrc) {
                    entry.setCrc(zipEntry.getCrc());
                } else {
                    entry.setCrc(-1);
                }
                entry.setDirectory(zipEntry.isDirectory());
                entry.setTime(zipEntry.getTime());
                entry.setSize(zipEntry.getSize());
                entries.add(entry);
            }

        
            // Sort entries by ascending name
            sortEntries(entries);
            
            // Return entries
            return entries;
            
        } catch (IOException ioe) {
            throw new CompressorException(ioe);
        }
    }

    /**
     * Sets the zip level (1..9).
     * 
     * @param zipLevel the desired zip level.
     * @throws CompressorException
     */
    public void setZipLevel(int zipLevel) throws CompressorException {
        if ((zipLevel < 1) || (zipLevel > 9)) {
            throw new CompressorException("Zip level " + zipLevel + " out of range (0 ... 9)");
        }
        this.zipLevel = zipLevel;
    }

    /**
     * Add a new entry to the zip file.
     * 
     * @param zos the output stream filter for writing files in the ZIP file format
     * @param name the name of the entry.
     * @param lastModified the modification date 
     * @param buffer an array of bytes
     * @throws IOException
     */
    private void addEntry(ZipOutputStream zos, String name, long lastModified, byte[] buffer) throws IOException {
        ZipEntry zipEntry = new ZipEntry(name);
        if (buffer != null) {
            zipEntry.setSize(buffer.length);
        } 
        zipEntry.setTime(lastModified);
        zos.putNextEntry(zipEntry);
        if (buffer != null) {
            zos.write(buffer);
        }
        zos.closeEntry();
    }

    /**
     * Zip the files of the given directory.
     * 
     * @param zipFile the File which is used to store the compressed data
     * @param zos the output stream filter for writing files in the ZIP file format
     * @param dir the directory to zip
     * @param relativeDir the name of each zip entry will be relative to this directory
     * @throws FileNotFoundException
     * @throws IOException
     */
    private void compress(File zipFile, ZipOutputStream zos, File dir, File relativeDir) throws FileNotFoundException, IOException {

        // Create an array of File objects
        File[] fileList = dir.listFiles();

        // Directory is not empty
        if (fileList.length != 0) {

            // Loop through File array
            for (int i = 0; i < fileList.length; i++) {

                // The zipfile itself may not be added
                if (!zipFile.equals(fileList[i])) {
                    // Directory
                    if (fileList[i].isDirectory()) {
                        compress(zipFile, zos, fileList[i], relativeDir);
                    }
                    // File
                    else {
                        byte[] buffer = getFileContents(fileList[i]);
                        if (buffer != null) {
                            // Get the path names
                            String filePath = fileList[i].getPath();
                            String relativeDirPath = relativeDir.getPath();

                            // Convert the absolute path name to a relative path name
                            if (filePath.startsWith(relativeDirPath)) {
                                filePath = filePath.substring(relativeDirPath.length());
                                if (filePath.startsWith("/") || filePath.startsWith("\\")) {
                                    if (filePath.length() == 1) {
                                        filePath = "";
                                    } else {
                                        filePath = filePath.substring(1);
                                    }
                                }
                            }

                            // Add the entry
                            addEntry(zos, filePath, fileList[i].lastModified(), buffer);
                        }
                    }
                }
            }
        }
        // Directory is empty
        else {
            // Get the path names
            String filePath = dir.getPath();
            String relativeDirPath = relativeDir.getPath();

            // Convert the absolute path name to a relative path name
            if (filePath.startsWith(relativeDirPath)) {
                filePath = filePath.substring(relativeDirPath.length());
                if (filePath.startsWith("/") || filePath.startsWith("\\")) {
                    if (filePath.length() == 1) {
                        filePath = "";
                    } else {
                        filePath = filePath.substring(1);
                    }
                }
            }

            // Add the entry
            if (!filePath.endsWith("\\") && !filePath.endsWith("/")) {
                addEntry(zos, filePath + "/", dir.lastModified(), null);
            }
            else {
                addEntry(zos, filePath, dir.lastModified(), null);
            }
        }
    }

    /**
     * Read the contents of a file for zipping.
     * 
     * @param file the File to read
     * @return an array of bytes
     * @throws FileNotFoundException
     * @throws IOException
     */
    private byte[] getFileContents(File file) throws FileNotFoundException, IOException {

        try (FileInputStream fis = new FileInputStream(file);) {
            long len = file.length();
            byte[] buffer = new byte[(int) len];
            fis.read(buffer);
            return buffer;
        }
    }
}

错误信息

Java解压报错

lib.util.compressors.CompressorException: java.util.zip.ZipException: invalid END header (bad central directory offset)
    at lib.util.compressors.zip.ZipCompressor.uncompress(ZipCompressor.java:151)
    at phases.core.deploy.UnCompressBuildResultPhase.execute(UnCompressBuildResultPhase.java:61)
    at Proxy6f3235bf_85bf_485a_9577_a60fb3dad49c.execute(Unknown Source)
    at phases.impl.DefaultPhaseExecutionImpl.execute(DefaultPhaseExecutionImpl.java:152)
    at daemons.agent.deployer.DeployerThread.run(DeployerThread.java:144)
Caused by: java.util.zip.ZipException: invalid END header (bad central directory offset)
    at java.base/java.util.zip.ZipFile$Source.zerror(ZipFile.java:1607)
    at java.base/java.util.zip.ZipFile$Source.initCEN(ZipFile.java:1519)
    at java.base/java.util.zip.ZipFile$Source.<init>(ZipFile.java:1308)
    at java.base/java.util.zip.ZipFile$Source.get(ZipFile.java:1271)
    at java.base/java.util.zip.ZipFile$CleanableResource.<init>(ZipFile.java:733)
    at java.base/java.util.zip.ZipFile$CleanableResource.get(ZipFile.java:850)
    at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:248)
    at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:177)
    at java.base/java.util.zip.ZipFile.<init>(ZipFile.java:148)
    at lib.util.compressors.zip.ZipCompressor.uncompress(ZipCompressor.java:72)
    ... 4 more

7zip报错与归档属性

C:\Users\REDACTED\Downloads\b5_CONTBUILD_B-6-0-0_win.zip
Unavailable start of archive
Warning
The archive is open with offset
Unavailable data : ant\bin\ant
Path: C:\Users\REDACTED\Downloads\b5_CONTBUILD_B-6-0-0_win.zip
Type: zip
Open WARNING:: Cannot open the file as expected archive type
Error Type: zip
Errors: Unavailable start of archive
Offset: -1024
Physical Size: 722 598 855

排查分析

  1. 流关闭问题排除:已确认try-with-resources会自动调用ZipOutputStream.close(),而该方法已包含finish()和flush()操作,因此排除流未正确关闭导致的归档不完整问题。
  2. 文件读取不完整(核心原因):getFileContents()方法中仅调用一次fis.read(buffer),该方法无法保证一次性读取完整文件内容。当文件较大、系统IO繁忙时,可能只读取部分字节,导致写入Zip的文件内容缺失,破坏整个Zip归档的结构(Zip的中央目录依赖每个条目的长度、偏移等元数据的准确性),这也是问题偶发的关键原因。
  3. 路径处理潜在问题:压缩时生成的条目路径可能包含反斜杠(Windows平台),不符合Zip规范(要求用正斜杠作为路径分隔符),可能导致部分工具解析异常,但这不是偶发问题的主要诱因。
  4. 解压方法冗余:uncompress()同时使用ZipFile和ZipInputStream,属于冗余操作,可能引发流冲突,但这是解压阶段的问题,与压缩生成无效归档无关。

解决办法

1. 修复文件读取逻辑,确保读取完整内容

替换原getFileContents()方法,使用循环读取或ByteArrayOutputStream保证读取完整文件:

方式一:循环读取(适用于已知文件大小不超过Integer.MAX_VALUE的场景)

private byte[] getFileContents(File file) throws FileNotFoundException, IOException {
    try (FileInputStream fis = new FileInputStream(file)) {
        long len = file.length();
        if (len > Integer.MAX_VALUE) {
            throw new IOException("File too large to read into byte array: " + file.getAbsolutePath());
        }
        byte[] buffer = new byte[(int) len];
        int bytesRead;
        int totalRead = 0;
        // 循环读取直到获取全部字节
        while (totalRead < len && (bytesRead = fis.read(buffer, totalRead, (int) len - totalRead)) != -1) {
            totalRead += bytesRead;
        }
        // 校验是否读取完整
        if (totalRead != len) {
            throw new IOException("Failed to read entire file. Expected: " + len + ", Read: " + totalRead);
        }
        return buffer;
    }
}

方式二:使用ByteArrayOutputStream(更稳妥,支持大文件)

private byte[] getFileContents(File file) throws FileNotFoundException, IOException {
    try (FileInputStream fis = new FileInputStream(file);
         ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
        byte[] buffer = new byte[8192]; // 使用8KB缓冲区提升读取效率
        int bytesRead;
        while ((bytesRead = fis.read(buffer)) != -1) {
            baos.write(buffer, 0, bytesRead);
        }
        return baos.toByteArray();
    }
}

2. 统一Zip条目路径分隔符

在压缩生成条目路径时,将所有反斜杠替换为正斜杠,符合Zip规范:
在compress()方法的文件路径转换后添加:

filePath = filePath.replace('\\', '/');

确保条目名称使用标准的正斜杠分隔路径。

3. 可选:优化解压方法(消除冗余流操作)

修改uncompress()方法,直接使用ZipFile读取条目内容,避免同时使用ZipFile和ZipInputStream:

public void uncompress(String fileName, String dirName) throws CompressorException {
    try (ZipFile zipFile = new ZipFile(fileName)) {
        Enumeration<? extends ZipEntry> entries = zipFile.entries();
        while (entries.hasMoreElements()) {
            ZipEntry zipEntry = entries.nextElement();
            String entryName = zipEntry.getName().replace('\\', '/');
            File target = new File(dirName, entryName);
            
            if (zipEntry.isDirectory()) {
                if (!target.exists() && !target.mkdirs
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 13:18:40