Java web----文件上傳

1 文件上傳對頁面的要求

  • 必須使用表單,而不能是超鏈接;
  • 表單的method必須是POST,而不能是GET;
  • 表單的enctype必須是multipart/form-data;
  • 在表單中添加file表單字段,即<input type=”file”…/>

  <body>
     <form action="<c:url value='/UploadServlet01'/>" method="post" enctype="multipart/form-data">
     	姓名:<input type="text" name="username"><br/>
     	照片:<input type="file" name="phone"><br/>     
     	<input type="submit" value="提交"/>	
     </form>	
  </body>

2 比對文件上傳表單和普通文本表單的區別

通過httpWatch查看“文件上傳表單”和“普通文本表單”的區別。

文件上傳表單的enctype=”multipart/form-data”,表示多部件表單數據;

普通文本表單可以不設置enctype屬性:

  • 當method=”post”時,enctype的默認值爲application/x-www-form-urlencoded,表示使用url編碼正文;
  • 當method=”get”時,enctype的默認值爲null,沒有正文,所以就不需要enctype了。

具體不同可以抓包查看

3 文件上傳對Servlet的要求

首先我們要肯定一點,文件上傳表單的數據也是被封裝到request對象中的。

request.getParameter(String)方法獲取指定的表單字段字符內容,但文件上傳表單已經不在是字符內容,而是字節內容,所以失效。 

這時可以使用request的getInputStream()方法獲取ServletInputStream對象,它是InputStream的子類,這個ServletInputStream對象對應整個表單的正文部分(從第一個分隔線開始,到最後),這說明我們需要的解析流中的數據。當然解析它是很麻煩的一件事情,而Apache已經幫我們提供瞭解析它的工具:commons-fileupload。

package com.cug.upload;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;

public class UploadServlet01 extends HttpServlet{
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		req.setCharacterEncoding("utf-8");
		resp.setContentType("text/html;charset=utf-8");
		
		ServletInputStream in = req.getInputStream();
		String s = IOUtils.toString(in);
		System.out.println(s);
	}
}

4 commons-fileupload

爲什麼使用fileupload:

上傳文件的要求比較多,需要記一下:

  • 必須是POST表單;
  • 表單的enctype必須是multipart/form-data;
  • 在表單中添加file表單字段,即<input type=”file”…/> 

Servlet的要求:

  • 不能再使用request.getParameter()來獲取表單數據;
  • 可以使用request.getInputStream()得到所有的表單數據,而不是一個表單項的數據;
  • 這說明不使用fileupload,我們需要自己來對request.getInputStream()的內容進行解析!!!

4.1 fileupload概述

fileupload是由apache的commons組件提供的上傳組件。它最主要的工作就是幫我們解析request.getInputStream()。

fileupload組件需要的JAR包有:

  • commons-fileupload.jar,核心包;
  • commons-io.jar,依賴包。

4.2 fileupload簡單應用

fileupload的核心類有:DiskFileItemFactory、ServletFileUpload、FileItem。

使用fileupload組件的步驟如下:

  • 創建工廠類DiskFileItemFactory對象:DiskFileItemFactoryfactory = new DiskFileItemFactory()
  • 使用工廠創建解析器對象:ServletFileUpload fileUpload = new ServletFileUpload(factory)
  • 使用解析器來解析request對象:List<FileItem>list = fileUpload.parseRequest(request)

隆重介紹FileItem類,它纔是我們最終要的結果。一個FileItem對象對應一個表單項(表單字段)。一個表單中存在文件字段和普通字段,可以使用FileItem類的isFormField()方法來判斷表單字段是否爲普通字段,如果不是普通字段,那麼就是文件字段了。

  • String getName():獲取文件字段的文件名稱;
  • String getString():獲取字段的內容,如果是文件字段,那麼獲取的是文件內容,當然上傳的文件必須是文本文件;
  • String getFieldName():獲取字段名稱,例如:<inputtype=”text” name=”username”/>,返回的是username;
  • String getContentType():獲取上傳的文件的類型,例如:text/plain。
  • int getSize():獲取上傳文件的大小;
  • boolean isFormField():判斷當前表單字段是否爲普通文本字段,如果返回false,說明是文件字段;
  • InputStream getInputStream():獲取上傳文件對應的輸入流;
  • void write(File):把上傳的文件保存到指定文件中。

4.3 簡單上傳示例

 <form action="${pageContext.request.contextPath }/FileUploadServlet" method="post" enctype="multipart/form-data">
    	用戶名:<input type="text" name="username"/><br/>
    	文件1:<input type="file" name="file1"/><br/>
    	<input type="submit" value="提交"/>
    </form>

