Download di un file dai controller di primavera


387

Ho un requisito in cui devo scaricare un PDF dal sito Web. Il PDF deve essere generato all'interno del codice, che ho pensato fosse una combinazione di freemarker e un framework di generazione PDF come iText. Qualche modo migliore?

Tuttavia, il mio problema principale è come posso consentire all'utente di scaricare un file tramite un controller Spring?


2
Vale la pena ricordare che Spring Framework è cambiato molto dal 2011, quindi puoi farlo anche in modo reattivo - ecco un esempio
Krzysztof Skrzynecki

Risposte:


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

}

In generale, quando hai response.getOutputStream(), puoi scrivere qualsiasi cosa lì. È possibile passare questo flusso di output come luogo in cui inserire il PDF generato nel generatore. Inoltre, se sai quale tipo di file stai inviando, puoi impostare

response.setContentType("application/pdf");

4
Questo è praticamente quello che stavo per dire, ma probabilmente dovresti anche impostare l'intestazione del tipo di risposta su qualcosa di appropriato per il file.
GaryF,

2
Sì, ho appena modificato il post. Ho generato vari tipi di file, quindi l'ho lasciato al browser per determinare il tipo di contenuto del file in base all'estensione.
Infeligo,

Ho dimenticato il flushBuffer, grazie al tuo post ho visto perché il mio non funzionava :-)
Jan Vladimir Mostert,

35
Qualche motivo particolare per usare Apache IOUtilsinvece di Spring FileCopyUtils?
Powerlord,


290

Sono stato in grado di eseguire lo streaming di questa linea utilizzando il supporto integrato in Spring con ResourceHttpMessageConverter. Ciò imposterà la lunghezza e il tipo di contenuto se è in grado di determinare il 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)); 
}

10
Questo funziona Ma il file (file .csv) viene visualizzato nel browser e non scaricato - come posso forzare il download del browser?
Chzbrgla,

41
Puoi aggiungere produce = MediaType.APPLICATION_OCTET_STREAM_VALUE a @RequestMapping per forzare il download
David Kago,

8
Inoltre è necessario aggiungere <bean class = "org.springframework.http.converter.ResourceHttpMessageConverter" /> all'elenco messageConverters (<mvc: annotation-driven> <mvc: message-
convertters

4
C'è un modo per impostare l' Content-Dispositionintestazione in questo modo?
Ralph,

8
Non ne avevo bisogno, ma penso che potresti aggiungere HttpResponse come parametro al metodo e quindi "response.setHeader (" Content-Disposition "," allegato; nomefile = somefile.pdf ");"
Scott Carlson,

82

Dovresti essere in grado di scrivere direttamente il file sulla risposta. Qualcosa di simile a

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

e quindi scrivere il file come flusso binario su response.getOutputStream(). Ricordati di fare response.flush()alla fine e questo dovrebbe farlo.


8
non è il modo "Primavera" di impostare il tipo di contenuto in questo modo? @RequestMapping(value = "/foo/bar", produces = "application/pdf")
Black,

4
@Francis cosa succede se l'applicazione scarica diversi tipi di file? La risposta di Lobster1234 ti consente di impostare dinamicamente la disposizione dei contenuti.
Rose,

2
questo è vero @Rose, ma credo che sarebbe una buona pratica definire diversi end-point per formato
Nero,

3
Immagino di no, perché non è scalabile. Attualmente stiamo supportando una dozzina di tipi di risorse. Potremmo supportare più tipi di file in base a ciò che gli utenti vogliono caricare in quel caso, potremmo finire con così tanti endpoint che essenzialmente fanno la stessa cosa. IMHO deve esserci solo un endpoint di download e gestisce una moltitudine di tipi di file. @Francis
Rose,

3
è assolutamente "scalabile", ma possiamo concordare di non essere d'accordo sul fatto che sia la migliore pratica
Black

74

Con Spring 3.0 puoi usare l' HttpEntityoggetto return. Se lo usi, il tuo controller non ha bisogno di un HttpServletResponseoggetto, quindi è più facile testarlo. Tranne questo, questa risposta è relativa uguale a quella di Infeligo .

Se il valore di ritorno del tuo framework pdf è un array di byte (leggi la seconda parte della mia risposta per altri valori di ritorno) :

@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 il tipo restituito del tuo PDF Framework ( documentBbody) non è già un array di byte (e anche no ByteArrayInputStream), sarebbe saggio NON renderlo prima un array di byte. Invece è meglio usare:

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

11
-1 questo caricherà inavvertitamente l'intero file in memoria e può facilmente incanalare OutOfMemoryErrors.
Faisal Feroz,

1
@FaisalFeroz: sì, è vero, ma il documento file è comunque creato in memoria (vedi la domanda: "Il PDF deve essere generato all'interno del codice"). In ogni caso, qual è la tua soluzione per superare questo problema?
Ralph

