首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 文件壓縮與解壓完整實戰:ZipOutputStream、ZipInputStream 與校驗和驗證

     編輯:Java 相關知識

Java java.util.zip 包提供標準 ZIP 格式壓縮解壓能力,無需第三方庫即可滿足絕多數業務場景。本文提供生產級工具類,涵蓋多文件壓縮、解壓到目錄、列表壓縮包內容、Adler32 校驗和完整性驗證,代碼健壯、異常處理完善,可直接集成。

核心工具類:ZipUtils


package com.example.util;

import java.io.*;
import java.util.*;
import java.util.zip.*;
import java.nio.file.*;

public class ZipUtils {

    /**
     * 壓縮多個文件/目錄到 ZIP
     * @param sourceFiles  源文件/目錄路徑列表
     * @param zipFilePath  目標 ZIP 文件路徑
     * @param comment      可選壓縮包註釋
     * @return 校驗和值
     */
    public static long zipFiles(List sourceFiles, String zipFilePath, String comment) throws IOException {
        Path zipPath = Paths.get(zipFilePath);
        Files.createDirectories(zipPath.getParent());

        try (FileOutputStream fos = new FileOutputStream(zipFilePath);
             CheckedOutputStream cos = new CheckedOutputStream(fos, new Adler32());
             ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(cos))) {

            if (comment != null) zos.setComment(comment);

            byte[] buffer = new byte[8192];
            for (String src : sourceFiles) {
                Path sourcePath = Paths.get(src);
                if (!Files.exists(sourcePath)) {
                    System.err.println("Skipped non-existent: " + src);
                    continue;
                }
                addToZip(zos, sourcePath, sourcePath.getFileName().toString(), buffer);
            }
            zos.finish(); // 重要:寫入中央目錄
            return cos.getChecksum().getValue();
        }
    }

    private static void addToZip(ZipOutputStream zos, Path source, String entryName, byte[] buffer) throws IOException {
        if (Files.isDirectory(source)) {
            // 目錄條目以 / 結尾
            if (!entryName.endsWith("/")) entryName += "/";
            zos.putNextEntry(new ZipEntry(entryName));
            zos.closeEntry();
            // 遞歸處理子項
            try (DirectoryStream stream = Files.newDirectoryStream(source)) {
                for (Path child : stream) {
                    addToZip(zos, child, entryName + child.getFileName(), buffer);
                }
            }
        } else {
            zos.putNextEntry(new ZipEntry(entryName));
            try (InputStream is = Files.newInputStream(source)) {
                int len;
                while ((len = is.read(buffer)) > 0) {
                    zos.write(buffer, 0, len);
                }
            }
            zos.closeEntry();
        }
    }

    /**
     * 解壓 ZIP 到指定目錄,保留目錄結構
     * @param zipFilePath  ZIP 文件路徑
     * @param destDir      解壓目標目錄
     * @return 解壓文件數量
     */
    public static int unzipFile(String zipFilePath, String destDir) throws IOException {
        Path dest = Paths.get(destDir);
        Files.createDirectories(dest);

        int count = 0;
        try (FileInputStream fis = new FileInputStream(zipFilePath);
             CheckedInputStream cis = new CheckedInputStream(fis, new Adler32());
             ZipInputStream zis = new ZipInputStream(new BufferedInputStream(cis))) {

            ZipEntry entry;
            byte[] buffer = new byte[8192];
            while ((entry = zis.getNextEntry()) != null) {
                Path target = dest.resolve(entry.getName()).normalize();
                // 安全檢查:防止 Zip Slip 攻擊(路徑遍歷)
                if (!target.startsWith(dest)) {
                    throw new SecurityException("Zip Slip detected: " + entry.getName());
                }

                if (entry.isDirectory()) {
                    Files.createDirectories(target);
                } else {
                    Files.createDirectories(target.getParent());
                    try (OutputStream os = Files.newOutputStream(target)) {
                        int len;
                        while ((len = zis.read(buffer)) > 0) {
                            os.write(buffer, 0, len);
                        }
                    }
                }
                zis.closeEntry();
                count++;
            }
            System.out.println("Checksum: " + cis.getChecksum().getValue());
        }
        return count;
    }

    /**
     * 列出 ZIP 內容
     * @return ZipEntry 列表(含名稱、大小、壓縮大小、時間、CRC32)
     */
    public static List listEntries(String zipFilePath) throws IOException {
        List entries = new ArrayList<>();
        try (ZipFile zf = new ZipFile(zipFilePath)) {
            Enumeration en = zf.entries();
            while (en.hasMoreElements()) {
                entries.add(en.nextElement());
            }
        }
        return entries;
    }
}

