JavaWeb世界(九):文件上傳與下載

一、文件上傳

先創建工程,寫一個簡易的JSP:

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h3>文件上傳</h3>
	<form action="" method="POST">
		賬號:<input type="text" name="username"/><br/>
		郵箱:<input type="text" name="email"/><br/>
		頭像:<input type="file" name="pic" accept="image/*"/><br/>
		<input type="submit" value=" 註冊 "/><br/>
	</form>
</body>
</html>

在上傳的頭像那裏,類型改爲 file ,後面的 accept 可以用來更改選擇文件的類型(爲圖片類型),效果如下:
文件上傳
在這裏插入圖片描述
要點:

  1. 上傳控件所在的表單method必須爲 POST,因爲GET方式傳入的數據大小不能超過2KB,而POST大小沒有限制。
  2. 上傳控件的類型爲 file.

使用一個Servlet來接收參數,並打印表單中的內容,如下:

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

	private static final long serialVersionUID = 3067915922682828536L;

	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		req.setCharacterEncoding("UTF-8");
		System.out.println(req.getParameter("username"));
		System.out.println(req.getParameter("email"));
		System.out.println(req.getParameter("pic"));
	}
}

在這裏插入圖片描述
顯然這裏的圖片是字符串。

因此表單的編碼必須是二進制編碼
因此我們需要用到表單中的 enctype 屬性中的 multipart/form-data
在這裏插入圖片描述
再提交一遍:
在這裏插入圖片描述
全部爲 NULL 了。

這是因爲在使用二進制編碼 multipart/form-data 後,在Servlet中再也不能通過request.getParameter 方法來獲得參數了,設置編碼都沒有效果了。

沒有設置二進制編碼時,POST爲:
未設置二進制編碼前
在設置了二進制編碼後的請求頭:
在這裏插入圖片描述
紅色框框裏的就是圖片的二維碼信息。
我們打印一下:

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

	private static final long serialVersionUID = 3067915922682828536L;

	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		Scanner sc = new Scanner(req.getInputStream());
		while (sc.hasNextLine()) {
			System.out.println(sc.nextLine());
		}
	}
}

在這裏插入圖片描述

基於 Apache FileUpload 組件

Apache commons-fileupload 的jar包下載
Apache commons-io 的jar包下載
我們按照官方文檔的提示來進行文件上傳操作:

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

	private static final long serialVersionUID = 3067915922682828536L;

	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		//檢查是否有文件上傳請求:請求方法爲POST,請求編碼爲multipart/form-data
		boolean isMultipart = ServletFileUpload.isMultipartContent(req);
		if (!isMultipart) {
			return;
		}

		try {
			//1.創建FileItemFactory對象
			//FileItemFactory是用來創建FileItem對象的
			//FileItem對象相當於form表單中的表單控件的封裝
			DiskFileItemFactory factory = new DiskFileItemFactory();

			//2.創建一個新的文件上傳處理程序(文件上傳處理器)
			ServletFileUpload upload = new ServletFileUpload(factory);
			
			//3.解析請求
			List<FileItem> items = upload.parseRequest(req);
			System.out.println(items);
		} catch (FileUploadException e) {
			e.printStackTrace();
		}
	}
}

