Fazendo download de um arquivo dos controladores de mola

387

Tenho um requisito no qual preciso fazer o download de um PDF no site. O PDF precisa ser gerado dentro do código, que eu pensei que seria uma combinação de freemarker e uma estrutura de geração de PDF como o iText. Alguma maneira melhor?

No entanto, meu principal problema é como permito que o usuário baixe um arquivo através de um Spring Controller?

MilindaD
fonte
2
É importante mencionar que Spring Framework mudou muito desde 2011, para que possa fazê-lo de forma reativa, bem como - aqui é um exemplo
Krzysztof Skrzynecki

Respostas:

397
@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
public void getFile(
    @PathVariable("file_name") String fileName, 
    HttpServletResponse response) {
    try {
      // get your file as InputStream
      InputStream is = ...;
      // copy it to response's OutputStream
      org.apache.commons.io.IOUtils.copy(is, response.getOutputStream());
      response.flushBuffer();
    } catch (IOException ex) {
      log.info("Error writing file to output stream. Filename was '{}'", fileName, ex);
      throw new RuntimeException("IOError writing file to output stream");
    }

}

De um modo geral, quando você tem response.getOutputStream(), pode escrever qualquer coisa lá. Você pode transmitir esse fluxo de saída como um local para colocar o PDF gerado no seu gerador. Além disso, se você souber que tipo de arquivo está enviando, poderá definir

response.setContentType("application/pdf");
Infeligo
fonte
4
Isso é praticamente o que eu estava prestes a dizer, mas você provavelmente também deve definir o cabeçalho do tipo de resposta como algo apropriado para o arquivo.
GaryF
2
Sim, apenas editei a postagem. Como eu tinha vários tipos de arquivo gerados, deixei o navegador para determinar o tipo de conteúdo do arquivo por sua extensão.
Infeligo
Esqueceu a flushBuffer, graças ao seu post, eu vi por que o meu não estava trabalhando :-)
Jan Vladimir Mostert
35
Algum motivo específico para usar o Apache em IOUtilsvez do Spring FileCopyUtils?
Powerlord 18/09/12
3
Aqui está uma solução melhor: stackoverflow.com/questions/16652760/…
Dmytro Plekhotkin
290

Consegui transmitir isso usando o suporte interno do Spring com o ResourceHttpMessageConverter. Isso definirá o comprimento e o tipo de conteúdo, se puder determinar o tipo mime

@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
@ResponseBody
public FileSystemResource getFile(@PathVariable("file_name") String fileName) {
    return new FileSystemResource(myService.getFileFor(fileName)); 
}
Scott Carlson
fonte
10
Isso funciona. Mas o arquivo (arquivo .csv) é exibido no navegador e não é baixado - como posso forçar o download do navegador?
chzbrgla
41
Você pode adicionar produz = MediaType.APPLICATION_OCTET_STREAM_VALUE ao @RequestMapping à força de download
David Kago
8
Além disso, você deve adicionar <bean class = "org.springframework.http.converter.ResourceHttpMessageConverter" /> à lista messageConverters (<mvc: acionado por anotação> <mvc: acionado por anotação> <mvc: conversores de mensagem>)
Sllouyssgort
4
Existe uma maneira de definir o Content-Dispositioncabeçalho dessa maneira?
21815 Ralph
8
Eu não precisava disso, mas acho que você pode adicionar HttpResponse como parâmetro ao método e, em seguida, "response.setHeader (" Content-Disposition "," anexo; filename; somefile.pdf ");"
22815 Scott Schaffner
82

Você deve poder escrever o arquivo diretamente na resposta. Algo como

response.setContentType("application/pdf");      
response.setHeader("Content-Disposition", "attachment; filename=\"somefile.pdf\""); 

e depois grave o arquivo como um fluxo binário response.getOutputStream(). Lembre-se de fazer response.flush()no final e isso deve ser feito.

