Scarica un file da un URL in Java

1. Introduzione

In questo tutorial vedremo diversi metodi che possiamo usare per scaricare un file.

Tratteremo esempi che vanno dall'utilizzo di base di Java IO al pacchetto NIO e alcune librerie comuni come Async Http Client e Apache Commons IO.

Infine, parleremo di come riprendere un download se la nostra connessione fallisce prima che l'intero file venga letto.

2. Utilizzo di Java IO

L'API più semplice che possiamo utilizzare per scaricare un file è Java IO. Possiamo usare la classe URL per aprire una connessione al file che vogliamo scaricare. Per leggere efficacemente il file, useremo il metodo openStream () per ottenere un InputStream:

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

Durante la lettura da un InputStream , si consiglia di avvolgerlo in un BufferedInputStream per aumentare le prestazioni.

L'aumento delle prestazioni deriva dal buffering. Quando si legge un byte alla volta utilizzando il metodo read () , ogni chiamata al metodo implica una chiamata di sistema al file system sottostante. Quando la JVM richiama la chiamata di sistema read () , il contesto di esecuzione del programma passa dalla modalità utente alla modalità kernel e viceversa.

Questo cambio di contesto è costoso dal punto di vista delle prestazioni. Quando leggiamo un numero elevato di byte, le prestazioni dell'applicazione saranno scarse, a causa di un numero elevato di cambi di contesto coinvolti.

Per scrivere i byte letti dall'URL nel nostro file locale, useremo il metodo write () dalla classe 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 }

Quando si utilizza un BufferedInputStream, il metodo read () leggerà tanti byte quanti sono stati impostati per la dimensione del buffer. Nel nostro esempio, lo stiamo già facendo leggendo blocchi di 1024 byte alla volta, quindi BufferedInputStream non è necessario.

L'esempio sopra è molto dettagliato, ma fortunatamente, a partire da Java 7, abbiamo la classe Files che contiene metodi di supporto per la gestione delle operazioni di I / O. Possiamo usare il metodo Files.copy () per leggere tutti i byte da un InputStream e copiarli in un file locale:

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

Il nostro codice funziona bene ma può essere migliorato. Il suo principale svantaggio è il fatto che i byte vengono memorizzati nel buffer.

Fortunatamente, Java ci offre il pacchetto NIO che ha metodi per trasferire byte direttamente tra 2 canali senza buffering.

Entreremo nei dettagli nella prossima sezione.

3. Utilizzo di NIO

Il pacchetto Java NIO offre la possibilità di trasferire byte tra 2 canali senza bufferizzarli nella memoria dell'applicazione.

Per leggere il file dal nostro URL, creeremo un nuovo ReadableByteChannel dal flusso URL :

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

I byte letti da ReadableByteChannel verranno trasferiti in un FileChannel corrispondente al file che verrà scaricato:

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

Useremo il metodo transferFrom () dalla classe ReadableByteChannel per scaricare i byte dall'URL fornito al nostro FileChannel :

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

I metodi transferTo () e transferFrom () sono più efficienti della semplice lettura da uno stream utilizzando un buffer. A seconda del sistema operativo sottostante, i dati possono essere trasferiti direttamente dalla cache del filesystem al nostro file senza copiare alcun byte nella memoria dell'applicazione .

Sui sistemi Linux e UNIX, questi metodi utilizzano la tecnica della copia zero che riduce il numero di cambi di contesto tra la modalità kernel e la modalità utente.

4. Utilizzo delle librerie

Negli esempi precedenti abbiamo visto come possiamo scaricare contenuti da un URL semplicemente utilizzando la funzionalità principale di Java. Possiamo anche sfruttare la funzionalità delle librerie esistenti per facilitare il nostro lavoro, quando non sono necessarie modifiche alle prestazioni.

Ad esempio, in uno scenario reale, avremmo bisogno che il nostro codice di download sia asincrono.

Potremmo racchiudere tutta la logica in un Callable , oppure potremmo usare una libreria esistente per questo.

4.1. Client HTTP asincrono

AsyncHttpClient è una libreria popolare per l'esecuzione di richieste HTTP asincrone utilizzando il framework Netty. Possiamo usarlo per eseguire una richiesta GET all'URL del file e ottenere il contenuto del file.

Innanzitutto, dobbiamo creare un client HTTP:

AsyncHttpClient client = Dsl.asyncHttpClient();

Il contenuto scaricato verrà inserito in un FileOutputStream :

FileOutputStream stream = new FileOutputStream(FILE_NAME);

Successivamente, creiamo una richiesta HTTP GET e registriamo un gestore AsyncCompletionHandler per elaborare il contenuto scaricato:

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

Notare che abbiamo sovrascritto il metodo onBodyPartReceived () . L'implementazione predefinita accumula i blocchi HTTP ricevuti in un ArrayList . Ciò potrebbe comportare un elevato consumo di memoria o un'eccezione OutOfMemory quando si tenta di scaricare un file di grandi dimensioni.

Invece di accumulare ogni HttpResponseBodyPart in memoria, utilizziamo un FileChannel per scrivere direttamente i byte nel nostro file locale . Useremo il metodo getBodyByteBuffer () per accedere al contenuto della parte del corpo tramite un ByteBuffer .

I ByteBuffer hanno il vantaggio che la memoria viene allocata al di fuori dell'heap JVM, quindi non influisce sulla memoria delle applicazioni.

4.2. Apache Commons IO

Un'altra libreria molto utilizzata per le operazioni di I / O è Apache Commons IO. Possiamo vedere da Javadoc che esiste una classe di utilità denominata FileUtils che viene utilizzata per attività generali di manipolazione dei file.

Per scaricare un file da un URL, possiamo utilizzare questa riga:

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

Dal punto di vista delle prestazioni, questo codice è lo stesso di quello che abbiamo esemplificato nella sezione 2.

Il codice sottostante utilizza gli stessi concetti di leggere in un ciclo alcuni byte da un InputStream e scriverli in un OutputStream .

Una differenza è il fatto che qui la classe URLConnection viene utilizzata per controllare i timeout di connessione in modo che il download non si blocchi per un lungo periodo di tempo:

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

5. Download ripristinabile

Considerando che le connessioni Internet di tanto in tanto falliscono, è utile per noi poter riprendere un download, invece di scaricare nuovamente il file dal byte zero.

Riscriviamo il primo esempio di prima, per aggiungere questa funzionalità.

La prima cosa che dovremmo sapere è che possiamo leggere la dimensione di un file da un dato URL senza effettivamente scaricarlo utilizzando il metodo HTTP HEAD:

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

Ora che abbiamo la dimensione totale del contenuto del file, possiamo verificare se il nostro file è stato parzialmente scaricato. In tal caso, riprenderemo il download dall'ultimo byte registrato su disco:

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

In questo articolo abbiamo visto diversi modi in cui possiamo scaricare un file da un URL in Java.

L'implementazione più comune è quella in cui bufferizziamo i byte durante le operazioni di lettura / scrittura. Questa implementazione è sicura da usare anche per file di grandi dimensioni perché non carichiamo l'intero file in memoria.

Abbiamo anche visto come possiamo implementare un download a copia zero utilizzando i canali NIO Java . Ciò è utile perché ha ridotto al minimo il numero di cambi di contesto eseguiti durante la lettura e la scrittura di byte e utilizzando i buffer diretti, i byte non vengono caricati nella memoria dell'applicazione.

Inoltre, poiché di solito il download di un file viene eseguito tramite HTTP, abbiamo mostrato come è possibile ottenere ciò utilizzando la libreria AsyncHttpClient.

Il codice sorgente dell'articolo è disponibile su GitHub.