運行發現報錯:找不到類:
ClassNotFoundException
缺少IO包,添加之後,運行結果如下:
在這裏插入圖片描述
我們參照文檔,將表單裏面的內容提取並將文件內容寫到磁盤中的文件裏:

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

	private static final long serialVersionUID = 3067915922682828536L;

	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		//檢查是否有文件上傳請求:請求方法爲POST,請求編碼爲multipart/form-data
		boolean isMultipart = ServletFileUpload.isMultipartContent(req);
		if (!isMultipart) {
			return;
		}
		try {
			//1.創建FileItemFactory對象
			//FileItemFactory是用來創建FileItem對象的
			//FileItem對象相當於form表單中的表單控件的封裝
			DiskFileItemFactory factory = new DiskFileItemFactory();

			//2.創建一個新的文件上傳處理程序(文件上傳處理器)
			ServletFileUpload upload = new ServletFileUpload(factory);
			
			//3.解析請求
			List<FileItem> items = upload.parseRequest(req);
			for (int i = 0; i < items.size(); i++) {
				System.out.println(items.get(i));
			}
			//4.迭代出每一個FileItem
			for (FileItem item : items) {
				//獲取表單屬性名稱
				String fieldName = item.getFieldName();
				if (item.isFormField()) {
					//普通表單控件
					String value = item.getString("UTF-8");//獲取當前普通表單控件的參數值
					System.out.println(fieldName + "-" + value);
				} else {
					//上傳文件控件
					System.out.println(fieldName + "-" + item.getName());//字段名-上傳的文件名
					item.write(new File("E:/", item.getName()));//把二進制文件寫到哪個文件中
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

最終結果如下:
在這裏插入圖片描述
在這裏插入圖片描述

文件上傳的文件名處理

文件名

IE6 中,通過 FileItem.getName 方法獲取到的文件名會帶有完整路徑,因此要進行處理。
我們可以用 FilenameUtils.getName(item.getName())進行轉化
解釋如下:

public class FileNameUtilsTest {
	public static void main(String[] args) {
		String path = "E:/Judy.png";
		//獲取文件名稱
		System.out.println(FilenameUtils.getName(path));
		//獲取文件名稱,不包括拓展名
		System.out.println(FilenameUtils.getBaseName(path));
		//獲取文件拓展名
		System.out.println(FilenameUtils.getExtension(path));
	}
}

在這裏插入圖片描述

上傳的文件名問題

我們要給上傳的文件起一個唯一的名稱:通過UUID

String fileName = UUID.randomUUID().toString() + "." +
					FilenameUtils.getExtension(item.getName());
item.write(new File("E:/", fileName));

上傳文件的保存路徑

如果我們想將文件保存在應用中,可以用super.getServletContext().getRealPath()函數獲得真實路徑:

String dir = super.getServletContext().getRealPath("/upload");
System.out.println(dir);
item.write(new File(dir, fileName));

這樣就可以在瀏覽器中訪問項目中的文件了,就不必從磁盤中訪問了。
注意:在寫文件的時候只能寫到文件中,不能寫到文件夾中。

緩存大小和臨時目錄

當文件大小超過 10KB 就不能放在緩存中了。
文件不放在內存中,其實放在了所謂的 “臨時目錄”,在 Tomcat 根目錄中的 temp 文件夾下。

DiskFileItemFactory factory = new DiskFileItemFactory();
//設置緩存大小
factory.setSizeThreshold(20 * 1024);
//設置臨時目錄
factory.setRepository(repository);

文件類型約束

//允許接收的圖片類型
private static final String ALLOWED_IMAGE_TYPE = "jpg;png;jpeg;gif";
String[] allowedImageType = ALLOWED_IMAGE_TYPE.split(";");
String ext = FilenameUtils.getExtension(item.getName());
List<String> list = Arrays.asList(allowedImageType);
//上傳文件類型不合法
if (!list.contains(ext)) {
	req.setAttribute("errorMsg", "上傳文件類型錯誤");
	req.getRequestDispatcher("/input.jsp").forward(req, resp);
	System.out.println("Invalid Type!");
	return;
}	

jsp中:

<span style="color: red">${errorMsg}</span>

或者更通用的辦法是通過上傳文件的 mime-type 進行判斷是否爲 image 類型:

String mimeType = super.getServletContext().getMimeType(FilenameUtils.getName(item.getName()));

抽取FileUtil工具類

在工具類中,因爲要複用代碼,因此應該具有一般性,並儘可能簡潔,我們可以刪掉方法中響應參數,並且通過拋出自定義異常來讓調用者接收:

if (!list.contains(ext)) {
throw new LogicException("上傳文件類型錯誤,請上傳圖片文件...");
}
...
catch (LogicException e) {
	throw e;
}

調用類 UploadServlet:

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

	private static final long serialVersionUID = 3067915922682828536L;

	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		try {
			FileUtil.upload(req);
		} catch (LogicException e) {
			String errorMsg = e.getMessage();
			req.setAttribute("errorMsg", errorMsg);
			req.getRequestDispatcher("/input.jsp").forward(req, resp);
		}
	}
}

這樣就重構了 FileUtil

文件大小約束問題

情況一:單個文件超過指定大小;

upload.setFileSizeMax(2*1024*1024);	//2M

否則會報出 FileUploadBase$FileSizeLimitExceededException 的異常
異常
因爲是異常,我們要再次捕捉這個異常:

catch (FileSizeLimitExceededException e) {
	throw new LogicException("文件大小超過2M,請重試", e);
}

情況二:該次請求的全部數據超過指定大小

upload.setSizeMax(10 * 1024 * 1024); //10M

否則會報出 SizeLimitExceededException 異常
繼續接受這個異常並拋出:

catch (SizeLimitExceededException e) {
	throw new LogicException("該次請求信息量超過10M,請重試", e);
}

效果如下:
效果圖

使用Map封裝請求信息

我們想要將表單中的值封裝到一個對象中,例如 User 類,我們可以在 upload 方法中加入 User 參數,將 Servlet 中創建的類的實例對象作爲參數傳遞進來。:
User類

public class User {
	private String username;
	private String email;
	private String imageUrl;//圖片路徑
	private String imageName;//圖片原始名稱
}
@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

	private static final long serialVersionUID = 3067915922682828536L;

	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		try {
			User user = new User();	//新建User實例	
			FileUtil.upload(req, user);//作爲參數傳遞
			System.out.println(user);
		} catch (LogicException e) {
			e.printStackTrace();
			String errorMsg = e.getMessage();
			req.setAttribute("errorMsg", errorMsg);
			req.getRequestDispatcher("/input.jsp").forward(req, resp);
		}
	}
}

