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

Скачать файл с URL-адреса в Java

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

1. Обзор

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

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

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

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

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

Чтобы эффективно прочитать файл, мы будем использовать метод openStream() для получения InputStream :

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

При чтении из InputStream рекомендуется обернуть его в 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, в котором есть методы для прямой передачи байтов между двумя каналами без буферизации.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4.1. AsyncHttpClient

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<FileOutputStream>() {

@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 в памяти, мы используем FileChannel для прямой записи байтов в наш локальный файл. Мы будем использовать метод getBodyByteBuffer() для доступа к содержимому части тела через 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 .