从spring控制器下载文件

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

然而,我的主要问题是如何允许用户通过Spring控制器下载文件?

561512 次浏览

我能很快想到的是,生成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";
@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");

您应该能够直接在响应上写入文件。类似的

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

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

我能够通过使用Spring的ResourceHttpMessageConverter中的内置支持来进行流处理。如果可以确定mime类型,这将设置内容长度和内容类型

@RequestMapping(value = "/files/{file_name}", method = RequestMethod.GET)
@ResponseBody
public FileSystemResource getFile(@PathVariable("file_name") String fileName) {
return new FileSystemResource(myService.getFileFor(fileName));
}
在Spring 3.0中,你可以使用HttpEntity返回对象。如果你使用这个,那么你的控制器不需要HttpServletResponse对象,因此更容易测试。 除此之外,这个答案是相对等于ineligo 的答案。

如果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),那么明智的将首先使它成为一个字节数组。相反,最好使用:

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

这段代码可以在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();
}


}

如果你:

  • 不想在发送到响应之前将整个文件加载到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()的更多信息:首先,根据HTTP 1.1 RFC, content-length报头是可选的。不过,如果你能提供一个价值,那就更好了。要获得这样的值,要知道在一般情况下File#length()应该足够好,所以它是一个安全的默认选择 不过,在非常特定的情况下,它可以是缓慢的,在这种情况下,你应该把它存储在之前(例如在DB中),而不是在运行中计算。慢的场景包括:如果文件是非常大,特别是如果它是在一个远程系统或一个更详细的东西——一个数据库,可能



InputStreamResource

如果你的资源不是一个文件,例如你从DB中获取数据,你应该使用InputStreamResource。例子:

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

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

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

如下图所示

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

下面的解决方案对我很有效

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


}

如果这对谁有帮助的话。你可以按照Infeligo给出的答案去做,但只需要在强制下载的代码中添加额外的内容即可。

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

在我的情况下,我需要生成一些文件,所以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}是由thymleaf在控制器中生成的,即:result_20200225.csv,因此链接的整个url是:example.com/aplication/dbreport/files/result_20200225.csv

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

  1. 从处理程序方法返回ResponseEntity<Resource>
  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<Resource>

返回ResponseEntity<Resource>时,ResourceHttpMessageConverter写入文件内容

Resource实现的例子:

显式地指定Content-Type:

原因:见"文件系统资源返回内容类型json"问题

选项:

  • 硬编码头文件
  • 使用Spring中的MediaTypeFactoryMediaTypeFactory使用/org/springframework/http/mime.types文件将Resource映射到MediaType
  • 使用第三方库,如Apache Tika

必要时设置Content-Disposition:

关于Content-Disposition头文件:

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

在应用中使用ContentDisposition:

  • 浏览器中的预览文件:

    ContentDisposition disposition = ContentDisposition
    .inline()
    .filename(resource.getFilename())
    .build();
    
  • 强制下载:

    ContentDisposition disposition = ContentDisposition
    .attachment()
    .filename(resource.getFilename())
    .build();
    

小心使用InputStreamResource:

使用HttpHeaders#setContentLength方法指定Content-Length,如果:

  1. 长度是已知的
  2. 你使用InputStreamResource

原因:Spring不会为__ABC1编写__ABC0,因为Spring不能确定资源的长度。下面是来自ResourceHttpMessageConverter的代码片段:

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

在其他情况下,Spring设置Content-Length:

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

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

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