Рубрики
Без рубрики

Загрузите файл с URL-адреса на Java

Изучите различные способы загрузки файла на Java.

Автор оригинала: baeldung.

1. введение

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

Мы рассмотрим примеры, начиная от базового использования Java IO и заканчивая пакетом NIO, а также некоторые общие библиотеки, такие как Async Http Client и Apache Commons IO.

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

2. Использование Java IO

Самый простой API, который мы можем использовать для загрузки файла, – это Java IO . Мы можем использовать класс URL , чтобы открыть соединение с файлом, который мы хотим загрузить. Чтобы эффективно прочитать файл, мы будем использовать метод openStream() для получения InputStream:

BufferedInputStream in = new BufferedInputStream(new URL(FILE_URL).openStream())

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

Увеличение производительности происходит за счет буферизации. При чтении одного байта за раз с помощью метода read() каждый вызов метода подразумевает системный вызов базовой файловой системы. Когда JVM вызывает системный вызов read () , контекст выполнения программы переключается из пользовательского режима в режим ядра и обратно.

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

Для записи байтов, считанных с URL-адреса, в ваш локальный файл мы будем использовать метод write() из класса FileOutputStream :

try (BufferedInputStream in = new BufferedInputStream(new URL(FILE_URL).openStream());
  FileOutputStream fileOutputStream = new FileOutputStream(FILE_NAME)) {
    byte dataBuffer[] = new byte[1024];
    int bytesRead;
    while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
        fileOutputStream.write(dataBuffer, 0, bytesRead);
    }
} catch (IOException e) {
    // handle exception
}

При использовании BufferedInputStream метод read() будет считывать столько байтов, сколько мы задали для размера буфера. В нашем примере мы уже делаем это, читая блоки по 1024 байта за раз, поэтому BufferedInputStream не нужен.

Приведенный выше пример очень многословен, но, к счастью, начиная с Java 7, у нас есть класс Files , который содержит вспомогательные методы для обработки операций ввода-вывода. Мы можем использовать метод Files.copy () , чтобы прочитать все байты из InputStream и скопировать их в локальный файл:

InputStream in = new URL(FILE_URL).openStream();
Files.copy(in, Paths.get(FILE_NAME), StandardCopyOption.REPLACE_EXISTING);

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

К счастью, Java предлагает нам пакет NIO, который имеет методы для передачи байтов непосредственно между 2 каналами без буферизации.

Мы подробно рассмотрим это в следующем разделе.

3. Использование NIO

Пакет Java NIO предлагает возможность передачи байтов между 2 каналами без буферизации их в память приложения.

Чтобы прочитать файл с нашего URL-адреса, мы создадим новый ReadableByteChannel из URL потока:

ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream());

Байты, считанные из ReadableByteChannel , будут переданы в FileChannel , соответствующий файлу, который будет загружен:

FileOutputStream fileOutputStream = new FileOutputStream(FILE_NAME);
FileChannel fileChannel = fileOutputStream.getChannel();

Мы будем использовать метод transferFrom() из класса ReadableByteChannel для загрузки байтов с заданного URL-адреса в наш файловый канал :

fileOutputStream.getChannel()
  .transferFrom(readableByteChannel, 0, Long.MAX_VALUE);

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

В системах Linux и UNIX эти методы используют нулевая копия метод, который уменьшает количество переключений контекста между режимом ядра и пользовательским режимом.

4. Использование Библиотек

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

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

Мы могли бы обернуть всю логику в Вызываемый , или мы могли бы использовать для этого существующую библиотеку.

4.1. Асинхронный HTTP-клиент

AsyncHttpClient-это популярная библиотека для выполнения асинхронных HTTP-запросов с использованием платформы Netty. Мы можем использовать его для выполнения запроса GET на URL-адрес файла и получения содержимого файла.

Во-первых, нам нужно создать HTTP-клиент:

AsyncHttpClient client = Dsl.asyncHttpClient();

