Скачать файл с URL в Java
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())
При чтении из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, который содержит вспомогательные методы для обработки операций ввода-вывода. We can use the Files.copy(), чтобы прочитать все байты изInputStream и скопировать их в локальный файл:
InputStream in = new URL(FILE_URL).openStream();
Files.copy(in, Paths.get(FILE_NAME), StandardCopyOption.REPLACE_EXISTING);
Наш код работает хорошо, но его можно улучшить. Его основным недостатком является то, что байты буферизируются в памяти.
К счастью, Java предлагает нам пакет NIO, в котором есть методы для передачи байтов напрямую между 2Channelsбез буферизации.
Подробности мы рассмотрим в следующем разделе.
3. Использование NIO
ПакетJava NIO предлагает возможность передавать байты между 2Channels без их буферизации в памяти приложения.
Чтобы прочитать файл по нашему 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() более эффективны, чем простое чтение из потока с использованием буфера. В зависимости от базовой операционной системыthe data can be transferred directly from the filesystem cache to our file without copying any bytes into the application memory.
В системах Linux и UNIX эти методы используют техникуzero-copy, которая сокращает количество переключений контекста между режимом ядра и режимом пользователя.
4. Использование библиотек
В приведенных выше примерах мы видели, как можно загружать контент по URL-адресу, просто используя основные функции Java. Мы также можем использовать функциональные возможности существующих библиотек, чтобы упростить нашу работу, когда не требуется настройка производительности.
Например, в реальном сценарии нам нужно, чтобы код загрузки был асинхронным.
Мы могли бы обернуть всю логику вCallable или использовать для этого существующую библиотеку.
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(). The default implementation accumulates the HTTP chunks received into an ArrayList. Это может привести к высокому потреблению памяти или исключениюOutOfMemory при попытке загрузить большой файл.
Вместо накопления каждогоHttpResponseBodyPart в памяти,we use a FileChannel to write the bytes to our local file directly. Мы будем использовать методgetBodyByteBuffer() для доступа к содержимому части тела черезByteBuffer.
ПреимуществоByteBuffers в том, что память выделяется вне кучи 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. Возобновляемая загрузка
Учитывая, что подключение к Интернету время от времени прерывается, для нас полезно иметь возможность возобновить загрузку вместо повторной загрузки файла с нулевого байта.
Давайте перепишем первый пример из предыдущего, чтобы добавить эту функциональность.
Первое, что мы должны знать, это то, чтоwe can read the size of a file from a given URL without actually downloading it by using the HTTP HEAD method:
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
);
}
Здесь происходит то, чтоwe’ve configured the URLConnection to request the file bytes in a specific range. Диапазон начинается с последнего загруженного байта и заканчивается байтом, соответствующим размеру удаленного файла.
Другой распространенный способ использования заголовкаRange - загрузка файла по частям путем установки разных диапазонов байтов. Например, чтобы загрузить файл размером 2 КБ, мы можем использовать диапазон от 0 до 1024 и от 1024 до 2048.
Еще одно тонкое отличие от кода в разделе 2. заключается в том, чтоFileOutputStream is opened with the append parameter set to true:
OutputStream os = new FileOutputStream(FILE_NAME, true);
После внесения этого изменения остальной код идентичен тому, который мы видели в разделе 2.
6. Заключение
В этой статье мы видели несколько способов загрузки файла по URL-адресу в Java.
Наиболее распространенная реализация - та, в которой мы буферизируем байты при выполнении операций чтения / записи. Эту реализацию безопасно использовать даже для больших файлов, поскольку мы не загружаем в память весь файл.
Мы также увидели, как реализовать загрузку с нулевым копированием с помощью Java NIOChannels. Это полезно, поскольку минимизирует количество переключений контекста, выполняемых при чтении и записи байтов, и благодаря использованию прямых буферов байты не загружаются в память приложения.
Кроме того, поскольку обычно загрузка файла осуществляется по протоколу HTTP, мы показали, как этого можно добиться с помощью библиотеки AsyncHttpClient.
Исходный код статьи доступенover on GitHub.