lobster1234
fonte
8
não é a maneira 'Spring' de definir o tipo de conteúdo dessa maneira? @RequestMapping(value = "/foo/bar", produces = "application/pdf")
Black
4
@Francis, e se o seu aplicativo baixar diferentes tipos de arquivo? A resposta do Lobster1234 permite definir dinamicamente a disposição do conteúdo.
Rose
2
isso é verdade @Rose, mas acredito que seria melhor prática para definir diferentes pontos finais por formato
preto
3
Acho que não, porque não é escalável. Atualmente, estamos apoiando uma dúzia de tipos de recursos. Podemos oferecer suporte a mais tipos de arquivos com base no que os usuários desejam fazer upload. Nesse caso, podemos acabar com tantos pontos finais que essencialmente fazem a mesma coisa. IMHO deve haver apenas um ponto final de download e lida com vários tipos de arquivos. @Francis
Rose
3
é absolutamente "escalável", mas podemos concordar em discordar se é a melhor prática
preto
74

Com o Spring 3.0, você pode usar o HttpEntityobjeto de retorno. Se você usar isso, seu controlador não precisará de um HttpServletResponseobjeto e, portanto, é mais fácil testar. Exceto isso, essa resposta é relativa igual à do infeligo .

Se o valor de retorno da sua estrutura pdf for uma matriz de bytes (leia a segunda parte da minha resposta para outros valores de retorno) :

@RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public HttpEntity<byte[]> createPdf(
                 @PathVariable("fileName") String fileName) throws IOException {

    byte[] documentBody = this.pdfFramework.createPdf(filename);

    HttpHeaders header = new HttpHeaders();
    header.setContentType(MediaType.APPLICATION_PDF);
    header.set(HttpHeaders.CONTENT_DISPOSITION,
                   "attachment; filename=" + fileName.replace(" ", "_"));
    header.setContentLength(documentBody.length);

    return new HttpEntity<byte[]>(documentBody, header);
}

Se o tipo de retorno do seu PDF Framework ( documentBbody) ainda não for uma matriz de bytes (e também não ByteArrayInputStream), seria sensato NÃO torná-la uma matriz de bytes primeiro. Em vez disso, é melhor usar:

exemplo com FileSystemResource:

@RequestMapping(value = "/files/{fileName}", method = RequestMethod.GET)
public HttpEntity<byte[]> createPdf(
                 @PathVariable("fileName") String fileName) throws IOException {

    File document = this.pdfFramework.createPdf(filename);

    HttpHeaders header = new HttpHeaders();
    header.setContentType(MediaType.APPLICATION_PDF);
    header.set(HttpHeaders.CONTENT_DISPOSITION,
                   "attachment; filename=" + fileName.replace(" ", "_"));
    header.setContentLength(document.length());

    return new HttpEntity<byte[]>(new FileSystemResource(document),
                                  header);
}
Ralph
fonte
11
-1, isso carregará desnecessariamente o arquivo inteiro na memória e pode facilmente gerar OutOfMemoryErrors.
Faisal Feroz
11
@FaisalFeroz: sim, está certo, mas o documento do arquivo é criado na memória (veja a pergunta: "O PDF precisa ser gerado dentro do código"). Enfim - qual é a sua solução que supera esse problema?
Ralph
11
Você também pode usar o ResponseEntity, que é um super HttpEntity, que permite especificar o código de status http da resposta. Exemplo:return new ResponseEntity<byte[]>(documentBody, headers, HttpStatus.CREATED)
Amr Mostafa
@ Amr Mostafa: ResponseEntityé uma subclasse de HttpEntity(mas entendo), por outro lado, 201 CREATED não é o que eu usaria quando retornasse apenas uma visualização dos dados. (consulte w3.org/Protocols/rfc2616/rfc2616-sec10.html para 201 CREATED )
Ralph
11
Existe uma razão pela qual você está substituindo espaços em branco por sublinhado no nome do arquivo? Você pode colocá-lo entre aspas para enviar o nome real.
Alexandru Severin
63

Se vocês:

  • Não deseja carregar o arquivo inteiro em um arquivo byte[]antes de enviá-lo para a resposta;
  • Deseja / precisa enviar / fazer o download via InputStream;
  • Deseja ter controle total do tipo MIME e do nome do arquivo enviado;
  • Tenha outras @ControllerAdviceexceções para você (ou não).

