1. 程式人生 > >關於Java解壓檔案的一些坑及經驗分享

關於Java解壓檔案的一些坑及經驗分享

就在本週, 測試人員找到我說現上的需求文件(zip格式的)無法預覽了, 讓我幫忙看看怎麼回事。 
這個功能也並不是我做的, 於是我便先看看線上日誌有沒有什麼錯誤,果不其然, 後臺果然報錯了。 
此處輸入圖片的描述

java.lang.IllegalArgumentException:MALFORMED
   at java.util.zip.ZipCoder.toString(ZipCoder.toString:58)
   ...
  • 1
  • 2
  • 3

異常大致是這樣,前臺無法預覽需求文件的原因是該zip檔案解壓失敗了。 
首先網上查了下這個異常的原因, 都說是因為編碼的問題, 要求將UTF-8改成GBK就可以了。 
然後定位程式碼, 看到有一個方法:unzip()

public static void unzip(File zipFile, String descDir) {
    try {
        File pathFile = new File(descDir);
        if (!pathFile.exists()) {
            pathFile.mkdirs();
        }
        ZipFile zip = getZipFile(zipFile);
        for (Enumeration entries = zip.entries(); entries.hasMoreElements(); ) {
            ZipEntry entry = (ZipEntry) entries.nextElement();
            String zipEntryName = entry.getName();
            if
(StringUtils.isNotBlank(pre)) { zipEntryName = zipEntryName.substring(pre.length()); } InputStream in = zip.getInputStream(entry); String outPath = (descDir + "/" + zipEntryName).replaceAll("\\*", "/"); ; //判斷路徑是否存在,不存在則建立檔案路徑 File file = new
File(outPath.substring(0, outPath.lastIndexOf('/'))); if (!file.exists()) { file.mkdirs(); } //判斷檔案全路徑是否為資料夾,如果是上面已經上傳,不需要解壓 if (new File(outPath).isDirectory()) { continue; } //輸出檔案路徑資訊 LOG.info("解壓檔案的當前路徑為:{}", outPath); OutputStream out = new FileOutputStream(outPath); IOUtils.copy(in, out); in.close(); out.close(); } zip.close(); LOG.info("******************解壓完畢********************"); } catch (Exception e) { LOG.error("[unzip] 解壓zip檔案出錯", e); } } private static ZipFile getZipFile(File zipFile) throws Exception { ZipFile zip = new ZipFile(zipFile, Charset.forName("UTF-8")); Enumeration entries = zip.entries(); while (entries.hasMoreElements()) { try { entries.nextElement(); zip.close(); zip = new ZipFile(zipFile, Charset.forName("UTF-8")); return zip; } catch (Exception e) { zip = new ZipFile(zipFile, Charset.forName("GBK")); return zip; } } return zip; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

於是便將線上的zip檔案down下來 然後本地除錯下, 發現在第9行中丟擲了異常, 如下程式碼:

ZipEntry entry = (ZipEntry) entries.nextElement();
  • 1

再由最開始的異常日誌找到ZipCoder中的58行:

String toString(byte[] ba, int length) {
    CharsetDecoder cd = decoder().reset();
    int len = (int)(length * cd.maxCharsPerByte());
    char[] ca = new char[len];
    if (len == 0)
        return new String(ca);
    // UTF-8 only for now. Other ArrayDeocder only handles
    // CodingErrorAction.REPLACE mode. ZipCoder uses
    // REPORT mode.
    if (isUTF8 && cd instanceof ArrayDecoder) {
        int clen = ((ArrayDecoder)cd).decode(ba, 0, length, ca);
        if (clen == -1)    // malformed
            throw new IllegalArgumentException("MALFORMED");
        return new String(ca, 0, clen);
    }
    ByteBuffer bb = ByteBuffer.wrap(ba, 0, length);
    CharBuffer cb = CharBuffer.wrap(ca);
    CoderResult cr = cd.decode(bb, cb, true);
    if (!cr.isUnderflow())
        throw new IllegalArgumentException(cr.toString());
    cr = cd.flush(cb);
    if (!cr.isUnderflow())
        throw new IllegalArgumentException(cr.toString());
    return new String(ca, 0, cb.position());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

這裡只有UTF-8才會進入if邏輯才會拋錯?果然如網上所說, 將編碼格式改為GBK即可。 
ZipCoder這個類似src.zip包中的, 既然這裡做了check當然會有它的道理, 單純的改為GBK來解決這個bug顯然是不合理的。

於是便要換種思路了, 線上有些zip是仍然可以預覽的。 我將線上的zip檔案解壓後, 在自己電腦重新打個包(我用的是好壓), 然後又運行了上述程式碼, 竟然解壓成功?? 這是為什麼? 於是上網上找了一下, 果然找到了答案:

Windows 壓縮的時候使用的是系統的編碼 GB2312,而 Mac 系統預設的編碼是 UTF-8,於是出現了亂碼。

最後去問了上傳的同事, 他是在Windows下用的winRar上傳的(看來不同的解壓工具還不同)。 
好了, 問題基本定位到了, 這裡就要想著怎麼解決了。 
又是一通找, 終於:

Apache commons-compress 解壓 zip 檔案是件很幸福的事,可以解決 zip 包中檔名有中文時跨平臺的亂碼問題,不管檔案是在 Windows 壓縮的還是在 Mac,Linux 壓縮的,解壓後都沒有再出現亂碼問題了。

看到這裡基本上問題就要解決了, 於是開始使用apache的commons-compress了, 下面直接上程式碼, 程式碼是基於上面程式碼進行改造的: 
首先引入pom檔案:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-compress</artifactId>
    <version>1.8.1</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
public static void main(String[] args) throws Exception{
    String path = "C:\\Users\\Isuzu\\Desktop\\test.zip";
    unzip(new File(path), "D:\\data",);
}

public static void unzip(File zipFile, String descDir) {
    try (ZipArchiveInputStream inputStream = getZipFile(zipFile)) {
        File pathFile = new File(descDir);
        if (!pathFile.exists()) {
            pathFile.mkdirs();
        }
        ZipArchiveEntry entry = null;
        while ((entry = inputStream.getNextZipEntry()) != null) {
            if (entry.isDirectory()) {
                File directory = new File(descDir, entry.getName());
                directory.mkdirs();
            } else {
                OutputStream os = null;
                try {
                    os = new BufferedOutputStream(new FileOutputStream(new File(descDir, entry.getName())));
                    //輸出檔案路徑資訊
                    LOG.info("解壓檔案的當前路徑為:{}", descDir + entry.getName());
                    IOUtils.copy(inputStream, os);
                } finally {
                    IOUtils.closeQuietly(os);
                }
            }
        }
        final File[] files = pathFile.listFiles();
        if (files != null && files.length == 1 && files[0].isDirectory()) {
            // 說明只有一個資料夾
            FileUtils.copyDirectory(files[0], pathFile);
            //免得刪除錯誤, 刪除的檔案必須在/data/demand/目錄下。
            boolean isValid = files[0].getPath().contains("/data/www/");
            if (isValid) {
                FileUtils.forceDelete(files[0]);
            }
        }
        LOG.info("******************解壓完畢********************");

    } catch (Exception e) {
        LOG.error("[unzip] 解壓zip檔案出錯", e);
    }
}

private static ZipArchiveInputStream getZipFile(File zipFile) throws Exception {
    return new ZipArchiveInputStream(new BufferedInputStream(new FileInputStream(zipFile)));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

到了這裡就大功告成了, 原先自己遇到這個問題時百度了一圈, 解決方案大都是改編碼格式為GBK, 但那也只是治標不治本的方法, 解壓的坑就講這麼多, 後續有新的坑還會繼續總結出來的。