Laden Sie eine Datei von einer URL in Java herunter

Laden Sie eine Datei von einer URL in Java herunter

1. Einführung

In diesem Tutorial werden verschiedene Methoden vorgestellt, 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.

Abschließend 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, istJava IO. Wir können dieURL -Skala verwenden, um eine Verbindung zu der Datei herzustellen, die wir herunterladen möchten. Um die Datei effektiv zu lesen, verwenden wir dieopenStream()-Methode, umInputStream: zu erhalten

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

Beim Lesen vonInputStream wird empfohlen, es inBufferedInputStream einzuwickeln, um die Leistung zu erhöhen.

Die Leistungssteigerung resultiert aus der Pufferung. Beim Lesen von jeweils einem Byte mit der Methoderead() impliziert jeder Methodenaufruf einen Systemaufruf an das zugrunde liegende Dateisystem. Wenn die JVM den Systemaufrufread()aufruft, wechselt der Programmausführungskontext vom Benutzermodus in den Kernelmodus und zurück.

Dieser Kontextwechsel ist aus Performance-Sicht 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 Methodewrite()aus der KlasseFileOutputStream :

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 vonBufferedInputStream, liest die Methoderead() so viele Bytes, wie wir für die Puffergröße festgelegt haben. In unserem Beispiel lesen wir bereits Blöcke mit jeweils 1024 Byte, sodassBufferedInputStream nicht erforderlich ist.

Das obige Beispiel ist sehr ausführlich, aber zum Glück haben wir ab Java 7 dieFiles-Klasse, die Hilfsmethoden für die Verarbeitung von E / A-Operationen enthält. We can use the Files.copy() Methode, um alle Bytes vonInputStream zu lesen und in eine lokale Datei zu 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 2Channels ohne Pufferung bietet.

Wir werden im nächsten Abschnitt auf Details eingehen.

3. Verwenden von NIO

Das PaketJava NIO bietet die Möglichkeit, Bytes zwischen 2Channels zu übertragen, ohne sie in den Anwendungsspeicher zu puffern.

Um die Datei von unserer URL zu lesen, erstellen wir ein neuesReadableByteChannel aus demURL -Sstream:

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

Die ausReadableByteChannel gelesenen Bytes werden inFileChannel übertragen, die der herunterzuladenden Datei entsprechen:

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

Wir verwenden dietransferFrom()-Methode aus derReadableByteChannel-Klasse, um die Bytes von der angegebenen URL in unsereFileChannel herunterzuladen:

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

Die MethodentransferTo() undtransferFrom() sind effizienter als das einfache Lesen aus einem Stream mit einem Puffer. Abhängig vom zugrunde liegenden Betriebssystemthe data can be transferred directly from the filesystem cache to our file without copying any bytes into the application memory.

Auf Linux- und UNIX-Systemen verwenden diese Methoden diezero-copy-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 muss unser Download-Code beispielsweise asynchron sein.

Wir könnten die gesamte Logik inCallable umschließen oder eine vorhandene Bibliothek dafür verwenden.

4.1. Asynchroner HTTP-Client

AsyncHttpClient ist eine beliebte Bibliothek zum Ausführen asynchroner HTTP-Anforderungen mit dem Netty-Framework. 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 inFileOutputStream abgelegt:

FileOutputStream stream = new FileOutputStream(FILE_NAME);

Als Nächstes erstellen wir eine HTTP-GET-Anforderung und registrieren einenAsyncCompletionHandler-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 dieonBodyPartReceived()-Methode überschrieben haben. The default implementation accumulates the HTTP chunks received into an ArrayList. Dies kann zu einem hohen Speicherverbrauch oder einer Ausnahme vonOutOfMemory führen, wenn versucht wird, eine große Datei herunterzuladen.

Anstatt jedesHttpResponseBodyPart im Speicher zu akkumulieren,we use a FileChannel to write the bytes to our local file directly. Wir verwenden diegetBodyByteBuffer()-Methode, um überByteBuffer auf den Körperteilinhalt zuzugreifen.

ByteBuffers haben den Vorteil, dass der Speicher außerhalb des JVM-Heaps zugewiesen wird, sodass der Anwendungsspeicher nicht beeinträchtigt wird.

4.2. Apache Commons IO

Eine weitere häufig verwendete Bibliothek für den E / A-Betrieb istApache Commons IO. Aus dem Javadoc geht hervor, dass es eine Dienstprogrammklasse mit dem NamenFileUtils gibt, die für allgemeine Dateimanipulationsaufgaben verwendet wird.

Um eine Datei von einer URL herunterzuladen, können wir diesen Einzeiler verwenden:

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

Unter Leistungsgesichtspunkten entspricht dieser Code dem Code, den wir in Abschnitt 2 veranschaulicht haben.

Der zugrunde liegende Code verwendet dieselben Konzepte, bei denen einige Bytes aus einemInputStream in einer Schleife gelesen und in einOutputStream geschrieben werden.

Ein Unterschied besteht darin, dass hier die KlasseURLConnectionverwendet wird, um die Verbindungszeitüberschreitungen so zu steuern, dass der Download nicht für längere Zeit blockiert wird:

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

5. Wiederaufnahme des Downloads

Da Internetverbindungen von Zeit zu Zeit fehlschlagen, ist es für uns hilfreich, den Download fortzusetzen, anstatt die Datei erneut von Byte Null herunterzuladen.

Schreiben wir das erste Beispiel von früher neu, um diese Funktionalität hinzuzufügen.

Das erste, was wir wissen sollten, ist, dasswe 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();

Nachdem wir die gesamte Inhaltsgröße der Datei haben, können wir prüfen, ob unsere Datei teilweise heruntergeladen wurde. In diesem Fall setzen wir den Download des letzten auf der Festplatte aufgezeichneten Bytes fort:

long existingFileSize = outputFile.length();
if (existingFileSize < fileLength) {
    httpFileConnection.setRequestProperty(
      "Range",
      "bytes=" + existingFileSize + "-" + fileLength
    );
}

Was hier passiert ist, dasswe’ve configured the URLConnection to request the file bytes in a specific range. Der Bereich beginnt mit dem zuletzt heruntergeladenen Byte und endet mit dem Byte, das der Größe der entfernten Datei entspricht.

Eine andere übliche Methode zur Verwendung desRange-Headers ist das Herunterladen einer Datei in Blöcken durch Festlegen verschiedener Bytebereiche. Zum Herunterladen einer 2-KB-Datei können Sie beispielsweise den Bereich 0 - 1024 und 1024 - 2048 verwenden.

Ein weiterer subtiler Unterschied zum Code in Abschnitt 2. ist, dass dieFileOutputStream is opened with the append parameter set to true:

OutputStream os = new FileOutputStream(FILE_NAME, true);

Nachdem wir diese Änderung vorgenommen haben, ist der Rest des Codes identisch mit dem in Abschnitt 2 gezeigten.

6. Fazit

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

Die gebräuchlichste Implementierung ist die, bei der wir die Bytes beim Ausführen der Lese- / Schreiboperationen puffern. 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 mit Java NIOChannels einen Download ohne Kopie implementieren können. Dies ist nützlich, weil dadurch die Anzahl der Kontextwechsel beim Lesen und Schreiben von Bytes minimiert und durch die Verwendung von direkten Puffern 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 istover on GitHub verfügbar.