Télécharger un fichier depuis une URL en Java

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

1. introduction

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

Nous allons couvrir 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 manière dont nous pouvons reprendre un téléchargement si notre connexion échoue avant la lecture de l'intégralité du fichier.

2. Utilisation de Java IO

L'API la plus basique que nous pouvons utiliser pour télécharger un fichier estJava IO. Nous pouvons utiliser la classeURL pour ouvrir une connexion avec le fichier que nous voulons télécharger. Pour lire efficacement le fichier, nous utiliserons la méthodeopenStream() pour obtenir unInputStream:

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

Lors de la lecture d'unInputStream, il est recommandé de l'envelopper dans unBufferedInputStream 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 à l'aide de la méthoderead(), 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èmeread(), le contexte d'exécution du programme passe du mode utilisateur au mode noyau et inversement.

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

Pour écrire les octets lus depuis l'URL vers notre fichier local, nous utiliserons la méthodewrite() de la classeFileOutputStream :

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'unBufferedInputStream,, la méthoderead() 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, doncBufferedInputStream n'est pas nécessaire.

L'exemple ci-dessus est très détaillé, mais heureusement, à partir de Java 7, nous avons la classeFiles qui contient des méthodes d'assistance pour gérer les opérations d'E / S. MéthodeWe can use the Files.copy() pour lire tous les octets d'unInputStream 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 2Channels sans mise en mémoire tampon.

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

3. Utilisation de NIO

Le packageJava NIO offre la possibilité de transférer des octets entre 2Channels 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 nouveauReadableByteChannel à partir du sstreamURL :

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

Les octets lus depuis lesReadableByteChannel seront transférés vers unFileChannel correspondant au fichier qui sera téléchargé:

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

Nous utiliserons la méthodetransferFrom() de la classeReadableByteChannel pour télécharger les octets de l'URL donnée vers nosFileChannel:

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

Les méthodestransferTo() ettransferFrom() sont plus efficaces que la simple lecture à partir d'un flux en utilisant un tampon. Selon le système d'exploitation sous-jacent,the data can be transferred directly from the filesystem cache to our file without copying any bytes into the application memory.

Sur les systèmes Linux et UNIX, ces méthodes utilisent la techniquezero-copy 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 unCallable, 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 unFileOutputStream:

FileOutputStream stream = new FileOutputStream(FILE_NAME);

Ensuite, nous créons une requête HTTP GET et enregistrons un gestionnaireAsyncCompletionHandler 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éthodeonBodyPartReceived(). The default implementation accumulates the HTTP chunks received into an ArrayList. Cela peut entraîner une consommation de mémoire élevée ou une exceptionOutOfMemory lors de la tentative de téléchargement d'un fichier volumineux.

Au lieu d'accumuler chaqueHttpResponseBodyPart en mémoire,we use a FileChannel to write the bytes to our local file directly. Nous utiliserons la méthodegetBodyByteBuffer() pour accéder au contenu de la partie du corps via unByteBuffer.

ByteBuffers a 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

Une autre bibliothèque très utilisée pour les opérations d'E / S estApache Commons IO. Nous pouvons voir dans la Javadoc qu'il existe une classe utilitaire nomméeFileUtils 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'unInputStream et de leur écriture dans unOutputStream.

Une différence est le fait qu'ici, la classeURLConnection 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 quewe 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();

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

Ce qui se passe ici, c'est quewe’ve configured the URLConnection to request the file bytes in a specific range. La plage commencera à partir du dernier octet téléchargé et se terminera à l'octet correspondant à la taille du fichier distant.

Une autre façon courante d'utiliser l'en-têteRange consiste à télécharger un fichier par blocs en définissant différentes plages d'octets. Par exemple, pour télécharger un fichier de 2 Ko, nous pouvons utiliser les plages 0 - 1024 et 1024 - 2048.

Une autre différence subtile avec le code de la section 2. est-ce que lesFileOutputStream is opened with the append parameter set to true:

OutputStream os = new FileOutputStream(FILE_NAME, true);

Après avoir effectué cette modification, le reste du code est identique à celui que nous avons vu dans la section 2.

6. Conclusion

Nous avons vu dans cet article plusieurs manières de télécharger un fichier à partir d'une URL en Java.

L'implémentation la plus courante est celle dans laquelle nous tampons les octets lors de l'exécution des opérations de lecture / écriture. Cette mise en œuvre peut être utilisée en toute sécurité, même pour les fichiers volumineux, car nous ne chargeons pas l’ensemble du fichier en mémoire.

Nous avons également vu comment nous pouvons implémenter un téléchargement sans copie à l'aide de Java NIOChannels. Cela est utile car cela réduit 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 disponibleover on GitHub.