0、写在前面的话

图片批量下载,要求下载时集成为一个压缩包进行下载。从昨天下午折腾到现在,踩坑踩得莫名其妙,还是来唠唠,给自己留个印象的同时,也希望给需要用到这个方法的人带来一些帮助。

1、先叨叨IO

叨叨IO是因为网络传输无非也就是流的传递,所以下载文件到本地的话实际上也是IO的东西,这个和读取本地文件然后写入到本地另一个文件的操作是基本一样的。

我在自己IO基础的博客中(《[03] 节点流和处理流》)其实也有提到示例,拿复写文件来说,大概是如下过程:
 
对于读取文件(不仅仅是文本)到服务器内存,常见的是通过InputStream读取File,所以你可能也经常看到如下类似的代码:
  1. outputStream = new FileOutputStream(file);
  2. byte[] temp = new byte[1024];
  3. int size = -1;
  4. while ((size = inputStream.read(temp)) != -1) { // 每次读取1KB,直至读完
  5. outputStream.write(temp, 0, size);
  6. }
 
1
  1. outputStream = new FileOutputStream(file);  
2
  1. byte[] temp = new byte[1024];
3
  1. int size = -1;
4
  1. while ((size = inputStream.read(temp)) != -1) { // 每次读取1KB,直至读完
5
  1.    outputStream.write(temp, 0, size);
6
  1. }

我这里想说的是,不论何种形式,只需要知道的是,在写出之前,要获取将写出的数据,这个数据常常是作为byte[ ]类型的。

同时,因为是写出到本地文件,所以这里图示中的OutputStream无非应该是使用FileOutputStream罢了。那么以此来类比网络传输下载的话,OutputStream显然就替换成响应HttpServletResponse中的OutputStream就可以了。本质都是输出流,不同的类型决定你输出的方式等。

2、再叨叨ajax

当你把文件数据的二进制放到了响应的流中,也确实在响应中返回了,可是浏览器就是不争气,不给面子,不启动下载。这个时候,你要看看,你发送请求的方式,是否采取了ajax请求。如下图采用ajax请求资源,确实收到了流信息,但是反馈在浏览器上却什么也没发生:
 
原因在于,ajax的返回值类型是json/text/html/xml类型,或者可以说ajax的发送,接受都只能是string字符串,不能流类型,所以无法实现文件下载,强用会出现response冲突。

但用ajax仍然可以获得文件的内容,该文件将被保留在内存中,无法将文件保存到磁盘。这是因为js无法和磁盘进行交互,否则这会是一个严重的安全问题,js无法调用到浏览器的下载处理机制和程序,会被浏览器阻塞。

所以在前端,简单一点,使用 window.location.href 的方式访问url,实现下载。

3、正儿八经的批量下载实现

下载前,因为批量下载需要打包为压缩包,所以要用到一个三方jar,maven地址如下:
  1. <dependency>
  2. <groupId>ant</groupId>
  3. <artifactId>ant</artifactId>
  4. <version>1.6.5</version>
  5. </dependency>
5
 
1
  1. <dependency>
2
  1.  <groupId>ant</groupId>
3
  1.  <artifactId>ant</artifactId>
4
  1.  <version>1.6.5</version>
5
  1. </dependency>

先贴代码,然后再进行说明:
  1. /**
  2. * 批量下载
  3. *
  4. * @param idxs 图片的id拼接字符串,用逗号隔开
  5. */
  6. public String downloadBatch(String idxs) {
  7. String[] ids = idxs.split(",");
  8. try {
  9. HttpServletResponse response = ServletActionContext.getResponse();
  10. OutputStream out = setDownloadOutputStream(response, String.valueOf(new Date().getTime()), "zip");
  11. ZipOutputStream zipOut = new ZipOutputStream(out);
  12. for (int i = 0; i < ids.length; i++) {
  13. Picture picture = Picture.get(Picture.class, Long.parseLong(ids[i]));
  14. byte[] data = picture.getData();
  15. zipOut.putNextEntry(new ZipEntry(i + "_" + picture.getName() + "." + picture.getFormat().getValue()));
  16. writeBytesToOut(zipOut, data, BUFFER_SIZE);
  17. zipOut.closeEntry();
  18. }
  19. zipOut.flush();
  20. zipOut.close();
  21. } catch (IOException e) {
  22. e.printStackTrace();
  23. log.warn("下载失败:" + e.getMessage());
  24. }
  25. return null;
  26. }
