Java多线程压缩文件——优雅的自动分类至文件夹

在日常开发中,我们经常遇到文件归档压缩的场景。如果是大量文件、或者需要按照分类目录打包压缩,一般串行压缩不仅耗时长,而且容易阻塞主线程。

今天我们就通过 Apache Commons Compress + ParallelScatterZipCreator 实现一个 支持多线程压缩且自动按文件夹分类 的工具方法,性能可靠,可用于生产环境大文件归档。

📦 场景需求

  • 按照不同的业务分类(如 资料A资料B)分别存入不同文件夹下;

  • 支持压缩大文件(可超过 4GB);

  • 多线程并发压缩,减少总耗时;

  • 支持部分文件不存在时自动跳过;

  • 稳定运行,避免内存溢出等问题。

🔧 核心依赖

我们采用的是 Apache Commons Compress 中的 ParallelScatterZipCreator

<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-compress</artifactId>
        <version>1.26.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.12.0</version>
    </dependency>
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.11.0</version>
    </dependency>
</dependencies>

🔍 核心代码实现

以下是完整的 compressByFolder 工具方法,核心功能包括:

  • 支持分类目录(folderName);

  • 使用线程池提高并发压缩效率;

  • 利用 ParallelScatterZipCreator 避免主线程阻塞;

  • 自动跳过不存在的文件,保证健壮性。

/**
 * 多线程压缩文件,按文件夹分类保存
 *
 * @param folderFileMap 分类结构:key 为文件夹名,value 为文件列表
 * @param zipFilePath 压缩输出路径
 */
public static void compressByFolder(Map<String, List<Map<String, Object>>> folderFileMap, String zipFilePath)
        throws IOException, ExecutionException, InterruptedException {

    long start = System.currentTimeMillis();

    // 1. 检查压缩目标目录
    File zipFile = new File(zipFilePath);
    File parentDir = zipFile.getParentFile();
    if (!parentDir.exists() && !parentDir.mkdirs()) {
        throw new IOException("无法创建目标目录: " + parentDir.getAbsolutePath());
    }

    // 2. 初始化线程池
    ExecutorService executor = new ThreadPoolExecutor(
            5, 20, 60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),
            Executors.defaultThreadFactory()
    );

    try (OutputStream outputStream = Files.newOutputStream(Paths.get(zipFilePath));
         ZipArchiveOutputStream zipArchiveOutputStream = new ZipArchiveOutputStream(outputStream)) {

        zipArchiveOutputStream.setEncoding("UTF-8");
        zipArchiveOutputStream.setUseZip64(Zip64Mode.AsNeeded); // 支持大文件
        zipArchiveOutputStream.setLevel(Deflater.DEFAULT_COMPRESSION);

        // 3. 创建并发压缩器
        ParallelScatterZipCreator parallelScatterZipCreator = new ParallelScatterZipCreator(executor);

        for (Map.Entry<String, List<Map<String, Object>>> entry : folderFileMap.entrySet()) {
            String folderName = entry.getKey();
            List<Map<String, Object>> fileMapList = entry.getValue();

            for (Map<String, Object> fileMap : fileMapList) {
                try {
                    String fileName = Optional.ofNullable(fileMap.get("fileName"))
                            .filter(f -> !StringUtils.isBlank(f.toString()))
                            .map(Object::toString)
                            .orElse("");

                    String filePath = (String) fileMap.get("filePath");
                    File file = new File(filePath);

                    if (StringUtils.isBlank(fileName)) {
                        fileName = file.getName();
                    }

                    if (!file.exists() || !file.isFile()) {
                        System.err.println("文件不存在或非法文件: " + filePath);
                        continue;
                    }

                    // 输入流供应器,懒加载
                    final InputStreamSupplier inputStreamSupplier = () -> {
                        try {
                            return new BufferedInputStream(new FileInputStream(file));
                        } catch (Exception e) {
                            e.printStackTrace();
                            return new NullInputStream(0); // 空输入流,保证任务不中断
                        }
                    };

                    // 拼接相对路径(/ 兼容)
                    String fullPath = StringUtils.isBlank(folderName)
                            ? fileName
                            : folderName + "/" + fileName;

                    ZipArchiveEntry zipArchiveEntry = new ZipArchiveEntry(fullPath.replace("\\", "/"));
                    zipArchiveEntry.setMethod(ZipArchiveEntry.DEFLATED);
                    zipArchiveEntry.setSize(file.length());
                    zipArchiveEntry.setUnixMode(UnixStat.FILE_FLAG | 436);

                    // 添加到并发压缩任务中
                    parallelScatterZipCreator.addArchiveEntry(zipArchiveEntry, inputStreamSupplier);

                } catch (Exception e) {
                    System.err.println("压缩文件出错:" + e.getMessage());
                    throw e; // 可选择记录日志后不抛出
                }
            }
        }

        // 写入 zip 输出流
        parallelScatterZipCreator.writeTo(zipArchiveOutputStream);

    } finally {
        executor.shutdown();
        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
            executor.shutdownNow();
        }
    }

    long end = System.currentTimeMillis();
    System.out.println("压缩完成,耗时:" + (end - start) + " ms");
}

🧪 示例调用

public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
    Map<String, List<Map<String, Object>>> folderFileMap = new HashMap<>();
    List<Map<String, Object>> fileList = new ArrayList<>();
    Map<String, Object> fileMap = new HashMap<>();
    fileMap.put("fileName", "showdoc.doc");
    fileMap.put("filePath", "D:\\Users\\lomo\\Downloads\\showdoc.doc");
    fileList.add(fileMap);
    folderFileMap.put("test1", fileList);
    fileList = new ArrayList<>();
    fileMap = new HashMap<>();
    fileMap.put("fileName", "showdoc.doc");
    fileMap.put("filePath", "D:\\Users\\lomo\\Downloads\\showdoc.doc");
    fileList.add(fileMap);
    folderFileMap.put("test2", fileList);
    String zipFilePath = "D:/test.zip";
    compressByFolder(folderFileMap, zipFilePath);
}

📌 打包效果样例

 

 

posted @ 2025-06-26 10:36  ~落辰~  阅读(63)  评论(0)    收藏  举报