Descargar un archivo de controladores de primavera

387

Tengo un requisito donde necesito descargar un PDF desde el sitio web. El PDF debe generarse dentro del código, que pensé que sería una combinación de freemarker y un marco de generación de PDF como iText. ¿Alguna mejor manera?

Sin embargo, mi problema principal es ¿cómo permito que el usuario descargue un archivo a través de un controlador Spring?

MilindaD
fuente
2
Vale la pena mencionar que Spring Framework cambió mucho desde 2011, por lo que también puede hacerlo de manera reactiva, aquí hay un ejemplo
Krzysztof Skrzynecki, el

Respuestas:

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

}

En términos generales, cuando tienes response.getOutputStream(), puedes escribir cualquier cosa allí. Puede pasar esta secuencia de salida como un lugar para colocar el PDF generado en su generador. Además, si sabe qué tipo de archivo está enviando, puede configurar

response.setContentType("application/pdf");
Infeligo
fuente
44
Esto es más o menos lo que estaba a punto de decir, pero probablemente también debería establecer el encabezado del tipo de respuesta en algo apropiado para el archivo.
GaryF
2
Sí, acabo de editar la publicación. Tenía varios tipos de archivos generados, así que lo dejé en el navegador para determinar el tipo de contenido del archivo por su extensión.
Infeligo
Olvidé el flushBuffer, gracias a tu publicación, vi por qué la mía no funcionaba :-)
Jan Vladimir Mostert
35
¿Alguna razón particular para usar Apache's en IOUtilslugar de Spring's FileCopyUtils?
Powerlord
3
Aquí hay una mejor solución: stackoverflow.com/questions/16652760/…
Dmytro Plekhotkin
290

Pude transmitir esta línea usando el soporte integrado en Spring con su ResourceHttpMessageConverter. Esto establecerá la longitud del contenido y el tipo de contenido si puede determinar el 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
fuente
10
Esto funciona. Pero el archivo (archivo .csv) se muestra en el navegador y no se descarga. ¿Cómo puedo forzar la descarga del navegador?
chzbrgla
41
Puede agregar produce = MediaType.APPLICATION_OCTET_STREAM_VALUE a @RequestMapping para forzar la descarga
David Kago,
8
También debe agregar <bean class = "org.springframework.http.converter.ResourceHttpMessageConverter" /> a la lista messageConverters (<mvc: annotation-driven> <mvc: message-converters>)
Sllouyssgort el
44
¿Hay alguna manera de configurar el Content-Dispositionencabezado de esta manera?
Ralph
8
No necesitaba eso, pero creo que podría agregar HttpResponse como parámetro al método y luego "response.setHeader (" Content-Disposition "," adjunto; nombre de archivo = somefile.pdf ");"
Scott Carlson
82

Debería poder escribir el archivo en la respuesta directamente. Algo como

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

y luego escribe el archivo como una secuencia binaria en response.getOutputStream(). Recuerde hacer response.flush()al final y eso debería hacerlo.

langosta1234
fuente
8
¿No es la forma 'Spring' de configurar el tipo de contenido de esta manera? @RequestMapping(value = "/foo/bar", produces = "application/pdf")
Negro
44
@Francis, ¿qué pasa si su aplicación descarga diferentes tipos de archivos? La respuesta de Lobster1234 le permite establecer dinámicamente la disposición del contenido.
Rose
2
eso es cierto @Rose, pero creo que sería una mejor práctica definir diferentes puntos finales por formato
Negro
3
Supongo que no, porque no es escalable. Actualmente estamos apoyando una docena de tipos de recursos. Podríamos admitir más tipos de archivos en función de lo que los usuarios quieran cargar en ese caso, podríamos terminar con tantos puntos finales esencialmente haciendo lo mismo. En mi humilde opinión, debe haber un solo punto final de descarga y maneja multitud de tipos de archivos. @Francis
Rose
3
es absolutamente "escalable", pero podemos estar de acuerdo en no estar de acuerdo si es la mejor práctica
Negro
74

Con Spring 3.0 puede usar el HttpEntityobjeto de retorno. Si usa esto, entonces su controlador no necesita un HttpServletResponseobjeto y, por lo tanto, es más fácil de probar. Excepto esto, esta respuesta es relativamente igual a la de Infeligo .

Si el valor de retorno de su marco pdf es una matriz de bytes (lea la segunda parte de mi respuesta para otros 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);
}

Si el tipo de retorno de su PDF Framework ( documentBbody) no es una matriz de bytes (y tampoco ByteArrayInputStream), entonces sería prudente NO convertirla primero en una matriz de bytes. En cambio, es mejor usar:

