Download een bestand van een URL in Java

1. Inleiding

In deze tutorial zullen we verschillende methoden zien die we kunnen gebruiken om een ​​bestand te downloaden.

We behandelen voorbeelden variërend van het basisgebruik van Java IO tot het NIO-pakket, en enkele veelgebruikte bibliotheken zoals Async Http Client en Apache Commons IO.

Ten slotte zullen we het hebben over hoe we een download kunnen hervatten als onze verbinding mislukt voordat het hele bestand is gelezen.

2. Java IO gebruiken

De eenvoudigste API die we kunnen gebruiken om een ​​bestand te downloaden, is Java IO. We kunnen de URL class om een ​​verbinding te openen met het bestand dat we willen downloaden. Om het bestand effectief te lezen, gebruiken we de openStream () methode om een Invoerstroom:

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

Bij het lezen van een InputStream, wordt aanbevolen om het in een BufferedInputStream om de prestaties te verbeteren.

De prestatieverbetering komt van buffering. Bij het lezen van één byte per keer met de lezen() method impliceert elke methodeaanroep een systeemaanroep naar het onderliggende bestandssysteem. Wanneer de JVM het lezen() systeemoproep, schakelt de context van de programma-uitvoering van gebruikersmodus naar kernelmodus en terug.

Deze contextwisseling is duur vanuit prestatieoogpunt. Als we een groot aantal bytes lezen, zullen de prestaties van de applicatie slecht zijn vanwege een groot aantal betrokken contextschakelaars.

Voor het schrijven van de gelezen bytes van de URL naar ons lokale bestand, gebruiken we de schrijven() methode van de FileOutputStream klasse:

