Перейти к основному содержимому

Руководство по асинхронному файловому каналу NIO2

· 7 мин. чтения

1. Обзор

В этой статье мы собираемся изучить один из ключевых дополнительных API нового ввода-вывода (NIO2) в Java 7 — API асинхронного файлового канала.

Если вы не знакомы с API асинхронных каналов в целом, у нас есть вводная статья на этом сайте, которую вы можете прочитать, перейдя по этой ссылке , прежде чем продолжить.

Вы также можете узнать больше об операциях с файлами NIO.2 и операциях с путями — их понимание значительно облегчит понимание этой статьи.

Чтобы использовать асинхронные файловые каналы NIO2 в наших проектах, мы должны импортировать пакет java.nio.channels , так как он объединяет все необходимые классы:

import java.nio.channels.*;

2. Асинхронный файловый канал

В этом разделе мы рассмотрим, как использовать основной класс, который позволяет нам выполнять асинхронные операции с файлами, класс AsynchronousFileChannel . Чтобы создать его экземпляр, мы вызываем статический метод открытия :

Path filePath = Paths.get("/path/to/file");

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
filePath, READ, WRITE, CREATE, DELETE_ON_CLOSE);

Все значения перечисления берутся из StandardOpenOption .

Первым параметром открытого API является объект Path , представляющий расположение файла. Чтобы узнать больше об операциях с путями в NIO2, перейдите по этой ссылке . Другие параметры составляют набор, определяющий параметры, которые должны быть доступны для возвращаемого файлового канала.

Созданный нами асинхронный файловый канал можно использовать для выполнения всех известных операций с файлом. Чтобы выполнить только подмножество операций, мы бы указали параметры только для них. Например, чтобы читать только:

Path filePath = Paths.get("/path/to/file");

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
filePath, StandardOpenOption.READ);

3. Чтение из файла

Как и все асинхронные операции в NIO2, чтение содержимого файла можно выполнить двумя способами. Использование Future и использование CompletionHandler . В каждом случае мы используем API чтения возвращаемого канала.

Внутри папки тестовых ресурсов maven или в исходном каталоге, если maven не используется, давайте создадим файл с именем file.txt , в начале которого будет только текст foreach.com . Теперь мы покажем, как читать этот контент.

3.1. Подход будущего

Во-первых, мы увидим, как асинхронно читать файл с помощью класса Future :

@Test
public void givenFilePath_whenReadsContentWithFuture_thenCorrect() {
Path path = Paths.get(
URI.create(
this.getClass().getResource("/file.txt").toString()));
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
path, StandardOpenOption.READ);

ByteBuffer buffer = ByteBuffer.allocate(1024);

Future<Integer> operation = fileChannel.read(buffer, 0);

// run other code as operation continues in background
operation.get();

String fileContent = new String(buffer.array()).trim();
buffer.clear();

assertEquals(fileContent, "foreach.com");
}

В приведенном выше коде после создания файлового канала мы используем API чтения , который использует ByteBuffer для хранения содержимого, прочитанного из канала, в качестве первого параметра.

Второй параметр — это тип long, указывающий позицию в файле, с которой следует начать чтение.

Метод возвращает сразу же, был ли файл прочитан или нет.

Затем мы можем выполнить любой другой код, поскольку операция продолжается в фоновом режиме. Когда мы закончим выполнение другого кода, мы можем вызвать API get() , который сразу же возвращает результат, если операция уже завершена, когда мы выполняли другой код, или блокируется до завершения операции.

Наше утверждение действительно доказывает, что содержимое файла было прочитано.

Если бы мы изменили параметр позиции в вызове API чтения с нуля на что-то другое, мы бы тоже увидели эффект. Например, седьмой символ в строке foreach.comg . Таким образом, изменение параметра position на 7 приведет к тому, что буфер будет содержать строку g.com .

3.2. Подход обработчика завершения

Далее мы увидим, как читать содержимое файла с помощью экземпляра CompletionHandler :