x
 
1
  1. /**
2
  1. * 批量下载
3
  1. *
4
  1. * @param idxs 图片的id拼接字符串,用逗号隔开
5
  1. */
6
  1. public String downloadBatch(String idxs) {
7
  1.    String[] ids = idxs.split(",");
8
  1.    try {
9
  1.        HttpServletResponse response = ServletActionContext.getResponse();
10
  1.        OutputStream out = setDownloadOutputStream(response, String.valueOf(new Date().getTime()), "zip");
11
  1.        ZipOutputStream zipOut = new ZipOutputStream(out);
12
  1.  
13
  1.        for (int i = 0; i < ids.length; i++) {
14
  1.            Picture picture = Picture.get(Picture.class, Long.parseLong(ids[i]));
15
  1.            byte[] data = picture.getData();
16
  1.            zipOut.putNextEntry(new ZipEntry(i + "_" + picture.getName() + "." + picture.getFormat().getValue()));
17
  1.            writeBytesToOut(zipOut, data, BUFFER_SIZE);
18
  1.            zipOut.closeEntry();
19
  1.       }
20
  1.        zipOut.flush();
21
  1.        zipOut.close();
22
  1.  
23
  1.   } catch (IOException e) {
24
  1.        e.printStackTrace();
25
  1.        log.warn("下载失败:" + e.getMessage());
26
  1.   }
27
  1.  
28
  1.    return null;
29
  1. }

  1. /**
  2. * 设置文件下载的response格式
  3. *
  4. * @param response 响应
  5. * @param fileName 文件名称
  6. * @param fileType 文件类型
  7. * @return 设置后响应的输出流OutputStream
  8. * @throws IOException
  9. */
  10. private static OutputStream setDownloadOutputStream(HttpServletResponse response, String fileName, String fileType) throws IOException {
  11. fileName = new String(fileName.getBytes(), "ISO-8859-1");
  12. response.setHeader("Content-Disposition", "attachment;filename=" + fileName + "." + fileType);
  13. response.setContentType("multipart/form-data");
  14. return response.getOutputStream();
  15. }
15
 
1
  1. /**
2
  1. * 设置文件下载的response格式
3
  1. *
4
  1. * @param response 响应
5
  1. * @param fileName 文件名称
6
  1. * @param fileType 文件类型
7
  1. * @return 设置后响应的输出流OutputStream
8
  1. * @throws IOException
9
  1. */
10
  1. private static OutputStream setDownloadOutputStream(HttpServletResponse response, String fileName, String fileType) throws IOException {
11
  1.    fileName = new String(fileName.getBytes(), "ISO-8859-1");
12
  1.    response.setHeader("Content-Disposition", "attachment;filename=" + fileName + "." + fileType);
13
  1.    response.setContentType("multipart/form-data");
14
  1.    return response.getOutputStream();
15
  1. }

  1. /**
  2. * 将byte[]类型的数据,写入到输出流中
  3. *
  4. * @param out 输出流
  5. * @param data 希望写入的数据
  6. * @param cacheSize 写入数据是循环读取写入的,此为每次读取的大小,单位字节,建议为4096,即4k
  7. * @throws IOException
  8. */
  9. private static void writeBytesToOut(OutputStream out, byte[] data, int cacheSize) throws IOException {
  10. int surplus = data.length % cacheSize;
  11. int count = surplus == 0 ? data.length / cacheSize : data.length / cacheSize + 1;
  12. for (int i = 0; i < count; i++) {
  13. if (i == count - 1 && surplus != 0) {
  14. out.write(data, i * cacheSize, surplus);
  15. continue;
  16. }
  17. out.write(data, i * cacheSize, cacheSize);
  18. }
  19. }
 
1
  1. /**
2
  1. * byte[]类型的数据,写入到输出流中
3
  1. *
4
  1. * @param out 输出流
5
  1. * @param data 希望写入的数据
6
  1. * @param cacheSize 写入数据是循环读取写入的,此为每次读取的大小,单位字节,建议为4096,即4k
7
  1. * @throws IOException
8
  1. */
