记一次spring boot使用ZipOutputStream压缩文件下载导致堆内存溢出的问题
由于网关的要求,控制器必须返回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(); }); }
|