Загруженный контент будет помещен в FileOutputStream :

FileOutputStream stream = new FileOutputStream(FILE_NAME);

Затем мы создаем HTTP-запрос GET и регистрируем обработчик AsyncCompletionHandler для обработки загруженного контента:

client.prepareGet(FILE_URL).execute(new AsyncCompletionHandler() {

    @Override
    public State onBodyPartReceived(HttpResponseBodyPart bodyPart) 
      throws Exception {
        stream.getChannel().write(bodyPart.getBodyByteBuffer());
        return State.CONTINUE;
    }

    @Override
    public FileOutputStream onCompleted(Response response) 
      throws Exception {
        return stream;
    }
})

Обратите внимание, что мы переопределили метод onBodyPartReceived () . Реализация по умолчанию накапливает полученные HTTP-фрагменты в ArrayList . Это может привести к высокому потреблению памяти или исключению OutOfMemory при попытке загрузить большой файл.

Вместо того, чтобы накапливать каждый HttpResponseBodyPart в память, мы используем Файловый канал для записи байтов в наш локальный файл напрямую . Мы будем использовать метод getBody Byte Buffer() для доступа к содержимому части тела через ByteBuffer .

Bytebuffer преимущество заключается в том, что память выделяется за пределами кучи JVM, поэтому она не влияет на память приложений.

4.2. Apache Commons IO

Другой широко используемой библиотекой для операций ввода-вывода является Apache Commons IO . Из Javadoc мы видим, что существует служебный класс с именем FileUtils , который используется для общих задач обработки файлов.

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

FileUtils.copyURLToFile(
  new URL(FILE_URL), 
  new File(FILE_NAME), 
  CONNECT_TIMEOUT, 
  READ_TIMEOUT);

С точки зрения производительности этот код такой же, как и тот, который мы проиллюстрировали в разделе 2.

Базовый код использует те же концепции чтения в цикле некоторых байтов из InputStream и записи их в OutputStream .

Одно из отличий заключается в том, что здесь класс URLConnection используется для управления тайм-аутами подключения, чтобы загрузка не блокировалась в течение большого количества времени:

URLConnection connection = source.openConnection();
connection.setConnectTimeout(connectionTimeout);
connection.setReadTimeout(readTimeout);

5. Возобновляемая Загрузка

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

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

Первое, что мы должны знать, это то, что мы можем прочитать размер файла с заданного URL-адреса, фактически не загружая его, используя метод HTTP HEAD:

URL url = new URL(FILE_URL);
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setRequestMethod("HEAD");
long removeFileSize = httpConnection.getContentLengthLong();

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

long existingFileSize = outputFile.length();
if (existingFileSize < fileLength) {
    httpFileConnection.setRequestProperty(
      "Range", 
      "bytes=" + existingFileSize + "-" + fileLength
    );
}

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

Другим распространенным способом использования заголовка Range является загрузка файла по частям путем установки различных диапазонов байтов. Например, для загрузки файла размером 2 КБ мы можем использовать диапазон 0 – 1024 и 1024 – 2048.

Еще одно тонкое отличие от кода в разделе 2. заключается в том, что FileOutputStream открывается с параметром append , установленным в true :

OutputStream os = new FileOutputStream(FILE_NAME, true);

После того, как мы внесли это изменение, остальная часть кода идентична той, которую мы видели в разделе 2.

6. Заключение

В этой статье мы рассмотрели несколько способов загрузки файла с URL-адреса на Java.

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

Мы также видели, как мы можем реализовать загрузку с нулевой копией с помощью Java NIO Channels . Это полезно, поскольку сводит к минимуму количество переключений контекста, выполняемых при чтении и записи байтов, а при использовании прямых буферов байты не загружаются в память приложения.

Кроме того, поскольку обычно загрузка файла выполняется по протоколу HTTP, мы показали, как этого можно достичь с помощью библиотеки AsyncHttpClient.

Исходный код статьи доступен на GitHub .