Скачать файл с URL в Java

Скачать файл с 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.