文件的上傳和下載,是非常常見的功能。很多的系統中,或者軟件中都經常使用文件的上傳和下載。比如:QQ 頭像,就使用了上傳。郵箱中也有附件的上傳和下載功能。OA 系統中審批有附件材料的上傳
一、上傳原理和代碼分析。
上傳:我們把需要上傳的資源,發送給服務器,在服務器上保存下來。
下載:下載某一個資源時,將服務器上的該資源發送給瀏覽器。
難點:服務器端獲取資源時比較麻煩,
瀏覽器端
1、要有一個 form 標簽,method=post 請求
2、form 標簽的 encType 屬性值必須為 multipart/form-data 值
3、在 form 標簽中使用 input type=file 添加上傳的文件
注意:enctype=multipart/form-data:該屬性表明發送的請求體的內容是多表單元素的,通俗點講,就是有各種各樣的數據,可能有二進制數據,也可能有表單數據,等等,所以使用該屬性也進行其區分,發送的格式如下(使用火狐中的Firebug插件進行捕捉的信息。)
文件上傳,HTTP 協議的說明
服務器端
編寫服務器代碼(Servlet 程序)接收,處理上傳的數據。
如果不使用commons-fileupload插件來幫我們處理上傳后的數據而讓我們自己手動處理的話,也是可以的,但是十分麻煩,因為我們需要將所有的請求體獲取到,然后通過字符串的分割,通過boundary這個屬性進行分割,然后一步步獲取到我們想要的數據。
commons-fileupload.jar 常用 API 介紹說明
commons-fileupload.jar 需要依賴 commons-io.jar 這個包,所以兩個包我們都要引入。
第一步,就是需要導入兩個 jar 包:
commons-fileupload-1.2.1.jar
commons-io-1.4.jar
commons-fileupload.jar 和 commons-io.jar 包中,我們常用的類有哪些?
ServletFileUpload 類,用於解析上傳的數據。
FileItem 類,表示每一個表單項。
boolean ServletFileUpload.isMultipartContent(HttpServletRequest request);
判斷當前上傳的數據格式是否是多段的格式。
public List
解析上傳的數據
boolean FileItem.isFormField()
判斷當前這個表單項,是否是普通的表單項。還是上傳的文件類型。
true 表示普通類型的表單項
false 表示上傳的文件類型
String FileItem.getFieldName()
獲取表單項的 name 屬性值
String FileItem.getString()
獲取當前表單項的值。
String FileItem.getName();
獲取上傳的文件名
void FileItem.write( file );
將上傳的文件寫到 參數 file 所指向抽硬盤位置 。
// 工廠
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 }
其中需要注意的是流的操作,和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 }
上傳代碼
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、獲取到上傳內容時,對其存儲位置進行設置
其中很多細節問題都在上面已經說清楚了,代碼中也有很多注釋。
二、下載原理和代碼實現
下載的常用 API 說明:
response.getOutputStream();
servletContext.getResourceAsStream();
servletContext.getMimeType();
response.setContentType();
response.setHeader("Content-Disposition", "attachment; fileName=1.jpg");
這個響應頭告訴瀏覽器。這是需要下載的。而 attachment 表示附件,也就是下載的一個文件。fileName=后面,
表示下載的文件名。
完成上面的兩個步驟,下載文件是沒問題了。但是如果我們要下載的文件是中文名的話。你會發現,下載無法正確
顯示出正確的中文名。
原因是在響應頭中,不能包含有中文字符,只能包含 ASCII 碼。
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);
}
文件下載示例:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
IOException {
// 1、獲取要下載的文件名
String downloadFileName = "2.jpg";
// 2、讀取要下載的文件內容 (通過 ServletContext 對象可以讀取)
ServletContext servletContext = getServletContext();
// 獲取要下載的文件類型
String mimeType = servletContext.getMimeType("/file/" + downloadFileName);
System.out.println("下載的文件類型:" + mimeType);
// 4、在回傳前,通過響應頭告訴客戶端返回的數據類型
resp.setContentType(mimeType);
// 5、還要告訴客戶端收到的數據是用於下載使用(還是使用響應頭)
// Content-Disposition 響應頭,表示收到的數據怎么處理
// attachment 表示附件,表示下載使用
// filename= 表示指定下載的文件名
resp.setHeader("Content-Disposition", "attachment; filename=" + downloadFileName);
/**
* /斜杠被服務器解析表示地址為 http://ip:prot/工程名/ 映射 到代碼的 Web 目錄
*/
InputStream resourceAsStream = servletContext.getResourceAsStream("/file/" +
downloadFileName);
// 獲取響應的輸出流
OutputStream outputStream = resp.getOutputStream();
// 3、把下載的文件內容回傳給客戶端
// 讀取輸入流中全部的數據,復制給輸出流,輸出給客戶端
IOUtils.copy(resourceAsStream,outputStream);
}
附件中文名亂碼問題解決方案:
先說解決辦法:
URLEncoder解決所有瀏覽器附件中文件名問題。
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//String filename=request.getParameter("filename");
String userAgent = request.getHeader("User-Agent");
String fileName="2922Ö中文";
String encodedFileName=fileName;
//String encodedFileName=URLEncoder.encode(fileName, "UTF-8");
// if (userAgent.contains("MSIE")||userAgent.contains("Trident") || userAgent.contains("Chrome") || userAgent.contains("Firefox")) {
// encodedFileName = URLEncoder.encode(fileName, "UTF-8");
// }
encodedFileName = new String(fileName.getBytes("UTF-8"),"ISO-8859-1");
//encodedFileName = new String(fileName.getBytes("gbk"),"iso-8859-1");
System.out.println(encodedFileName);
//String folder="/download/";
System.out.println("hello");
String ua = request.getHeader("User-Agent");
// 判斷是否是火狐瀏覽器
if (userAgent.contains("Edg") || userAgent.contains("MSIE")||userAgent.contains("Trident") || userAgent.contains("Chrome") || userAgent.contains("Firefox")) {
// 使用下面的格式進行 BASE64 編碼后
String str = "attachment; fileName=" + "=?utf-8?B?"
+ new BASE64Encoder().encode("中文14.jpg".getBytes("utf-8")) + "?=";
// 設置到響應頭中
//response.setHeader("Content-Disposition", str);
} else {
// 把中文名進行 UTF-8 編碼操作。
String str = "attachment; fileName=" + URLEncoder.encode("中文14.jpg", "UTF-8");
// 然后把編碼后的字符串設置到響應頭中
//response.setHeader("Content-Disposition", str);
}
//response.addHeader("Content-Type", "application/octet-stream");
response.addHeader("Content-Disposition", "attachment;filename=" +encodedFileName);
// response.setHeader("Content-disposition", String.format("attachment; filename=\"%s.png\"; filename*=utf-8''%s.png", encodedFileName, encodedFileName));
InputStream is = getServletContext().getResourceAsStream("/download/123.png");
System.out.println(is);
ServletOutputStream out = response.getOutputStream();
System.out.println(out);
int b ;
byte[] bf = new byte[1024];
while((b=is.read(bf)) != -1){
out.write(bf,0,b);
}
}
}
下面是一種測試,也有中規范里面的解決辦法
response.setHeader("Content-disposition", String.format("attachment; filename=\"%s.txt\"; filename*=utf-8''%s.txt", encodedFileName, encodedFileName));//為了兼容 IE6,原始文件名必須包含英文擴展名!
Content-Disposition: attachment;
filename="$encoded_fname";
filename*=utf-8''$encoded_fname
其中, $encoded_fname 指的是將 UTF-8 編碼的原始文件名按照 RFC 3986 進行百分號編碼(percent encoding)后得到的( PHP 中使用 rawurlencode() 函數)。這幾行也可以合並為一行(推薦使用一個空格隔開)。
另外,為了兼容 IE6 ,請保證原始文件名必須包含英文擴展名!
Historically, HTTP has allowed field content with text in the ISO-8859-1 charset [ISO-8859-1], supporting other charsets only through use of [RFC2047] encoding. In practice, most HTTP header field values use only a subset of the US-ASCII charset [USASCII]. Newly defined header fields SHOULD limit their field values to US-ASCII octets. A recipient SHOULD treat other octets in field content (obs-text) as opaque data.
從歷史上看,HTTP 允許包含 ISO-8859-1 字符集 [ISO-8859-1] 中的文本的字段內容,僅通過 [RFC2047] 編碼支持其他字符集。實際上,大多數 HTTP 標頭字段值僅使用 US-ASCII 字符集 [USASCII] 的子集。新定義的標頭字段應限制其字段值為 US-ASCII 八位字節。收件人應將字段內容(obs-text)中的其他八位字節視為不透明數據。(純機器翻譯)