1. 概述
本教程,我们将学习如何使用Java实现文件压缩和解压功能。
用到了Java 中的 java.util.zip 包,这里包含了所有与压缩和解压缩相关的工具。
2. 压缩单个文件
首先,我们来看一个简单的操作:把名为test1.txt的文件压缩到名为compressed.zip的存档中。
public class ZipFile {
public static void main(String[] args) throws IOException {
String sourceFile = "test1.txt";
FileOutputStream fos = new FileOutputStream("compressed.zip");
ZipOutputStream zipOut = new ZipOutputStream(fos);
File fileToZip = new File(sourceFile);
FileInputStream fis = new FileInputStream(fileToZip);
ZipEntry zipEntry = new ZipEntry(fileToZip.getName());
zipOut.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while((length = fis.read(bytes)) >= 0) {
zipOut.write(bytes, 0, length);
}
zipOut.close();
fis.close();
fos.close();
}
}
3. 压缩多个文件
接下来,我们将看到如何将多个文件压缩到一个zip文件。我们将把 test1.txt 和 test2.txt 压缩到 multiCompressed.zip:
public class ZipMultipleFiles {
public static void main(String[] args) throws IOException {
String file1 = "src/main/resources/zipTest/test1.txt";
String file2 = "src/main/resources/zipTest/test2.txt";
final List<String> srcFiles = Arrays.asList(file1, file2);
final FileOutputStream fos = new FileOutputStream(Paths.get(file1).getParent().toAbsolutePath() + "/compressed.zip");
ZipOutputStream zipOut = new ZipOutputStream(fos);
for (String srcFile : srcFiles) {
File fileToZip = new File(srcFile);
FileInputStream fis = new FileInputStream(fileToZip);
ZipEntry zipEntry = new ZipEntry(fileToZip.getName());
zipOut.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while((length = fis.read(bytes)) >= 0) {
zipOut.write(bytes, 0, length);
}
fis.close();
}
zipOut.close();
fos.close();
}
}
4. 压缩文件夹
现在,让学习如何将整个目录压缩到zip中。我们将把 zipTest 文件夹压缩到 dirCompressed.zip 文件中:
public class ZipDirectory {
public static void main(String[] args) throws IOException {
String sourceFile = "zipTest";
FileOutputStream fos = new FileOutputStream("dirCompressed.zip");
ZipOutputStream zipOut = new ZipOutputStream(fos);
File fileToZip = new File(sourceFile);
zipFile(fileToZip, fileToZip.getName(), zipOut);
zipOut.close();
fos.close();
}
private static void zipFile(File fileToZip, String fileName, ZipOutputStream zipOut) throws IOException {
if (fileToZip.isHidden()) {
return;
}
if (fileToZip.isDirectory()) {
if (fileName.endsWith("/")) {
zipOut.putNextEntry(new ZipEntry(fileName));
zipOut.closeEntry();
} else {
zipOut.putNextEntry(new ZipEntry(fileName + "/"));
zipOut.closeEntry();
}
File[] children = fileToZip.listFiles();
for (File childFile : children) {
zipFile(childFile, fileName + "/" + childFile.getName(), zipOut);
}
return;
}
FileInputStream fis = new FileInputStream(fileToZip);
ZipEntry zipEntry = new ZipEntry(fileName);
zipOut.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while ((length = fis.read(bytes)) >= 0) {
zipOut.write(bytes, 0, length);
}
fis.close();
}
}
注意:
- 对于子目录,我们递归地遍历它们。
- 每次找到一个目录时,我们都会将其名称附加到子ZipEntry名称后,以保存层次结构。
- 我们也为每个空目录创建一个目录。
5. 追加新文件到Zip
接下来,我们将向现有的Zip压缩包中添加新文件。例如,我们将把 file3.txt 添加到 compressed.zip 中:
String file3 = "src/main/resources/zipTest/file3.txt";
Map<String, String> env = new HashMap<>();
env.put("create", "true");
Path path = Paths.get(Paths.get(file3).getParent() + "/compressed.zip");
URI uri = URI.create("jar:" + path.toUri());
try (FileSystem fs = FileSystems.newFileSystem(uri, env)) {
Path nf = fs.getPath("newFile3.txt");
Files.write(nf, Files.readAllBytes(Paths.get(file3)), StandardOpenOption.CREATE);
}
简而言之,我们使用 FileSystems 类提供的 .newFileSystem() 方法挂载Zip文件,自JDK 1.7以来就可用。然后,我们在压缩文件夹中创建了一个新的 newFile3.txt,并将 file3.txt 的所有内容添加进来。
6. 解压文件
现在,我们将实现解压并提取其内容。
在这个例子中,我们将把 compressed.zip 解压到名为 unzipTest 的新文件夹中:
public class UnzipFile {
public static void main(String[] args) throws IOException {
String fileZip = "src/main/resources/unzipTest/compressed.zip";
File destDir = new File("src/main/resources/unzipTest");
byte[] buffer = new byte[1024];
ZipInputStream zis = new ZipInputStream(new FileInputStream(fileZip));
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
// ...
}
zis.closeEntry();
zis.close();
}
}
在 while 循环中,我们逐个遍历每个ZipEntry,首先检查它是否是目录。如果是目录,我们就使用mkdirs()方法创建目录;否则,我们将继续创建文件:
while (zipEntry != null) {
File newFile = newFile(destDir, zipEntry);
if (zipEntry.isDirectory()) {
if (!newFile.isDirectory() && !newFile.mkdirs()) {
throw new IOException("Failed to create directory " + newFile);
}
} else {
// fix for Windows-created archives
File parent = newFile.getParentFile();
if (!parent.isDirectory() && !parent.mkdirs()) {
throw new IOException("Failed to create directory " + parent);
}
// write file content
FileOutputStream fos = new FileOutputStream(newFile);
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
fos.close();
}
zipEntry = zis.getNextEntry();
}
这里需要注意的是,在 else 分支上,我们还检查文件的父目录是否存在。对于在Windows上创建的压缩包,这是必要的,因为根目录在zip文件中没有相应的条目。
另一个关键点在newFile()方法中:
public static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException {
File destFile = new File(destinationDir, zipEntry.getName());
String destDirPath = destinationDir.getCanonicalPath();
String destFilePath = destFile.getCanonicalPath();
if (!destFilePath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
}
return destFile;
}
newFile() 方法防止将文件写入目标文件夹之外的文件系统。这种漏洞被称为Zip Slip漏洞。
7. 总结
在这篇文章中,我们展示了如何使用Java进行文件的压缩和解压缩。
这些示例的实现可以在GitHub上找到。