1. 程式人生 > >JSP 實用程式之簡易檔案上傳元件

JSP 實用程式之簡易檔案上傳元件

Edit:6-19 更新,提供了基於 Servlet(非 JSP)的元件下載,這是一個完整的 Java Web 專案,用 Eclipse 匯入即可,專案原始碼在這裡

檔案上傳,包括但不限於圖片上傳,是 Web 開發中司空見慣的場景,相信各位或多或少都曾寫過這方面相關的程式碼。Java 界若說檔案上傳,則言必稱 Apache Commons FileUpload,論必及  SmartUpload。更甚者,Servlet 3.0 將檔案上傳列入 JSR 標準,使得通過幾個註解就可以在 Servlet 中配置上傳,無須依賴任何元件。使用第三方元件或 Servlet 自帶元件固然強大,但只靠 JSP 亦能完成任務,且短小而精悍,豈不美哉?本文實現的方法純然基於 JSP 程式碼,沒有弄成 Servlet 和專門的 Class(.java),實現方法純粹是基於 JSP,沒有太高的技術難度。實際使用過程中直接部署即可。

操作元件的程式碼行數不超過 10 行,只需幾個步驟:

  1. 生成元件例項
  2. 設定例項屬性
  3. 呼叫上傳/下載方法
  4. 處理呼叫結果

首先是上傳頁面,本例是一張靜態的 HTML。

上傳成功如下圖所示。


使用 POST 的表單,設定 ContentType 為 multipart/form-data 多段資料,還要記得 input 的 name 屬性。

<html>
<body>
	<form action="action.jsp" enctype="multipart/form-data" method="POST">
		selectimage: <input type="file" name="myfile" /><br> <input
			type="submit" value="upload" />
	</form>
</body>
</html>

action 中接受客戶端請求的服務端程式碼在 action.jsp 中。action.jsp 通過 <%@include file="Upload.jsp"%>包含了核心 Java 程式碼,而 Upload.jsp 裡面又包含了另外一個 UploadRequest.jsp 檔案。總之,我們這個小小的 Java 程式,一共包含了 UploadRequest 請求資訊類、UploadException 自定義異常類和最重要的 Upload 類這三個類。

<%@page pageEncoding="UTF-8"%>
<%@include file="Upload.jsp"%>
<%
	UploadRequest ur = new UploadRequest();// 建立請求資訊,所有引數都在這兒設定
	ur.setRequest(request);	//一定要傳入 request
	ur.setFileOverwrite(true);// 相同檔名是否覆蓋?true=允許覆蓋

	Upload upload = new Upload();// 上傳器

	try {
		upload.upload(ur);
	} catch (UploadException e) {
		response.getWriter().println(e.toString());
	}

	if (ur.isOk()) // 上傳成功
		response.getWriter().println("上傳成功:" + ur.getUploaded_save_fileName());
	else
		response.getWriter().println("上傳失敗!");
%>

這裡建立了 UploadRequest 例項。檔案上傳操作通常會附加一些限制,如:檔案型別、上傳檔案總大小、每個檔案的最大大小等。除此以外,作為一個通用元件還需要考慮更多的問題, 如:支援自定義檔案儲存目錄、支援相對路徑和絕對路徑、支援自定義儲存的檔案的檔名稱等。這些配置通通在 UploadRequest 裡設定。

至於 JSP 裡面的類,我願意多說說。 JSP 裡面允許我們定義 Java 的類,類本是可以是 static,但不能有 static 成員。實際上 JSP 類都是內部類,定義 static 與否關係不大。如果不能定義 static 方法,就把 static 方法移出類體外,書寫成,

 <%!

    /**
     * 獲取開頭資料頭佔用的長度
     *
     * @param dateBytes
     *            檔案二進位制資料
     * @return
     */
    private static int getStartPos(byte[] dateBytes) {

      ....

    }

%>

 <%! ... %> 和 <% ... %> 不同,前者是定義類成員的。

好~我們在看看 UploadRequest.jsp,就知道具體配置些什麼。