將User賦值

if ("username".equals(fieldName)) {
	user.setUsername(value);
} else if ("email".equals(fieldName)) {
	user.setEmail(value);
}

但是這樣並不能處理通用情況,因此我們可以考慮設置一個Map進行傳遞,而我們知道類和接口對象都是引用傳遞,類似於C中的指針,因此得到Map中的值後再設置到User中。但是Map不能封裝圖片、複選框等需要有多個信息的控件。
如果還要用Map進行封裝的話,我們可以將圖片的兩個信息(imageUrl和imageName)再用一個類進行封裝,例如 CFile 類,然後作爲 Map 的參數傳遞進去:
CFile類:

@Data
public class CFile {
	private String imageUrl;//圖片路徑
	private String imageName;//圖片原始名稱
	
	CFile(String imageUrl, String imageName) {
		this.imageName = imageName;
		this.imageUrl = imageUrl;
	};
}
Map<String, CFile> binaryMap = new HashMap<>();
FileUtil.upload(req, fieldMap, binaryMap);
binaryMap.put(fileName, new CFile("/upload" + fileName, FilenameUtils.getName(item.getName())));

設置值到 User 中,並共享到 show.jsp

user.setUsername(fieldMap.get("username"));
user.setEmail(fieldMap.get("email"));
user.setImageName(binaryMap.get("pic").getImageName());
user.setImageUrl(binaryMap.get("pic").getImageUrl());			
req.setAttribute("user", user);
req.getRequestDispatcher("/show.jsp").forward(req, resp);

然後我們在網頁中顯示:

<body>
	註冊名稱:${user.username}</br>
	註冊郵箱:${user.email}</br>
	頭像原始名稱:${user.imageName}</br>
	頭像:<img src="${user.imageUrl}">
</body>

結果如下:
在這裏插入圖片描述

完整代碼

UploadServlet.java:

