CodeGym /课程 /JAVA 25 SELF /归档/压缩:java.util.zip

归档/压缩:java.util.zip

JAVA 25 SELF
第 41 级 , 课程 4
可用

1. 引言:为什么在 Java 中需要归档与压缩

在当今世界,处理归档和压缩File是常见任务:备份、File交换、日志记录、存储海量数据。Java 通过包 java.util.zip 提供了处理 ZIP 归档和 GZIP 压缩File的标准工具。

Java 能做什么:

  • 读取与创建 ZIP 归档(多File容器)。
  • 读取与创建 GZIP File(单File压缩)。
  • 管理归档内容,按通配符过滤File。
  • 控制压缩级别。
  • 在解压时进行安全检查(防范 zip slipzip bomb)。

2. 主要类

ZipInputStream 和 ZipOutputStream

这是用于顺序读取/写入 ZIP 归档的流式类。何时使用:当需要“即时”读/写归档,而不需要对单个File进行随机访问时。

示例:读取归档

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

try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {
        System.out.println("File: " + entry.getName());
        // 可以通过 zis.read(...) 读取该条目的内容
        zis.closeEntry();
    }
}

示例:创建归档

import java.io.FileOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
    ZipEntry entry = new ZipEntry("hello.txt");
    zos.putNextEntry(entry);
    zos.write("Hello, archive!".getBytes());
    zos.closeEntry();
}

ZipFile

用于随机访问 ZIP 归档内容的类:可以快速获取File列表,按名称打开任意File并读取其内容。

示例:

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

ZipFile zipFile = new ZipFile("archive.zip");
zipFile.stream().forEach(entry -> System.out.println(entry.getName()));

ZipEntry entry = zipFile.getEntry("hello.txt");
try (InputStream is = zipFile.getInputStream(entry)) {
    // 读取File内容
}
zipFile.close();

何时使用 ZipFile

  • 当需要快速获取File列表、元数据、大小、日期时。
  • 当需要在不顺序遍历整个归档的情况下读取单个File时。

ZipEntry

表示归档中单个File或File夹的对象。包含名称、大小、日期、标志、压缩级别等。

import java.util.zip.ZipEntry;

ZipEntry entry = new ZipEntry("docs/readme.txt");
entry.setComment("File说明");
entry.setTime(System.currentTimeMillis());

压缩级别(Deflater)

在创建归档时可以控制压缩程度(从 0 — 不压缩,到 9 — 最大压缩):

import java.io.FileOutputStream;
import java.util.zip.Deflater;
import java.util.zip.ZipOutputStream;

try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
    zos.setLevel(Deflater.BEST_COMPRESSION); // 或者 0..9
    // ...
}
  • Deflater.NO_COMPRESSION (0)
  • Deflater.BEST_SPEED (1)
  • Deflater.BEST_COMPRESSION (9)
  • Deflater.DEFAULT_COMPRESSION (-1)

规则:级别越高——速度越慢,但压缩得越小。

3. 压缩单个File

GZIP 是用于压缩单个File(不是归档!)的格式,常用于日志、临时File、网络传输。

示例:压缩File

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

try (GZIPOutputStream gos = new GZIPOutputStream(new FileOutputStream("file.txt.gz"));
     FileInputStream fis = new FileInputStream("file.txt")) {
    fis.transferTo(gos);
}

示例:解压File

import java.util.zip.GZIPInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

try (GZIPInputStream gis = new GZIPInputStream(new FileInputStream("file.txt.gz"));
     FileOutputStream fos = new FileOutputStream("file.txt")) {
    gis.transferTo(fos);
}

请记住:GZIP 只处理单个File——它不保存File夹结构及其他元数据。与之不同,ZIP 可以一次性打包多个File和整个File夹,并保留其结构以及每个元素的信息。

4. 目录的打包/解包,基于 PathMatcher 的过滤

将目录打包为 ZIP

要打包包含File和子目录的File夹,可递归遍历File树,并以正确的相对路径将每个File添加到归档中(ZIP 中的分隔符始终是 "/")。

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

Path sourceDir = Paths.get("myfolder");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {
    Files.walk(sourceDir)
        .filter(Files::isRegularFile)
        .forEach(path -> {
            String entryName = sourceDir.relativize(path).toString().replace("\\", "/");
            try (InputStream is = Files.newInputStream(path)) {
                zos.putNextEntry(new ZipEntry(entryName));
                is.transferTo(zos);
                zos.closeEntry();
            } catch (IOException e) { e.printStackTrace(); }
        });
}

将归档解压到目录

