从Spring Controller下载文件


387

我有一个需要从网站下载PDF的要求。PDF需要在代码中生成,我认为这将是freemarker和iText等PDF生成框架的结合。还有更好的方法吗?

但是,我的主要问题是如何允许用户通过Spring Controller下载文件?


2
值得一提的是,自2011年以来,Spring Framework发生了很大的变化,因此您也可以以响应方式进行操作- 是一个示例
Krzysztof Skrzynecki

Answers:


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

}

一般来说,当您拥有时response.getOutputStream(),您可以在其中编写任何内容。您可以将此输出流作为将生成的PDF放置到生成器的地方传递。另外,如果您知道要发送的文件类型,则可以设置

response.setContentType("application/pdf");

4
这几乎就是我要说的,但是您可能还应该将响应类型标头设置为适合文件的内容。
GaryF 2011年

2
是的,刚刚编辑了帖子。我生成了各种文件类型,因此我将其留给浏览器以通过文件扩展名确定文件的内容类型。
Infeligo

忘记了flushBuffer,多亏了您的帖子,我明白了为什么我的系统不工作:-)
Jan Vladimir Mostert

35
使用Apache IOUtils而不是Spring的任何特殊原因FileCopyUtils
Powerlord 2012年

3
这里是一个更好的解决方案:stackoverflow.com/questions/16652760/...
德米特罗Plekhotkin

290

通过使用Spring的ResourceHttpMessageConverter中的内置支持,我能够简化这一过程。如果可以确定mime类型,则将设置content-length和content-type

@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
@ResponseBody
public FileSystemResource getFile(@PathVariable("file_name") String fileName) {
    return new FileSystemResource(myService.getFileFor(fileName)); 
}

10
这可行。但是文件(.csv文件)显示在浏览器中而不是下载-如何强制浏览器下载?
chzbrgla

41
您可以将Produces = MediaType.APPLICATION_OCTET_STREAM_VALUE添加到@RequestMapping以强制下载
David Kago

8
另外,您还应该将<bean class =“ org.springframework.http.converter.ResourceHttpMessageConverter” />添加到messageConverters列表中(<mvc:annotation-driven> <mvc:message-converters>)
Sllouyssgort,2013年

4
有没有办法Content-Disposition用这种方法设置标题?
拉尔夫(Ralph)

8
我不需要,但是我认为您可以将HttpResponse作为参数添加到方法中,然后添加“ response.setHeader(“ Content-Disposition”,“ attachment; filename = somefile.pdf”);“
斯科特·卡尔森

82

您应该能够直接在响应上写文件。就像是

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

然后将文件作为二进制流写入response.getOutputStream()。记住要response.flush()在最后做,那应该做。


8
是不是像这样设置内容类型的“春季”方式?@RequestMapping(value = "/foo/bar", produces = "application/pdf")
黑色

4
@Francis如果您的应用程序下载不同的文件类型怎么办?Lobster1234的答案使您可以动态设置内容的处理方式。
2015年

2
@Rose是正确的,但我认为最好为每种格式定义不同的端点
黑色

3
我猜不是,因为它不可扩展。我们目前正在支持十二种类型的资源。在这种情况下,我们可能会根据用户要上传的内容支持更多文件类型,而最终可能会有那么多端点实际上在做同样的事情。恕我直言,只有一个下载端点,它可以处理多种文件类型。@Francis
罗斯,

3
这绝对是“可扩展的”,但我们可以不同意这是否是最佳做法
Black Black

74

在Spring 3.0中,您可以使用HttpEntityreturn对象。如果使用此选项,则控制器不需要HttpServletResponse对象,因此测试起来更容易。 除此之外,这个答案相对于Infeligo之一

如果您的pdf框架的返回值是一个字节数组(请参阅我的答案的第二部分以获取其他返回值)

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

如果您的PDF Framework(documentBbody)的返回类型还不是字节数组(并且也没有ByteArrayInputStream),那么最好不要先使其成为字节数组。相反,最好使用:

