目录
前言
本章主要是对 ZipInputStream 和 ZipOutputStream 的使用,提供了对文件/文件夹的压缩/解压方法。
一、ZipUtil
Zip工具类,提供压缩/解压方法。主要有以下特色:
- 输入输出流使用缓冲流装饰。
- 使用NIO的Files类操作文件和目录。
- 使用Java8函数式接口(Predicate、Consumer)。
- 少用或不用else,使用return;或continue;终止当前代码。
- 更倾向方法而不是注释来解释代码。
提示:在使用zip()方法压缩文件时,似乎只能用unzip()方法解压文件(使用360Zip解压失败)。
package com.rocketmq.test;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
/** 压缩/解压工具类
* @Create: 2024/8/4
* @Description:
* @FileName: ZipUtil
*/
public class ZipUtil {
/**
* 缓冲区大小
*/
private static final int BUFFER_SIZE = 256;
/**
* 文件末尾
*/
private static final int EOF = -1;
/**
* 空字符串
*/
private static final String EMPTY = "";
/**
* 断言字符串为空
*/
private static final Predicate<String> isEmpty = str -> Objects.equals(str, EMPTY);
/**
* 压缩文件后缀
*/
private static final String SUFFIX = ".zip";
private ZipUtil() {
}
/**
* 压缩文件/目录
*
* @param src 源目录/源文件
* @param tar 目标目录
* @Description:
* @date: 2024/8/4
* @return: void
*/
public static void zip(String src, String tar) throws IOException {
tar = Files.isDirectory(Paths.get(src)) ?
getZipNameOfDir(src, tar) : getZipNameOfFile(src, tar);
try (ZipOutputStream zipOut = new ZipOutputStream(
new BufferedOutputStream(Files.newOutputStream(Paths.get(tar))))) {
zipOut.setComment("Zip done by Java");
Path start = Paths.get(src);
Files.walk(start) // 遍历文件/文件夹
.skip(Files.isDirectory(start) ? 1L : 0L) // 如果是文件夹,跳过根目录,因为文件已包含
.forEach(path -> writeToZip(zipOut, path)); // 将文件写入Zip流
}
}
/**
* 获取文件压缩包名
*
* @param src 源文件
* @param tar 目标目录
* @Description:
* @date: 2024/8/4
* @return: java.lang.String
*/
private static String getZipNameOfFile(String src, String tar) {
// temp.txt --> temp.zip
return tar + src.substring(0, src.lastIndexOf(".")) + SUFFIX;
}
/**
* 获取目录压缩包名
*
* @param src 源目录
* @param tar 目标目录
* @Description:
* @date: 2024/8/4
* @return: java.lang.String
*/
private static String getZipNameOfDir(String src, String tar) {
// temp --> temp.zip
return tar + (isEmpty.test(tar) ? EMPTY : File.separator) + src + SUFFIX;
}
/**
* 将文件写入zip流
*
* @param zipOut zip输出流
* @param path 文件路径
* @Description:
* @date: 2024/8/4
* @return: void
*/
private static void writeToZip(ZipOutputStream zipOut, Path path) {
// 写入目录
if (Files.isDirectory(path)) {
try {
zipOut.putNextEntry(new ZipEntry(path.toString() + File.separator));
zipOut.closeEntry();
} catch (IOException e) {
e.printStackTrace();
}
return;
}
// 写入文件
try (BufferedInputStream input = new BufferedInputStream(Files.newInputStream(path))) {
zipOut.putNextEntry(new ZipEntry(path.toString())); // 添加进入点
// 输出流数据
int len;
final byte[] buff = new byte[BUFFER_SIZE];
while (!Objects.equals(len = input.read(buff), EOF)) {
zipOut.write(buff, 0, len);
}
zipOut.closeEntry();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 解压文件/目录
*
* @param zipFilePath 压缩文件路径
* @param tar 保存目录
* @Description:
* @date: 2024/8/4
* @return: boolean
*/
public static boolean unzip(String zipFilePath, String tar) throws IOException {
// 如果存在tar,添加文件分隔符
if (!isEmpty.test(tar)) tar += File.separator;
// 创建压缩输入流
try (ZipInputStream zipIn = new ZipInputStream(
new BufferedInputStream(Files.newInputStream(Paths.get(zipFilePath))))) {
ZipFile zipFile = new ZipFile(zipFilePath);
ZipEntry entry;
// 遍历压缩包文件
while (Objects.nonNull(entry = zipIn.getNextEntry())) {
// 如果是目录,创建目录
if (isDirectory(entry)) {
Files.createDirectories(Paths.get(tar + entry.getName()));
continue;
}
// 读取文件
createParentIfNotExists(tar, entry);
readFromZip(tar, zipFile, entry);
}
}
return true;
}
/** 判断是否为目录
* @Description:
* @date: 2024/8/4
* @param entry 进入点
* @return: boolean
*/
private static boolean isDirectory(ZipEntry entry) {
return entry.getName().endsWith(File.separator);
}
/**
* 如果父目录不存在,创建父目录
*
* @param tar 目标目录
* @param entry Zip流进入点
* @Description:
* @date: 2024/8/4
* @return: void
*/
private static void createParentIfNotExists(String tar, ZipEntry entry) throws IOException {
Path parent = Paths.get(entry.getName()).getParent(); // 获取当前父目录
parent = Paths.get(tar + (Objects.isNull(parent) ? EMPTY : parent)); // 获取完整父目录
Files.createDirectories(parent); // 创建目录
}
/**
* 从zip流读文件
*
* @param tar 目标目录
* @param zipFile 压缩文件
* @param entry 进入点
* @Description:
* @date: 2024/8/4
* @return: void
*/
private static void readFromZip(String tar, ZipFile zipFile, ZipEntry entry) throws IOException {
tar += entry.getName(); // 保存文件路径
try (BufferedInputStream input = new BufferedInputStream(zipFile.getInputStream(entry));
BufferedOutputStream output = new BufferedOutputStream(Files.newOutputStream(Paths.get(tar)))) {
int len;
final byte[] buff = new byte[BUFFER_SIZE];
while (!Objects.equals(len = input.read(buff), EOF)) {
output.write(buff, 0, len);
}
}
}
/**
* 断言 <code>condition == true</code>,
* 如果<code>condition == false</code>, 抛出异常
*
* @param cls 异常类
* @param condition 断言条件
* @param message <code>condition == false</code>时的异常信息
* @Description:
* @date: 2024/8/4
* @return: void
*/
private static void require(Class<? extends Throwable> cls, boolean condition, String message) {
try {
if (!condition)
throw cls.getConstructor(String.class).newInstance(message);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
}
二、测试
package com.rocketmq.test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.Objects;
import java.util.function.Consumer;
/**
* @Create: 2024/7/31
* @Description:
* @FileName: Main
*/
public class Main {
public static void main(String[] args) throws IOException {
Consumer<String> print = text ->
System.out.println(LocalDateTime.now() + ": " + text);
print.accept("项目根目录:" + System.getProperty("user.dir") + "\n\n");
Path path = Paths.get("temp");
if (Files.exists(path)) deleteDir(path);
print.accept("创建临时文件夹...");
createTempDir(path, 5, 2);
print.accept("创建完成. [" + path + "]\n");
print.accept("开始压缩文件...");
ZipUtil.zip(path.toString(), "");
print.accept("压缩完成. [" + path + ".zip" + "]\n");
print.accept("开始解压文件...");
ZipUtil.unzip(path.toString() + ".zip", "dir");
print.accept("解压完成. [dir]\n");
print.accept("压缩解压后文件大小是否相等:" +
Objects.equals(Files.size(path), Files.size(Paths.get("dir" + File.separator + path))));
}
/**
* 删除文件夹及文件
*
* @param path 文件夹路径
* @Description:
* @date: 2024/8/4
* @return: void
*/
private static void deleteDir(Path path) throws IOException {
Consumer<Path> consumer = p -> {
try {
Files.deleteIfExists(p);
} catch (IOException e) {
e.printStackTrace();
}
};
Files.walk(path) // 遍历文件夹
.sorted(Comparator.reverseOrder()) // 逆序排序(文件在前,文件夹在后)
.forEach(consumer); // 删除文件/文件夹
}
/**
* @param root
* @param dirNum
* @param fileNum
* @Description:
* @date: 2024/8/4
* @return: void
*/
private static void createTempDir(Path root, int dirNum, int fileNum) throws IOException {
Files.createDirectories(root);
for (int i = 0; i < dirNum || i < fileNum; i++) {
String name = randomName(root.toString());
if (Math.random() > 0.4) createTempDir(Paths.get(name), dirNum - 1, fileNum - 1);
else createTempFile(name + ".txt");
}
}
private static void createTempFile(String filename) throws IOException {
Path path = Paths.get(filename);
Files.deleteIfExists(path);
Files.write(path, "Hello World".getBytes(),
StandardOpenOption.WRITE, StandardOpenOption.CREATE);
}
private static String randomName(String prefix) {
return prefix + File.separator + System.currentTimeMillis();
}
}
总结
通过对Zip流的学习和代码编写,了解到Zip流最重要的一点是 putNextEntry 方法,这个方法定位了文件/文件夹在Zip流中的位置。
在编写也有一些问题,如 ZipEntry 的 isDirectory() 方法以 "/" 来判断是否为目录,而不是File.seperator。