關鍵技術點深度解析

1. 壓縮流鏈路設計


FileOutputStream → CheckedOutputStream(Adler32) → BufferedOutputStream → ZipOutputStream
  • CheckedOutputStream:流式計算校驗和,無需二次讀取
  • BufferedOutputStream:減少底層系統調用,提升寫入吞吐
  • ZipOutputStream:核心壓縮邏輯,putNextEntry() 開啟新條目,write() 寫入數據,closeEntry() 結束條目
  • zos.finish() 必須調用:寫入中央目錄結構,否則壓縮包損壞不可讀

2. 目錄遞歸與條目命名

  • 目錄條目名以 / 結尾,標準 ZIP 規範
  • 相對路徑構建:entryName + child.getFileName() 保證跨平台路徑分隔符統一為 /
  • 空目錄也會創建條目,保證解壓後結構一致

3. 解壓安全:防禦 Zip Slip 漏洞

惡意 ZIP 可能包含 ../../etc/passwd 類路徑遍歷條目,解壓時若不校驗會寫入任意路徑。核心防禦:


Path target = dest.resolve(entry.getName()).normalize();
if (!target.startsWith(dest)) {
    throw new SecurityException("Zip Slip detected: " + entry.getName());
}

normalize() 消除 . ..,startsWith(dest) 確保目標在合法目錄樹內。

4. 校驗和驗證完整性

  • 壓縮端:CheckedOutputStream + Adler32 計算壓縮數據校驗和
  • 解壓端:CheckedInputStream + Adler32 計算解壓數據校驗和
  • 雙端值一致 → 傳輸/存儲過程無損壞
  • Adler32 比 CRC32 速度快,適合大文件;關鍵場景可雙算法並行

5. 內存效率與大文件支援

  • 固定 8KB 緩衝區 byte[8192],避免大文件載入內存
  • ZipInputStream 流式解壓,不載入整個壓縮包
  • 超大文件(>4GB)需使用 Zip64 模式(JDK 7+ ZipOutputStream 自動支援)

使用範例


public class ZipDemo {
    public static void main(String[] args) throws IOException {
        List sources = Arrays.asList(
            "data/report.pdf",
            "data/images",
            "config/app.properties"
        );

        // 壓縮
        long checksum = ZipUtils.zipFiles(sources, "backup/project.zip", "Project backup 2024-01-15");
        System.out.println("Compression checksum: " + checksum);

        // 列表
        for (ZipEntry e : ZipUtils.listEntries("backup/project.zip")) {
            System.out.printf("%-40s %10d %10d %s%n",
                e.getName(), e.getSize(), e.getCompressedSize(),
                new java.util.Date(e.getTime()));
        }

        // 解壓
        int count = ZipUtils.unzipFile("backup/project.zip", "restore/");
        System.out.println("Extracted " + count + " entries");
    }
}

進階擴展建議

  1. 密碼保護:標準 JDK 不支援加密 ZIP,需引入 Apache Commons Compress 或 zip4j 實現 AES-256 加密。
  2. 並行壓縮:大量小文件可使用 ForkJoinPool 並行 addToZip,但需注意 ZipOutputStream 非線程安全,需串行寫入或分段壓縮後合併。
  3. 進度回調:傳入 Consumer 回調已處理字節數,支援 UI 進度條。
  4. 內存映射:超大文件可用 MappedByteBuffer 減少用戶態/內核態拷貝。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved