Télécharger un fichier à partir d'une URL en Java

1. Introduction

Dans ce tutoriel, nous verrons plusieurs méthodes que nous pouvons utiliser pour télécharger un fichier.

Nous couvrirons des exemples allant de l'utilisation de base de Java IO au package NIO, en passant par certaines bibliothèques courantes telles que Async Http Client et Apache Commons IO.

Enfin, nous parlerons de la façon dont nous pouvons reprendre un téléchargement si notre connexion échoue avant que le fichier entier ne soit lu.

2. Utilisation de Java IO

L'API la plus basique que nous pouvons utiliser pour télécharger un fichier est Java IO. Nous pouvons utiliser la classe URL pour ouvrir une connexion avec le fichier que nous voulons télécharger. Pour lire efficacement le fichier, nous utiliserons la méthode openStream () pour obtenir un InputStream:

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

Lors de la lecture à partir d'un InputStream , il est recommandé de l'envelopper dans un BufferedInputStream pour augmenter les performances.

L'augmentation des performances provient de la mise en mémoire tampon. Lors de la lecture d'un octet à la fois en utilisant la méthode read () , chaque appel de méthode implique un appel système au système de fichiers sous-jacent. Lorsque la machine virtuelle Java appelle l' appel système read () , le contexte d'exécution du programme passe du mode utilisateur au mode noyau et inversement.

Ce changement de contexte est coûteux du point de vue des performances. Lorsque nous lisons un grand nombre d'octets, les performances de l'application seront médiocres, en raison d'un grand nombre de changements de contexte impliqués.

Pour écrire les octets lus depuis l'URL dans notre fichier local, nous utiliserons la méthode write () de la 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 }

Lors de l'utilisation d'un BufferedInputStream, la méthode read () lira autant d'octets que nous avons défini pour la taille du tampon. Dans notre exemple, nous le faisons déjà en lisant des blocs de 1024 octets à la fois, donc BufferedInputStream n'est pas nécessaire.

L'exemple ci-dessus est très détaillé, mais heureusement, à partir de Java 7, nous avons la classe Files qui contient des méthodes d'assistance pour gérer les opérations d'E / S. Nous pouvons utiliser la méthode Files.copy () pour lire tous les octets d'un InputStream et les copier dans un fichier local:

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

Notre code fonctionne bien mais peut être amélioré. Son principal inconvénient est le fait que les octets sont mis en mémoire tampon.

Heureusement, Java nous offre le package NIO qui a des méthodes pour transférer des octets directement entre 2 canaux sans mise en mémoire tampon.

Nous entrerons dans les détails dans la section suivante.

3. Utilisation de NIO

Le package Java NIO offre la possibilité de transférer des octets entre 2 canaux sans les mettre en mémoire tampon dans la mémoire de l'application.

Pour lire le fichier à partir de notre URL, nous allons créer un nouveau ReadableByteChannel à partir du flux d' URL :

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

Les octets lus depuis le ReadableByteChannel seront transférés vers un FileChannel correspondant au fichier qui sera téléchargé:

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

Nous utiliserons la méthode transferFrom () de la classe ReadableByteChannel pour télécharger les octets de l'URL donnée vers notre FileChannel :

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

Les méthodes transferTo () et transferFrom () sont plus efficaces que la simple lecture à partir d'un flux en utilisant un tampon. Selon le système d'exploitation sous-jacent, les données peuvent être transférées directement du cache du système de fichiers vers notre fichier sans copier d'octets dans la mémoire de l'application .

Sur les systèmes Linux et UNIX, ces méthodes utilisent la technique de copie zéro qui réduit le nombre de changements de contexte entre le mode noyau et le mode utilisateur.

4. Utilisation des bibliothèques

Nous avons vu dans les exemples ci-dessus comment nous pouvons télécharger du contenu à partir d'une URL simplement en utilisant la fonctionnalité principale de Java. Nous pouvons également tirer parti des fonctionnalités des bibliothèques existantes pour faciliter notre travail, lorsque des ajustements de performances ne sont pas nécessaires.

