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

