Baixar um arquivo de um URL em Java

Baixar um arquivo de um URL em Java

1. Introdução

Neste tutorial, veremos vários métodos que podemos usar para baixar um arquivo.

Cobriremos exemplos que vão desde o uso básico de Java IO ao pacote NIO, e algumas bibliotecas comuns como Async Http Client e Apache Commons IO.

Por fim, falaremos sobre como podemos retomar um download se nossa conexão falhar antes de todo o arquivo ser lido.

2. Usando Java IO

A API mais básica que podemos usar para baixar um arquivo éJava IO. Podemos usar o sclassURL para abrir uma conexão com o arquivo que queremos baixar. Para ler o arquivo de forma eficaz, usaremos o métodoopenStream() para obter umInputStream:

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

Ao ler de umInputStream, é recomendado envolvê-lo em umBufferedInputStream para aumentar o desempenho.

O aumento de desempenho vem do buffer. Ao ler um byte por vez usando o métodoread(), cada chamada de método implica uma chamada de sistema para o sistema de arquivos subjacente. Quando a JVM invoca a chamada de sistemaread(), o contexto de execução do programa muda do modo do usuário para o modo kernel e vice-versa.

Essa troca de contexto é cara da perspectiva do desempenho. Quando lemos um grande número de bytes, o desempenho do aplicativo será ruim, devido a um grande número de alternâncias de contexto envolvidas.

Para gravar os bytes lidos do URL em nosso arquivo local, usaremos o métodowrite() do sclassFileOutputStream :

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
}

Ao usar umBufferedInputStream,, o métodoread() lerá tantos bytes quanto definimos para o tamanho do buffer. Em nosso exemplo, já estamos fazendo isso lendo blocos de 1024 bytes por vez, entãoBufferedInputStream não é necessário.

O exemplo acima é muito detalhado, mas felizmente, a partir do Java 7, temos a classeFiles que contém métodos auxiliares para lidar com operações de E / S. MétodoWe can use the Files.copy() para ler todos os bytes de umInputStreame copiá-los para um arquivo local:

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

Nosso código funciona bem, mas pode ser aprimorado. Sua principal desvantagem é o fato de os bytes serem armazenados em buffer na memória.

Felizmente, Java nos oferece o pacote NIO que possui métodos para transferir bytes diretamente entre 2Channels sem buffer.

Entraremos em detalhes na próxima seção.

3. Usando NIO

O pacoteJava NIO oferece a possibilidade de transferir bytes entre 2Channels sem armazená-los em buffer na memória do aplicativo.

Para ler o arquivo de nosso URL, criaremos um novoReadableByteChannel do fluxoURL :

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

Os bytes lidos deReadableByteChannel serão transferidos para umFileChannel correspondente ao arquivo que será baixado:

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

Usaremos o métodotransferFrom() da classeReadableByteChannel para baixar os bytes do URL fornecido para nossoFileChannel:

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

Os métodostransferTo()etransferFrom() são mais eficientes do que simplesmente ler de um fluxo usando um buffer. Dependendo do sistema operacional subjacente,the data can be transferred directly from the filesystem cache to our file without copying any bytes into the application memory.

Em sistemas Linux e UNIX, esses métodos usam a técnicazero-copy que reduz o número de alternâncias de contexto entre o modo kernel e o modo de usuário.

4. Usando Bibliotecas

Vimos nos exemplos acima como podemos baixar conteúdo de um URL apenas usando a funcionalidade principal do Java. Também podemos aproveitar a funcionalidade das bibliotecas existentes para facilitar nosso trabalho, quando ajustes de desempenho não são necessários.

Por exemplo, em um cenário do mundo real, precisaríamos que nosso código de download fosse assíncrono.

Poderíamos agrupar toda a lógica emCallable, ou poderíamos usar uma biblioteca existente para isso.

4.1. Cliente Async HTTP

AsyncHttpClient é uma biblioteca popular para a execução de solicitações HTTP assíncronas usando a estrutura Netty. Podemos usá-lo para executar uma solicitação GET no URL do arquivo e obter o conteúdo do arquivo.

