在上一篇文章中,我们讨论了直接使用<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方式进行文件下载呢,这个下回再分析。
今天就先到这里,把大文件通过流的方式进行处理,而不是一下子全部加载到内存中,从性能上讲已经进了一小步了。
领取专属 10元无门槛券
私享最新 技术干货