9
  1. private static void writeBytesToOut(OutputStream out, byte[] data, int cacheSize) throws IOException {
10
  1.    int surplus = data.length % cacheSize;
11
  1.    int count = surplus == 0 ? data.length / cacheSize : data.length / cacheSize + 1;
12
  1.    for (int i = 0; i < count; i++) {
13
  1.        if (i == count - 1 && surplus != 0) {
14
  1.            out.write(data, i * cacheSize, surplus);
15
  1.            continue;
16
  1.       }
17
  1.        out.write(data, i * cacheSize, cacheSize);
18
  1.   }
19
  1. }

第一段代码为下载的主方法,用到了两个子方法,分别贴在之后的第二段代码和第三段代码。

文件的下载其实很简单,刚才在叨叨IO中也提到了,所以对于网络传输下载的IO来说,整体也就三个步骤:
  • 设置文件ContentType类型和文件头
  • 读取文件数据为byte[]
  • 将数据写入到响应response的输出流中

设置请求头信息,在方法 setDownloadOutputStream() 中已经写明了,是文件所以要告知文件处理应该为 attachement,并附上文件名(转码ISO-8859-1避免中文乱码)。而文件内容的类型,统一设置为 multipart/form-data 即可,交给浏览器自行判断下载的文件类型。

读取文件数据,因为本例中我的图片数据直接存储在数据库字段中,所以取出时直接获取的就是byte[]。如果你的方式是文件存放在本地,数据库只是存储了文件的物理地址,那么你得多做做操作,用FileInputStream把文件的内容先读出来,结果和目的都是一样的,就是获取文件的byte[]。

获得了数据,那么直接通过OutputStream的write方法写入即可。(这里的写入方法我用了循环写入,当初是想着避免内存紧张,可是现在回过头来想不对啊,读文件的时候才吃内存的,这里我已经读完了再写,循环与否已经不重要了。所以实际上应该是边读边写,才是良性的,可是我的byte[]存在数据库字段里,取出来时就全部读入到内存中了,所以这里实际上是不需要循环写入的,我这是画蛇添足了。另外,如果是从File读的话,则边读边写,见目录1叨叨IO中的小段代码)

写入到输出流了,flush()刷新一下,即可。

上面这些是对于文件下载通用的,如果是批量压缩包形式,在第一段代码中黄色部分,来进行重点说明:
  • ZipOutputStream zipOut = new ZipOutputStream(out);
  • //把响应的输出流包装一下而已,便于使用相关压缩方法,就像多了个包装袋

  • zipOut.putNextEntry(new ZipEntry(i + "_" + picture.getName() + "." + picture.getFormat().getValue()));
  • zipOut.closeEntry();
  • //压缩包中的多个文件,实际上每个就是这里的ZipEntry对象,每开始写入某个文件的内容时,必须先putNextEntry(new ZipEntry(String fileName)),然后才可以写入,写完这个文件,必须使用closeEntry()说明,已经写完了第一个文件。就好像putNextEntry是在说 “我要开始写压缩包的下一个文件啦”,而closeEntry则是在说“压缩包里的这个文件我已经写完啦”。循环反复,最终把所有文件写入这个“披着压缩输出流外壳的响应输出流”

  • zipOut.flush();
  • 写入完成后,刷一下即可。就像去超市买东西,购物车装好了,总得结一次帐才能把东西拿走吧。当然,最后别忘了close关闭。

4、参考链接