<%@page pageEncoding="UTF-8"%>
<%!/**
	 * 上傳請求的 bean,包含所有有關請求的資訊
	 * @author frank
	 *
	 */
	public static class UploadRequest {
		/**
		 * 上傳最大檔案大小,預設 1 MB
		 */
		private int MaxFileSize = 1024 * 1000;

		/**
		 * 儲存檔案的目錄
		 */
		private String upload_save_folder = "E:\\temp\\";

		/**
		 * 上傳是否成功
		 */
		private boolean isOk;

		/**
		 * 是否更名
		 */
		private boolean isNewName;

		/**
		 * 成功上傳之後的檔名。如果 isNewName = false,則是原上傳的名字
		 */
		private String uploaded_save_fileName;

		/**
		 * 相同檔名是否覆蓋?true=允許覆蓋
		 */
		private boolean isFileOverwrite = true;

		private HttpServletRequest request;

		/**
		 * @return the maxFileSize
		 */
		public int getMaxFileSize() {
			return MaxFileSize;
		}

		/**
		 * @param maxFileSize the maxFileSize to set
		 */
		public void setMaxFileSize(int maxFileSize) {
			MaxFileSize = maxFileSize;
		}

		/**
		 * @return the upload_save_folder
		 */
		public String getUpload_save_folder() {
			return upload_save_folder;
		}

		/**
		 * @param upload_save_folder the upload_save_folder to set
		 */
		public void setUpload_save_folder(String upload_save_folder) {
			this.upload_save_folder = upload_save_folder;
		}

		/**
		 * @return the isOk
		 */
		public boolean isOk() {
			return isOk;
		}

		/**
		 * @param isOk the isOk to set
		 */
		public void setOk(boolean isOk) {
			this.isOk = isOk;
		}

		/**
		 * @return the isNewName
		 */
		public boolean isNewName() {
			return isNewName;
		}

		/**
		 * @param isNewName the isNewName to set
		 */
		public void setNewName(boolean isNewName) {
			this.isNewName = isNewName;
		}

		/**
		 * @return the uploaded_save_fileName
		 */
		public String getUploaded_save_fileName() {
			return uploaded_save_fileName;
		}

		/**
		 * @param uploaded_save_fileName the uploaded_save_fileName to set
		 */
		public void setUploaded_save_fileName(String uploaded_save_fileName) {
			this.uploaded_save_fileName = uploaded_save_fileName;
		}

		/**
		 * @return the isFileOverwrite
		 */
		public boolean isFileOverwrite() {
			return isFileOverwrite;
		}

		/**
		 * 相同檔名是否覆蓋?true=允許覆蓋
		 * @param isFileOverwrite the isFileOverwrite to set
		 */
		public void setFileOverwrite(boolean isFileOverwrite) {
			this.isFileOverwrite = isFileOverwrite;
		}

		/**
		 * @return the request
		 */
		public HttpServletRequest getRequest() {
			return request;
		}

		/**
		 * @param request the request to set
		 */
		public void setRequest(HttpServletRequest request) {
			this.request = request;
		}

	}
	
%>

這是一個普通的 Java bean。完成上傳邏輯的是 Upload 類。 其原理是,1、由客戶端把要上傳的檔案生成 request 資料流,與伺服器端建立連線;2、在伺服器端接收 request 流,將流快取到記憶體中;3、由伺服器端的記憶體把檔案輸出到指定的目錄。Upload.jsp 完整程式碼如下所示。

<%@page pageEncoding="UTF-8" import="java.io.*"%>
<%@include file="UploadRequest.jsp"%>
<%!

public static class UploadException extends Exception {
	
	private static final long serialVersionUID = 579958777177500819L;

	public UploadException(String msg) {
		super(msg);
	}

}