package com.cherry.upload;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Templates;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FilenameUtils;

@WebServlet("/upload")
public class UploadServlet extends HttpServlet {

	private static final long serialVersionUID = 3067915922682828536L;

	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		try {
			User user = new User();			
			//封裝普通表單的數據
			//key:參數名稱
			//value:參數的值
			Map<String, String> fieldMap = new HashMap<>();
			Map<String, CFile> binaryMap = new HashMap<>();
			FileUtil.upload(req, fieldMap, binaryMap);
			//再從fieldMap中設置到User中
			System.out.println(fieldMap);
			System.out.println(binaryMap);
			
			user.setUsername(fieldMap.get("username"));
			user.setEmail(fieldMap.get("email"));
			user.setImageName(binaryMap.get("pic").getImageName());
			user.setImageUrl(binaryMap.get("pic").getImageUrl());
			
			req.setAttribute("user", user);
			req.getRequestDispatcher("/show.jsp").forward(req, resp);
			System.out.println(user);
		} catch (LogicException e) {
			e.printStackTrace();
			String errorMsg = e.getMessage();
			req.setAttribute("errorMsg", errorMsg);
			req.getRequestDispatcher("/input.jsp").forward(req, resp);
		}
	}
}

FileUtil.java

package com.cherry.upload;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;

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

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException;
import org.apache.commons.fileupload.FileUploadBase.SizeLimitExceededException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FilenameUtils;

import lombok.Value;

public class FileUtil {
	//允許接收的圖片類型
	private static final String ALLOWED_IMAGE_TYPE = "jpg;png;jpeg;gif;pdf";

