首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

SpringWebMvc框架下载文件的第一个优化,不要一次性将文件加载到内存里,而是以...

在上一篇文章中,我们讨论了直接使用<a>标签链接到下载文件,有一个致命的缺点,这个缺点只有在下载大文件时(大于几个M的文件),才能显现出来。小文件,几k的文件是看不出这个问题的。

那就是在下载的时候,文件会一次性加载到内存,给服务器的内存、带宽和客户端的内存、带宽带来很大的压力。不管是内存不够,还是带宽不够,都会造成下载缓慢,影响下载性能,给用户带来不好的下载体验。

现在就来解决这个问题,如何防止大文件一次性加载到内存里。

一、一个反面案例

在网上看到这样一个下载方法,这个方法适合下载大文件吗?

@RequestMapping("/download")

public ResponseEntity<byte[]> download() throws IOException {

// 1、

File file = new File("/path/to/your/cs.zip");

// 2、

byte[] fileContent = Files.readAllBytes(file.toPath());

// 3、

HttpHeaders headers = new HttpHeaders();

// 对文件名进行 UTF-8 编码

String encodedFileName = UriUtils.encode(file.getName(), "UTF-8");

// 指定文件名和附件方式,告知浏览器这是一个下载文件而非直接显示的内容。

headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + encodedFileName);

// 指定文件大小,有助于浏览器在下载时显示下载进度。

headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(file.length()));

// 设置ContentType为application/octet-stream:用于通用的二进制数据下载,适用于大多数文件下载场景。

headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);

// 4、设置响应,构建一个 HTTP 200 OK 响应

return ResponseEntity.ok()

.headers(headers)

.body(fileContent);

}

我把上面的代码分为四步。

第一步,通过路径获取File文件对象。

第二步,将文件的所有字节读取到byte数组。

第三步,设置响应头。

文件名编码:对文件名进行 UTF-8 编码,防止文件名中有特殊字符导致的问题。

Content-Disposition:设置为附件下载,并指定文件名,告知浏览器这是一个下载文件而非直接显示的内容。

Content-Length:指定文件大小,帮助浏览器显示下载进度。

Content-Type:设置为 application/octet-stream,表示这是一个通用的二进制文件下载类型,适用于大多数文件下载场景。

第四步,构建响应体。构建一个 HTTP 200 OK 响应,包含上述设置的请求头和文件内容。

在第二步中,Files的readAllBytes方法将转换为Path的文件对象以byte的方式全部读出到变量fileContent。

这个方法做了<a>标签所没做到的一件事:设置响应头。其他的和<a>标签下载文件没什么区别,文件还是一次性加入内存的。这个方法写了等于白写。

核心类介绍:

File 类是 java.io 包中的一部分,它提供了与平台无关的接口来处理与文件和目录(路径)相关的操作。这包括创建新文件、查询文件属性、删除文件或目录、列出目录内容等。

File 类的一大局限是它不能很好地处理符号链接或文件元数据,如文件所有者或安全属性,并且它的一些方法在出现错误时不会提供足够的错误信息,只是返回 false。

二、使用Streaming方式下载

先看代码,如何将文件内容作为流(Stream)传输,而不是一次性将整个文件加载到内存中。

/**

* 1、使用Streaming方式下载

* @return

* @throws IOException

*/

@GetMapping(value="/download")

public ResponseEntity<InputStreamResource> downloadFile1() throws IOException {

// 1、

File file = new File("/path/to/your/cs.zip");

// 2、

if (!file.exists()) {

// 构建文找不到的响应状态

return new ResponseEntity<>(HttpStatus.NOT_FOUND);

}

// 3、

InputStreamResource resource = new InputStreamResource(new FileInputStream(file));

// 4、设置请求头

HttpHeaders headers = new HttpHeaders();

// 对文件名进行 UTF-8 编码

String encodedFileName = UriUtils.encode(file.getName(), "UTF-8");

// 指定文件名和附件方式,告知浏览器这是一个下载文件而非直接显示的内容。

headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + encodedFileName);

