Laden Sie eine Datei von einer URL in Java herunter

1. Einleitung

In diesem Tutorial sehen wir verschiedene Methoden, mit denen wir eine Datei herunterladen können.

Wir werden Beispiele behandeln, die von der grundlegenden Verwendung von Java IO bis zum NIO-Paket reichen, sowie einige gängige Bibliotheken wie Async Http Client und Apache Commons IO.

Schließlich werden wir darüber sprechen, wie wir einen Download fortsetzen können, wenn unsere Verbindung fehlschlägt, bevor die gesamte Datei gelesen wird.

2. Verwenden von Java IO

Die grundlegendste API, die wir zum Herunterladen einer Datei verwenden können, ist Java IO. Wir können die URL- Klasse verwenden, um eine Verbindung zu der Datei herzustellen, die wir herunterladen möchten. Um die Datei effektiv zu lesen, verwenden wir die openStream () -Methode, um einen InputStream zu erhalten :

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

Wenn Sie aus einem InputStream lesen , wird empfohlen, ihn in einen BufferedInputStream zu packen , um die Leistung zu erhöhen.

Die Leistungssteigerung resultiert aus der Pufferung. Wenn jeweils ein Byte mit der Methode read () gelesen wird, impliziert jeder Methodenaufruf einen Systemaufruf an das zugrunde liegende Dateisystem. Wenn die JVM den Systemaufruf read () aufruft , wechselt der Programmausführungskontext vom Benutzermodus in den Kernelmodus und zurück.

Dieser Kontextwechsel ist aus Sicht der Leistung teuer. Wenn wir eine große Anzahl von Bytes lesen, ist die Anwendungsleistung aufgrund einer großen Anzahl von beteiligten Kontextwechseln schlecht.

Zum Schreiben der von der URL gelesenen Bytes in unsere lokale Datei verwenden wir die write () -Methode aus der FileOutputStream- Klasse:

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 }

Bei Verwendung eines BufferedInputStream liest die read () -Methode so viele Bytes, wie wir für die Puffergröße festgelegt haben. In unserem Beispiel lesen wir bereits Blöcke mit jeweils 1024 Byte, sodass BufferedInputStream nicht erforderlich ist.

Das obige Beispiel ist sehr ausführlich, aber zum Glück haben wir ab Java 7 die Files- Klasse, die Hilfsmethoden für die Verarbeitung von E / A-Operationen enthält. Mit der Files.copy () -Methode können wir alle Bytes aus einem InputStream lesen und in eine lokale Datei kopieren:

InputStream in = new URL(FILE_URL).openStream(); Files.copy(in, Paths.get(FILE_NAME), StandardCopyOption.REPLACE_EXISTING);

Unser Code funktioniert gut, kann aber verbessert werden. Sein Hauptnachteil ist die Tatsache, dass die Bytes in den Speicher gepuffert werden.

Glücklicherweise bietet uns Java das NIO-Paket an, das Methoden zum direkten Übertragen von Bytes zwischen zwei Kanälen ohne Pufferung bietet .

Wir werden im nächsten Abschnitt auf Details eingehen.

3. Verwenden von NIO

Das Java NIO-Paket bietet die Möglichkeit, Bytes zwischen zwei Kanälen zu übertragen, ohne sie in den Anwendungsspeicher zu puffern.

Um die Datei von unserer URL zu lesen, erstellen wir einen neuen ReadableByteChannel aus dem URL- Stream:

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

Die vom ReadableByteChannel gelesenen Bytes werden in einen FileChannel übertragen , der der herunterzuladenden Datei entspricht:

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

Wir verwenden die transferFrom () -Methode aus der ReadableByteChannel- Klasse, um die Bytes von der angegebenen URL in unseren FileChannel herunterzuladen :

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

Die Methoden transferTo () und transferFrom () sind effizienter als das einfache Lesen aus einem Stream mithilfe eines Puffers. Abhängig vom zugrunde liegenden Betriebssystem können die Daten direkt aus dem Dateisystem-Cache in unsere Datei übertragen werden, ohne dass Bytes in den Anwendungsspeicher kopiert werden müssen .

Auf Linux- und UNIX-Systemen verwenden diese Methoden die Nullkopie- Technik, mit der die Anzahl der Kontextwechsel zwischen dem Kernelmodus und dem Benutzermodus verringert wird.

4. Verwenden von Bibliotheken