	public static void upload(HttpServletRequest req, Map<String, String> fieldMap,
			Map<String, CFile> binaryMap) {
		//檢查是否有文件上傳請求:請求方法爲POST,請求編碼爲multipart/form-data
		boolean isMultipart = ServletFileUpload.isMultipartContent(req);
		if (!isMultipart) {
			return;
		}

		try {
			//1.創建FileItemFactory對象
			//FileItemFactory是用來創建FileItem對象的
			//FileItem對象相當於form表單中的表單控件的封裝
			DiskFileItemFactory factory = new DiskFileItemFactory();
			//設置緩存大小
			//factory.setSizeThreshold(20 * 1024);
			//設置臨時目錄
			//factory.setRepository(repository);

			//2.創建一個新的文件上傳處理程序(文件上傳處理器)
			ServletFileUpload upload = new ServletFileUpload(factory);
			//設置單個文件上傳的大小限制
			upload.setFileSizeMax(2 * 1024 * 1024); //2M
			//設置所有請求的總數據的大小
			upload.setSizeMax(4 * 1024 * 1024); //10M

			//3.解析請求
			List<FileItem> items = upload.parseRequest(req);
			/*for (int i = 0; i < items.size(); i++) {
				System.out.println(items.get(i));
			}*/
			//4.迭代出每一個FileItem
			for (FileItem item : items) {
				//獲取表單屬性名稱
				String fieldName = item.getFieldName();
				if (item.isFormField()) {
					//普通表單控件
					String value = item.getString("UTF-8");//獲取當前普通表單控件的參數值
					System.out.println(fieldName + "-" + value);
					fieldMap.put(fieldName, value);
				} else {
					//當前上傳文件的mime類型
					/*String mimeType = req.getServletContext()
							.getMimeType(FilenameUtils.getName(item.getName()));
					System.out.println(mimeType);*/

					String[] allowedImageType = ALLOWED_IMAGE_TYPE.split(";");
					String ext = FilenameUtils.getExtension(item.getName()).toLowerCase();
					List<String> list = Arrays.asList(allowedImageType);
					//上傳文件類型不合法
					if (!list.contains(ext)) {
						throw new LogicException("上傳文件類型錯誤,請上傳圖片文件...");
					}
					//上傳文件控件
					System.out.println(fieldName + "-" + FilenameUtils.getName(item.getName()));//字段名-上傳的文件名
					String fileName = UUID.randomUUID().toString() + "."
							+ FilenameUtils.getExtension(item.getName());
					String dir = req.getServletContext().getRealPath("/upload");
					item.write(new File(dir, fileName));//把二進制文件寫到哪個文件中
					//是否存儲在內存中
					System.out.println(item.isInMemory());

					binaryMap.put(fieldName, new CFile("/upload/" + fileName,
							FilenameUtils.getName(item.getName())));
				}
			}
		} catch (FileSizeLimitExceededException e) {
			throw new LogicException("文件大小超過2M,請重試", e);
		} catch (SizeLimitExceededException e) {
			throw new LogicException("該次請求信息量超過10M,請重試", e);
		} catch (LogicException e) {
			throw e;
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

User.java:

package com.cherry.upload;

import lombok.Data;

@Data
public class User {
	private String username;
	private String email;
	private String imageUrl;//圖片路徑
	private String imageName;//圖片原始名稱
}

CFile.java:

package com.cherry.upload;

import lombok.Data;

@Data
public class CFile {
	private String imageUrl;//圖片路徑
	private String imageName;//圖片原始名稱
	
	CFile(String imageUrl, String imageName) {
		this.imageName = imageName;
		this.imageUrl = imageUrl;
	};
}

input.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h3>文件上傳</h3>
	<span style="color: red">${errorMsg}</span>
	<form action="/upload" method="POST" enctype="multipart/form-data">
		賬號:<input type="text" name="username"/><br/>
		郵箱:<input type="text" name="email"/><br/>
		頭像:<input type="file" name="pic" accept="image/*"/><br/>
		<input type="submit" value=" 註冊 "/><br/>
	</form>
</body>
</html>

show.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	註冊名稱:${user.username}</br>
	註冊郵箱:${user.email}</br>
	頭像原始名稱:${user.imageName}</br>
	頭像:<img src="${user.imageUrl}">
</body>
</html>

二、文件下載

沒什麼好說的,需要注意的是,下載資源不能暴露在 WEB-INF 外面,必須通過 Servlet 響應下載,直接上代碼:

DownloadServlet.java

package com.cherry._02_download;

import java.io.IOException;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Paths;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/down")
public class DownloadServlet extends HttpServlet {

	private static final long serialVersionUID = -6406195121839970110L;

	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		//權限檢查等
		//1.獲取被下載的資源文件名
		String fileName = req.getParameter("filename");
		System.out.println(fileName);
		if (hasLength(fileName)) {
			fileName = new String(fileName.getBytes("ISO-8859-1"), "UTF-8");
		}
		//2.從服務器中找到被下載資源的絕對路徑
		String realPath = super.getServletContext().getRealPath("/WEB-INF/download/" + fileName);
		System.out.println(realPath);

		//①不要讓瀏覽器打開文件,而是要彈出下載框並保存文件
		resp.setContentType("application/x-msdownload");
		//②設置下載文件的建議文件名
		String userAgent = req.getHeader("User-Agent");
		if (userAgent.contains("MSIE")) {
			//IE
			fileName = URLEncoder.encode(fileName, "UTF-8");
		} else {
			//非IE
			fileName = new String(fileName.getBytes("UTF-8"), "ISO-8859-1");
		}
		resp.setHeader("Content-Disposition", "attachment; filename =" + fileName);
		//3.通過文件輸出流將磁盤中的文件讀取到程序中,然後輸出響應到瀏覽器
		Files.copy(Paths.get(realPath), resp.getOutputStream());
	}

	private boolean hasLength(String str) {
		return str != null && "".equals(str.trim());
	}
}

download.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h3>下載資源列表</h3>
	<a href="/down?filename=Judy.zip">Judy.zip</a></br>
	<a href="/down?filename=Tom貓.zip">Tom貓.zip</a></br>	
</body>
</html>
發佈了54 篇原創文章 · 獲贊 19 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章