這一篇我們來看看Java代碼怎么操作zip文件(jar文件),然后SpringBoot的特殊處理,文章分為2部分
- Zip API解釋,看看我們工具箱里有哪些工具能用
- SpringBoot的特殊處理,看看SpringBoot Jar和普通Jar的不同
1. Zip API解釋
1. ZipFile
我們先通過ZipFile來讀取jar文件,通過ZipFile#entries()方法返回Zip內的每一個元素,每個元素可能是目錄或文件,如果是目錄則在目標文件夾下創建對應目錄,否則拷貝文件到目標位置
private static void unzipByZipFile(String org, String dest) throws IOException {clean(dest);ZipFile zip = new ZipFile(org);Enumeration<? extends ZipEntry> ez = zip.entries();while (ez.hasMoreElements()) {ZipEntry ze = ez.nextElement();if (ze.isDirectory()) {Files.createDirectories(Path.of(dest, ze.getName()));} else {Path target = Path.of(dest, ze.getName());try (InputStream is = zip.getInputStream(ze)) {Files.copy(is, target);}}}
}
接下來在main方法內調用unzipByZipFile來查看測試效果,并查看輸出的目錄
public static void main(String[] args) throws IOException {unzipByZipFile("D:\\Workspace\\yangsi\\target\\yangsi-0.0.1-SNAPSHOT.jar", "d:/temp");
}
2. ZipInputStream
使用ZipInputStream讀取和ZipFile讀取基本類似,通過getNextEntry先獲取一個ZipEntry,讀取完畢后用closeEntry編譯當前ZipEntry。
private static void unzipByZipInputStream(String org, String dest) throws IOException {clean(dest);try (ZipInputStream zis = new ZipInputStream(new FileInputStream(org))) {ZipEntry ze = null;while ((ze = zis.getNextEntry()) != null) {if (ze.isDirectory()) {Files.createDirectories(Path.of(dest, ze.getName()));} else {Files.copy(zis, Path.of(dest, ze.getName()));}zis.closeEntry();}}
}
3. ZipOuputStream
現在我們使用ZipOutputStream將之前解壓出來的文件重新打包成jar,代碼如下
private static void zipByZipOutputStream(String dir, String dst) throws IOException {Files.deleteIfExists(Path.of(dst));try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dst))) {Path root = Path.of(dir);for (Path x : Files.list(root).toList()) {addToZip(root, x, zos);}}
}private static void addToZip(Path root, Path file, ZipOutputStream zos) throws IOException {if (Files.isDirectory(file)) {for (Path x : Files.list(file).toList()) {addToZip(root, x, zos);}} else {ZipEntry e = new ZipEntry(root.relativize(file).toString());zos.putNextEntry(e);Files.copy(file, zos);zos.closeEntry();}
}
2. SpringBoot的特殊處理
1. 對比文件
到現在為止,一切都看起來很沒好,我們通過ZipInputStream解壓了jar包,然后又通過ZipOutputStream重新打成可執行jar。 直到我們嘗試執行這個通過ZipOutputStream打包的jar,才發現了問題。
~$ java -jar temp.jar
Error: Invalid or corrupt jarfile temp.jar
問題發生在哪呢?處在ZipOutputStream的壓縮級別上,SpringBoot的jar對文件壓縮做了特殊處理。如果我們有3個壓縮文件,分別標號為1、2、3
- 文件1,是正常SpringBoot項目通過Maven打包后的結果
- 文件2,是將文件1中的jar解壓后,通過ZipOutputStream采用0壓縮級別(不壓縮)打包的文件
- 文件3,是將文件1中的jar解壓后,采用默認壓縮級別打包的文件
可以看到org、META-INF在文件1、文件3中的文件大小是完全一致的,所以這部分文件在SpringBoot JAR也是被壓縮的。
而BOOT-INF卻3中方式都不同,我們進入BOOT-INF看看,文件1、文件3中的普通文件(classes、idx)文件是一樣的,也就是普通文件不做壓縮。而文件1、文件2的lib文件夾是一樣的。
所以總結下來,Spring Boot Maven Plugin打成的可執行jar,對普通文件采用了壓縮,而jar文件僅僅打包而不壓縮。這也是為什么我們執行java -jar temp.jar時報錯的原因。
2. 設置jar不壓縮
現在我們要修改ZipOutputStream的輸出,jar文件僅存儲不壓縮,需要在代碼中設置jar的ZipEntry.setMethod(ZipEntry.STORED),同時要自己計算crc和文件大小。
private static void zipByZipOutputStream(String dir, String dst) throws IOException {Files.deleteIfExists(Path.of(dst));try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dst))) {Path root = Path.of(dir);for (Path x : Files.list(root).toList()) {addToZip(root, x, zos);}}
}private static void addToZip(Path root, Path file, ZipOutputStream zos) throws IOException {if (Files.isDirectory(file)) {for (Path x : Files.list(file).toList()) {addToZip(root, x, zos);}} else if (isJar(file)) {ZipEntry e = new ZipEntry(root.relativize(file).toString());long size = Files.size(file);e.setSize(size);e.setCompressedSize(size);e.setMethod(ZipEntry.STORED);try (InputStream fis = Files.newInputStream(file, StandardOpenOption.READ); CheckedInputStream cis = new CheckedInputStream(fis, new CRC32()); ByteArrayOutputStream bos = new ByteArrayOutputStream();) {cis.transferTo(bos);long crc = cis.getChecksum().getValue();e.setCrc(crc & 0xFFFFFFFF);}zos.putNextEntry(e);Files.copy(file, zos);zos.closeEntry();} else {ZipEntry e = new ZipEntry(root.relativize(file).toString());zos.putNextEntry(e);Files.copy(file, zos);zos.closeEntry();}
}private static boolean isJar(Path file) {return file.getFileName().toString().toLowerCase().endsWith(".jar");
}
再次打包后可以看到(文件4),我們打包的文件大小和原始文件是一摸一樣的了。
應該說Spring Boot的這種特殊處理是合理且必要的,jar文件本身已經做過壓縮,再次壓縮意義不大。
現在我們有足夠的背景知識了,下一篇我們來看看SpringBoot可執行Jar是怎么引導并啟動我們的應用的。