O código abaixo é o que você precisa:

@RequestMapping(value = "/stuff/{stuffId}", method = RequestMethod.GET)
public ResponseEntity<FileSystemResource> downloadStuff(@PathVariable int stuffId)
                                                                      throws IOException {
    String fullPath = stuffService.figureOutFileNameFor(stuffId);
    File file = new File(fullPath);
    long fileLength = file.length(); // this is ok, but see note below

    HttpHeaders respHeaders = new HttpHeaders();
    respHeaders.setContentType("application/pdf");
    respHeaders.setContentLength(fileLength);
    respHeaders.setContentDispositionFormData("attachment", "fileNameIwant.pdf");

    return new ResponseEntity<FileSystemResource>(
        new FileSystemResource(file), respHeaders, HttpStatus.OK
    );
}

Sobre a parte do tamanho do arquivo : File#length()deve ser boa o suficiente no caso geral, mas pensei em fazer essa observação porque ela pode ser lenta ; nesse caso, você deve armazená-la anteriormente (por exemplo, no DB). Os casos em que pode ser lento incluem: se o arquivo é grande, especialmente se o arquivo estiver em um sistema remoto ou algo mais elaborado como esse - um banco de dados, talvez.



InputStreamResource

Se o seu recurso não for um arquivo, por exemplo, você coleta os dados do banco de dados, você deve usá-lo InputStreamResource. Exemplo:

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);
acdcjunior
fonte
Você não recomenda o uso da classe FileSystemResource?
Stephane
Na verdade, eu acredito que não há problema em usar o FileSystemResourcelá. É ainda aconselhável se o seu recurso for um arquivo . Nesta amostra, FileSystemResourcepode ser usado onde InputStreamResourceestá.
Acdcjunior
Sobre a parte do cálculo do tamanho do arquivo: Se você está preocupado, não fique. File#length()deve ser bom o suficiente no caso geral. Eu acabei de mencionar porque pode ser lento , especialmente se o arquivo estiver em um sistema remoto ou em algo mais elaborado assim - um banco de dados, talvez? Mas preocupe-se apenas se isso se tornar um problema (ou se você tiver provas concretas de que ele se tornará um), não antes. O ponto principal é: você está fazendo um esforço para transmitir o arquivo, se tiver que pré-carregar tudo antes, então o streaming acaba sem fazer diferença, não é?
Acdcjunior
por que o código acima não está funcionando para mim? Ele baixa 0 arquivo de bytes. Eu verifiquei e verifiquei se os conversores ByteArray e ResourceMessage estão lá. Estou esquecendo de algo ?
Codigo_idiot
Por que você está se preocupando com os conversores ByteArray e ResourceMessage?
precisa saber é o seguinte
20

Este código está funcionando bem para baixar um arquivo automaticamente do spring controller clicando em um link no jsp.

@RequestMapping(value="/downloadLogFile")
public void getLogFile(HttpSession session,HttpServletResponse response) throws Exception {
    try {
        String filePathToBeServed = //complete file name with path;
        File fileToDownload = new File(filePathToBeServed);
        InputStream inputStream = new FileInputStream(fileToDownload);
        response.setContentType("application/force-download");
        response.setHeader("Content-Disposition", "attachment; filename="+fileName+".txt"); 
        IOUtils.copy(inputStream, response.getOutputStream());
        response.flushBuffer();
        inputStream.close();
    } catch (Exception e){
        LOGGER.debug("Request could not be completed at this moment. Please try again.");
        e.printStackTrace();
    }

}
Sunil
fonte
14

O código abaixo funcionou para gerar e baixar um arquivo de texto.

@RequestMapping(value = "/download", method = RequestMethod.GET)
public ResponseEntity<byte[]> getDownloadData() throws Exception {

    String regData = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
    byte[] output = regData.getBytes();

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("charset", "utf-8");
    responseHeaders.setContentType(MediaType.valueOf("text/html"));
    responseHeaders.setContentLength(output.length);
    responseHeaders.set("Content-disposition", "attachment; filename=filename.txt");

    return new ResponseEntity<byte[]>(output, responseHeaders, HttpStatus.OK);
}
Siva Kumar
fonte
5

