Изтеглете файл от 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, на четене () метод, ще прочетете като много байтове, а ние междувременно за размера на буфера. В нашия пример ние вече правим това, като четем блокове от по 1024 байта наведнъж, така че BufferedInputStream не е необходимо.

Примерът по-горе е много подробен, но за щастие от Java 7 имаме клас Files, който съдържа помощни методи за обработка на IO операции. Можем да използваме метода 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 адрес, за да ни FileChannel :

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

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

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

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

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

Например, в реален сценарий ще ни е необходим нашият код за изтегляне да бъде асинхронен.

Можем да обгърнем цялата логика в Callable или да използваме съществуваща библиотека за това.

4.1. Async HTTP клиент

AsyncHttpClient е популярна библиотека за изпълнение на асинхронни HTTP заявки с помощта на Netty framework. Можем да го използваме, за да изпълним 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 в паметта, ние използваме FileChannel за директно записване на байтовете в нашия локален файл . Ще използваме метода getBodyByteBuffer () за достъп до съдържанието на част от тялото чрез ByteBuffer .

ByteBuffers имат предимството, че паметта се разпределя извън JVM купчината, така че това не засяга паметта на приложенията.

4.2. Apache Commons IO

Друга високо използвана библиотека за 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 ); }

What happens here is that we've configured the URLConnection to request the file bytes in a specific range. The range will start from the last downloaded byte and will end at the byte corresponding to the size of the remote file.

Another common way to use the Range header is for downloading a file in chunks by setting different byte ranges. For example, to download 2 KB file, we can use the range 0 – 1024 and 1024 – 2048.

Another subtle difference from the code at section 2. is that the FileOutputStream is opened with the append parameter set to true:

OutputStream os = new FileOutputStream(FILE_NAME, true);

After we've made this change the rest of the code is identical to the one we've seen in section 2.

6. Conclusion

В тази статия видяхме няколко начина, по които можем да изтеглим файл от URL в Java.

Най-често срещаната реализация е тази, при която буферираме байтовете, когато изпълняваме операциите за четене / запис. Тази реализация е безопасна за използване дори за големи файлове, защото не зареждаме целия файл в паметта.

Видяхме също как можем да реализираме изтегляне с нулево копие, използвайки Java NIO Channels . Това е полезно, защото намалява до минимум броя превключватели на контекста, извършени при четене и запис на байтове и чрез използване на директни буфери байтовете не се зареждат в паметта на приложението.

Освен това, тъй като обикновено изтеглянето на файл се извършва през HTTP, ние показахме как можем да постигнем това с помощта на библиотеката AsyncHttpClient.

Изходният код на статията е достъпен в GitHub.