關於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, 但那也只是治標不治本的方法, 解壓的坑就講這麼多, 後續有新的坑還會繼續總結出來的。