今天心態正常。。。繼續努力。。
--WH
一、上傳原理和程式碼分析。
上傳:我們把需要上傳的資源,傳送給伺服器,在伺服器上儲存下來。
下載:下載某一個資源時,將伺服器上的該資源傳送給瀏覽器。
難點:伺服器端獲取資源時比較麻煩,
瀏覽器端
注意:enctype=multipart/form-data:該屬性表明傳送的請求體的內容是多表單元素的,通俗點講,就是有各種各樣的資料,可能有二進位制資料,也可能有表單資料,等等,所以使用該屬性也進行其區分,傳送的格式如下(使用火狐中的Firebug外掛進行捕捉的資訊。)
使用multipart/form-data會有一個boundary屬性,來用將提交的表單資料進行分隔,以用來讓伺服器知道哪個是我們上傳的資源,哪個是普通的表單資料。
伺服器端,
如果不使用commons-fileupload外掛來幫我們處理上傳後的資料而讓我們自己手動處理的話,也是可以的,但是十分麻煩,因為我們需要將所有的請求體獲取到,然後通過字串的分割,通過boundary這個屬性進行分割,然後一步步獲取到我們想要的資料。
使用commons-fileupload進行處理上傳內容。
程式碼
1 try { 2 3 //1 工廠 4 FileItemFactory fileItemFactory = new DiskFileItemFactory(); 5 6 //2 核心類 7 ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory); 8 9 //3 解析request ,List存放 FileItem (表單元素的封裝物件,一個<input>對應一個物件) 10 List<FileItem> list = servletFileUpload.parseRequest(request); 11 12 //4 遍歷集合獲得資料 13 for (FileItem fileItem : list) { 14 if(fileItem.isFormField()){ 15 // 5 是否為表單欄位(普通表單元素) 16 //5.1.表單欄位名稱 17 String fieldName = fileItem.getFieldName(); 18 System.out.println(fieldName); 19 //5.2.表單欄位值 20 String fieldValue = fileItem.getString(); //中文會出現亂碼 21 System.out.println(fieldValue); 22 } else { 23 //6 上傳欄位(上傳表單元素) 24 //6.1.表單欄位名稱 fileItem.getFieldName(); 25 //6.2.上傳檔名 26 String fileName = fileItem.getName(); 27 // * 相容瀏覽器, IE : C:\Users\xxx\Desktop\abc.txt ; 其他瀏覽器 : abc.txt 28 fileName = fileName.substring(fileName.lastIndexOf("\\") + 1); 29 System.out.println(fileName); //上傳的檔名會中文亂碼, 30 //6.3.上傳內容 31 InputStream is = fileItem.getInputStream(); //獲得輸入流, 32 String parentDir = this.getServletContext().getRealPath("/WEB-INF/upload"); 33 File file = new File(parentDir,fileName); 34 if(! file.getParentFile().exists()){ //父目錄不存在 35 file.getParentFile().mkdirs(); //mkdirs():建立資料夾,如果上級目錄沒有的話,也一併建立出來。 36 } 37 FileOutputStream out = new FileOutputStream(file); 38 byte[] buf = new byte[1024]; 39 int len = -1; 40 while( (len = is.read(buf)) != -1){ 41 out.write(buf, 0, len); 42 } 43 44 //關閉 45 out.close(); 46 is.close(); 47 } 48 } 49 50 } catch (Exception e) { 51 e.printStackTrace(); 52 53 throw new RuntimeException(e); 54 55 } 56 }
其中需要注意的是流的操作,和File類的操作,mkdirs()和mkdir()的區別是什麼?它們都是用來建立資料夾的,區別在mkdirs能建立多級目錄,而mkdir()只能建立當前的資料夾,如果發現上一層目錄並還沒有建立時,它也會無動於衷,什麼也不幹,也就不能建立當前資料夾,必須先要建立了上一級資料夾才可以。
上面還有很多的問題需要解決,比如上傳檔名的亂碼問題,比如單表內容的亂碼問題,比如上傳檔案同名問題,如果上傳的檔案很大,該如何進行處理,比如,放在同一級目錄下的檔案過多如何處理。每次都自己手動寫輸出流,將內容存到指定位置,太過麻煩,等等問題,現在寫一個加強版,在解決上面所有的問題。
上傳檔名亂碼問題:使用servletFileUpload.setHeaderEncoding("UTF-8");或者request.setCharacterEncoding("UTF-8")都可以
表單內容亂碼問題:使用getString("utf-8")即可,也就是在獲取內容時,就可以設定碼錶。
上傳檔案同名問題:使用UUID.randomUUID().toString().replace("-", "").獲得一個獨一無二的32位數字
使用FileUtils.copyInputStreamToFile(is, file);來將內容輸出到指定路徑檔案中去,mkdirs() 自動建立目錄
同一級目錄下的檔案過多問題:建立多級目錄,通過檔名的hashcode的值,hashCode為int型,佔4個位元組,一個位元組佔8位,也就是佔32位,將其分組,4位4位一組,就能分8組,每一次代表一層目錄,也就能夠分8層目錄,每一層中,有可以建立16種不同的資料夾。這樣算下來,就能有很多很多個資料夾了,每個資料夾下面都可以存很多檔案,看我們的業務需求,來決定要建立幾層目錄,就從hashcode中拿幾組資料出來。原理圖如下
生成兩級目錄
程式碼
1 public class StringUtils { 2 3 /** 4 * 生成二級目錄 5 * @param fileName abc.txt 6 * @return /4/5 7 */ 8 public static String getDir(String fileName) { 9 //1 hashCode值 10 int hashCode = fileName.hashCode(); 11 System.out.println(hashCode); 12 //2 第一層 0xf表示15的16進位制數。 13 int dir1 = hashCode & 0xf; 14 //3 第二層目錄 15 int dir2 = hashCode >>> 4 & 0xf; 16 17 //4 拼寫 18 return "/" + dir1 + "/" + dir2; 19 } 20 21 public static void main(String[] args) { 22 System.out.println(getDir("abc.txt")); 23 } 24 25 }
上傳程式碼
1 public void doGet(HttpServletRequest request, HttpServletResponse response) 2 throws ServletException, IOException { 3 try { 4 5 //0.5 檢查是否支援檔案上傳 ,檢查請求頭Content-Type : multipart/form-data 6 if(!ServletFileUpload.isMultipartContent(request)){ 7 throw new RuntimeException("不要得瑟,沒用"); 8 } 9 10 //1 工廠 11 DiskFileItemFactory fileItemFactory = new DiskFileItemFactory(); 12 // 1.1 設定是否生產臨時檔案臨界值。大於2M生產臨時檔案。保證:上傳資料完整性。 13 fileItemFactory.setSizeThreshold(1024 * 1024 * 2); //2MB 14 // 1.2 設定臨時檔案存放位置 15 // * 臨時副檔名 *.tmp ,臨時檔案可以任意刪除。 16 String tempDir = this.getServletContext().getRealPath("/temp"); 17 fileItemFactory.setRepository(new File(tempDir)); 18 19 //2 核心類 20 ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory); 21 // 2.1 如果使用無參構造 ServletFileUpload() ,手動設定工廠 22 //servletFileUpload.setFileItemFactory(fileItemFactory); 23 // 2.2 單個上傳檔案大小 24 //servletFileUpload.setFileSizeMax(1024*1024 * 2); //2M 25 // 2.3 整個上傳檔案總大小 26 //servletFileUpload.setSizeMax(1024*1024*10); //10M 27 // 2.4 設定上傳檔名的亂碼 28 // * 首先使用 setHeaderEncoding 設定編碼 29 // * 如果沒有設定將使用請求編碼 request.setCharacterEncoding("UTF-8") 30 // * 以上都沒有設定,將使用平臺預設編碼 31 servletFileUpload.setHeaderEncoding("UTF-8"); 32 // 2.5 上傳檔案進度,提供監聽器進行監聽。 33 servletFileUpload.setProgressListener(new MyProgressListener()); 34 35 36 //3 解析request ,List存放 FileItem (表單元素的封裝物件,一個<input>對應一個物件) 37 List<FileItem> list = servletFileUpload.parseRequest(request); 38 39 //4 遍歷集合獲得資料 40 for (FileItem fileItem : list) { 41 // 判斷 42 if(fileItem.isFormField()){ 43 // 5 是否為表單欄位(普通表單元素) 44 //5.1.表單欄位名稱 45 String fieldName = fileItem.getFieldName(); 46 System.out.println(fieldName); 47 //5.2.表單欄位值 , 解決普通表單內容的亂碼 48 String fieldValue = fileItem.getString("UTF-8"); 49 System.out.println(fieldValue); 50 } else { 51 //6 上傳欄位(上傳表單元素) 52 //6.1.表單欄位名稱 fileItem.getFieldName(); 53 //6.2.上傳檔名 54 String fileName = fileItem.getName(); 55 // * 相容瀏覽器, IE : C:\Users\liangtong\Desktop\abc.txt ; 其他瀏覽器 : abc.txt 56 fileName = fileName.substring(fileName.lastIndexOf("\\") + 1); 57 // * 檔案重名 58 fileName = UUID.randomUUID().toString().replace("-", "") + fileName; 59 // * 單個資料夾檔案個數過多? 60 String subDir = StringUtils.getDir(fileName); 61 62 System.out.println(fileName); 63 //6.3.上傳內容 64 InputStream is = fileItem.getInputStream(); 65 String parentDir = this.getServletContext().getRealPath("/WEB-INF/upload"); 66 File file = new File(parentDir + subDir,fileName); 67 68 // 將指定流 寫入 到 指定檔案中 -- mkdirs() 自動建立目錄 69 FileUtils.copyInputStreamToFile(is, file); 70 71 //7刪除臨時檔案 72 fileItem.delete(); 73 } 74 } 75 76 } catch (Exception e) { 77 e.printStackTrace(); 78 79 throw new RuntimeException(e); 80 81 } 82 }
其中如果需要做上傳的進度條時,就可以使用監聽器來進行監聽上傳資料的進度,實現ProgressListener即可。
總結上傳:
其實理解了也不是很難,就是上傳檔案後的處理比較麻煩,各種小問題,儲存過程最為麻煩。
1、建立工廠類
2、使用核心類,
3、解析request請求,
4、遍歷請求體的內容,將上傳內容和普通表單內容都獲取出來
5、獲取到上傳內容時,對其儲存位置進行設定
其中很多細節問題都在上面已經說清楚了,程式碼中也有很多註釋。
二、下載原理和程式碼實現
實現下載有兩種方式,
第一種,使用a標籤,也就是使用超連結,如果瀏覽器能夠解析,則直接顯示出來,如果不能解析,則進行下載,這種方式不太好,
我使用的是火狐,第一個和第三個都能直接解析出來,而第二個則不能解析,顯示的是下載頁面
第二種方式:
1、設定響應頭,讓瀏覽器知道應該下載,而不是解析
response.setHeader("content-disposition", "attachment;filename=" +fileName); //設定content-disposition響應頭
2、獲取輸入流,指向需要下載的檔案
InputStream is = this.getServletContext().getResourceAsStream("/download/1.jpg");
3、獲取輸出流,將其檔案傳到瀏覽器端
ServletOutputStream out = response.getOutputStream();
4、使用IOUtils.copy(is,out);直接將輸入流和輸出流傳進去,就會幫我們把輸出流讀到的內容通過輸出流輸出到瀏覽器。內部實現原理應該就如下所示
int b = -1;
byte[] bf = new byte[1024];
while((b=is.read(bf)) != -1){
out.write(bf,0,b);
}
注意:這裡下載時寫的下載檔名不包含中文,所以能夠正常顯示,如果寫中文的話,則會亂碼,甚至是中文都不會顯示出來。有兩種方式處理這個中文亂碼問題
1、簡單處理
fileName = new String(fileName.getBytes("gbk"),"ISO8859-1");
fileName = new String(fileName.getBytes("utf-8"),"ISO8859-1");
fileName = new String(fileName.getBytes(),"ISO8859-1"); //這種跟第一種是一樣的,預設使用的編碼就是gbk。
原理
上面程式碼意思是,先將fileName使用gbk或者utf-8進行編碼,然後在使用ISO8859-1進行解碼,此時的fileName是一個亂碼文字。具體原理圖可以看下面兩張圖
這張圖解釋了上面這段程式碼所做的事情,而整個編碼過程,我來簡單口述一下,在伺服器端,寫上面這段程式碼,美女是中文字,將其使用GBK碼錶進行編碼後,就會獲得一個計算機認識的符號,比如是123,然後我們在將這個計算機認識的符號,123使用ISO8859-1碼錶進行解碼,變成我們所認識的漢字,但是因為編碼和解碼所用碼錶不一樣,所以不能正常顯示,也就是此時的fileName本身就是一個亂碼的文字,然後在響應頭中,因為是使用ISO8859-1的碼錶進行編碼,所以fileName本身亂碼的文字,就會變為機器認識的123,當到了瀏覽器端,因為123是由gbk進行編碼的,所以如果使用gbk進行的解碼的話,就會使其正確的顯示,過程就是這樣
如果還不知道什麼是編碼,什麼是解碼,那麼就去看一下request和response亂碼問題的解決那篇博文,應該能解決你的疑問。
程式碼:
1 2 String fileName = "美女.jpg"; 3 fileName = new String(fileName.getBytes("gbk"),"ISO8859-1"); 4 //設定響應頭,通知瀏覽器應該進行下載,而不是解析。 5 response.setHeader("content-disposition", "attachment;filename=" +fileName); 6 7 // 讀取資源,傳送給瀏覽器 8 InputStream is = this.getServletContext().getResourceAsStream("/download/1.jpg"); 9 ServletOutputStream out = response.getOutputStream(); 10 IOUtils.copy(is, out);
2、複雜處理
根據每個瀏覽器的不同,而進行不同的操作。根據瀏覽器不同進行設定。IE和谷歌 URL編碼,火狐採用BASE64編碼
IE和谷歌
// * IE 谷歌 採用 URL編碼,就用URL對fileName進行編碼即可。其內部跟我們上面講解編碼解碼時的思路類似,可以將fileName輸出看看是個什麼結果,還是一個xxx.jpg,說明就是將fileName進行編碼,解碼的過程。
if(userAgent.contains("MSIE") || userAgent.contains("Chrome")){ //判斷是不是google或者IE瀏覽器
fileName = URLEncoder.encode(fileName, "UTF-8");
}
火狐 採用 Base64編碼
這裡會出現兩個問題,
一個是BASE64Encoder這個類找不到包,這個問題的解決參考
http://blog.csdn.net/jbxiaozi/article/details/7351768
http://www.cnblogs.com/silentjesse/archive/2013/03/07/2948146.html
另一個是格式問題。這個非常麻煩,我是記不住,具體檢視圖中的註釋,格式已經寫出來了。
下載原理總程式碼,包含上面所講解到的。
1 //下載 中文檔名 亂碼 2 String fileName = "美眉.jpg"; 3 //方案1:簡單方案 4 //fileName = new String(fileName.getBytes("GBK"),"ISO8859-1"); 5 //方案2:不同的瀏覽器對檔名解析採用不同方案。 6 String userAgent = request.getHeader("User-Agent"); 7 // * IE 谷歌 採用 URL編碼 8 if(userAgent.contains("MSIE") || userAgent.contains("Chrome")){ 9 fileName = URLEncoder.encode(fileName, "UTF-8"); 10 } 11 // * 火狐 採用 Base64編碼 12 if(userAgent.contains("Firefox")){ 13 BASE64Encoder base64Encoder = new BASE64Encoder(); 14 String encStr = base64Encoder.encode(fileName.getBytes("UTF-8")); 15 // 格式 : =?字符集?編碼方式?....?= 16 // * 編碼方式:B base64 ,Q q編碼 17 fileName = "=?UTF-8?B?"+encStr+"?="; 18 } 19 20 //設定響應頭,通知瀏覽器應該進行下載,而不是解析。 21 response.setHeader("content-disposition", "attachment;filename=" +fileName); 22 23 // 讀取資源,傳送給瀏覽器 24 InputStream is = this.getServletContext().getResourceAsStream("/download/1.jpg"); 25 ServletOutputStream out = response.getOutputStream(); 26 IOUtils.copy(is, out);
總結:下載其實非常簡單,就是在編寫下載檔名出現的中文亂碼問題比較麻煩,其他的套路很容易,
1、設定響應頭,讓瀏覽器知道該檔案是需要下載的
2、就是找準要下載的檔案的路徑從而拿到輸入流,在通過response拿到輸出流,
3、通過OUtils.copy(is, out);就解決了,不用在乎其中內部的實現。
三、總結
到這裡,上傳和下載的原理和程式碼都已經講解完了,你學會了嗎?其中只能總結一點,基礎知識很重要,我在編寫這博文時,發現連最基礎的對File類的操作都模糊不清,對流的操作也不是很感冒,但是通過翻閱資料,也差不多知道了自己曾不知道的東西。