public static class Upload {
	/**
	 * 接受上傳
	 * 
	 * @param uRequest
	 *            上傳 POJO
	 * @return
	 * @throws UploadException
	 */
	public UploadRequest upload(UploadRequest uRequest) throws UploadException {
		HttpServletRequest req = uRequest.getRequest();
		
		// 取得客戶端上傳的資料型別
		String contentType = req.getContentType();

		if(!req.getMethod().equals("POST")){
			throw new UploadException("必須 POST 請求");
		}
		
		if (contentType.indexOf("multipart/form-data") == -1) {
			throw new UploadException("未設定表單  multipart/form-data");
		}
		
		int formDataLength = req.getContentLength();
		
		if (formDataLength > uRequest.getMaxFileSize()) { // 是否超大
			throw new UploadException("檔案大小超過系統限制!");
		}
		
		// 儲存上傳的檔案資料
		byte dateBytes[] = new byte[formDataLength];
		int byteRead = 0, totalRead = 0;

		try(DataInputStream in = new DataInputStream(req.getInputStream());){
			while (totalRead < formDataLength) {
				byteRead = in.read(dateBytes, totalRead, formDataLength);
				totalRead += byteRead;
			}
		} catch (IOException e) {
			e.printStackTrace();
			throw new UploadException(e.toString());
		}				
				
		// 取得資料分割字串
		int lastIndex = contentType.lastIndexOf("="); // 資料分割線開始位置boundary=---------------------------
		String boundary = contentType.substring(lastIndex + 1, contentType.length());// ---------------------------257261863525035

		// 計算開頭資料頭佔用的長度
		int startPos = getStartPos(dateBytes);
		// 邊界位置
		int endPos = byteIndexOf(dateBytes, boundary.getBytes(), (dateBytes.length - startPos)) - 4;

		// 建立檔案
		String fileName = uRequest.getUpload_save_folder() + getFileName(dateBytes, uRequest.isNewName());
		uRequest.setUploaded_save_fileName(fileName);
		File checkedFile = initFile(uRequest);

		// 寫入檔案
		try(FileOutputStream fileOut = new FileOutputStream(checkedFile);){
			fileOut.write(dateBytes, startPos, endPos - startPos);
			fileOut.flush();
			
			uRequest.setOk(true);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
			throw new UploadException(e.toString());
		} catch (IOException e) {
			e.printStackTrace();
			throw new UploadException(e.toString());
		} 
		
		return uRequest;
	}
}

	/**
	 * 獲取開頭資料頭佔用的長度
	 * 
	 * @param dateBytes
	 *            檔案二進位制資料
	 * @return
	 */
	private static int getStartPos(byte[] dateBytes) {
		int startPos;
		startPos = byteIndexOf(dateBytes, "filename=\"".getBytes(), 0);
		startPos = byteIndexOf(dateBytes, "\n".getBytes(), startPos) + 1; // 遍歷掉3個換行符到資料塊
		startPos = byteIndexOf(dateBytes, "\n".getBytes(), startPos) + 1;
		startPos = byteIndexOf(dateBytes, "\n".getBytes(), startPos) + 1;
		
		return startPos;
	}
	
	/**
	 * 在位元組數組裡查詢某個位元組陣列,找到返回>=0,未找到返回-1
	 * @param data
	 * @param search
	 * @param start
	 * @return
	 */
	private static int byteIndexOf(byte[] data, byte[] search, int start) {
		int index = -1;
		int len = search.length;
		for (int i = start, j = 0; i < data.length; i++) {
			int temp = i;
			j = 0;
			while (data[temp] == search[j]) {
				// System.out.println((j+1)+",值:"+data[temp]+","+search[j]);
				// 計數
				j++;
				temp++;
				if (j == len) {
					index = i;
					return index;
				}
			}
		}
		return index;
	}
	
	/**
	 * 如果沒有指定目錄則建立;檢測是否可以覆蓋檔案
	 * 
	 * @param uRequest
	 *            上傳 POJO
	 * @return
	 * @throws UploadException
	 */
	private static File initFile(UploadRequest uRequest) throws UploadException {
		File dir = new File(uRequest.getUpload_save_folder());
		if (!dir.exists())
			dir.mkdirs();
		
		File checkFile = new File(uRequest.getUploaded_save_fileName());
		
		if (!uRequest.isFileOverwrite() && checkFile.exists()) {
			throw new UploadException("檔案已經存在,禁止覆蓋!");
		}
		
		return checkFile;
	}
	
	/**
	 * 獲取 POST Body 中的檔名
	 * 
	 * @param dateBytes
	 *            檔案二進位制資料
	 * @param isAutoName
	 *            是否自定命名,true = 時間戳檔名
	 * @return
	 */
	private static String getFileName(byte[] dateBytes, boolean isAutoName) {
		String saveFile = null;
		
		if(isAutoName){
			saveFile = "2016" + System.currentTimeMillis();
		} else {
			String data = null;
			try {
				data = new String(dateBytes, "UTF-8");
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
				data = "errFileName";
			}
			
			// 取得上傳的檔名
			saveFile = data.substring(data.indexOf("filename=\"") + 10);
			saveFile = saveFile.substring(0, saveFile.indexOf("\n"));
			saveFile = saveFile.substring(saveFile.lastIndexOf("\\") + 1, saveFile.indexOf("\""));
		}
		
		return saveFile;
	}
%>

通過 DataInputStream 讀取流資料到 dataBytes 中然後寫入 FileOutputStream。另外還有些圍繞配置的邏輯。

值得一提的是,Tomcat 7 下 JSP 預設的 Java 語法仍舊是 1.6 的。在 JSP 裡面嵌入 Java 1.7 特性的程式碼會丟擲“Resource specification not allowed here for source level below 1.7”的異常。於是需要修改 Tomcat/conf/web.xml 裡面的配置檔案,找到 <servlet> 節點,加入下面粗體部分才可以。注意是 jsp 節點,不是 default 節點(很相似)。

 <servlet>
        <servlet-name>jsp</servlet-name>
        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
        <init-param>
            <param-name>fork</param-name>
            <param-value>false</param-value>
        </init-param>
        <init-param>
            <param-name>xpoweredBy</param-name>
            <param-value>false</param-value>
        </init-param>
<strong>        <init-param>
            <param-name>compilerSourceVM</param-name>
            <param-value>1.7</param-value>
        </init-param>
        <init-param>
            <param-name>compilerTargetVM</param-name>
            <param-value>1.7</param-value>
        </init-param></strong>
        <load-on-startup>3</load-on-startup>
    </servlet>

至此,一個簡單的檔案上傳器就完成了。但是本元件的缺點還是很明顯的,試列舉兩項:一、上傳流佔用記憶體而非磁碟,所以上傳大檔案時記憶體會吃緊;二、尚不支援多段檔案上傳,也就是一次只能上傳一個檔案。