Par exemple, dans un scénario réel, nous aurions besoin que notre code de téléchargement soit asynchrone.

Nous pourrions envelopper toute la logique dans un Callable , ou nous pourrions utiliser une bibliothèque existante pour cela.

4.1. Client HTTP asynchrone

AsyncHttpClient est une bibliothèque populaire pour exécuter des requêtes HTTP asynchrones à l'aide du framework Netty. Nous pouvons l'utiliser pour exécuter une requête GET sur l'URL du fichier et obtenir le contenu du fichier.

Tout d'abord, nous devons créer un client HTTP:

AsyncHttpClient client = Dsl.asyncHttpClient();

Le contenu téléchargé sera placé dans un FileOutputStream :

FileOutputStream stream = new FileOutputStream(FILE_NAME);

Ensuite, nous créons une requête HTTP GET et enregistrons un gestionnaire AsyncCompletionHandler pour traiter le contenu téléchargé:

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

Notez que nous avons remplacé la méthode onBodyPartReceived () . L'implémentation par défaut accumule les fragments HTTP reçus dans une ArrayList . Cela peut entraîner une consommation de mémoire élevée ou une exception OutOfMemory lors de la tentative de téléchargement d'un fichier volumineux.

Au lieu d'accumuler chaque HttpResponseBodyPart en mémoire, nous utilisons un FileChannel pour écrire directement les octets dans notre fichier local . Nous utiliserons la méthode getBodyByteBuffer () pour accéder au contenu de la partie du corps via un ByteBuffer .

Les ByteBuffers ont l'avantage que la mémoire est allouée en dehors du tas JVM, donc cela n'affecte pas la mémoire des applications.

4.2. Apache Commons IO

Apache Commons IO est une autre bibliothèque très utilisée pour les opérations d'E / S. Nous pouvons voir dans Javadoc qu'il existe une classe utilitaire nommée FileUtils qui est utilisée pour les tâches générales de manipulation de fichiers.

Pour télécharger un fichier à partir d'une URL, nous pouvons utiliser ce one-liner:

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

Du point de vue des performances, ce code est le même que celui que nous avons illustré dans la section 2.

Le code sous-jacent utilise les mêmes concepts de lecture en boucle de certains octets d'un InputStream et de leur écriture dans un OutputStream .

Une différence est le fait qu'ici la classe URLConnection est utilisée pour contrôler les délais de connexion afin que le téléchargement ne se bloque pas pendant une longue période:

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

5. Téléchargement avec reprise

Étant donné que les connexions Internet échouent de temps en temps, il est utile pour nous de pouvoir reprendre un téléchargement, au lieu de télécharger à nouveau le fichier à partir de l'octet zéro.

Réécrivons le premier exemple précédent, pour ajouter cette fonctionnalité.

La première chose que nous devons savoir est que nous pouvons lire la taille d'un fichier à partir d'une URL donnée sans le télécharger réellement en utilisant la méthode HTTP HEAD:

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

Maintenant que nous avons la taille totale du contenu du fichier, nous pouvons vérifier si notre fichier est partiellement téléchargé. Si tel est le cas, nous reprendrons le téléchargement à partir du dernier octet enregistré sur le disque:

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

Nous avons vu dans cet article plusieurs façons dont nous pouvons télécharger un fichier à partir d'une URL en Java.

L'implémentation la plus courante est celle dans laquelle nous tamponnons les octets lors de l'exécution des opérations de lecture / écriture. Cette implémentation est sûre à utiliser même pour les fichiers volumineux car nous ne chargeons pas le fichier entier en mémoire.

Nous avons également vu comment nous pouvons implémenter un téléchargement sans copie à l'aide des canaux Java NIO . Cela est utile car cela minimise le nombre de changements de contexte effectués lors de la lecture et de l'écriture d'octets et en utilisant des tampons directs, les octets ne sont pas chargés dans la mémoire de l'application.

De plus, comme le téléchargement d'un fichier se fait généralement via HTTP, nous avons montré comment nous pouvons y parvenir en utilisant la bibliothèque AsyncHttpClient.

Le code source de l'article est disponible à l'adresse over sur GitHub.