0、寫在前面的話
圖片批量下載,要求下載時集成為一個壓縮包進行下載。從昨天下午折騰到現在,踩坑踩得莫名其妙,還是來嘮嘮,給自己留個印象的同時,也希望給需要用到這個方法的人帶來一些幫助。
1、先叨叨IO
叨叨IO是因為網絡傳輸無非也就是流的傳遞,所以下載文件到本地的話實際上也是IO的東西,這個和讀取本地文件然后寫入到本地另一個文件的操作是基本一樣的。
我在自己IO基礎的博客中(《
[03] 節點流和處理流》)其實也有提到示例,拿復寫文件來說,大概是如下過程:
對於讀取文件(不僅僅是文本)到服務器內存,常見的是通過InputStream讀取File,所以你可能也經常看到如下類似的代碼:
outputStream = new FileOutputStream(file);
byte[] temp = new byte[1024];
int size = -1;
while ((size = inputStream.read(temp)) != -1) { // 每次讀取1KB,直至讀完
outputStream.write(temp, 0, size);
}
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
}
我這里想說的是,不論何種形式,只需要知道的是,在寫出之前,要獲取將寫出的數據,這個數據常常是作為
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地址如下:
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.6.5</version>
</dependency>
5
1
<dependency>
2
<groupId>ant</groupId>
3
<artifactId>ant</artifactId>
4
<version>1.6.5</version>
5
</dependency>
先貼代碼,然后再進行說明:
/**
* 批量下載
*
* @param idxs 圖片的id拼接字符串,用逗號隔開
*/
public String downloadBatch(String idxs) {
String[] ids = idxs.split(",");
try {
HttpServletResponse response = ServletActionContext.getResponse();
OutputStream out = setDownloadOutputStream(response, String.valueOf(new Date().getTime()), "zip");
ZipOutputStream zipOut = new ZipOutputStream(out);
for (int i = 0; i < ids.length; i++) {
Picture picture = Picture.get(Picture.class, Long.parseLong(ids[i]));
byte[] data = picture.getData();
zipOut.putNextEntry(new ZipEntry(i + "_" + picture.getName() + "." + picture.getFormat().getValue()));
writeBytesToOut(zipOut, data, BUFFER_SIZE);
zipOut.closeEntry();
}
zipOut.flush();
zipOut.close();
} catch (IOException e) {
e.printStackTrace();
log.warn("下載失敗:" + e.getMessage());
}
return null;
}
x
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
13
for (int i = 0; i < ids.length; i++) {
14
Picture picture = Picture.get(Picture.class, Long.parseLong(ids[i]));
15
byte[] data = picture.getData();
16
zipOut.putNextEntry(new ZipEntry(i + "_" + picture.getName() + "." + picture.getFormat().getValue()));
17
writeBytesToOut(zipOut, data, BUFFER_SIZE);
18
zipOut.closeEntry();
19
}
20
zipOut.flush();
21
zipOut.close();
22
23
} catch (IOException e) {
24
e.printStackTrace();
25
log.warn("下載失敗:" + e.getMessage());
26
}
27
28
return null;
29
}
/**
* 設置文件下載的response格式
*
* @param response 響應
* @param fileName 文件名稱
* @param fileType 文件類型
* @return 設置后響應的輸出流OutputStream
* @throws IOException
*/
private static OutputStream setDownloadOutputStream(HttpServletResponse response, String fileName, String fileType) throws IOException {
fileName = new String(fileName.getBytes(), "ISO-8859-1");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + "." + fileType);
response.setContentType("multipart/form-data");
return response.getOutputStream();
}
15
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
}
/**
* 將byte[]類型的數據,寫入到輸出流中
*
* @param out 輸出流
* @param data 希望寫入的數據
* @param cacheSize 寫入數據是循環讀取寫入的,此為每次讀取的大小,單位字節,建議為4096,即4k
* @throws IOException
*/
private static void writeBytesToOut(OutputStream out, byte[] data, int cacheSize) throws IOException {
int surplus = data.length % cacheSize;
int count = surplus == 0 ? data.length / cacheSize : data.length / cacheSize + 1;
for (int i = 0; i < count; i++) {
if (i == count - 1 && surplus != 0) {
out.write(data, i * cacheSize, surplus);
continue;
}
out.write(data, i * cacheSize, cacheSize);
}
}
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
}
第一段代碼為下載的主方法,用到了兩個子方法,分別貼在之后的第二段代碼和第三段代碼。
文件的下載其實很簡單,剛才在叨叨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關閉。