import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {
        Path outPath = Paths.get("output", entry.getName());
        if (entry.isDirectory()) {
            Files.createDirectories(outPath);
        } else {
            Files.createDirectories(outPath.getParent());
            try (OutputStream os = Files.newOutputStream(outPath)) {
                zis.transferTo(os);
            }
        }
        zis.closeEntry();
    }
}

按通配符过滤File(PathMatcher)

可以按通配符过滤要打包/解包的File,例如只处理 "*.txt"

import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.PathMatcher;

PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*.txt");
Files.walk(sourceDir)
    .filter(matcher::matches)
    .forEach(/* ... */);

5. 安全性:Zip Slip、zip bomb、路径规范化检查

Zip Slip(路径穿越攻击)

问题:攻击者可以创建一个名称为 "../../../../etc/passwd" 的条目。若在解压时不做检查,此类File可能会覆盖系统File!

解决方案:在写出File前先规范化路径,并确保它不会越出目标目录。

import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Path targetDir = Paths.get("output").toAbsolutePath();
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {
        Path outPath = targetDir.resolve(entry.getName()).normalize();
        if (!outPath.startsWith(targetDir)) {
            throw new IOException("Zip Slip: 尝试写入目标File夹之外!");
        }
        if (entry.isDirectory()) {
            Files.createDirectories(outPath);
        } else {
            Files.createDirectories(outPath.getParent());
            try (OutputStream os = Files.newOutputStream(outPath)) {
                zis.transferTo(os);
            }
        }
        zis.closeEntry();
    }
}

Zip bomb(压缩炸弹)

问题:归档可能包含某个条目,解压后占用数 GB,但归档本身只有几 KB。这可能“拖垮”服务器或磁盘。

解决方案:限制每个解压File的最大大小以及解压总量,超过阈值立即中断。

import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

long maxSize = 100 * 1024 * 1024; // 100 MB
long totalUnzipped = 0;

try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {
        Path outPath = Paths.get("output", entry.getName()).normalize();
        Files.createDirectories(outPath.getParent());

        long written = 0;
        try (OutputStream os = Files.newOutputStream(outPath)) {
            byte[] buf = new byte[8192];
            int len;
            while ((len = zis.read(buf)) > 0) {
                os.write(buf, 0, len);
                written += len;
                totalUnzipped += len;
                if (written > maxSize || totalUnzipped > maxSize) {
                    throw new IOException("Zip bomb detected!");
                }
            }
        }
        zis.closeEntry();
    }
}

6. 实战:带通配符的 “zip/unzip” CLI 工具

我们编写一个简单的命令行工具,用于按通配符打包和解包File。

示例:打包

// java ZipUtil zip myfolder archive.zip *.txt
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public static void zip(String sourceDir, String zipFile, String glob) throws IOException {
    Path src = Paths.get(sourceDir);
    PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + glob);
    try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
        Files.walk(src)
            .filter(Files::isRegularFile)
            .filter(matcher::matches)
            .forEach(path -> {
                String entryName = src.relativize(path).toString().replace("\\", "/");
                try (InputStream is = Files.newInputStream(path)) {
                    zos.putNextEntry(new ZipEntry(entryName));
                    is.transferTo(zos);
                    zos.closeEntry();
                } catch (IOException e) { e.printStackTrace(); }
            });
    }
}

示例:带 Zip Slip 防护的解包

// java ZipUtil unzip archive.zip output
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public static void unzip(String zipFile, String outDir) throws IOException {
    Path targetDir = Paths.get(outDir).toAbsolutePath();
    try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
        ZipEntry entry;
        while ((entry = zis.getNextEntry()) != null) {
            Path outPath = targetDir.resolve(entry.getName()).normalize();
            if (!outPath.startsWith(targetDir)) {
                throw new IOException("Zip Slip: 尝试写入目标File夹之外!");
            }
            if (entry.isDirectory()) {
                Files.createDirectories(outPath);
            } else {
                Files.createDirectories(outPath.getParent());
                try (OutputStream os = Files.newOutputStream(outPath)) {
                    zis.transferTo(os);
                }
            }
            zis.closeEntry();
        }
    }
}

运行示例:

java ZipUtil zip myfolder archive.zip "*.txt"
java ZipUtil unzip archive.zip output

在第一个示例中,命令 java ZipUtil zip myfolder archive.zip "*.txt" 会将File夹 myfolder 中的所有 .txt File打包到归档 archive.zip。第二个示例中,java ZipUtil unzip archive.zip output 会把归档解压到File夹 output,同时会检查是否有任何File被写到目标目录之外——这正是对 Zip Slip 的防护。

1
调查/小测验
IO 优化第 41 级,课程 4
不可用
IO 优化
IO 优化
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION