Apache Commons 壓縮專案簡介
一、簡介
在本教程中,我們將學習如何使用Apache Commons Compress來壓縮、存檔和提取檔案。我們還將了解其支援的格式及其一些限制。
2. 什麼是 Apache Commons 壓縮
Apache Commons Compress 是一個為最廣泛使用的壓縮和歸檔格式建立標準介面的函式庫。它從無處不在的 TAR、ZIP 和 GZIP 到鮮為人知但也常用的格式,如 BZIP2、XZ、LZMA 和 Snappy。
2.1.壓縮器和歸檔器之間的區別
歸檔器(例如 TAR)將目錄結構捆綁到單一檔案中,而壓縮器則採用位元組流並使它們更小,從而節省空間。某些格式(如 ZIP)可以充當存檔器和壓縮器,但被圖書館視為存檔器。
我們可以透過查看 Commons Compress 提供的ArchiveStreamFactory
類別的一些靜態欄位來檢查支援的存檔格式。相反,我們可以查看CompressorStreamFactory
以取得支援的壓縮器格式。
2.2. Commons 壓縮和附加依賴項
讓我們先在我們的專案中加入commons-compress
:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.1</version>
</dependency>
Commons Compress 開箱即用,可與 TAR、ZIP、BZIP2、CPIO 和 GZIP 搭配使用。但是,對於其他格式,我們需要額外的依賴項。讓我們加入XZ、7z 和 LZMA支援:
<dependency>
<groupId>org.tukaani</groupId>
<artifactId>xz</artifactId>
<version>1.9</version>
</dependency>
最後,對於LZ4 和 ZSTD :
<dependency>
<groupId>com.github.luben</groupId>
<artifactId>zstd-jni</artifactId>
<version>1.5.5-11</version>
</dependency>
有了這些,我們就可以避免在讀取或寫入這些類型的檔案時發生錯誤。
3. 壓縮和解壓縮流
雖然該庫為這些不同格式的共同操作創建了抽象,但它們也具有獨特的功能。我們使用特定的實作來存取它們,例如GzipCompressorInputStream
和LZMACompressorInputStream
。相反,我們將重點關注CompressorStreamFactory
,它可以幫助我們獲得無需特定類別的實現,這有助於創建與格式無關的程式碼。
3.1.壓縮檔案
壓縮檔案時,我們必須將所需的壓縮格式傳遞給工廠方法。 Commons Compress 包含一個FileNameUtils
類,我們將使用它來取得檔案副檔名並將其作為格式傳遞。然後,我們打開一個輸出流,取得一個壓縮器實例,並將Path
中的位元組寫入其中:
public class CompressUtils {
public static void compressFile(Path file, Path destination) {
String format = FileNameUtils.getExtension(destination);
try (OutputStream out = Files.newOutputStream(destination);
BufferedOutputStream buffer = new BufferedOutputStream(out);
CompressorOutputStream compressor = new CompressorStreamFactory()
.createCompressorOutputStream(format, buffer)) {
IOUtils.copy(Files.newInputStream(file), compressor);
}
}
// ...
}
讓我們用一個簡單的文字檔案來測試它:
@Test
void givenFile_whenCompressing_thenCompressed() {
Path destination = Paths.get("/tmp/simple.txt.gz");
CompressUtils.compressFile(Paths.get("/tmp/simple.txt"), destination);
assertTrue(Files.isRegularFile(destination));
}
請注意,我們在這裡使用 GZIP,它由“gz”擴展名表示。我們可以使用任何其他支援的格式,只需更改所需destination
的副檔名即可。此外,我們可以使用任何文件類型作為輸入。
3.2.解壓縮壓縮文件
讓我們解壓縮使用任何支援的格式壓縮的檔案。首先,我們需要打開檔案的緩衝輸入流並建立壓縮器輸入流(它透過讀取檔案的第一個位元組來偵測壓縮格式)。然後,將壓縮器輸入寫入輸出流,從而產生解壓縮的檔案或存檔:
public static void decompress(Path file, Path destination) {
try (InputStream in = Files.newInputStream(file);
BufferedInputStream inputBuffer = new BufferedInputStream(in);
OutputStream out = Files.newOutputStream(destination);
CompressorInputStream decompressor = new CompressorStreamFactory()
.createCompressorInputStream(inputBuffer)) {
IOUtils.copy(decompressor, out);
}
}
讓我們用「tar.gz」檔案來測試它,該檔案表明它是用 GZIP 壓縮的 TAR 檔案:
@Test
void givenCompressedArchive_whenDecompressing_thenArchiveAvailable() {
Path destination = Paths.get("/tmp/decompressed-archive.tar");
CompressUtils.decompress("/tmp/archive.tar.gz", destination);
assertTrue(Files.isRegularFile(destination));
}
請注意,受支援的歸檔器和壓縮器的任何組合都可以在此處工作,而無需更改任何程式碼。例如,我們可以使用“archive.cpio.xz”檔案作為輸入。我們甚至可以解壓縮經過 GZIP 處理的 ZIP 檔案。最重要的是,此方法並非存檔文件所獨有。任何壓縮檔案都可以用它解壓縮。
4. 建立和操作檔案
要建立檔案,我們需要指定所需的格式。為了簡化事情, Archiver
類別有一個方便的方法,可以將整個目錄存檔到目標檔案:
public static void archive(Path directory, Path destination) {
String format = FileNameUtils.getExtension(destination);
new Archiver().create(format, destination, directory);
}
4.1.將存檔器與壓縮器結合
我們也可以將歸檔器和壓縮器結合起來,在一次操作中建立壓縮歸檔。為了簡化這一點,我們將把擴展名視為壓縮器格式,將其前面的擴展名視為存檔器格式。然後,我們為產生的壓縮檔案開啟一個緩衝輸出流,根據我們的壓縮格式建立一個壓縮器,並實例化一個ArchiveOutputStream
來消耗壓縮器的輸出:
public static void archiveAndCompress(Path directory, Path destination) {
String compressionFormat = FileNameUtils.getExtension(destination);
String archiveFormat = FilenameUtils.getExtension(
destination.getFileName().toString().replace("." + compressionFormat, ""));
try (OutputStream archive = Files.newOutputStream(destination);
BufferedOutputStream archiveBuffer = new BufferedOutputStream(archive);
CompressorOutputStream compressor = new CompressorStreamFactory()
.createCompressorOutputStream(compressionFormat, archiveBuffer);
ArchiveOutputStream<?> archiver = new ArchiveStreamFactory()
.createArchiveOutputStream(archiveFormat, compressor)) {
new Archiver().create(archiver, directory);
}
}
最後,我們仍然使用Archiver
,但現在使用接收ArchiveOutputStream
的create()
版本。
4.2.取消存檔
使用Expander
類,我們可以在一行中解壓縮未壓縮的檔案:
public static void extract(Path archive, Path destination) {
new Expander().expand(archive, destination);
}
我們傳遞存檔檔案和我們想要將檔案提取到的目錄。此實用程式方法負責開啟(和關閉)輸入流、偵測存檔類型、迭代存檔中的所有條目,並將它們複製到我們選擇的目錄。
4.3.從現有檔案中擷取條目
讓我們編寫一個從存檔中提取單一條目而不是整個內容的方法:
public static void extractOne(Path archivePath, String fileName, Path destinationDirectory) {
try (InputStream input = Files.newInputStream(archivePath);
BufferedInputStream buffer = new BufferedInputStream(input);
ArchiveInputStream<?> archive = new ArchiveStreamFactory()
.createArchiveInputStream(buffer)) {
ArchiveEntry entry;
while ((entry = archive.getNextEntry()) != null) {
if (entry.getName().equals(fileName)) {
Path outFile = destinationDirectory.resolve(fileName);
Files.createDirectories(outFile.getParent());
try (OutputStream os = Files.newOutputStream(outFile)) {
IOUtils.copy(archive, os);
}
break;
}
}
}
}
打開ArchiveInputStream
後,我們不斷對檔案呼叫getNextEntry()
直到找到同名的條目。如有必要,將建立任何父目錄。然後,其內容被寫入我們的目標目錄中。請注意,檔案名稱可以表示存檔內的子目錄。考慮到我們的存檔在「子目錄」下包含一個名為「some.txt」的檔案:
@Test
void givenExistingArchive_whenExtractingSingleEntry_thenFileExtracted() {
Path archive = Paths.get("/tmp/archive.tar.gz");
String targetFile = "sub-directory/some.txt";
CompressUtils.extractOne(archive, targetFile, Paths.get("/tmp/"));
assertTrue(Files.isRegularFile("/tmp/sub-directory/some.txt"));
}
4.4.在現有檔案中新增條目
不幸的是,該庫並沒有為我們提供一種簡單的方法來將新條目包含到現有檔案中。如果我們打開存檔並呼叫putArchiveEntry()
,我們將覆蓋其內容。因此,在插入新條目之前,還需要重寫所有現有條目。我們將重複使用我們建立的方法,而不是建立具有此邏輯的新方法。我們將提取存檔,將新檔案複製到目錄結構,再次存檔目錄,然後刪除舊存檔:
@Test
void givenExistingArchive_whenAddingSingleEntry_thenArchiveModified() {
Path archive = Paths.get("/tmp/archive.tar");
Path newArchive = Paths.get("/tmp/modified-archive.tar");
Path tmpDir = Paths.get("/tmp/extracted-archive");
Path newEntry = Paths.get("/tmp/new-entry.txt");
CompressUtils.extract(archive, tmpDir);
assertTrue(Files.isDirectory(tmpDir));
Files.copy(newEntry, tmpDir.resolve(newEntry.getFileName()));
CompressUtils.archive(tmpDir, newArchive);
assertTrue(Files.isRegularFile(newArchive));
FileUtils.deleteDirectory(tmpDir.toFile());
Files.delete(archive);
Files.move(newArchive, archive);
assertTrue(Files.isRegularFile(archive));
}
這會破壞舊的存檔,因此建議保留備份。
4.5.直接使用具體實作來獲得專有功能
如果我們想要每種格式獨有的功能,我們可以直接使用特定的實作類別。例如,我們不使用ArchiveOutputStream
,而是實例化ZipArchiveOutputStream
以便可以直接設定其壓縮方法和層級:
public static void zip(Path file, Path destination) {
try (InputStream input = Files.newInputStream(file);
OutputStream output = Files.newOutputStream(destination);
ZipArchiveOutputStream archive = new ZipArchiveOutputStream(output)) {
archive.setMethod(ZipEntry.DEFLATED);
archive.setLevel(Deflater.BEST_COMPRESSION);
archive.putArchiveEntry(new ZipArchiveEntry(file.getFileName().toString()));
IOUtils.copy(input, archive);
archive.closeArchiveEntry();
}
}
它比僅僅使用Archiver
需要更多的程式碼,但給了我們更多的控制權。
5. 局限性
雖然 Apache Commons Compress 提供了檔案壓縮和歸檔的多功能工具包,但必須承認某些限制和注意事項。首先,雖然該程式庫為各種壓縮和存檔格式提供了廣泛的支持,但處理多卷存檔可能會帶來需要仔細考慮的挑戰。此外,可能會出現編碼問題。主要是在處理不同的檔案系統或非標準化資料時。
此外,雖然該程式庫提供了全面的功能,但Apache 建議利用 ZipFile 在特定場景中增強控制。最後,TAR 格式還有一個專門的頁面,其中包含注意事項。
六,結論
在本文中,我們了解了 Apache Commons Compress 如何成為高效能檔案壓縮和歸檔解決方案的寶貴資源。透過了解其功能、限制和最佳實踐,我們可以有效地利用該程式庫以獨立於格式的方式簡化文件管理流程。
與往常一樣,原始碼可以在 GitHub 上取得。