In den obigen Beispielen haben wir gesehen, wie wir Inhalte von einer URL herunterladen können, indem wir nur die Java-Kernfunktionalität verwenden. Wir können auch die Funktionalität vorhandener Bibliotheken nutzen, um unsere Arbeit zu vereinfachen, wenn keine Leistungsverbesserungen erforderlich sind.

In einem realen Szenario müsste unser Download-Code beispielsweise asynchron sein.

Wir könnten die gesamte Logik in ein Callable einbinden oder eine vorhandene Bibliothek dafür verwenden.

4.1. Asynchroner HTTP-Client

AsyncHttpClient ist eine beliebte Bibliothek zum Ausführen asynchroner HTTP-Anforderungen mithilfe des Netty-Frameworks. Wir können es verwenden, um eine GET-Anforderung an die Datei-URL auszuführen und den Dateiinhalt abzurufen.

Zuerst müssen wir einen HTTP-Client erstellen:

AsyncHttpClient client = Dsl.asyncHttpClient();

Der heruntergeladene Inhalt wird in einen FileOutputStream gestellt :

FileOutputStream stream = new FileOutputStream(FILE_NAME);

Als Nächstes erstellen wir eine HTTP-GET-Anforderung und registrieren einen AsyncCompletionHandler- Handler, um den heruntergeladenen Inhalt zu verarbeiten:

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; } })

Beachten Sie, dass wir die onBodyPartReceived () -Methode überschrieben haben . Die Standardimplementierung sammelt die empfangenen HTTP-Chunks in einer ArrayList . Dies kann zu einem hohen Speicherverbrauch oder einer OutOfMemory- Ausnahme führen, wenn versucht wird, eine große Datei herunterzuladen.

Anstatt jedes HttpResponseBodyPart im Speicher zu akkumulieren , verwenden wir einen FileChannel , um die Bytes direkt in unsere lokale Datei zu schreiben . Wir werden die Methode getBodyByteBuffer () verwenden, um über einen ByteBuffer auf den Inhalt des Körperteils zuzugreifen .

ByteBuffers have the advantage that the memory is allocated outside of the JVM heap, so it doesn't affect out applications memory.

4.2. Apache Commons IO

Another highly used library for IO operation is Apache Commons IO. We can see from the Javadoc that there's a utility class named FileUtils that is used for general file manipulation tasks.

To download a file from a URL, we can use this one-liner:

FileUtils.copyURLToFile( new URL(FILE_URL), new File(FILE_NAME), CONNECT_TIMEOUT, READ_TIMEOUT);

From a performance standpoint, this code is the same as the one we've exemplified in section 2.

The underlying code uses the same concepts of reading in a loop some bytes from an InputStream and writing them to an OutputStream.

One difference is the fact that here the URLConnection class is used to control the connection timeouts so that the download doesn't block for a large amount of time:

URLConnection connection = source.openConnection(); connection.setConnectTimeout(connectionTimeout); connection.setReadTimeout(readTimeout);

5. Resumable Download

Considering internet connections fail from time to time, it's useful for us to be able to resume a download, instead of downloading the file again from byte zero.

Let's rewrite the first example from earlier, to add this functionality.

The first thing we should know is that 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();

Now that we have the total content size of the file, we can check whether our file is partially downloaded. If so, we'll resume the download from the last byte recorded on disk:

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

Wir haben in diesem Artikel verschiedene Möglichkeiten gesehen, wie wir eine Datei von einer URL in Java herunterladen können.

Die häufigste Implementierung ist die, bei der die Bytes beim Ausführen der Lese- / Schreiboperationen gepuffert werden. Diese Implementierung kann auch für große Dateien sicher verwendet werden, da nicht die gesamte Datei in den Speicher geladen wird.

Wir haben auch gesehen, wie wir einen Zero-Copy-Download mithilfe von Java NIO- Kanälen implementieren können . Dies ist nützlich, da dadurch die Anzahl der Kontextwechsel beim Lesen und Schreiben von Bytes minimiert wird und durch die Verwendung direkter Puffer die Bytes nicht in den Anwendungsspeicher geladen werden.

Da das Herunterladen einer Datei normalerweise über HTTP erfolgt, haben wir gezeigt, wie dies mithilfe der AsyncHttpClient-Bibliothek erreicht werden kann.

Der Quellcode für den Artikel ist auf GitHub verfügbar.