【问题标题】:Downloading a file from spring controllers从弹簧控制器下载文件
【发布时间】:2011-08-06 02:33:54
【问题描述】:

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

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

【问题讨论】:

  • 值得一提的是,自 2011 年以来,Spring 框架发生了很大变化,因此您也可以通过响应式方式进行更改 - here 就是一个示例
  • 使用更高版本的spring,您只需要在ResponseEntity中返回带有适当标题的字节数组。这是一个完整的例子:allaboutspringframework.com/…

标签: java spring file download controller


【解决方案1】:
@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");

【讨论】:

  • 这就是我要说的内容,但您可能还应该将响应类型标头设置为适合文件的内容。
  • 是的,刚刚编辑了帖子。我生成了各种文件类型,所以我把它留给浏览器通过扩展名来确定文件的内容类型。
  • 使用 Apache 的 IOUtils 而不是 Spring 的 FileCopyUtils 有什么特别的原因吗?
  • 这里有一个更好的解决方案:stackoverflow.com/questions/16652760/…
  • @Powerlord Spring 方法关闭流,Apache 没有。 Servlet 响应输出流是否应该在 Controller 代码或 Servlet 容器中关闭存在争议...
【解决方案2】:

我能够通过使用 Spring 中的内置支持和它的 ResourceHttpMessageConverter 来简化这一点。如果它可以确定 mime-type,这将设置 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)); 
}

【讨论】:

  • 这行得通。但是文件(.csv 文件)显示在浏览器中并且没有下载 - 如何强制浏览器下载?
  • 您可以将produce = MediaType.APPLICATION_OCTET_STREAM_VALUE 添加到@RequestMapping 以强制下载
  • 另外你应该添加 到 messageConverters 列表 ()
  • 有没有办法用这种方式设置Content-Disposition标头?
  • 我不需要那个,但我认为你可以将 HttpResponse 作为参数添加到方法中,然后 "response.setHeader("Content-Disposition", "attachment; filename= somefile.pdf");"
【解决方案3】:

您应该能够直接在响应中写入文件。像

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

然后将文件作为二进制流写入response.getOutputStream()。记得在最后做response.flush(),这样就可以了。

【讨论】:

  • 不是像这样设置内容类型的“春天”方式吗? @RequestMapping(value = "/foo/bar", produces = "application/pdf")
  • @Francis 如果您的应用程序下载不同的文件类型怎么办? Lobster1234 的回答使您能够动态设置内容处置。
  • 这是真的@Rose,但我相信为每种格式定义不同的端点会更好
  • 我猜不是,因为它不可扩展。我们目前支持十几种资源。我们可能会根据用户想要上传的内容支持更多的文件类型,在这种情况下,我们最终可能会有这么多端点在做同样的事情。恕我直言,必须只有一个下载端点,它可以处理多种文件类型。 @弗朗西斯
  • 它绝对是“可扩展的”,但我们可以同意不同意它是否是最佳实践
【解决方案4】:

在 Spring 3.0 中,您可以使用 HttpEntity 返回对象。如果你使用它,那么你的控制器就不需要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 框架 (documentBbody) 的返回类型还不是字节数组(也不是 ByteArrayInputStream),那么明智的做法是 NOT首先使它成为一个字节数组。相反,最好使用:

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

【讨论】:

  • -1 这将不必要地将整个文件加载到内存中很容易导致 OutOfMemoryErrors。
  • @FaisalFeroz:是的,这是正确的,但文件文档无论如何都是在内存中创建的(请参阅问题:“PDF 需要在代码中生成”)。无论如何 - 您的解决方案是什么?
  • 您也可以使用 ResponseEntity ,它是 HttpEntity 的超级,它允许您指定响应 http 状态代码。示例:return new ResponseEntity&lt;byte[]&gt;(documentBody, headers, HttpStatus.CREATED)
  • @Amr Mostafa: ResponseEntityHttpEntity 的子类(但我明白了)另一方面,当我只返回数据视图时,我不会使用 201 CREATED。 (见w3.org/Protocols/rfc2616/rfc2616-sec10.html 201 已创建)
  • 您在文件名中用下划线替换空格是否有原因?您可以将其用引号括起来以发送实际名称。