批量下载,多文件压缩打包zip下载的更多相关文章

  1. 打包zip下载

    //首先引入的文件为org.apache的切记不是jdk的import org.apache.tools.zip.ZipOutputStream;import org.apache.tools.zip ...

  2. java实现将文件压缩成zip格式

    以下是将文件压缩成zip格式的工具类(复制后可以直接使用): zip4j.jar包下载地址:http://www.lingala.net/zip4j/download.php package util ...

  3. Web端文件打包.zip下载

    使用ant.jar包的API进行文件夹打包.直接上代码: String zipfilename = "test.zip"; File zipfile = new File(zipf ...

  4. Linux文件压缩/打包/解压

    在Linux日常维护中,经常需要备份同步一些比较重要的文件,而在传输过程中如果文件比较大往往会非常慢,而且还会非常占用空间,这时候就需要我们使用压缩工具对大文件进行压缩打包,下面我们来介绍一下常用的压 ...

  5. Linux中的文件压缩,打包和备份命令

    压缩解压命令 gzip  文件   -c : 将压缩数据输出到屏幕,可用来重定向 -v   显示压缩比等信息 -d   解压参数 -t    用来检验一个压缩文件的一致性看看档案有没错 -数字 : 压 ...

  6. 文件压缩:zip

    [root@localhost ~]# yum install -y zip unzip // 安装 zip 和 unzip [root@localhost ~]# ..txt // 压缩文件,要同时 ...

  7. Java实现多文件压缩打包的方法

    package com.biao.test; import java.io.File; import java.io.FileInputStream; import java.io.FileOutpu ...

  8. [转]C#如何把文件夹压缩打包然后下载

    public partial class _Default2 : System.Web.UI.Page{ protected void Page_Load(object sender, EventAr ...

  9. Java Springboot 根据图片链接生成图片下载链接 及 多个图片打包zip下载链接

    现有一些图片在服务器上的链接,在浏览器中打开这些链接是直接显示在浏览器页面的形式. 现在需要生成这些图片的单独下载以及打包下载链接,即在浏览器中打开下载链接后弹出下载框提示下载.由于前端存在跨域问题, ...

随机推荐

  1. avalonjs 中的if else实现的几种方法

    在学习avalonjs的过程中,发现模板中并没有if else这样的写法,不像tempalte ejs这些,所以总结了三种方法来实现,仅供在使用avalonjs的同学参考,主要是通过ms-if 表达式 ...

  2. DOM的查找,新增,删除操作

    查找 1. document.getElementById()  通过ID获取元素,由于ID唯一,所以获取的是一个元素 2. document.getElementsByTagName() 通过标签名 ...

  3. JavaSE——多线程

    进程和线程: 进程是指运行中的应用程序,每一个进程都有自己独立的内存空间.一个应用程序可以启动多个进程. 线程是指进程中的一个执行流程,有时也称为执行情景. 线程和进程的主要区别在于:每个进程都需要操 ...

  4. JavaScript Data.parse()转化时间戳安卓和ISO不兼容

    Data.parse()获取时间戳,在Android是没有问题的,但是在ISO就不行了,原因在于转化成时间戳的时间格式不一样. Android的格式是如“2017-12-12 12:12:12”,IS ...

  5. tinymce4.x 上传本地图片(自己写个插件)

    tinymce是一款挺不错的html文本编辑器.但是添加图片是直接添加链接,不能直接选择本地图片. 下面我写了一个插件用于直接上传本地图片. 在tinymce的plugins目录下新建一个upload ...

  6. Python+Selenium笔记(十七):操作cookie

    (一)方法 方法 简单说明 add_cookie(cookie_dict) 在当前会话中添加cookie信息 cookie_dict:字典,name和value是必须的 delete_all_cook ...

  7. 使用python快速搭建本地网站

    如果你急需一个简单的Web Server,但你又不想去下载并安装那些复杂的HTTP服务程序,比如:Apache,ISS,Nodejs等.那么, Python 可能帮助你.使用Python可以完成一个简 ...

  8. Azure 虚拟机诊断设置问题排查

    Azure 为用户提供了可以自己配置的性能监控功能:Azure 诊断扩展.但是在具体配置中,经常会遇到各种各样的问题.不了解监控的工作机制常常给排查带来一定难度.这里我们整理了关于 Azure 虚拟机 ...

  9. python自学——函数-strftime

    strftime()函数的用法   strftime()函数可以把YYYY-MM-DD HH:MM:SS格式的日期字符串转换成其它形式的字符串. strftime()的语法是strftime(格式, ...

  10. [Python_2] Python 基础

    0. 说明 Python 基础笔记,使用的版本为 Python 3.6.2 Python 的变量.字符串操作.list.元组.字典.循环.range.类型转换.运算等操作. 1. 引号的使用 字符串使 ...