public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		// 因爲要使用response打印,所以設置其編碼
		response.setContentType("text/html;charset=utf-8");
		
		// 創建工廠
		DiskFileItemFactory dfif = new DiskFileItemFactory();
		// 使用工廠創建解析器對象
		ServletFileUpload fileUpload = new ServletFileUpload(dfif);
		try {
			// 使用解析器對象解析request,得到FileItem列表
			List<FileItem> list = fileUpload.parseRequest(request);
			// 遍歷所有表單項
			for(FileItem fileItem : list) {
				// 如果當前表單項爲普通表單項
				if(fileItem.isFormField()) {
					// 獲取當前表單項的字段名稱
					String fieldName = fileItem.getFieldName();
					// 如果當前表單項的字段名爲username
					if(fieldName.equals("username")) {
						// 打印當前表單項的內容,即用戶在username表單項中輸入的內容
						response.getWriter().print("用戶名:" + fileItem.getString() + "<br/>");
					}
				} else {//如果當前表單項不是普通表單項,說明就是文件字段
					String name = fileItem.getName();//獲取上傳文件的名稱
					// 如果上傳的文件名稱爲空,即沒有指定上傳文件
					if(name == null || name.isEmpty()) {
						continue;
					}
					// 獲取真實路徑,對應${項目目錄}/uploads,當然,這個目錄必須存在
					String savepath = this.getServletContext().getRealPath("/uploads");
					// 通過uploads目錄和文件名稱來創建File對象
					File file = new File(savepath, name);
					// 把上傳文件保存到指定位置
					fileItem.write(file);
					// 打印上傳文件的名稱
					response.getWriter().print("上傳文件名:" + name + "<br/>");
					// 打印上傳文件的大小
					response.getWriter().print("上傳文件大小:" + fileItem.getSize() + "<br/>");
					// 打印上傳文件的類型
					response.getWriter().print("上傳文件類型:" + fileItem.getContentType() + "<br/>");
				}
			}
		} catch (Exception e) {
			throw new ServletException(e);
		} 
	}
5 文件上傳之細節

5.1  把上傳的文件放到WEB-INF目錄下

如果沒有把用戶上傳的文件存放到WEB-INF目錄下,那麼用戶就可以通過瀏覽器直接訪問上傳的文件,這是非常危險的。

假如說用戶上傳了一個a.jsp文件,然後用戶在通過瀏覽器去訪問這個a.jsp文件,那麼就會執行a.jsp中的內容,如果在a.jsp中有如下語句: Runtime.getRuntime().exec(“shutdown –s –t 1”);,那麼你就會…

 

通常我們會在WEB-INF目錄下創建一個uploads目錄來存放上傳的文件,而在Servlet中找到這個目錄需要使用ServletContext的getRealPath(String)方法,例如在我的upload1項目中有如下語句:

ServletContextservletContext = this.getServletContext();

String savepath= servletContext.getRealPath(“/WEB-INF/uploads”); 

其中savepath爲:F:\tomcat6_1\webapps\upload1\WEB-INF\uploads。

5.2 文件名稱(完整路徑、文件名稱)

使用不同瀏覽器測試,其中IE6就會返回上傳文件的完整路徑,不知道IE6在搞什麼,這給我們帶來了很大的麻煩,就是需要處理這一問題。

處理這一問題也很簡單,無論是否爲完整路徑,我們都去截取最後一個“\\”後面的內容就可以了。

String name = file1FileItem.getName();
int lastIndex = name.lastIndexOf("\\");//獲取最後一個“\”的位置
if(lastIndex != -1) {//注意,如果不是完整路徑,那麼就不會有“\”的存在。
	name = name.substring(lastIndex + 1);//獲取文件名稱
}
response.getWriter().print(name);
5.3 中文亂碼問題

上傳文件名稱中包含中文

當上傳的誰的名稱中包含中文時,需要設置編碼,commons-fileupload組件爲我們提供了兩種設置編碼的方式:

  • request.setCharacterEncoding(String):這種方式是我們最爲熟悉的方式了;
  • fileUpload.setHeaderEncdoing(String):這種方式的優先級高與前一種。

上傳文件的文件內容包含中文:

通常我們不需關心上傳文件的內容,因爲我們會把上傳文件保存到硬盤上!也就是說,文件原來是什麼樣子,到服務器這邊還是什麼樣子!