@Test
public void
givenPath_whenReadsContentWithCompletionHandler_thenCorrect() {

Path path = Paths.get(
URI.create( this.getClass().getResource("/file.txt").toString()));
AsynchronousFileChannel fileChannel
= AsynchronousFileChannel.open(path, StandardOpenOption.READ);

ByteBuffer buffer = ByteBuffer.allocate(1024);

fileChannel.read(
buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {

@Override
public void completed(Integer result, ByteBuffer attachment) {
// result is number of bytes read
// attachment is the buffer containing content
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {

}
});
}

В приведенном выше коде мы используем второй вариант API чтения . Он по-прежнему принимает ByteBuffer и начальную позицию операции чтения в качестве первого и второго параметров соответственно. Третий параметр — экземпляр CompletionHandler .

Первый общий тип обработчика завершения — это возвращаемый тип операции, в данном случае Integer, представляющий число прочитанных байтов.

Во-вторых, тип крепления. Мы решили прикрепить буфер таким образом, чтобы после завершения чтения мы могли использовать содержимое файла внутри завершенного API обратного вызова.

С семантической точки зрения это не совсем корректный модульный тест, поскольку мы не можем сделать утверждение внутри завершенного метода обратного вызова. Однако мы делаем это из соображений согласованности и потому, что хотим, чтобы наш код можно было копировать и вставлять, насколько это возможно.

4. Запись в файл

Java NIO2 также позволяет нам выполнять операции записи в файл. Как и в случае с другими операциями, мы можем записывать в файл двумя способами. Использование Future и использование CompletionHandler . В каждом случае мы используем API записи возвращаемого канала.

Создание AsynchronousFileChannel для записи в файл можно сделать следующим образом:

AsynchronousFileChannel fileChannel
= AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);

4.1. Особые соображения

Обратите внимание на параметр, переданный открытому API. Мы также можем добавить еще одну опцию StandardOpenOption.CREATE , если мы хотим, чтобы файл, представленный путем, был создан, если он еще не существует. Другим распространенным параметром является StandardOpenOption.APPEND , который не перезаписывает существующее содержимое в файле.

Мы будем использовать следующую строку для создания нашего файлового канала в целях тестирования:

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
path, WRITE, CREATE, DELETE_ON_CLOSE);

Таким образом, мы укажем любой произвольный путь и будем уверены, что файл будет создан. После завершения теста созданный файл будет удален. Чтобы созданные файлы не удалялись после завершения теста, вы можете удалить последнюю опцию.

Чтобы запустить утверждения, нам нужно будет прочитать содержимое файла, где это возможно, после записи в него. Спрячем логику чтения в отдельный метод, чтобы избежать избыточности:

public static String readContent(Path file) {
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
file, StandardOpenOption.READ);

ByteBuffer buffer = ByteBuffer.allocate(1024);

Future<Integer> operation = fileChannel.read(buffer, 0);

// run other code as operation continues in background
operation.get();

String fileContent = new String(buffer.array()).trim();
buffer.clear();
return fileContent;
}

4.2. Подход будущего _

Для асинхронной записи в файл с помощью класса Future :

@Test
public void
givenPathAndContent_whenWritesToFileWithFuture_thenCorrect() {

String fileName = UUID.randomUUID().toString();
Path path = Paths.get(fileName);
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
path, WRITE, CREATE, DELETE_ON_CLOSE);

ByteBuffer buffer = ByteBuffer.allocate(1024);

buffer.put("hello world".getBytes());
buffer.flip();

Future<Integer> operation = fileChannel.write(buffer, 0);
buffer.clear();

//run other code as operation continues in background
operation.get();

String content = readContent(path);
assertEquals("hello world", content);
}

Давайте проверим, что происходит в приведенном выше коде. Мы создаем случайное имя файла и используем его для получения объекта Path . Мы используем этот путь, чтобы открыть асинхронный файловый канал с ранее упомянутыми параметрами.

Затем мы помещаем содержимое, которое хотим записать в файл, в буфер и выполняем запись . Мы используем наш вспомогательный метод для чтения содержимого файла и действительно подтверждаем, что это то, что мы ожидаем.

4.3. Подход обработчика завершения

Мы также можем использовать обработчик завершения, чтобы нам не приходилось ждать завершения операции в цикле while:

@Test
public void
givenPathAndContent_whenWritesToFileWithHandler_thenCorrect() {

String fileName = UUID.randomUUID().toString();
Path path = Paths.get(fileName);
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
path, WRITE, CREATE, DELETE_ON_CLOSE);

ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello world".getBytes());
buffer.flip();

fileChannel.write(
buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {

@Override
public void completed(Integer result, ByteBuffer attachment) {
// result is number of bytes written
// attachment is the buffer
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {

}
});
}

Когда мы на этот раз вызываем API записи, единственная новая вещь — это третий параметр, в который мы передаем анонимный внутренний класс типа CompletionHandler .

Когда операция завершается, класс вызывает завершенный метод, в котором мы можем определить, что должно произойти.

5. Вывод

В этой статье мы рассмотрели некоторые из наиболее важных функций API-интерфейсов асинхронного файлового канала Java NIO2.

Чтобы получить все фрагменты кода и полный исходный код для этой статьи, вы можете посетить проект Github .