Java實現解壓縮文件和文件夾

一 前言

項目開發中,總會遇到解壓縮文件的時候。比如,用戶下載多個文件時,服務端可以將多個文件壓縮成一個文件(例如xx.zip或xx.rar)。用戶上傳資料時,允許上傳壓縮文件,服務端進行解壓讀取每一個文件。

基於通用性,以下介紹幾種解壓縮文件的方式,包裝成工具類,供平時開發使用。

二 壓縮文件

壓縮文件,顧名思義,即把一個或多個文件壓縮成一個文件。壓縮也有2種形式,一種是將所有文件壓縮到同一目錄下,此種方式要注意文件重名覆蓋的問題。另一種是按原有文件樹結構進行壓縮,即壓縮後的文件樹結構保持不變。

壓縮文件操作,會使用到一個類,即ZipOutputStream。

2.1 壓縮多個文件

此方法將所有文件壓縮到同一個目錄下。方法傳入多個文件列表,和一個最終壓縮到的文件路徑名。

    /**
     * 壓縮多個文件,壓縮後的所有文件在同一目錄下
     * 
     * @param zipFileName 壓縮後的文件名
     * @param files 需要壓縮的文件列表
     * @throws IOException IO異常
     */
    public static void zipMultipleFiles(String zipFileName, File... files) throws IOException {
        ZipOutputStream zipOutputStream = null;
        try {
            // 輸出流
            zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFileName));
            // 遍歷每一個文件,進行輸出
            for (File file : files) {
                zipOutputStream.putNextEntry(new ZipEntry(file.getName()));
                FileInputStream fileInputStream = new FileInputStream(file);
                int readLen;
                byte[] buffer = new byte[1024];
                while ((readLen = fileInputStream.read(buffer)) != -1) {
                    zipOutputStream.write(buffer, 0, readLen);
                }
                // 關閉流
                fileInputStream.close();
                zipOutputStream.closeEntry();
            }
        } finally {
            if (null != zipOutputStream) {
                try {
                    zipOutputStream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

測試,將D盤下的infp.txt和infp1.txt文件壓縮到D盤下,壓縮文件名為my.zip。

    public static void main(String[] args) throws Exception {
        zipMultipleFiles("D:/my.zip", new File("D:/infp.txt"), new File("D:/infp1.txt"));
    }

2.2 壓縮文件或文件樹

此方法將文件夾下的所有文件按原有的樹形結構壓縮到文件中,也支持壓縮單個文件。原理也簡單,無非就是遞歸遍歷文件樹中的每一個文件,進行壓縮。有個注意的點每一個文件的寫入路徑是基於壓縮文件位置的相對路徑。

package com.nobody.zip;

import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

public class ZipUtils {

    /**
     * 壓縮文件或文件夾(包括所有子目錄文件)
     *
     * @param sourceFile 源文件
     * @param format 格式(zip或rar)
     * @throws IOException 異常信息
     */
    public static void zipFileTree(File sourceFile, String format) throws IOException {
        ZipOutputStream zipOutputStream = null;
        try {
            String zipFileName;
            if (sourceFile.isDirectory()) { // 目錄
                zipFileName = sourceFile.getParent() + File.separator + sourceFile.getName() + "."
                        + format;
            } else { // 單個文件
                zipFileName = sourceFile.getParent()
                        + sourceFile.getName().substring(0, sourceFile.getName().lastIndexOf("."))
                        + "." + format;
            }
            // 壓縮輸出流
            zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFileName));
            zip(sourceFile, zipOutputStream, "");
        } finally {
            if (null != zipOutputStream) {
                // 關閉流
                try {
                    zipOutputStream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    /**
     * 遞歸壓縮文件
     * 
     * @param file 當前文件
     * @param zipOutputStream 壓縮輸出流
     * @param relativePath 相對路徑
     * @throws IOException IO異常
     */
    private static void zip(File file, ZipOutputStream zipOutputStream, String relativePath)
            throws IOException {

        FileInputStream fileInputStream = null;
        try {
            if (file.isDirectory()) { // 當前為文件夾
                // 當前文件夾下的所有文件
                File[] list = file.listFiles();
                if (null != list) {
                    // 計算當前的相對路徑
                    relativePath += (relativePath.length() == 0 ? "" : "/") + file.getName();
                    // 遞歸壓縮每個文件
                    for (File f : list) {
                        zip(f, zipOutputStream, relativePath);
                    }
                }
            } else { // 壓縮文件
                // 計算文件的相對路徑
                relativePath += (relativePath.length() == 0 ? "" : "/") + file.getName();
                // 寫入單個文件
                zipOutputStream.putNextEntry(new ZipEntry(relativePath));
                fileInputStream = new FileInputStream(file);
                int readLen;
                byte[] buffer = new byte[1024];
                while ((readLen = fileInputStream.read(buffer)) != -1) {
                    zipOutputStream.write(buffer, 0, readLen);
                }
                zipOutputStream.closeEntry();
            }
        } finally {
            // 關閉流
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        String path = "D:/test";
        String format = "zip";
        zipFileTree(new File(path), format);
    }
}

上例將test目錄下的所有文件壓縮到同一目錄下的test.zip文件中。

2.3 藉助文件訪問器壓縮

還有一種更簡單的方式,我們不自己寫遞歸遍歷。藉助Java原生類,SimpleFileVisitor,它提供了幾個訪問文件的方法,其中有個方法visitFile,對於文件樹中的每一個文件(文件夾除外),都會調用這個方法。我們只要寫一個類繼承SimpleFileVisitor,然後重寫visitFile方法,實現將每一個文件寫入到壓縮文件中即可。

當然,除了visitFile方法,它裏面還有preVisitDirectory,postVisitDirectory,visitFileFailed等方法,通過方法名大家也猜出什麼意思了。

package com.nobody.zip;

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

/**
 * @Description
 * @Author Mr.nobody
 * @Date 2021/3/8
 * @Version 1.0.0
 */
public class ZipFileTree extends SimpleFileVisitor<Path> {

    // zip輸出流
    private ZipOutputStream zipOutputStream;
    // 源目錄
    private Path sourcePath;

    public ZipFileTree() {}

    /**
     * 壓縮目錄以及所有子目錄文件
     *
     * @param sourceDir 源目錄
     */
    public void zipFile(String sourceDir) throws IOException {
        try {
            // 壓縮後的文件和源目錄在同一目錄下
            String zipFileName = sourceDir + ".zip";
            this.zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFileName));
            this.sourcePath = Paths.get(sourceDir);

            // 開始遍歷文件樹
            Files.walkFileTree(sourcePath, this);
        } finally {
            // 關閉流
            if (null != zipOutputStream) {
                zipOutputStream.close();
            }
        }
    }

    // 遍歷到的每一個文件都會執行此方法
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
        // 取相對路徑
        Path targetFile = sourcePath.relativize(file);
        // 寫入單個文件
        zipOutputStream.putNextEntry(new ZipEntry(targetFile.toString()));
        byte[] bytes = Files.readAllBytes(file);
        zipOutputStream.write(bytes, 0, bytes.length);
        zipOutputStream.closeEntry();
        // 繼續遍歷
        return FileVisitResult.CONTINUE;
    }

    // 遍歷每一個目錄時都會調用的方法
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
            throws IOException {
        return super.preVisitDirectory(dir, attrs);
    }

    // 遍歷完一個目錄下的所有文件後,再調用這個目錄的方法
    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        return super.postVisitDirectory(dir, exc);
    }

    // 遍歷文件失敗後調用的方法
    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
        return super.visitFileFailed(file, exc);
    }

    public static void main(String[] args) throws IOException {
        // 需要壓縮源目錄
        String sourceDir = "D:/test";
        // 壓縮
        new ZipFileTree().zipFile(sourceDir);
    }
}

三 解壓文件

解壓壓縮包,藉助ZipInputStream類,可以讀取到壓縮包中的每一個文件,然後根據讀取到的文件屬性,寫入到相應路徑下即可。對於解壓壓縮包中是文件樹的結構,每讀取到一個文件後,如果是多層路徑下的文件,需要先創建父目錄,再寫入文件流。

package com.nobody.zip;

import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * @Description 解壓縮文件工具類
 * @Author Mr.nobody
 * @Date 2021/3/8
 * @Version 1.0.0
 */
public class ZipUtils {

    /**
     * 解壓
     * 
     * @param zipFilePath 帶解壓文件
     * @param desDirectory 解壓到的目錄
     * @throws Exception
     */
    public static void unzip(String zipFilePath, String desDirectory) throws Exception {

        File desDir = new File(desDirectory);
        if (!desDir.exists()) {
            boolean mkdirSuccess = desDir.mkdir();
            if (!mkdirSuccess) {
                throw new Exception("創建解壓目標文件夾失敗");
            }
        }
        // 讀入流
        ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(zipFilePath));
        // 遍歷每一個文件
        ZipEntry zipEntry = zipInputStream.getNextEntry();
        while (zipEntry != null) {
            if (zipEntry.isDirectory()) { // 文件夾
                String unzipFilePath = desDirectory + File.separator + zipEntry.getName();
                // 直接創建
                mkdir(new File(unzipFilePath));
            } else { // 文件
                String unzipFilePath = desDirectory + File.separator + zipEntry.getName();
                File file = new File(unzipFilePath);
                // 創建父目錄
                mkdir(file.getParentFile());
                // 寫出文件流
                BufferedOutputStream bufferedOutputStream =
                        new BufferedOutputStream(new FileOutputStream(unzipFilePath));
                byte[] bytes = new byte[1024];
                int readLen;
                while ((readLen = zipInputStream.read(bytes)) != -1) {
                    bufferedOutputStream.write(bytes, 0, readLen);
                }
                bufferedOutputStream.close();
            }
            zipInputStream.closeEntry();
            zipEntry = zipInputStream.getNextEntry();
        }
        zipInputStream.close();
    }

    // 如果父目錄不存在則創建
    private static void mkdir(File file) {
        if (null == file || file.exists()) {
            return;
        }
        mkdir(file.getParentFile());
        file.mkdir();
    }

    public static void main(String[] args) throws Exception {
        String zipFilePath = "D:/test.zip";
        String desDirectory = "D:/a";
        unzip(zipFilePath, desDirectory);
    }
}

四 總結

  • 在解壓縮文件過程中,主要是對流的讀取操作,注意進行異常處理,以及關閉流。
  • web應用中,通過接口可以實現文件上傳下載,對應的我們只要把壓縮後的文件,寫入到response.getOutputStream()輸出流即可。
  • 解壓縮文件時,注意空文件夾的處理。

此演示項目已上傳到Github,如有需要可自行下載,歡迎 Star 。 //github.com/LucioChn/common-utils