ejemplo 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);
}
Ralph
fuente
11
-1 esto cargará innecesariamente todo el archivo en la memoria, puede causar fácilmente OutOfMemoryErrors.
Faisal Feroz
1
@FaisalFeroz: sí, esto es correcto, pero el documento de archivo se crea de todos modos en la memoria (vea la pregunta: "El PDF debe generarse dentro del código"). De todos modos, ¿cuál es su solución para superar este problema?
Ralph
1
También puede usar ResponseEntity, que es un super de HttpEntity que le permite especificar el código de estado http de respuesta. Ejemplo:return new ResponseEntity<byte[]>(documentBody, headers, HttpStatus.CREATED)
Amr Mostafa
@Amr Mostafa: ResponseEntityes una subclase de HttpEntity(pero lo entiendo) 201 CREATED no es lo que usaría cuando devuelva solo una vista de los datos. (ver w3.org/Protocols/rfc2616/rfc2616-sec10.html para 201 CREATED)
Ralph
1
¿Hay alguna razón por la que está reemplazando espacios en blanco con guiones bajos en el nombre del archivo? Puede envolverlo entre comillas para enviar el nombre real.
Alexandru Severin
63

Si tu:

  • No quiero cargar todo el archivo en un byte[]antes de enviarlo a la respuesta;
  • Desea / necesita enviar / descargar a través de InputStream;
  • Quiere tener el control total del Tipo Mime y el nombre del archivo enviado;
  • Tenga otras @ControllerAdviceexcepciones para usted (o no).

El siguiente código es lo que necesita:

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

Acerca de la parte de la longitud del archivo : File#length()debería ser lo suficientemente bueno en el caso general, pero pensé que haría esta observación porque puede ser lenta , en cuyo caso debería tenerla almacenada previamente (por ejemplo, en la base de datos). Los casos en que puede ser lento incluyen: si el archivo es grande, especialmente si el archivo está en un sistema remoto o algo más elaborado como eso, una base de datos, tal vez.



InputStreamResource

Si su recurso no es un archivo, por ejemplo, recoge los datos de la base de datos, debe usarlos InputStreamResource. Ejemplo:

    InputStreamResource isr = new InputStreamResource(new FileInputStream(file));
    return new ResponseEntity<InputStreamResource>(isr, respHeaders, HttpStatus.OK);
acdcjunior
fuente
¿No aconseja el uso de la clase FileSystemResource?
Stephane
En realidad, creo que está bien usar FileSystemResourceallí. Incluso es recomendable si su recurso es un archivo . En esta muestra, FileSystemResourcese puede usar donde InputStreamResourceestá.
acdcjunior
Acerca de la parte de cálculo de la longitud del archivo: si está preocupado, no lo esté. File#length()debería ser lo suficientemente bueno en el caso general. Lo acabo de mencionar porque puede ser lento , especialmente si el archivo está en un sistema remoto o algo más elaborado, ¿una base de datos, tal vez? Pero solo preocúpese si se convierte en un problema (o si tiene pruebas sólidas de que se convertirá en uno), no antes. El punto principal es: está haciendo un esfuerzo para transmitir el archivo, si tiene que precargarlo todo antes, la transmisión termina sin hacer ninguna diferencia, ¿eh?
acdcjunior
¿Por qué el código anterior no funciona para mí? Descarga el archivo de 0 bytes. Verifiqué y me aseguré de que los convertidores ByteArray y ResourceMessage estén allí. Me estoy perdiendo de algo ?
coding_idiot
¿Por qué le preocupan los convertidores ByteArray y ResourceMessage?
acdcjunior
20

Este código funciona bien para descargar un archivo automáticamente desde el controlador de Spring al hacer clic en un enlace en 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
fuente
14

El siguiente código funcionó para mí para generar y descargar un archivo 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
fuente
5

Lo que puedo pensar rápidamente es generar el pdf y almacenarlo en webapp / downloads / <RANDOM-FILENAME> .pdf desde el código y enviar un reenvío a este archivo utilizando HttpServletRequest

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

o si puede configurar su resolución de vista algo así 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>

entonces solo regresa

return "RANDOM-FILENAME";
kalyan
fuente
1
Si necesito dos resolvers de vista, ¿cómo puedo devolver el nombre del resolutor o elegirlo en el controlador?
azerafati
3

La siguiente solución me funciona

    @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
fuente
2

algo como abajo

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

Puede mostrar PDF o descargarlo aquí.

xxy
fuente
1

Si ayuda a alguien. Puede hacer lo que sugiere la respuesta aceptada por Infeligo, pero solo ponga este bit extra en el código para una descarga forzada.

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

En mi caso, estoy generando algún archivo a pedido, por lo que también se debe generar la URL.

Para mí funciona algo así:

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

Muy importante es el tipo mime producesy también, ese nombre del archivo es parte del enlace, por lo que debe usarlo @PathVariable.

El código HTML se ve así:

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

Cuando ${file_name}se genera por thymeleaf en el controlador y es decir: result_20200225.csv, de modo que enlace todo url behing es: example.com/aplication/dbreport/files/result_20200225.csv.

Después de hacer clic en el navegador de enlaces, me pregunta qué hacer con el archivo: guardar o abrir.

Tomasz Dzięcielewski
fuente