Descargar un archivo desde una URL en Java

1. Introducción

En este tutorial, veremos varios métodos que podemos usar para descargar un archivo.

Cubriremos ejemplos que van desde el uso básico de Java IO hasta el paquete NIO y algunas bibliotecas comunes como Async Http Client y Apache Commons IO.

Finalmente, hablaremos sobre cómo podemos reanudar una descarga si nuestra conexión falla antes de que se lea todo el archivo.

2. Usando Java IO

La API más básica que podemos usar para descargar un archivo es Java IO. Podemos usar la clase URL para abrir una conexión al archivo que queremos descargar. Para leer el archivo de manera efectiva, usaremos el método openStream () para obtener un InputStream:

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

Al leer de un InputStream , se recomienda envolverlo en un BufferedInputStream para aumentar el rendimiento.

El aumento de rendimiento proviene del almacenamiento en búfer. Al leer un byte a la vez usando el método read () , cada llamada al método implica una llamada al sistema al sistema de archivos subyacente. Cuando la JVM invoca la llamada al sistema read () , el contexto de ejecución del programa cambia del modo de usuario al modo de kernel y viceversa.

Este cambio de contexto es caro desde la perspectiva del rendimiento. Cuando leemos una gran cantidad de bytes, el rendimiento de la aplicación será deficiente debido a la gran cantidad de cambios de contexto involucrados.

Para escribir los bytes leídos desde la URL a nuestro archivo local, usaremos el método write () de la clase 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 }

Cuando se utiliza un BufferedInputStream, el método read () leerá tantos bytes como establezcamos para el tamaño del búfer. En nuestro ejemplo, ya lo estamos haciendo leyendo bloques de 1024 bytes a la vez, por lo que BufferedInputStream no es necesario.

El ejemplo anterior es muy detallado, pero afortunadamente, a partir de Java 7, tenemos la clase Archivos que contiene métodos auxiliares para manejar operaciones IO. Podemos usar el método Files.copy () para leer todos los bytes de un InputStream y copiarlos a un archivo local:

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

Nuestro código funciona bien pero se puede mejorar. Su principal inconveniente es el hecho de que los bytes se almacenan en la memoria.

Afortunadamente, Java nos ofrece el paquete NIO que tiene métodos para transferir bytes directamente entre 2 canales sin búfer.

Entraremos en detalles en la siguiente sección.

3. Usando NIO

El paquete Java NIO ofrece la posibilidad de transferir bytes entre 2 canales sin almacenarlos en búfer en la memoria de la aplicación.

Para leer el archivo de nuestra URL, crearemos un nuevo ReadableByteChannel a partir de la secuencia de URL :

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

Los bytes leídos del ReadableByteChannel se transferirán a un FileChannel correspondiente al archivo que se descargará:

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

Usaremos el método transferFrom () de la clase ReadableByteChannel para descargar los bytes de la URL dada a nuestro FileChannel :

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

Los métodos transferTo () y transferFrom () son más eficientes que simplemente leer de una secuencia usando un búfer. Dependiendo del sistema operativo subyacente, los datos se pueden transferir directamente desde la caché del sistema de archivos a nuestro archivo sin copiar ningún byte en la memoria de la aplicación .

En los sistemas Linux y UNIX, estos métodos utilizan la técnica de copia cero que reduce el número de cambios de contexto entre el modo de kernel y el modo de usuario.

4. Uso de bibliotecas

Hemos visto en los ejemplos anteriores cómo podemos descargar contenido desde una URL simplemente usando la funcionalidad principal de Java. También podemos aprovechar la funcionalidad de las bibliotecas existentes para facilitar nuestro trabajo, cuando no se necesitan ajustes de rendimiento.

Por ejemplo, en un escenario del mundo real, necesitaríamos que nuestro código de descarga sea asincrónico.

Podríamos envolver toda la lógica en un Callable , o podríamos usar una biblioteca existente para esto.

4.1. Cliente HTTP asíncrono

AsyncHttpClient es una biblioteca popular para ejecutar solicitudes HTTP asincrónicas utilizando el marco de Netty. Podemos usarlo para ejecutar una solicitud GET a la URL del archivo y obtener el contenido del archivo.

Primero, necesitamos crear un cliente HTTP:

AsyncHttpClient client = Dsl.asyncHttpClient();

El contenido descargado se colocará en un FileOutputStream :

FileOutputStream stream = new FileOutputStream(FILE_NAME);

A continuación, creamos una solicitud HTTP GET y registramos un controlador AsyncCompletionHandler para procesar el contenido descargado:

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

Observe que hemos anulado el método onBodyPartReceived () . La implementación predeterminada acumula los fragmentos HTTP recibidos en una ArrayList . Esto podría provocar un alto consumo de memoria o una excepción de OutOfMemory al intentar descargar un archivo grande.

En lugar de acumular cada HttpResponseBodyPart en la memoria, usamos un FileChannel para escribir los bytes en nuestro archivo local directamente . Usaremos el método getBodyByteBuffer () para acceder al contenido de la parte del cuerpo a través de un ByteBuffer .

Los ByteBuffers tienen la ventaja de que la memoria se asigna fuera del montón de JVM, por lo que no afecta la memoria de las aplicaciones.

4.2. Apache Commons IO

Otra biblioteca muy utilizada para el funcionamiento de IO es Apache Commons IO. Podemos ver en el Javadoc que hay una clase de utilidad llamada FileUtils que se usa para tareas generales de manipulación de archivos.

Para descargar un archivo desde una URL, podemos usar este resumen:

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

Desde el punto de vista del rendimiento, este código es el mismo que ejemplificamos en la sección 2.

El código subyacente usa los mismos conceptos de leer en un bucle algunos bytes de un InputStream y escribirlos en un OutputStream .

Una diferencia es el hecho de que aquí la clase URLConnection se usa para controlar los tiempos de espera de la conexión para que la descarga no se bloquee durante una gran cantidad de tiempo:

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

5. Descarga reanudable

Teniendo en cuenta que las conexiones a Internet fallan de vez en cuando, es útil para nosotros poder reanudar una descarga, en lugar de descargar el archivo nuevamente desde el byte cero.

Reescribamos el primer ejemplo anterior para agregar esta funcionalidad.

Lo primero que debemos saber es que podemos leer el tamaño de un archivo de una URL determinada sin tener que descargarlo utilizando el método HTTP HEAD:

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

Ahora que tenemos el tamaño total del contenido del archivo, podemos verificar si nuestro archivo está parcialmente descargado. Si es así, reanudaremos la descarga desde el último byte grabado en el 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

Hemos visto en este artículo varias formas en las que podemos descargar un archivo desde una URL en Java.

La implementación más común es aquella en la que almacenamos los bytes cuando realizamos las operaciones de lectura / escritura. Esta implementación es segura de usar incluso para archivos grandes porque no cargamos el archivo completo en la memoria.

También hemos visto cómo podemos implementar una descarga de copia cero mediante los canales NIO de Java . Esto es útil porque minimiza el número de cambios de contexto que se realizan al leer y escribir bytes y, al usar búferes directos, los bytes no se cargan en la memoria de la aplicación.

Además, debido a que generalmente la descarga de un archivo se realiza a través de HTTP, hemos mostrado cómo podemos lograr esto usando la biblioteca AsyncHttpClient.

El código fuente del artículo está disponible en GitHub.