但是如果你有這樣的需求,非要在控制檯顯示上傳的文件內容,那麼你可以使用fileItem.getString(“utf-8”)來處理編碼。

文本文件內容和普通表單項內容使用FileItem類的getString(“utf-8”)來處理編碼。


5.4  上傳文件同名問題(文件重命名)

通常我們會把用戶上傳的文件保存到uploads目錄下,但如果用戶上傳了同名文件呢?這會出現覆蓋的現象。處理這一問題的手段是使用UUID生成唯一名稱,然後再使用“_”連接文件上傳的原始名稱。

例如用戶上傳的文件是“我的一寸照片.jpg”,在通過處理後,文件名稱爲:“891b3881395f4175b969256a3f7b6e10_我的一寸照片.jpg”,這種手段不會使文件丟失擴展名,並且因爲UUID的唯一性,上傳的文件同名,但在服務器端是不會出現同名問題的。

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		request.setCharacterEncoding("utf-8");
		DiskFileItemFactory dfif = new DiskFileItemFactory();
		ServletFileUpload fileUpload = new ServletFileUpload(dfif);
		try {
			List<FileItem> list = fileUpload.parseRequest(request);
			//獲取第二個表單項,因爲第一個表單項是username,第二個纔是file表單項
			FileItem fileItem = list.get(1);
			String name = fileItem.getName();//獲取文件名稱
			
			// 如果客戶端使用的是IE6,那麼需要從完整路徑中獲取文件名稱
			int lastIndex = name.lastIndexOf("\\");
			if(lastIndex != -1) {
				name = name.substring(lastIndex + 1);
			}
			
			// 獲取上傳文件的保存目錄
			String savepath = this.getServletContext().getRealPath("/WEB-INF/uploads");
			String uuid = CommonUtils.uuid();//生成uuid
			String filename = uuid + "_" + name;//新的文件名稱爲uuid + 下劃線 + 原始名稱
			
			//創建file對象,下面會把上傳文件保存到這個file指定的路徑
			//savepath,即上傳文件的保存目錄
			//filename,文件名稱
			File file = new File(savepath, filename);
			
			// 保存文件
			fileItem.write(file);
		} catch (Exception e) {
			throw new ServletException(e);
		} 
	}

5.5 一個目錄不能存放過多的文件(存放目錄打散)

 我們這裏使用hash算法來打散:

  • 獲取文件名稱的hashCode:int hCode = name.hashCode();;
  • 獲取hCode的低4位,然後轉換成16進制字符;
  •  獲取hCode的5~8位,然後轉換成16進制字符;
  • 使用這兩個16進制的字符生成目錄鏈。例如低4位字符爲“5”

  這種算法的好處是,在uploads目錄下最多生成16個目錄,而每個目錄下最多再生成16個目錄,即256個目錄,所有上傳的文件都放到這256個目錄下。如果每個目錄上限爲1000個文件,那麼一共可以保存256000個文件。 

例如上傳文件名稱爲:新建文本文檔.txt,那麼把“新建 文本文檔.txt”的哈希碼獲取到,再獲取哈希碼的低4位,和5~8位。假如低4位爲:9,5~8位爲1,那麼文件的保存路徑爲uploads/9/1/。

	int hCode = name.hashCode();//獲取文件名的hashCode
	//獲取hCode的低4位,並轉換成16進制字符串
	String dir1 = Integer.toHexString(hCode & 0xF);
	//獲取hCode的低5~8位,並轉換成16進制字符串
	String dir2 = Integer.toHexString(hCode >>> 4 & 0xF);
	//與文件保存目錄連接成完整路徑
	savepath = savepath + "/" + dir1 + "/" + dir2;
	//因爲這個路徑可能不存在,所以創建成File對象,再創建目錄鏈,確保目錄在保存文件之前已經存在
	new File(savepath).mkdirs();
5.6 上傳的單個文件的大小限制

限制上傳文件的大小很簡單,ServletFileUpload類的setFileSizeMax(long)就可以了。參數就是上傳文件的上限字節數,例如servletFileUpload.setFileSizeMax(1024*10)表示上限爲10KB。

