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

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

1. обзор

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

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

Вы также можете узнать больше о NIO.2file operations иpath operations - понимание их значительно облегчит чтение этой статьи.

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

import java.nio.channels.*;

2. AsynchronousFileChannel

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

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

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

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

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

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

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

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

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

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

Внутри папки тестовых ресурсов maven или в исходном каталоге, если maven не используется, давайте создадим файл с именемfile.txt только с текстомexample.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 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, "example.com");
}

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

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

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

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

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

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

3.2. ПодходCompletionHandler

Далее мы увидим, как читать содержимое файла с помощью экземпляра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() {

        @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) {

        }
    });
}

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

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

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

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

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

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

СоздатьAsynchronousFileChannel для записи в файл можно так:

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

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

Обратите внимание на параметр, переданный в APIopen. Мы также можем добавить еще одну опциюStandardOpenOption.CREATE, если мы хотим, чтобы файл, представленныйpath, был создан, если он еще не существует. Другой распространенный вариант -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 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

Чтобы записать в файл асинхронно с использованием класса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 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. Мы используем этот путь для открытия асинхронного файлового канала с ранее упомянутыми параметрами.

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

4.3. ПодходCompletionHandler

Мы также можем использовать обработчик завершения, чтобы нам не приходилось ждать завершения операции в цикле 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() {

        @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 project.