Primeiro, precisamos criar um cliente HTTP:

AsyncHttpClient client = Dsl.asyncHttpClient();

O conteúdo baixado será colocado em umFileOutputStream:

FileOutputStream stream = new FileOutputStream(FILE_NAME);

A seguir, criamos uma solicitação HTTP GET e registramos um manipuladorAsyncCompletionHandler para processar o conteúdo baixado:

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 substituímos o métodoonBodyPartReceived(). The default implementation accumulates the HTTP chunks received into an ArrayList. Isso pode levar a um alto consumo de memória ou a uma exceçãoOutOfMemory ao tentar baixar um arquivo grande.

Em vez de acumular cadaHttpResponseBodyPart na memória,we use a FileChannel to write the bytes to our local file directly. Usaremos o métodogetBodyByteBuffer() para acessar o conteúdo da parte do corpo por meio de umByteBuffer.

ByteBuffers tem a vantagem de que a memória é alocada fora do heap da JVM, portanto, não afeta a memória dos aplicativos.

4.2. Apache Commons IO

Outra biblioteca muito usada para operação de E / S éApache Commons IO. Podemos ver no Javadoc que existe uma classe de utilitário chamadaFileUtils que é usada para tarefas gerais de manipulação de arquivos.

Para baixar um arquivo de uma URL, podemos usar esta linha única:

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

Do ponto de vista do desempenho, este código é o mesmo que exemplificamos na seção 2.

O código subjacente usa os mesmos conceitos de leitura em um loop de alguns bytes de umInputStreame gravá-los em umOutputStream.

Uma diferença é o fato de que aqui a classeURLConnection é usada para controlar os tempos limite de conexão para que o download não seja bloqueado por um longo período:

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

5. Download retomável

Considerando que as conexões com a Internet falham de vez em quando, é útil para nós podermos retomar um download, em vez de baixar o arquivo novamente do byte zero.

Vamos reescrever o primeiro exemplo anterior para adicionar essa funcionalidade.

A primeira coisa que devemos saber é 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();

Agora que temos o tamanho total do conteúdo do arquivo, podemos verificar se nosso arquivo foi parcialmente baixado. Nesse caso, retomaremos o download a partir do último byte gravado no disco:

long existingFileSize = outputFile.length();
if (existingFileSize < fileLength) {
    httpFileConnection.setRequestProperty(
      "Range",
      "bytes=" + existingFileSize + "-" + fileLength
    );
}

O que acontece aqui é quewe’ve configured the URLConnection to request the file bytes in a specific range. O intervalo começará no último byte baixado e terminará no byte correspondente ao tamanho do arquivo remoto.

Outra maneira comum de usar o cabeçalhoRange é fazer o download de um arquivo em partes, definindo diferentes intervalos de bytes. Por exemplo, para baixar um arquivo de 2 KB, podemos usar o intervalo de 0 a 1024 e 1024 a 2048.

Outra diferença sutil do código na seção 2. é que oFileOutputStream is opened with the append parameter set to true:

OutputStream os = new FileOutputStream(FILE_NAME, true);

Depois de fazermos essa alteração, o resto do código é idêntico ao que vimos na seção 2.

6. Conclusão

Vimos neste artigo várias maneiras de baixar um arquivo de um URL em Java.

A implementação mais comum é aquela na qual armazenamos em buffer os bytes ao executar as operações de leitura / gravação. Essa implementação é segura para usar até mesmo para arquivos grandes, porque não carregamos o arquivo inteiro na memória.

Também vimos como podemos implementar um download de cópia zero usando Java NIOChannels. Isso é útil porque minimizou o número de alternâncias de contexto feitas ao ler e gravar bytes e, usando buffers diretos, os bytes não são carregados na memória do aplicativo.

Além disso, como normalmente o download de um arquivo é feito por HTTP, mostramos como podemos fazer isso usando a biblioteca AsyncHttpClient.

O código-fonte do artigo está disponívelover on GitHub.