// 指定文件大小,有助于浏览器在下载时显示下载进度。

headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(file.length()));

// 设置ContentType为application/octet-stream:用于通用的二进制数据下载,适用于大多数文件下载场景。

headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);

// 5、设置响应,构建一个 HTTP 200 OK 响应

return ResponseEntity.ok()

// 设置响应头

.headers(headers)

// 设置响应体

.body(resource);

}

我把上面的代码分为五步。

第一步,首先通过路径获取File文件对象。

第二步,根据文件对象判断文件是否存在,如果文件不存在,则返回 HTTP 404 Not Found 响应。如果存在,继续往下走。

第三步,使用 InputStreamResource 对 FileInputStream 进行包装,使文件内容通过流的方式进行读取。这种方式不会一次性将文件加载到内存中,适合处理大文件。

第四步,设置请求头。

第五步,构建响应体。

有了这个方法,前端依旧可以使用<a>标签,标签的href直接填充下载接口的地址即可。

核心类介绍:

FileInputStream 是 java.io包中的文件输入流对象,它是 InputStream 的一个子类,用于从文件系统的文件中读取字节流。同时它也是同步和阻塞的。当调用读取方法时,如果数据未准备好,调用将会阻塞,直到数据可读。

直接使用 InputStreamResource:适合于直接从文件流读取内容并传输的场景,避免了文件系统资源的包装和额外的IO操作。

三、使用NIO方式进行文件下载

Spring 核心 io 框架不仅有 InputStreamResource,还有一个更顺手的工具类,一起看下。

/**

* 2、使用NIO进行文件下载

* @return

* @throws IOException

*/

@GetMapping("/link2")

public ResponseEntity<FileSystemResource> downloadFile2() throws IOException {

// 1、根据文件路径获取文件

File file = new File("/path/to/your/cs.zip");

// 2、判断文件是否存在

if (!file.exists()) {

// 构建文找不到的响应状态

return new ResponseEntity<>(HttpStatus.NOT_FOUND);

}

// 3、Spring提供的文件系统资源包装类,优化了文件的读取和传输过程

FileSystemResource resource = new FileSystemResource(file);

// 4、设置请求头

HttpHeaders headers = new HttpHeaders();

// 对文件名进行 UTF-8 编码

String encodedFileName = UriUtils.encode(file.getName(), "UTF-8");

// 指定文件名和附件方式,告知浏览器这是一个下载文件而非直接显示的内容。

headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + encodedFileName);

// 指定文件大小,有助于浏览器在下载时显示下载进度。

headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(file.length()));

// 根据文件扩展名确定MediaType

String contentType = Files.probeContentType(file.toPath());

// 设置默认值

if (StringUtil.isEmpty(contentType)) {

contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;

}

// 设置ContentType为application/octet-stream:用于通用的二进制数据下载,适用于大多数文件下载场景。

headers.add(HttpHeaders.CONTENT_TYPE, contentType);

// 5、设置响应,构建一个 HTTP 200 OK 响应

return ResponseEntity.ok()

.headers(headers)

.body(resource);

}

我把上面的代码分为五步。

第一步,首先通过路径获取File文件对象。

第二步,根据文件对象判断文件是否存在。

第三步,使用FileSystemResource 文件系统资源包装类直接操作文件对象,优化了文件的读取和传输过程,特别适合大文件和流式处理。

第四步,设置请求头。

第五步,构建响应体。

FileSystemResource 可以直接读取文件,操作文件,不需要像上个案例中一样经过两次处理。通过 FileSystemResource 可以更有效地管理文件资源,减少了每次请求的资源开销。

为什么说使用Spring 框架的 FileSystemResource 就是使用NIO方式进行文件下载呢,这个下回再分析。

今天就先到这里,把大文件通过流的方式进行处理,而不是一下子全部加载到内存中,从性能上讲已经进了一小步了。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OXzJWiFdCskwFJPzsGiML46Q0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券