项目中需要用到zip压缩,但是zip4j不满足需求,所以写了一个自定义的zip压缩类。支持功能如下:
- 指定一次性传入多个路径进行压缩,使用绝对路径
- 指定压缩文件存储路径
- 支持自定义压缩文件内部目录,例:压缩文件路径为 /opt/user/depdence/java/doc,zip 包的内部路径为 /depdence/java/doc,多 /depdence/java 两个层级,若不指定则默认压缩父目录。这是为了保留目录结构方便解压后直接覆盖。
- 支持指定 excludes 排除指定目录/文件,支持 *? 通配符
引入包
commons-io: 作通配符的匹配
commons-lang3: 字符串和列表为空的判断,可不用
实现代码如下
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* zip 文件压缩
* @author Chenfei
* @date 2021/12/1
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
public class ZipUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ZipUtil.class);
private static final int BUFFER_SIZE = 2 * 1024;
/**
* 排除文件/文件夹,文件夹以 / 结尾, 使用 ThreadLocal 存储
*/
private final static ThreadLocal<String[]> EXCLUDES_LOCAL = ThreadLocal.withInitial(() -> new String[0]);
public static final String FILE_SEPARATOR = ";";
/**
* 将指定文件或文件夹压缩成ZIP
* @param sourceFile 压缩文件夹
* @param parentSub 父级目录截断,即压缩文件保留指定路径之外的目录结构
* 例 srcDir=D:\opt\files\search\index, parentSub=D:\opt\files\
* 则压缩后,zip包内目录结构则为search/index,若不指定,则为 index
* @param out 压缩文件输出流
* @param excludes 排除文件/文件夹
* @throws RuntimeException 压缩失败抛出异常
*/
public static void compress(File sourceFile, String parentSub, OutputStream out,
String[] excludes) throws IOException {
long start = System.currentTimeMillis();
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(out);
// 处理截断父目录
String parentDir = parentDir(sourceFile, parentSub);
// 预处理排除路径
excludePrepare(excludes);
// 压缩文件
compressFile(sourceFile, zos, parentDir);
long end = System.currentTimeMillis();
LOGGER.info("压缩完成 compress end, cost: " + (end - start));
} catch (IOException e) {
LOGGER.error("zip compress error!", e);
throw new IOException("zip compress error!", e);
} finally {
IOUtils.closeQuietly(zos);
// ThreadLocal 使用后清除,避免内存泄露
EXCLUDES_LOCAL.remove();
}
}
/**
* 将指定的多个文件或文件夹压缩到指定路径ZIP包内,支出排除文件和路径,
* 压缩和排除路径支持 ; 分割多个路径
* @param sourceDir 待压缩文件夹,支持 ; 分割多个路径
* @param parentSub 父级目录截断,即压缩文件保留指定路径之外的目录结构
* 例 srcDir=D:\opt\files\search\index, parentSub=D:\opt\files\
* 则压缩后,zip包内目录结构则为search/index,若不指定,则为 index
* @param outPath 压缩文件指定地址
* @param excludes 排除文件/文件夹,支持 ; 分割多个路径
* @throws IOException 异常抛出
*/
public static void compress(String sourceDir, String parentSub, String outPath,
String excludes) throws IOException {
// 输出流
FileOutputStream outStream = outStream(outPath);
// 处理排除路径,支持以 ; 分割匹配
String[] excludesArray = null;
if (StringUtils.isNotEmpty(excludes)) {
if (excludes.indexOf(FILE_SEPARATOR) > 0) {
excludesArray = excludes.split(FILE_SEPARATOR);
} else {
excludesArray = new String[]{excludes};
}
}
// 处理压缩文件路径,支持以 ; 分割将多个文件压缩至一个压缩包
if (sourceDir.indexOf(FILE_SEPARATOR) > 0) {
String[] sources = sourceDir.split(FILE_SEPARATOR);
for (String src : sources) {
if (StringUtils.isNotBlank(src)) {
File srcFile = new File(src);
compress(srcFile, parentSub, outStream, excludesArray);
}
}
} else {
File sourceFile = new File(sourceDir);
compress(sourceFile, parentSub, outStream, excludesArray);
}
}
/**
* 将指定的多个文件或文件夹压缩到指定路径ZIP包内,支出排除文件和路径
* @param sourceList 待压缩文件夹列表
* @param parentSub 父级目录截断,即压缩文件保留指定路径之外的目录结构
* 例 srcDir=D:\opt\files\search\index, parentSub=D:\opt\files\
* 则压缩后,zip包内目录结构则为search/index,若不指定,则为 index
* @param outPath 压缩文件指定地址
* @param excludeList 排除文件/文件夹
* @throws IOException 异常抛出
*/
public static void compress(List<String> sourceList, String parentSub, String outPath,
List<String> excludeList) throws IOException {
// 输出流
FileOutputStream outStream = outStream(outPath);
// 排除文件处理
String[] excludesArray = null;
if (excludeList != null) {
excludesArray = excludeList.toArray(new String[0]);
}
// 压缩文件处理
for (String src : sourceList) {
if (StringUtils.isNotBlank(src)) {
File srcFile = new File(src);
compress(srcFile, parentSub, outStream, excludesArray);
}
}
}
/**
* 压缩文件父目录创建并创建输出流
* @param outPath 压缩路径
* @return 输出流
* @throws FileNotFoundException 异常
*/
private static FileOutputStream outStream(String outPath) throws FileNotFoundException {
// 输出路径处理
File outFile = new File(outPath);
if (!outFile.getParentFile().exists() && !outFile.getParentFile().mkdirs()) {
LOGGER.error("create dir failed. path: " + outFile.getParentFile().getAbsolutePath());
throw new FileNotFoundException("create dir failed. path: " + outFile.getParentFile().getAbsolutePath());
}
return new FileOutputStream(new File(outPath));
}
/**
* 处理截断父目录,判断截断父目录是否存在,并规范路径格式,以“/”或“\”分隔符结尾
* @param sourceFile 压缩文件
* @param parentSub 需截断目录
* @return 截断后目录结构
*/
private static String parentDir(File sourceFile, String parentSub) {
// 处理截断父目录,判断截断父目录是否存在,并规范路径格式,以“/”或“\”分隔符结尾
String parentDir = "";
if (StringUtils.isNotEmpty(parentSub)) {
String parentPath = sourceFile.getParent() + File.separator;
File parentSubFile = new File(parentSub);
if (parentSubFile.exists() && parentSubFile.isDirectory()) {
String psub = parentSubFile.getAbsolutePath() + File.separator;
if (parentPath.startsWith(psub)) {
parentDir = parentPath.substring(psub.length());
}
}
}
return parentDir;
}
/**
* 对排除列表做预处理,转换路径,并存入 threadLocal
* @param excludes 排除列表
*/
private static void excludePrepare(String[] excludes) {
if (ArrayUtils.isEmpty(excludes) || EXCLUDES_LOCAL.get().length > 0) {
// 排除列表一次压缩只处理一次
return;
}
// 处理排除路径列表,确认路径是否存在,并格式化路径,若是目录则以“/”或“\”分隔符结尾
List<String> excludeList = new ArrayList<>();
for (String exclude : excludes) {
if (StringUtils.isBlank(exclude)) {
continue;
}
if (exclude.indexOf('?') != -1 || exclude.indexOf('*') != -1) {
excludeList.add(FilenameUtils.separatorsToSystem(exclude));
} else {
File exFile = new File(exclude);
if (exFile.exists()) {
// 若是目录则以“/”或“\”分隔符结尾
// String exPath = exFile.getAbsolutePath() + (exFile.isFile() ? "" : File.separator);
String exPath = exFile.getAbsolutePath();
excludeList.add(exPath);
}
}
}
// 排除文件/文件夹,文件夹以 / 结尾, 使用 ThreadLocal 存储,少传一个参数
if (!excludeList.isEmpty()) {
EXCLUDES_LOCAL.set(excludeList.toArray(new String[0]));
}
}
/**
* 递归压缩方法
* @param sourceFile 源文件
* @param zos zip输出流
* @param parentDir 父级目录路径,以 / 结尾
* @throws IOException 压缩文件流异常
*/
private static void compressFile(File sourceFile, ZipOutputStream zos, String parentDir) throws IOException {
// 判断当前路径是否被排除
if (excludePath(sourceFile.getAbsolutePath())) {
// FIXME 如果文件被排除,则创建父目录的节点,否则文件夹内如果没有其他文件,会导致所有空的父目录节点不创建
// 必须判断当前节点是否已创建才可以,但没有这个接口,直接创建如果匹配了多个文件则会导致节点重复报错,不想捕捉异常,不做了
// zos.putNextEntry(new ZipEntry(FilenameUtils.separatorsToUnix(parentDir)));
// zos.closeEntry();
return;
}
byte[] buf = new byte[BUFFER_SIZE];
if (sourceFile.isFile()) {
// 向 zip 输出流中添加一个 zip 实体,构造器中 name 为 zip 实体的文件的路径加名字
// 此处 FilenameUtils.separatorsToUnix 不用也可以,用了是为了风格名统一
zos.putNextEntry(new ZipEntry(FilenameUtils.separatorsToUnix(parentDir) + sourceFile.getName()));
// copy文件到zip输出流中
int len;
FileInputStream in = new FileInputStream(sourceFile);
while ((len = in.read(buf)) != -1) {
zos.write(buf, 0, len);
}
// 压缩文件写入完成
zos.closeEntry();
IOUtils.closeQuietly(in);
} else {
File[] listFiles = sourceFile.listFiles();
if (ArrayUtils.isEmpty(listFiles)) {
// 保留空文件夹,只增加路径,不写入内容
// // 此处 FilenameUtils.separatorsToUnix 不用也可以,但最后一定要以 / 结尾才是文件夹,\ 不行
zos.putNextEntry(new ZipEntry(FilenameUtils.separatorsToUnix(parentDir) + sourceFile.getName() + "/"));
zos.closeEntry();
} else {
// 循环处理子文件和子文件夹
assert listFiles != null;
for (File file : listFiles) {
// 传入父文件夹的目录结构,并在最后加一斜杠,
// 否则最后压缩包中无法保留原始文件结构,所有文件都压缩到包根目录下
compressFile(file, zos, parentDir + sourceFile.getName() + File.separator);
}
}
}
}
/**
* 判断路径是否排除
* @param path 被判断路径
* @return 排除 true,未排除 false
*/
private static boolean excludePath(String path) {
// 排除文件/文件夹,文件夹以 / 结尾, 使用 ThreadLocal 存储,少传一个参数
String[] excludes = EXCLUDES_LOCAL.get();
if (ArrayUtils.isEmpty(excludes)) {
return false;
}
for (String exPath : excludes) {
/* // 全匹配判断
if (exPath.endsWith(File.separator)) {
if (path.startsWith(exPath)) {
return true;
}
} else {
if (path.equals(exPath)) {
return true;
}
}
*/
// 支持通配符 *? 的判断
if (FilenameUtils.wildcardMatch(path, exPath)) {
return true;
}
}
return false;
}
public static void main(String[] args) throws Exception {
/** 测试 */
String outPath = "E:\\zProfiler2\\xunjian\\210315.zip";
ZipUtil.compress("E:\\zProfiler\\xunjian\\210315", "E:\\zProfiler\\xunjian\\210315\\", outPath,
"E:/zProfiler/xunjian/210315/solrhome/**/conf/*.txt");
}
}