probeer (BufferedInputStream in = nieuwe BufferedInputStream (nieuwe URL (FILE_URL) .openStream ()); FileOutputStream fileOutputStream = nieuwe FileOutputStream (FILE_NAME)) {byte dataBuffer [] = nieuwe byte [1024]; int bytesRead; while ((bytesRead = in.read (dataBuffer, 0, 1024))! = -1) {fileOutputStream.write (dataBuffer, 0, bytesRead); }} catch (IOException e) {// handle exception}

Bij gebruik van een BufferedInputStream, de lezen() methode leest zoveel bytes als we hebben ingesteld voor de buffergrootte. In ons voorbeeld doen we dit al door blokken van 1024 bytes tegelijk te lezen, dus BufferedInputStream is niet nodig.

Het bovenstaande voorbeeld is erg uitgebreid, maar gelukkig hebben we vanaf Java 7 de Bestanden klasse die hulpmethoden bevat voor het afhandelen van IO-bewerkingen. We kunnen de Files.copy () methode om alle bytes van een InputStream en kopieer ze naar een lokaal bestand:

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

Onze code werkt goed, maar kan worden verbeterd. Het belangrijkste nadeel is dat de bytes in het geheugen worden gebufferd.

Gelukkig biedt Java ons het NIO-pakket dat methoden heeft om bytes rechtstreeks tussen 2 Kanalen zonder buffering.

We zullen in de volgende sectie op details ingaan.

3. NIO gebruiken

Het Java NIO-pakket biedt de mogelijkheid om bytes tussen 2 Kanalen zonder ze in het toepassingsgeheugen te bufferen.

Om het bestand van onze URL te lezen, maken we een nieuw ReadableByteChannel van de URL stroom:

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

De bytes die worden gelezen van het ReadableByteChannel wordt overgebracht naar een FileChannel overeenkomend met het bestand dat wordt gedownload:

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

We gebruiken de transferFrom () methode van de ReadableByteChannel class om de bytes van de opgegeven URL naar onze FileChannel:

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

De Overzetten naar() en transferFrom () methoden zijn efficiënter dan simpelweg lezen uit een stream met behulp van een buffer. Afhankelijk van het onderliggende besturingssysteem, de gegevens kunnen rechtstreeks van de bestandssysteemcache naar ons bestand worden overgebracht zonder enige bytes naar het toepassingsgeheugen te kopiëren.

Op Linux- en UNIX-systemen gebruiken deze methoden de nul-kopie techniek die het aantal contextwisselingen tussen de kernelmodus en gebruikersmodus vermindert.

4. Bibliotheken gebruiken

We hebben in de bovenstaande voorbeelden gezien hoe we inhoud van een URL kunnen downloaden door alleen de Java-kernfunctionaliteit te gebruiken. We kunnen ook gebruikmaken van de functionaliteit van bestaande bibliotheken om ons werk te vergemakkelijken, wanneer prestatieaanpassingen niet nodig zijn.

In een realistisch scenario hebben we bijvoorbeeld onze downloadcode asynchroon nodig.

We kunnen alle logica in een Oproepbaar, of we kunnen hiervoor een bestaande bibliotheek gebruiken.

4.1. Async HTTP-client

AsyncHttpClient is een populaire bibliotheek voor het uitvoeren van asynchrone HTTP-verzoeken met behulp van het Netty-framework. We kunnen het gebruiken om een ​​GET-verzoek naar de bestands-URL uit te voeren en de bestandsinhoud op te halen.

Eerst moeten we een HTTP-client maken:

AsyncHttpClient-client = Dsl.asyncHttpClient ();

De gedownloade inhoud wordt in een FileOutputStream:

FileOutputStream stream = nieuwe FileOutputStream (FILE_NAME);

Vervolgens maken we een HTTP GET-verzoek en registreren we een AsyncCompletionHandler handler om de gedownloade inhoud te verwerken:

client.prepareGet (FILE_URL) .execute (new AsyncCompletionHandler () {@Override public State onBodyPartReceived (HttpResponseBodyPart bodyPart) genereert uitzondering {stream.getChannel (). write (bodyPart.getBodyByteideBuffer (). FileOutputStream onCompleted (antwoordrespons) genereert uitzondering {retourstroom;}})

Merk op dat we het onBodyPartReceived () methode. De standaardimplementatie verzamelt de ontvangen HTTP-chunks in een ArrayList. Dit kan leiden tot een hoog geheugengebruik of een OutOfMemory uitzondering bij het downloaden van een groot bestand.

In plaats van elk te verzamelen HttpResponseBodyPart in het geheugen, we gebruiken een FileChannel om de bytes rechtstreeks naar ons lokale bestand te schrijven. We gebruiken de getBodyByteBuffer () methode om toegang te krijgen tot de inhoud van het lichaamsdeel via een ByteBuffer.

ByteBuffers het voordeel hebben dat het geheugen buiten de JVM-heap wordt toegewezen, zodat het geen invloed heeft op het geheugen van de applicaties.

4.2. Apache Commons IO

Een andere veelgebruikte bibliotheek voor IO-bewerkingen is Apache Commons IO. We kunnen aan de Javadoc zien dat er een utility-klasse is met de naam FileUtils dat wordt gebruikt voor algemene bestandsmanipulatietaken.

Om een ​​bestand van een URL te downloaden, kunnen we deze oneliner gebruiken:

FileUtils.copyURLToFile (nieuwe URL (FILE_URL), nieuw bestand (FILE_NAME), CONNECT_TIMEOUT, READ_TIMEOUT);

Vanuit het oogpunt van prestaties is deze code dezelfde als degene die we hebben toegelicht in sectie 2.

De onderliggende code gebruikt dezelfde concepten voor het in een lus lezen van enkele bytes van een InputStream en ze schrijven naar een OutputStream.

Een verschil is het feit dat hier de URLConnection class wordt gebruikt om de time-outs van de verbinding te regelen, zodat de download niet gedurende een lange tijd wordt geblokkeerd:

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

5. Hervatbare download

Aangezien internetverbindingen af ​​en toe mislukken, is het handig voor ons om een ​​download te kunnen hervatten in plaats van het bestand opnieuw te downloaden vanaf byte nul.

Laten we het eerste voorbeeld van eerder herschrijven om deze functionaliteit toe te voegen.

Het eerste dat we moeten weten, is dat we kunnen de grootte van een bestand van een bepaalde URL lezen zonder het daadwerkelijk te downloaden door de HTTP HEAD-methode te gebruiken:

URL url = nieuwe URL (FILE_URL); HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection (); httpConnection.setRequestMethod ("HEAD"); lang removeFileSize = httpConnection.getContentLengthLong ();

Nu we de totale inhoudsgrootte van het bestand hebben, kunnen we controleren of ons bestand gedeeltelijk is gedownload. Als dit het geval is, hervatten we de download vanaf de laatste byte die op schijf is geregistreerd:

lang bestaandeFileSize = outputFile.length (); if (bestaandeFileSize <bestandsLengte) {httpFileConnection.setRequestProperty ("Bereik", "bytes =" + bestaandeFileSize + "-" + bestandslengte); }

Wat hier gebeurt, is dat we hebben de URLConnection om de bestandsbytes in een specifiek bereik op te vragen. Het bereik begint vanaf de laatst gedownloade byte en eindigt bij de byte die overeenkomt met de grootte van het externe bestand.

Een andere veel voorkomende manier om de Bereik header is voor het downloaden van een bestand in brokken door verschillende bytebereiken in te stellen. Om bijvoorbeeld een bestand van 2 KB te downloaden, kunnen we het bereik 0 - 1024 en 1024 - 2048 gebruiken.

Een ander subtiel verschil met de code in sectie 2. is dat de FileOutputStream wordt geopend met de toevoegen parameter ingesteld op true:

OutputStream os = nieuwe FileOutputStream (FILE_NAME, true);

Nadat we deze wijziging hebben aangebracht, is de rest van de code identiek aan degene die we in sectie 2 hebben gezien.

6. Conclusie

We hebben in dit artikel verschillende manieren gezien waarop we een bestand kunnen downloaden vanaf een URL in Java.

De meest gebruikelijke implementatie is die waarin we de bytes bufferen bij het uitvoeren van de lees- / schrijfbewerkingen. Deze implementatie is veilig te gebruiken, zelfs voor grote bestanden, omdat we niet het hele bestand in het geheugen laden.

We hebben ook gezien hoe we een download zonder kopieën kunnen implementeren met behulp van Java NIO Kanalen. Dit is handig omdat het het aantal contextwisselingen dat wordt uitgevoerd bij het lezen en schrijven van bytes tot een minimum beperkt, en door directe buffers te gebruiken, worden de bytes niet in het toepassingsgeheugen geladen.

Omdat het downloaden van een bestand meestal gebeurt via HTTP, hebben we laten zien hoe we dit kunnen bereiken met behulp van de AsyncHttpClient-bibliotheek.

De broncode voor het artikel is beschikbaar op GitHub.