一旦上傳的文件超出了上限,那麼就會拋出FileUploadBase.FileSizeLimitExceededException異常。我們可以在Servlet中獲取這個異常,然後向頁面輸出“上傳的文件超出限制”。

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		request.setCharacterEncoding("utf-8");
		DiskFileItemFactory dfif = new DiskFileItemFactory();
		ServletFileUpload fileUpload = new ServletFileUpload(dfif);
		// 設置上傳的單個文件的上限爲10KB
		fileUpload.setFileSizeMax(1024 * 10); 
		try {
			List<FileItem> list = fileUpload.parseRequest(request);
			//獲取第二個表單項,因爲第一個表單項是username,第二個纔是file表單項
			FileItem fileItem = list.get(1);
			String name = fileItem.getName();//獲取文件名稱
			
			// 如果客戶端使用的是IE6,那麼需要從完整路徑中獲取文件名稱
			int lastIndex = name.lastIndexOf("\\");
			if(lastIndex != -1) {
				name = name.substring(lastIndex + 1);
			}
			
			// 獲取上傳文件的保存目錄
			String savepath = this.getServletContext().getRealPath("/WEB-INF/uploads");
			String uuid = CommonUtils.uuid();//生成uuid
			String filename = uuid + "_" + name;//新的文件名稱爲uuid + 下劃線 + 原始名稱
			
			int hCode = name.hashCode();//獲取文件名的hashCode
			//獲取hCode的低4位,並轉換成16進制字符串
			String dir1 = Integer.toHexString(hCode & 0xF);
			//獲取hCode的低5~8位,並轉換成16進制字符串
			String dir2 = Integer.toHexString(hCode >>> 4 & 0xF);
			//與文件保存目錄連接成完整路徑
			savepath = savepath + "/" + dir1 + "/" + dir2;
			//因爲這個路徑可能不存在,所以創建成File對象,再創建目錄鏈,確保目錄在保存文件之前已經存在
			new File(savepath).mkdirs();
			
			//創建file對象,下面會把上傳文件保存到這個file指定的路徑
			//savepath,即上傳文件的保存目錄
			//filename,文件名稱
			File file = new File(savepath, filename);
			
			// 保存文件
			fileItem.write(file);
		} catch (Exception e) {
			// 判斷拋出的異常的類型是否爲FileUploadBase.FileSizeLimitExceededException
			// 如果是,說明上傳文件時超出了限制。
			if(e instanceof FileUploadBase.FileSizeLimitExceededException) {
				// 在request中保存錯誤信息
				request.setAttribute("msg", "上傳失敗!上傳的文件超出了10KB!");
				// 轉發到index.jsp頁面中!在index.jsp頁面中需要使用${msg}來顯示錯誤信息
				request.getRequestDispatcher("/index.jsp").forward(request, response);
				return;
			}
 			throw new ServletException(e);
		} 
	}
5.7 上傳文件的總大小限制

有時我們需要限制一個請求的大小。也就是說這個請求的最大字節數(所有表單項之和)!實現這一功能也很簡單,只需要調用ServletFileUpload類的setSizeMax(long)方法即可。

例如fileUpload.setSizeMax(1024 * 10);,顯示整個請求的上限爲10KB。當請求大小超出10KB時,ServletFileUpload類的parseRequest()方法會拋出FileUploadBase.SizeLimitExceededException異常。


5.8 緩存大小與臨時目錄
	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		request.setCharacterEncoding("utf-8");
		<strong>DiskFileItemFactory dfif = new DiskFileItemFactory(1024*20, new File("F:\\temp"));</strong>
		ServletFileUpload fileUpload = new ServletFileUpload(dfif);
		
		try {
			List<FileItem> list = fileUpload.parseRequest(request);
			FileItem fileItem = list.get(1);
			String name = fileItem.getName();
			String savepath = this.getServletContext().getRealPath("/WEB-INF/uploads");
			
			// 保存文件
			fileItem.write(path(savepath, name));
		} catch (Exception e) {
			throw new ServletException(e);
		} 
	}
	
	private File path(String savepath, String filename) {
		// 從完整路徑中獲取文件名稱
		int lastIndex = filename.lastIndexOf("\\");
		if(lastIndex != -1) {
			filename = filename.substring(lastIndex + 1);
		}
		
		// 通過文件名稱生成一級、二級目錄
		int hCode = filename.hashCode();
		String dir1 = Integer.toHexString(hCode & 0xF);
		String dir2 = Integer.toHexString(hCode >>> 4 & 0xF);
		savepath = savepath + "/" + dir1 + "/" + dir2;
		// 創建目錄
		new File(savepath).mkdirs();
		
		// 給文件名稱添加uuid前綴
		String uuid = CommonUtils.uuid();
		filename = uuid + "_" + filename;
		
		// 創建文件完成路徑
		return new File(savepath, filename);
	}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章