在Web应用程序中创build和下载大量ZIP(来自多个BLOB)的最佳实践
我将需要从我的Web应用程序执行大量的文件下载。
很显然,这是一个长期的行动( 每年使用一次(每个客户) ),所以时间不是问题(除非它遇到一些超时,但我可以通过处理创造一些forms的keepalive心跳)。 我知道如何创build一个隐藏的iframe,并使用它的content-disposition: attachment
试图下载文件,而不是在浏览器中打开它,以及如何实例客户端 – 服务器通信绘制一个进度计;
下载的实际大小(和文件数量)是未知的,但为了简单起见,我们可以将其视为1GB,由100个文件组成,每个10MB。
由于这应该是一个单击操作,我的第一个想法是在dynamic生成的ZIP中将所有文件从数据库中读取,然后要求用户保存ZIP。
问题是: 在WebApp中创build一个来自多个小字节数组的巨大档案的最佳实践是什么,已知的缺点和陷阱是什么?
这可以随机分成:
- 应该将每个字节数组转换为物理临时文件,还是可以将它们添加到内存中的ZIP中?
- 如果是的话,我知道我将不得不处理名称可能的平等(它们可以在数据库中的不同logging中具有相同的名称,但不在相同的文件系统或ZIP中):是否还有其他可能的问题介意(假设文件系统总是有足够的物理空间)?
- 因为我不能依靠有足够的内存来执行整个内存的操作,所以我猜测ZIP应该被创build并送到文件系统,然后再发送给用户。 有什么办法可以做不同的事情(例如,用websocket ),就像询问用户在哪里保存文件,然后开始从服务器到客户端的数据stream(我猜)。
- 任何其他相关的已知问题或最好的做法,你会不胜感激。
对于一次不适合内存的大内容,将数据库中的内容stream式传输到响应中。
这种事情其实很简单。 您不需要AJAX或WebSockets,可以通过用户点击的简单链接来stream式传输大文件。 而现代浏览器有自己的进度条体面的下载pipe理器 – 为什么重新发明轮子?
如果为此从头开始编写servlet,请访问数据库BLOB,获取其inputstream并将内容复制到HTTP响应输出stream。 如果你有Apache Commons IO库,你可以使用IOUtils.copy() ,否则你可以自己做。
dynamic创buildZIP文件可以使用ZipOutputStream完成。 在响应输出stream(从servlet或任何您的框架提供的)上创build其中的一个,然后从数据库中获取每个BLOB,首先使用putNextEntry()
,然后按照前面所述的方式对每个BLOB进行stream式处理。
潜在的陷阱/问题:
- 根据下载大小和networking速度的不同,请求可能需要很长时间才能完成。 防火墙等可以阻碍这一点,并尽早终止请求。
- 希望你的用户在请求这些文件时在一个体面的公司networking上。 远程/通行/移动连接(如果下载1.9G的2.0G后退出,用户必须重新启动)会更糟糕。
- 它可以把一些负载在你的服务器,特别是压缩巨大的ZIP文件。 如果这是一个问题,创build
ZipOutputStream
时可能需要closures/closures压缩。 - 超过2GB(或4GB)的ZIP文件在某些ZIP程序中可能有问题。 我认为最新的Java 7使用ZIP64扩展,所以这个版本的Java将正确地写入巨大的ZIP,但客户端将有支持大型zip文件的程序? 以前我肯定遇到过这些问题,特别是在旧的Solaris服务器上
通过将每个BLOB从数据库直接传输到客户端的文件系统创build一个完全dynamic的ZIP文件 。
testing与巨大档案以下表演:
- 服务器磁盘空间成本:0兆字节
- 服务器RAM成本:
〜xx兆字节。内存消耗是不可testing的(或至less我不知道如何正确地做),因为我得到了不同的,显然随机结果多次运行相同的例程(通过使用Runtime.getRuntime().freeMemory()
)循环之前,之中和之后)。 但是,内存消耗低于使用byte [],这就够了。
使用InputStream
而不是byte[]
FileStreamDto.java
public class FileStreamDto implements Serializable { @Getter @Setter private String filename; @Getter @Setter private InputStream inputStream; }
Java Servlet (或Struts2 Action)
/* Read the amount of data to be streamed from Database to File System, summing the size of all Oracle's BLOB, PostgreSQL's ABYTE etc: SELECT sum(length(my_blob_field)) FROM my_table WHERE my_conditions */ Long overallSize = getMyService().precalculateZipSize(); // Tell the browser is a ZIP response.setContentType("application/zip"); // Tell the browser the filename, and that it needs to be downloaded instead of opened response.addHeader("Content-Disposition", "attachment; filename=\"myArchive.zip\""); // Tell the browser the overall size, so it can show a realistic progressbar response.setHeader("Content-Length", String.valueOf(overallSize)); ServletOutputStream sos = response.getOutputStream(); ZipOutputStream zos = new ZipOutputStream(sos); // Set-up a list of filenames to prevent duplicate entries HashSet<String> entries = new HashSet<String>(); /* Read all the ID from the interested records in the database, to query them later for the streams: SELECT my_id FROM my_table WHERE my_conditions */ List<Long> allId = getMyService().loadAllId(); for (Long currentId : allId){ /* Load the record relative to the current ID: SELECT my_filename, my_blob_field FROM my_table WHERE my_id = :currentId Use resultset.getBinaryStream("my_blob_field") while mapping the BLOB column */ FileStreamDto fileStream = getMyService().loadFileStream(currentId); // Create a zipEntry with a non-duplicate filename, and add it to the ZipOutputStream ZipEntry zipEntry = new ZipEntry(getUniqueFileName(entries,fileStream.getFilename())); zos.putNextEntry(zipEntry); // Use Apache Commons to transfer the InputStream from the DB to the OutputStream // on the File System; at this moment, your file is ALREADY being downloaded and growing IOUtils.copy(fileStream.getInputStream(), zos); zos.flush(); zos.closeEntry(); fileStream.getInputStream().close(); } zos.close(); sos.close();
用于处理重复条目的 Helper方法
private String getUniqueFileName(HashSet<String> entries, String completeFileName){ if (entries.contains(completeFileName)){ int extPos = completeFileName.lastIndexOf('.'); String extension = extPos>0 ? completeFileName.substring(extPos) : ""; String partialFileName = extension.length()==0 ? completeFileName : completeFileName.substring(0,extPos); int x=1; while (entries.contains(completeFileName = partialFileName + "(" + x + ")" + extension)) x++; } entries.add(completeFileName); return completeFileName; }
非常感谢@prunge给我直接stream媒体的想法。
也许你想同时尝试多个下载。 我在这里发现了一个与此有关的讨论 – Javamultithreading文件下载性能
希望这可以帮助。