的示例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这将不必要地将整个文件加载到内存中,很容易引起OutOfMemoryErrors。
Faisal Feroz

1
@FaisalFeroz:是的,这是正确的,但是文件文档还是在内存中创建的(请参阅问题:“需要在代码内生成PDF”)。无论如何-克服这个问题的解决方案是什么?
拉夫2014年

1
您也可以使用ResponseEntity,它是HttpEntity的上级,它使您可以指定响应http状态代码。示例:return new ResponseEntity<byte[]>(documentBody, headers, HttpStatus.CREATED)
Amr Mostafa 2014年

@Amr Mostafa:另一方面,它ResponseEntityHttpEntity(但我理解)的子类201 CREATED不是我只返回数据视图时要使用的东西。(请参阅w3.org/Protocols/rfc2616/rfc2616-sec10.html了解“ 201创建”)
拉尔夫(Ralph

1
为什么要用文件名中的下划线替换空格?您可以将其括在引号中以发送实际名称。
亚历山德鲁·塞弗林

63

如果你:

  • 不想byte[]在发送到响应之前将整个文件加载到中;
  • 想要/需要通过InputStream; 发送/下载
  • 希望完全控制发送的Mime类型和文件名;
  • @ControllerAdvice为您(或没有)提供其他接机例外。

下面的代码是您需要的:

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

关于文件长度部分:File#length()在一般情况下应该足够好,但是我认为我会做这个观察,因为它确实很慢,在这种情况下,您应该事先将其存储(例如,存储在数据库中)。可能很慢的情况包括:如果文件很大,特别是如果文件在远程系统中或类似的东西(例如数据库)中。



InputStreamResource

如果您的资源不是文件,例如,您从数据库中提取数据,则应使用InputStreamResource。例:

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

您不建议使用FileSystemResource类吗?
Stephane 2014年

实际上,我确实相信可以在FileSystemResource那里使用它。如果您的资源是文件,甚至是可取的。在此示例中,FileSystemResource可以在哪里使用InputStreamResource
acdcjunior 2015年

关于文件长度计算部分:如果您担心,请不要担心。File#length()在一般情况下应该足够好。我刚刚提到它是因为它的速度可能很慢,特别是如果文件位于远程系统中或诸如此类的更复杂的数据库中?但是,只担心是否会成为问题(或者如果您有确凿的证据会成为问题),就不必担心。要点是:您正在努力流式传输文件,如果必须在之前预加载所有文件,则流式传输最终没有任何影响,是吗?
acdcjunior 2015年

为什么上面的代码对我不起作用?下载0字节文件。我检查并确保ByteArray和ResourceMessage转换器在那里。我想念什么吗?
—coding_idiot15年

您为什么担心ByteArray和ResourceMessage转换器?
acdcjunior

20

在单击jsp上的链接时,该代码可以很好地从spring控制器自动下载文件。

@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

下面的代码为我工作,以生成和下载文本文件。

@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

我可以快速想到的是,生成pdf并将其从代码存储在webapp / downloads / <RANDOM-FILENAME> .pdf中,然后使用HttpServletRequest将其转发给该文件

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

或者,如果您可以配置视图解析器,例如

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

然后返回

return "RANDOM-FILENAME";

1
如果我需要两个视图解析器,又如何返回解析器的名称或在控制器中选择它呢?
azerafati 2014年

3

以下解决方案为我工作

    @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

像下面这样

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

您可以在此处显示PDF或下载示例


1

如果有帮助的话。您可以执行Infeligo所接受的答案所建议的操作,但只需在代码中多加一点即可进行强制下载。

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


0

就我而言,我是按需生成一些文件,因此还必须生成url。

对我来说,是这样的:

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

非常重要的是要输入mime,produces而且文件的名称是链接的一部分,因此您必须使用@PathVariable

HTML代码如下所示:

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

${file_name}由Thymeleaf在控制器生成并即:result_20200225.csv,让整个URL behing链接:example.com/aplication/dbreport/files/result_20200225.csv

单击链接浏览器后,询问我如何处理文件-保存或打开。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.