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 .