Вступление
В этой статье я расскажу об основах создания, взаимодействия, проверки и извлечения файлов zip-архива с помощью Java (в частности, OpenJDK 11). Образец кода, используемый в этой статье, представляет собой проект Gradle и размещен в этом репозитории GitHub, чтобы вы могли запускать и экспериментировать. Будьте осторожны при изменении кода, удаляющего файлы.
Как уже упоминалось, приведенные здесь примеры кода написаны с
использованием Java 11 и используют var
которое было введено в Java
10, и парадигмы функционального программирования в Java 8, поэтому для
их запуска требуется минимальная версия Java 10.
СОДЕРЖАНИЕ
- Ключевые классы Java для работы с Zip-архивами
- Общие пути к файлам для примеров кода
- Проверка содержимого Zip-архива
- Извлечение Zip-архива
- Запись файлов непосредственно в новый Zip-архив
- Архивирование существующего файла в новый Zip-архив
- Архивирование папки в новый ZIP-архив
Ключевые классы Java для работы с Zip-архивами
Я считаю, что неплохо начать с определения некоторых известных классов,
которые обычно используются при работе с zip-архивами в Java. Эти классы
java.nio.file
пакетах java.util.zip
или java.nio.file.
- java.util.zip.ZipFile
используется для чтения и взаимодействия с элементами (
ZipEntry
) в zip-архиве - java.util.zip.ZipEntry -
это абстракция, представляющая такой элемент, как файл или каталог в
zip-архиве (т. е. экземпляр
ZipFile
- java.util.zip.ZipOutputStream - это реализация абстрактного класса OutputStream, который используется для записи элементов в Zip-файл.
- java.nio.file.Files - очень удобный класс утилит для потоковой передачи и копирования файловых данных в экземпляры ZipOutputStream или из экземпляров ZipFile.
- java.nio.file.Path - еще один удобный класс утилит для эффективной работы с путями к файлам
Общие пути к файлам для примеров кода
В примере кода я использую два общих каталога для записи и чтения данных в / из которых оба относятся к корню проекта Gradle. Взгляните на связанный репо во введении или, что еще лучше, запустите образцы. Просто помните об этих двух переменных Path, поскольку они часто используются в качестве начального каталога для входов и выходов.
public class App {
static final Path zippedDir = Path.of("ZippedData");
static final Path inputDataDir = Path.of("InputData");
// ... other stuff
}
Проверка содержимого Zip-архива
Вы можете создать экземпляр ZipFile
и передать ему путь к
существующему zip-архиву, который, по сути, открывает его, как любой
другой файл, а затем проверяет содержимое, запрашивая ZipEntry
содержащееся внутри него. Обратите внимание на то, что ZipFile
реализует интерфейс
AutoCloseable
, что делает его отличным кандидатом на конструкцию программирования
Java «попытка с ресурсами»,
показанную ниже и во всех приведенных здесь примерах.
static void showZipContents() {
try (var zf = new ZipFile("ZipToInspect.zip")) {
System.out.println(String.format("Inspecting contents of: %s\n", zf.getName()));
Enumeration<? extends ZipEntry> zipEntries = zf.entries();
zipEntries.asIterator().forEachRemaining(entry -> {
System.out.println(String.format(
"Item: %s \nType: %s \nSize: %d\n",
entry.getName(),
entry.isDirectory() ? "directory" : "file",
entry.getSize()
));
});
} catch (IOException e) {
e.printStackTrace();
}
}
Запуск проекта Gradle с использованием следующего:
$ ./gradlew run
Это дает результат для метода App.showZipContents
> Task :run
Inspecting contents of: ZipToInspect.zip
Item: ZipToInspect/
Type: directory
Size: 0
Item: ZipToInspect/greetings.txt
Type: file
Size: 160
Item: ZipToInspect/InnerFolder/
Type: directory
Size: 0
Item: ZipToInspect/InnerFolder/About.txt
Type: file
Size: 39
Здесь вы можете видеть, что это распечатывает все файлы и каталоги в zip-архиве, даже файлы в каталогах.
Извлечение Zip-архива
Извлечение содержимого zip-архива на диск не требует ничего, кроме
репликации той же структуры каталогов, что и внутри ZipFile
, которую
можно определить с помощью ZipEntry.isDirectory
а затем копирования
файлов, представленных в ZipEntry
на диск.
static void unzipAZip() {
var outputPath = Path.of("UnzippedContents");
try (var zf = new ZipFile("ZipToInspect.zip")) {
// Delete if exists, then create a fresh empty directory to put the zip archive contents
initialize(outputPath);
Enumeration<? extends ZipEntry> zipEntries = zf.entries();
zipEntries.asIterator().forEachRemaining(entry -> {
try {
if (entry.isDirectory()) {
var dirToCreate = outputPath.resolve(entry.getName());
Files.createDirectories(dirToCreate);
} else {
var fileToCreate = outputPath.resolve(entry.getName());
Files.copy(zf.getInputStream(entry), fileToCreate);
}
} catch(IOException ei) {
ei.printStackTrace();
}
});
} catch(IOException e) {
e.printStackTrace();
}
}
Запись файлов непосредственно в новый Zip-архив
Поскольку запись zip-архива на самом деле является не чем иным, как
записью потока данных в какое-то место назначения (в данном случае
Zip-файл), запись данных, таких как String, в zip-архив, отличается
только тем, что вам нужно сопоставить данные, которые записано в
ZipEntry
добавленные в ZipOutputStream
.
Опять же, ZipOutputStream
реализует AutoCloseable
, поэтому его
лучше всего использовать с оператором try-with-resources. Единственная
реальная проблема - не забыть закрыть свой ZipEntry
когда вы закончите
с каждым из них, чтобы было ясно, когда он больше не должен получать
данные.
static void zipSomeStrings() {
Map<String, String> stringsToZip = Map.ofEntries(
entry("file1", "This is the first file"),
entry("file2", "This is the second file"),
entry("file3", "This is the third file")
);
var zipPath = zippedDir.resolve("ZipOfStringData.zip");
try (var zos = new ZipOutputStream(
new BufferedOutputStream(Files.newOutputStream(zipPath)))) {
for (var entry : stringsToZip.entrySet()) {
zos.putNextEntry(new ZipEntry(entry.getKey()));
zos.write(entry.getValue().getBytes());
zos.closeEntry();
}
} catch (IOException e) {
e.printStackTrace();
}
}
Архивирование существующего файла в новый Zip-архив
Если вы ранее копировали файл на Java,
то вы уже являетесь ПРОФЕССИОНАЛОМ в создании zip-архива из
существующего файла (или каталога, если на то пошло). Опять же,
единственное реальное отличие состоит в том, что вам нужно проявить
дополнительную осторожность, чтобы убедиться, что вы сопоставляете файлы
с соответствующими экземплярами ZipEntry
В этом примере я создаю входной файл «FileToZip.txt» и записываю в него
данные «Привет, друзья Java!» а затем используйте Files.copy (Path,
OutputStream),
чтобы связать ZipEntry
с файлом FileToZip.txt внутри zip-архива
ZippedFile.zip, который я создаю с экземпляром ZipOutoutStream
static void zipAFile() {
var inputPath = inputDataDir.resolve("FileToZip.txt");
var zipPath = zippedDir.resolve("ZippedFile.zip");
try (var zos = new ZipOutputStream(
new BufferedOutputStream(Files.newOutputStream(zipPath)))) {
Files.writeString(inputPath, "Howdy There Java Friends!\n");
zos.putNextEntry(new ZipEntry(inputPath.toString()));
Files.copy(inputPath, zos);
zos.closeEntry();
} catch (IOException e) {
e.printStackTrace();
}
}
Архивирование папки в новый ZIP-архив
Архивирование непустого каталога становится немного сложнее, особенно
если вы хотите сохранить пустые каталоги в родительском каталоге. Чтобы
поддерживать наличие пустого каталога в zip-архиве, вам необходимо
обязательно создать запись с суффиксом разделителя каталогов файловой
системы при создании ее ZipEntry
, а затем немедленно закрыть ее.
В этом примере я создаю каталог с именем «foldertozip», содержащий структуру, показанную ниже, а затем заархивирую его в zip-архив.
tree .
.
└── foldertozip
├── emptydir
├── file1.txt
└── file2.txt
В следующем коде обратите внимание на то, что я использую метод
Files.walk(Path)
для обхода дерева каталогов «foldertozip» и поиска
пустых каталогов («emptydir» в этом примере), и если / когда обнаружен,
я присоединяю разделитель каталогов к имя в ZipEntry
. После этого я
закрываю его, как только добавляю в экземпляр ZipOutputStream
Я также использую несколько иной подход к внедрению файлов, не
являющихся каталогами, в ZipOutputStream
по сравнению с предыдущим
примером, но я просто использую этот другой подход для разнообразия
примеров.
static void zipADirectoryWithFiles() {
var foldertozip = inputDataDir.resolve("foldertozip");
var dirFile1 = foldertozip.resolve("file1.txt");
var dirFile2 = foldertozip.resolve("file2.txt");
var zipPath = zippedDir.resolve("ZippedDirectory.zip");
try (var zos = new ZipOutputStream(
new BufferedOutputStream(Files.newOutputStream(zipPath)))) {
Files.createDirectory(foldertozip);
Files.createDirectory(foldertozip.resolve("emptydir"));
Files.writeString(dirFile1, "Does this Java get you rev'd up or what?");
Files.writeString(dirFile2, "Java Java Java ... Buz Buz Buz!");
Files.walk(foldertozip).forEach(path -> {
try {
var reliativePath = inputDataDir.relativize(path);
var file = path.toFile();
if (file.isDirectory()) {
var files = file.listFiles();
if (files == null || files.length == 0) {
zos.putNextEntry(new ZipEntry(
reliativePath.toString() + File.separator));
zos.closeEntry();
}
} else {
zos.putNextEntry(new ZipEntry(reliativePath.toString()));
zos.write(Files.readAllBytes(path));
zos.closeEntry();
}
} catch(IOException e) {
e.printStackTrace();
}
});
} catch(IOException e) {
e.printStackTrace();
}
}
Заключение
В этой статье я обсудил и продемонстрировал современный подход к работе
с zip-архивами на Java с использованием чистой Java и без сторонних
библиотек. Вы также можете заметить, что я использую еще несколько
современных функций языка Java, таких как парадигмы функционального
программирования и var
для предполагаемых переменных, поэтому при
выполнении этих примеров убедитесь, что вы используете как минимум Java
10.
Как всегда, спасибо за чтение и не стесняйтесь комментировать или критиковать ниже.