O que consigo pensar rapidamente é: gerar o pdf e armazená-lo em webapp / downloads / <RANDOM-FILENAME> .pdf a partir do código e enviar um encaminhamento para este arquivo usando HttpServletRequest

request.getRequestDispatcher("/downloads/<RANDOM-FILENAME>.pdf").forward(request, response);

ou se você pode configurar seu resolvedor de visualizações como,

  <bean id="pdfViewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="viewClass"
              value="org.springframework.web.servlet.view.JstlView" />
    <property name="order" value=”2″/>
    <property name="prefix" value="/downloads/" />
    <property name="suffix" value=".pdf" />
  </bean>

então é só voltar

return "RANDOM-FILENAME";
kalyan
fonte
11
Se eu precisar de dois resolvedores de exibição, como também posso retornar o nome do resolvedor ou escolhê-lo no controlador?
Azerafati
3

A seguinte solução funciona para mim

    @RequestMapping(value="/download")
    public void getLogFile(HttpSession session,HttpServletResponse response) throws Exception {
        try {

            String fileName="archivo demo.pdf";
            String filePathToBeServed = "C:\\software\\Tomcat 7.0\\tmpFiles\\";
            File fileToDownload = new File(filePathToBeServed+fileName);

            InputStream inputStream = new FileInputStream(fileToDownload);
            response.setContentType("application/force-download");
            response.setHeader("Content-Disposition", "attachment; filename="+fileName); 
            IOUtils.copy(inputStream, response.getOutputStream());
            response.flushBuffer();
            inputStream.close();
        } catch (Exception exception){
            System.out.println(exception.getMessage());
        }

    }
Jorge Santos Neill
fonte
2

algo como abaixo

@RequestMapping(value = "/download", method = RequestMethod.GET)
public void getFile(HttpServletResponse response) {
    try {
        DefaultResourceLoader loader = new DefaultResourceLoader();
        InputStream is = loader.getResource("classpath:META-INF/resources/Accepted.pdf").getInputStream();
        IOUtils.copy(is, response.getOutputStream());
        response.setHeader("Content-Disposition", "attachment; filename=Accepted.pdf");
        response.flushBuffer();
    } catch (IOException ex) {
        throw new RuntimeException("IOError writing file to output stream");
    }
}

Você pode exibir PDF ou fazer o download de exemplos aqui

xxy
fonte
1

Se isso ajuda alguém. Você pode fazer o que a resposta aceita pelo Infeligo sugeriu, mas basta colocar esse bit extra no código para um download forçado.

response.setContentType("application/force-download");
Sagar Nair
fonte
0

Esta pode ser uma resposta útil.

É possível exportar dados como formato pdf no frontend?

Estendendo a isso, adicionar a disposição do conteúdo como um anexo (padrão) fará o download do arquivo. Se você quiser visualizá-lo, precisará configurá-lo para embutido.

a próxima grande coisa
fonte
0

No meu caso, estou gerando algum arquivo sob demanda, portanto também é necessário gerar um URL.

Para mim, funciona algo assim:

@RequestMapping(value = "/files/{filename:.+}", method = RequestMethod.GET, produces = "text/csv")
@ResponseBody
public FileSystemResource getFile(@PathVariable String filename) {
    String path = dataProvider.getFullPath(filename);
    return new FileSystemResource(new File(path));
}

Muito importante é o tipo MIME producese, além disso, esse nome do arquivo faz parte do link, portanto você deve usar @PathVariable.

O código HTML é assim:

<a th:href="@{|/dbreport/files/${file_name}|}">Download</a>

Onde ${file_name}é gerado por Thymeleaf no controlador e é isto: result_20200225.csv, de modo que apontam toda url behing é: example.com/aplication/dbreport/files/result_20200225.csv.

Depois de clicar no link, o navegador me pergunta o que fazer com o arquivo - salve ou abra.

Tomasz Dzięcielewski
fonte