【解决方案5】:

如果你:

  • 不想在发送到响应之前将整个文件加载到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
    );
}

更多关于setContentLength()首先是content-length header is optional per the HTTP 1.1 RFC。不过,如果你能提供一个价值,那就更好了。要获得这样的值,要知道File#length() 在一般情况下应该足够好,所以它是一个安全的默认选择。
但是,在非常具体的情况下,它是can be slow,在这种情况下,您应该将其预先存储(例如,在数据库中),而不是动态计算。缓慢的场景包括:如果文件非常很大,特别是如果它位于远程系统或类似的更详细的东西 - 也许是数据库。



InputStreamResource

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

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

【讨论】:

  • 你不建议使用 FileSystemResource 类吗?
  • 实际上,我相信在那里使用FileSystemResource 是可以的。 如果你的资源是一个文件,它甚至是可取的。在此示例中,FileSystemResource 可用于InputStreamResource 所在的位置。
  • 关于文件长度计算部分:如果您担心,请不要担心。 File#length() 在一般情况下应该足够好。我刚刚提到它是因为it does can be slow,特别是如果文件位于远程系统中或更详细的类似的东西 - 数据库,也许?。但是只担心它是否会成为一个问题(或者如果你有确凿的证据它会成为一个问题),而不是之前。要点是:您正在努力流式传输文件,如果您必须预先加载所有文件,那么流式传输最终没有任何区别,是吗?
  • 为什么上面的代码对我不起作用?它下载 0 个字节的文件。我检查并确保 ByteArray & ResourceMessage 转换器在那里。我错过了什么吗?
  • 您为什么要担心 ByteArray 和 ResourceMessage 转换器?
【解决方案6】:

此代码可以正常工作,可以在单击 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();
    }

}

【讨论】:

  • 如果文件名包含空格、;等,您将面临问题
【解决方案7】:

不要手动做任何事情,而是将工作委托给框架:

  1. 从处理程序方法返回ResponseEntity&lt;Resource&gt;
  2. 明确指定Content-Type
  3. 必要时设置Content-Disposition
    1. 文件名
    2. 类型
      1. inline 强制在浏览器中预览
      2. attachment强制下载
@Controller
public class DownloadController {
    @GetMapping("/downloadPdf.pdf")
    // 1.
    public ResponseEntity<Resource> downloadPdf() {
        FileSystemResource resource = new FileSystemResource("/home/caco3/Downloads/JMC_Tutorial.pdf");
        // 2.
        MediaType mediaType = MediaTypeFactory
                .getMediaType(resource)
                .orElse(MediaType.APPLICATION_OCTET_STREAM);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(mediaType);
        // 3
        ContentDisposition disposition = ContentDisposition
                // 3.2
                .inline() // or .attachment()
                // 3.1
                .filename(resource.getFilename())
                .build();
        headers.setContentDisposition(disposition);
        return new ResponseEntity<>(resource, headers, HttpStatus.OK);
    }
}

说明

返回ResponseEntity&lt;Resource&gt;

当您返回 ResponseEntity&lt;Resource&gt; 时,ResourceHttpMessageConverter 会启动并写入适当的响应。

resource 可能是:

如果您需要从应用程序资源目录下载文件,请查看 my answer:它解释了如何使用 ClassPathResource 在类路径中定位资源

注意Content-Type 标头集可能有误(请参阅FileSystemResource is returned with content type json)。这就是为什么这个答案建议明确设置Content-Type

明确指定Content-Type

一些选项是:

MediaTypeFactory 允许发现适合ResourceMediaType(另请参见/org/springframework/http/mime.types 文件)

必要时设置Content-Disposition

有时需要在浏览器中强制下载或让浏览器打开文件作为预览。您可以使用Content-Disposition 标头来满足此要求:

