Como disponibilizar arquivos para download em Java
Entenda como sua aplicação Web pode disponibilizar arquivos para download usando a API Servlet
Cedo ou tarde você precisará lidar com upload e download de arquivos de seus clientes na sua aplicação Web. Seja um relatório em PDF ou Excel, a foto do perfil do cliente, um arquivo XML, um arquivo de backup zipado, ou mesmo o instalador .exe
da sua aplicação de caixinha. Elas são tarefas rotineiras no desenvolvimento Web que são triviais de implementar quando usamos algum framework MVC. Nesse caso, o framework se responsabiliza de abstrair todos os detalhes para o desenvolvedor, ele faz toda a mágica... mas o que acontece por debaixo dos panos?
Independente do framework MVC que você usa, é importante que você entenda parte desse processo. Você não precisa entender detalhe por detalhe, mas também não pode ficar como um cego num tiroteio. Por se tratar de algo inerente a qualquer aplicação Web acaba sendo ainda mais importante conhecer como as coisas funcionam. Já se perguntou alguma vez como o download de um arquivo funciona?
Por exemplo, para disponibilizar um arquivo para download com o VRaptor basta o código abaixo:
@Controller
public class PerfilController {
public Download foto(Perfil perfil) {
File file = new File("/caminho/foto." + perfil.getId() + ".jpg");
String contentType = "image/jpg";
String filename = perfil.getNome() + ".jpg";
return new FileDownload(file, contentType, filename);
}
}
Se você quiser o VRaptor pode abstrair ainda mais para você. Olha o que podemos fazer para simplificar a vida do desenvolvedor:
public File foto(Perfil perfil) {
return new File("/caminho/foto-" + perfil.getId() + ".jpg");
}
Fantástico, não é? Mais fácil que isso só tendo o arquivo em um diretório público no servidor Web onde o usuário possa baixá-lo através de um link.
Mas o que acontece com o arquivo? Como ele é enviado para o navegador? O arquivo será exibido na página ou vai abrir a janelinha de download? Se você parar para pensar você perceberá que não é comum fazer estes questionamentos até ter algum tipo de problema, até a coisa toda feder...
Fazendo download de arquivos
Em qualquer aplicação Web, independente do framework MVC que você utiliza, para disponibilizar um arquivo para download será preciso escrever seu conteúdo na resposta do protocolo HTTP. Escrever o conteúdo de um arquivo no response é tão simples quanto escrever em um arquivo em disco, basicamente precisamos obter o fluxo de saída do response e escrever todos os bytes nele. Algo como:
File arquivo = new File("/Users/rponte/minha-foto.png");
Path path = arquivo.toPath();
HttpServletResponse response = // pega response da servlet ou framework mvc
OutputStream output = response.getOutputStream();
Files.copy(path, output); // escreve bytes no fluxo de saída
Pronto! Basicamente o que fizemos foi encontrar o arquivo na máquina e escrevê-lo na saída padrão da resposta HTTP. Sem mistérios! Este código é suficiente para disponibilizar qualquer arquivo para o browser, porém ele seria aberto diretamente na página do seu navegador, como abaixo:
Muitas vezes não queremos isso. Não queremos renderizar o arquivo diretamente no navegador, mas somente disponibilizá-lo para download. Assim o usuário pode decidir se grava o arquivo em disco ou abre diretamente no navegador. O problema é que não passamos informações suficientes na resposta para o navegador tratar o arquivo. Para você ter idéia, as únicas informações enviadas pela aplicação foram:
rponte$ curl -X GET -I "http://localhost:8080/downloads/perfil/foto"
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
Date: Wed, 18 Nov 2015 00:03:52 GMT
No código acima eu usei o comando curl do Linux para enviar uma requisição HTTP do tipo GET e depurar a resposta. Assim podemos ver exatamente o que o servidor devolveu para o browser.
Neste caso, o browser por padrão tentará exibir o arquivo da mesma forma como ele faria com um HTML. Mas como dizer pro navegador que queremos que o download seja feito? Estou falando daquela janelinha Salvar como..., lembra?
Para que o download aconteça nós precisamos ajudar o navegador informando alguns detalhes do que estamos enviando para ele, como o nome e tipo do conteúdo do arquivo, seu tamanho e, claro, que seja feito o download. Nós conseguimos isso definindo alguns cabeçalhos na resposta HTTP. Dessa forma, vamos escrever estes cabeçalhos antes de começar a escrever o arquivo no fluxo de saída:
File arquivo = new File("/Users/rponte/minha-foto.png");
int tamanho = (int) arquivo.length();
HttpServletResponse response = // obtem response da servlet ou framework
response.setContentType("image/png"); // tipo do conteúdo na resposta
response.setContentLength(tamanho); // opcional. ajuda na barra de progresso
response.setHeader("Content-Disposition", "attachment; filename=perfil.png");
OutputStream output = response.getOutputStream();
Files.copy(arquivo.toPath(), output); // escreve bytes no fluxo de saída
A mágica para disponibilizar o arquivo para download está no cabeçalho Content-Disposition
. O valor attachment
indica que queremos que um download seja feito; para renderizar diretamente na página usamos inline
. Aproveitamos ainda para definir o nome (filename
) do arquivo para o caso do usuário baixá-lo. No final, o que o navegador exibe para o usuário é algo similar a esta janela:
Se depurarmos novamente a resposta HTTP referente ao código acima teremos o resultado a seguir:
rponte$ curl -X GET -I "http://localhost:8080/downloads/perfil/foto"
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Disposition: attachment; filename="perfil.png"
Content-Type: image/png
Content-Length: 44323
Date: Wed, 18 Nov 2015 00:03:34 GMT
Definimos apenas 3 cabeçalhos HTTP para ajudar o browser a manipular o corpo da resposta. Eles são:
- Content-Type: informa o tipo do conteúdo na resposta. Numa página html normalmente retornamos
Content-Type: text/html; charset=utf-8
; - Content-Length: informa o tamanho do corpo da resposta, algo como
Content-Length: 348
; - Content-Disposition: informa como o arquivo deve ser arranjado e nos dá a oportunidade de sugerir o nome do arquivo pro navegador. Exemplo:
Content-Disposition: attachment; filename="planilha-vendas.xls"
;
Legal, agora vamos mover nosso código para dentro de uma Servlet e ver como fica. Nossa Servlet não seria muito diferente disto:
@WebServlet("/perfil/foto")
public class DownloadFileServlet extends HttpServlet {
@Override
void doGet(HttpServletRequest request, HttpServletResponse response) {
File arquivo = new File("/Users/rponte/minha-foto.png");
String nome = arquivo.getName();
int tamanho = (int) arquivo.length();
response.setContentType("image/png"); // tipo do conteúdo
response.setContentLength(tamanho); // opcional
response.setHeader("Content-Disposition", "attachment; filename=\"" + nome + "\"");
OutputStream output = response.getOutputStream();
Files.copy(path, output);
}
}
Agora basta acessar a url http://localhost:8080/app/perfil/foto
no seu navegador para baixar a minha foto personalizada.
Repare que no código acima buscamos o arquivo dentro do sistema de arquivos. De um diretório pré definido pela aplicação. Se o arquivo estiver dentro da aplicação, precisaríamos de um código um pouquinho diferente:
ServletContext contexto = request.getServletContext();
String path = contexto.getRealPath("/fotos/minha-foto.png");
O método getRealPath
busca o arquivo a partir da raiz da aplicação e retorna o caminho absoluto no sistema de arquivos. O caminho que passamos é relativo ao diretório web do projeto na sua IDE. No caso do Eclipse com WTP seria por padrão o diretório WebContent
, como a seguir:
MinhaAppWeb
|-- src
| :
|
|-- WebContent
| |-- META-INF
| | `-- MANIFEST.MF
| |-- WEB-INF
| | `-- web.xml
| |-- fotos
| | `-- minha-foto.png // sua foto aqui!
| |-- index.jsp
:
Os frameworks web MVC nos ajudam a ser mais produtivos abstraindo o que é comum e complicado. Isto é muito válido e devemos valorizá-los por isso... além do que, nas vagas de emprego as empresas pedem isso, certo? No entanto, é importante que você, desenvolvedor, tenha uma idéia de como as coisas ocorrem na sua aplicação e servidor Web, como o protocolo HTTP funciona minimamente e como analisá-lo a fim de encontrar problemas e gargalos.
Existe uma lei no desenvolvimento de software que diz que toda abstração vaza seus detalhes cedo ou tarde. Não importa o framework ou tecnologia que você usa, ela vai vazar na sua abstração e, você desenvolvedor, deve estar preparado para isso. O JSF vai vazar detalhes da API Servlet e HTTP; a JPA vai vazar detalhes do EclipseLink; o Hibernate vai vazar complexidades da JDBC e SQL; o Spring e CDI complicarão sua vida com uma API baixo nível em determinados casos; e assim todos os frameworks...
O que estou querendo dizer é que entender como uma Servlet ou servidor Web funciona é mais importante do que aprender o último framework da moda. É o que sempre discutimos em todos os nossos cursos, em especial o curso Java para Web: quando você aprende como um framework web MVC funciona você passa a entender como todos os frameworks MVC funcionam, sejam eles Java, Ruby, .Net e por aí vai. Por esse motivo, em nosso curso, reforçamos a importância desse aprendizado construindo juntamente com os alunos um framework MVC.
E aí, você já precisou abrir mão do seu framework e trabalhar mais baixo nível porque ele não resolvia seu problema?
Desenvolvedor e instrutor na TriadWorks