由于网关的要求,控制器必须返回ResponseEntity对象进行文件输出,而且文件是存储在minio上,要求是对minio上的多个文件进行压缩下载,而且要求是不限制数量大小,但是开发时错估了生产环境下真正的文件大小,所以一开始的代码主要如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public ResponseEntity<InputStreamResource> downloadAsZip(List<Long> idList) throws Exception {  
List<Media> mediaList = lambdaQuery().select(Media::getId, Media::getType, Media::getPath, Media::getObjectKey, Media::getName)
.in(Media::getId, idList)
.list();
// 压缩文件
ByteArrayOutputStream out = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(out);
for (Media media: mediaList) {
InputStream inputStream = MinioUtils.getFileInputStream(media.getPath(), media.getObjectKey());
ZipEntry zipEntry = new ZipEntry(media.getName());
zipOut.putNextEntry(zipEntry);
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
zipOut.write(buffer, 0, bytesRead);
}
zipOut.closeEntry();
inputStream.close();
}
zipOut.close();
out.close();
// 写出文件流
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", URLEncoder.encode("下载文件.zip", "UTF-8")));
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(new InputStreamResource(new ByteArrayInputStream(out.toByteArray())));
}

当然这些代码是我后面举例的,与实际我使用的代码还有很多逻辑差距,包括计算content-length等,这段代码主要的问题在于压缩文件时ZipOutputStream内传入的是一个ByteArrayOutputStream,而ByteArrayOutputStream实质上是在内部维护了一个byte数组用来存储字节到内存中,所以当压缩的文件越来越多时会导致堆内存溢出的这么一个现象;

找到问题根源后将代码修改如下,将压缩流直接输出到客户端,这样有一个弊端,就是前端的进度条不好算,因为文件是动态压缩的,不能一开始就给出总文件大小,但是可以通过提前计算压缩前的总文件大小,再配合前端做一下对偏差的修饰,来满足这个需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public ResponseEntity<StreamingResponseBody> downloadAsZip(List<Long> idList) throws Exception {  
List<Media> mediaList = lambdaQuery().select(Media::getId, Media::getType, Media::getPath, Media::getObjectKey, Media::getName)
.in(Media::getId, idList)
.list();
// 写出文件流
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", URLEncoder.encode("下载文件.zip", "UTF-8")));
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(outputStream -> {
ZipOutputStream zipOut = new ZipOutputStream(outputStream);
for (Media media: mediaList) {
InputStream inputStream = MinioUtils.getFileInputStream(media.getPath(), media.getObjectKey());
ZipEntry zipEntry = new ZipEntry(media.getName());
zipOut.putNextEntry(zipEntry);
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
zipOut.write(buffer, 0, bytesRead);
}
zipOut.closeEntry();
inputStream.close();
}
zipOut.close();
});
}