HTTP 上下文中的第一个参数是inline(默认值,表示它可以显示在网页内,也可以作为网页显示)或attachment(表示应该下载它;大多数浏览器显示“另存为”对话框,预填充文件名参数的值(如果存在)。

在 Spring Framework 中,可以使用 ContentDisposition

在浏览器中预览文件:

ContentDisposition disposition = ContentDisposition
        .builder("inline") // Or .inline() if you're on Spring MVC 5.3+
        .filename(resource.getFilename())
        .build();

强制下载

ContentDisposition disposition = ContentDisposition
        .builder("attachment") // Or .attachment() if you're on Spring MVC 5.3+
        .filename(resource.getFilename())
        .build();

谨慎使用InputStreamResource

由于InputStream 只能读取一次,因此如果您返回InputStreamResource,Spring 将不会写入Content-Length 标头(这里是来自ResourceHttpMessageConverter 的sn-p 代码):

@Override
protected Long getContentLength(Resource resource, @Nullable MediaType contentType) throws IOException {
    // Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
    // Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
    if (InputStreamResource.class == resource.getClass()) {
        return null;
    }
    long contentLength = resource.contentLength();
    return (contentLength < 0 ? null : contentLength);
}

在其他情况下它工作正常:

~ $ curl -I localhost:8080/downloadPdf.pdf  | grep "Content-Length"
Content-Length: 7554270

【讨论】:

  • 这应该是公认的答案。它似乎提供了对 ContentDisposition 的唯一干净处理并给出了清晰的解释。
  • 绝对是最佳答案
  • 完美运行,与 "springdoc-openapi-ui" v1.5.11, swagger-ui 一起使用。 “下载”链接按预期显示,带有“attachment()”标志。
【解决方案8】:

下面的代码可以帮助我生成和下载一个文本文件。

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

【讨论】:

    【解决方案9】:

    我能很快想到的是,从代码中生成 pdf 并将其存储在 webapp/downloads/.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";
    

    【讨论】:

    • 如果我需要两个视图解析器,我该如何返回解析器的名称或在控制器中选择它??
    【解决方案10】:

    以下解决方案对我有用

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

    【讨论】:

      【解决方案11】:

      如下所示

      @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或下载它的例子here

      【讨论】:

        【解决方案12】:

        如果它可以帮助任何人。您可以按照 Infeligo 接受的答案的建议进行操作,但只需在代码中添加这个额外的位以强制下载。

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

        【讨论】:

          【解决方案13】:

          在我的例子中,我是按需生成一些文件,所以也必须生成 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));
          }
          

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

          HTML 代码如下所示:

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

          其中${file_name}由控制器中的Thymeleaf生成,即:result_20200225.csv,因此整个网址链接为:example.com/aplication/dbreport/files/result_20200225.csv

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

          【讨论】:

            【解决方案14】:

            这可能是一个有用的答案。

            Is it ok to export data as pdf format in frontend?

            扩展至此,将 content-disposition 添加为附件(默认)将下载文件。如果要查看,需要设置为内联。

            【讨论】:

              【解决方案15】:

              我必须添加这个才能下载任何文件

                  response.setContentType("application/octet-stream");
                  response.setHeader("Content-Disposition",
                          "attachment;filename="+"file.txt");
              

              所有代码:

              @Controller
              public class FileController {
              
              @RequestMapping(value = "/file", method =RequestMethod.GET)
              @ResponseBody
              public FileSystemResource getFile(HttpServletResponse response) {
              
                  final File file = new File("file.txt");
                  response.setContentType("application/octet-stream");
                  response.setHeader("Content-Disposition",
                          "attachment;filename="+"file.txt");
                  return new FileSystemResource(file);
               }
              }
              

              【讨论】:

                猜你喜欢
                • 2013-05-03
                • 1970-01-01
                • 1970-01-01
                • 2018-05-06
                • 1970-01-01
                • 2014-10-28
                • 2014-12-14
                • 1970-01-01
                相关资源
                最近更新 更多