1
Puoi anche utilizzare ResponseEntity che è un super di HttpEntity che ti consente di specificare il codice di stato http di risposta. Esempio:return new ResponseEntity<byte[]>(documentBody, headers, HttpStatus.CREATED)
Amr Mostafa,

@Amr Mostafa: ResponseEntityè una sottoclasse di HttpEntity(ma ho capito) d'altra parte 201 CREATED non è quello che userei quando restituisco solo una vista ai dati. (vedi w3.org/Protocols/rfc2616/rfc2616-sec10.html per 201 CREATED)
Ralph

1
C'è un motivo per cui stai sostituendo gli spazi bianchi con trattino basso nel nome del file? Puoi racchiuderlo tra virgolette per inviare il nome effettivo.
Alexandru Severin,

63

Se tu:

  • Non voglio caricare l'intero file in a byte[]prima di inviarlo alla risposta;
  • Vuoi / devi inviarlo / scaricarlo via InputStream;
  • Desideri avere il pieno controllo del tipo Mime e del nome file inviato;
  • Avere altre @ControllerAdviceeccezioni di raccolta per te (o meno).

Il codice qui sotto è quello che ti serve:

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

Informazioni sulla parte della lunghezza del file : File#length()dovrebbe essere abbastanza buona nel caso generale, ma ho pensato di fare questa osservazione perché può essere lenta , nel qual caso dovresti averla memorizzata in precedenza (ad es. Nel DB). I casi che possono essere lenti includono: se il file è grande, specialmente se il file si trova in un sistema remoto o qualcosa di più elaborato in quel modo - un database, forse.



InputStreamResource

Se la tua risorsa non è un file, ad esempio raccogli i dati dal DB, dovresti usare InputStreamResource. Esempio:

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);

Non consigli per l'uso della classe FileSystemResource?
Stephane,

In realtà, credo che sia OK usare il FileSystemResourcelì. È anche consigliabile se la tua risorsa è un file . In questo esempio, FileSystemResourcepuò essere utilizzato dov'è InputStreamResource.
acdcjunior,

Informazioni sulla parte di calcolo della lunghezza del file: se sei preoccupato, non esserlo. File#length()dovrebbe essere abbastanza buono nel caso generale. L'ho appena detto perché può essere lento , specialmente se il file si trova in un sistema remoto o qualcosa di più elaborato in questo modo - un database, forse ?. Ma preoccupati solo se diventa un problema (o se hai prove concrete che lo diventeranno), non prima. Il punto principale è: stai facendo uno sforzo per eseguire lo streaming del file, se devi precaricare tutto prima, quindi lo streaming finisce per non fare alcuna differenza, eh?
acdcjunior,

perché il codice sopra non funziona per me? Scarica il file 0 byte. Ho verificato e verificato che i convertitori ByteArray e ResourceMessage siano presenti. Mi sto perdendo qualcosa ?
coding_idiot

Perché ti preoccupi dei convertitori ByteArray e ResourceMessage?
acdcjunior,

20

Questo codice funziona correttamente per scaricare automaticamente un file dal controller di primavera facendo clic su un collegamento su 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();
    }

}

14

Di seguito il codice ha funzionato per me per generare e scaricare un file di testo.

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

5

Quello a cui riesco a pensare rapidamente è, generare il pdf e memorizzarlo in webapp / download / <RANDOM-FILENAME> .pdf dal codice e inviare un inoltro a questo file utilizzando HttpServletRequest

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

o se è possibile configurare il risolutore della vista in modo simile,

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

poi torna e basta

return "RANDOM-FILENAME";

1
Se ho bisogno di due resolver di vista, come posso anche restituire il nome del resolver o sceglierlo nel controller ??
Azerafati,

3

La seguente soluzione funziona per me

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

    }

2

qualcosa come sotto

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

Puoi visualizzare PDF o scaricare esempi qui


1

Se aiuta qualcuno. Puoi fare ciò che la risposta accettata da Infeligo ha suggerito, ma aggiungi questo bit in più nel codice per un download forzato.

response.setContentType("application/force-download");


0

Nel mio caso sto generando alcuni file su richiesta, quindi anche l'URL deve essere generato.

Per me funziona qualcosa del genere:

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

Molto importante è digitare mime producese anche quello, quel nome del file fa parte del collegamento, quindi devi usarlo@PathVariable .

Il codice HTML è simile al seguente:

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

Dove ${file_name}viene generato da Thymeleaf nel controller ed è cioè: result_20200225.csv, in modo che l'intero collegamento behing dell'URL sia:example.com/aplication/dbreport/files/result_20200225.csv .

Dopo aver fatto clic sul browser dei collegamenti, mi viene chiesto cosa fare con il file: salva o apri.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.