简介
Commons FileUpload可以轻松地为web应用程序添加强大,高性能的文件上传功能。Servlet3.0之前的web应用程序需要使用Commons FileUpload组件上传文件,但是从Servlet3.0开始,文件上传就成了一个内置的功能。文件上传时,需要使用POST方法提交HTTP请求,并且内容类型(Content-Type)为 multipart/form-data。
enctype
表单的enctype属性表示在发送到服务器之前应该如何对表单数据进行编码,默认值是 application/x-www-form-urlencoded。另一个重要的值就是 multipart/form-data。
application/x-www-form-urlencoded
发送到服务器的HTTP消息的正文本质上是一个大的查询字符串,字符串中的名称/值对被 & 分隔,并且名称与值由 = 分隔。如果值包含非字母数字的字符,那么该字符将被“%HH”取代,即一个百分比符号和两个十六进制数字表示的字符串。HH与字符集有关,如果 Content-Type = application/x-www-form-urlencoded;charset=utf-8,那么该字符将首先被编码为UTF-8字节数组,再将每个字节转换为HH。特别要注意,空格会转换为 +。
其实用 application/x-www-form-urlencoded 来上传文件也未尝不可,只是文件内容必须被序列化为字符串,服务端再将该字符串反序列化。
例如:
key1=%E7%AD%96&key2=abcd1234。
multipart/form-data
不对字符编码,在使用包含文件上传控件的表单时,必须使用该值。
例如:
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN,zh;q=0.8
Cache-Control:max-age=0
Connection:keep-alive
Content-Length:755
Content-Type:multipart/form-data; boundary=-----------------------------65982022822840
Cookie:JSESSIONID=wg5h37bt3rcr1dcpes14s42jz
Host:localhost:9443
Origin:https://localhost:9443
Referer:https://localhost:9443/ice-web1/test/test1
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36
-----------------------------65982022822840
Content-Disposition: form-data; name="userName"
匿名
-----------------------------65982022822840
Content-Disposition: form-data; name="userAge"
20
-----------------------------65982022822840
Content-Disposition: form-data; name="uploadFile"; filename="测试文件"
Content-Type: application/octet-stream
文件内容xxxxx
-----------------------------65982022822840--
发送到服务器的HTTP消息的正文没有被编码,而是Content-Type请求头中的boundary表示的边界符分隔成几个部分。Content-Disposition 是 MIME 协议的扩展,可以用于文件上传和下载。上传时可以表示参数的名称,如上。下载时表示默认的文件名称,例如 Content-Disposition: attachment; filename=FileName.txt。
文件上传页面
<!DOCTYPE html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<html>
<style>
.progress {
260px;
height: 20px;
border: 1px solid white;
border-radius: 20px;
overflow: hidden;
}
.step {
height: 100%;
0;
background: dodgerblue;
}
</style>
<script>
function upload() {
var file1 = document.getElementById("file1").files[0];
var file2 = document.getElementById("file2").files[0];
var remark = document.getElementById("remark");
if (file1 === undefined || file1 === null) {
alert("请选择文件1");
return;
}
if (file2 === undefined || file2 === null) {
alert("请选择文件2");
return;
}
if (remark.value === null || remark.value === "") {
alert("请输入备注");
return;
}
var data = new FormData(document.getElementById('form'));
var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
var percent = event.loaded / event.total * 100 + '%';
document.querySelector('.step').style.width = percent;
document.getElementById("progressPercent").innerHTML = percent;
}
}
xhr.onload = function (event) {
alert(xhr.responseText);
};
xhr.open("post", "upload");
xhr.send(data);
}
</script>
<body>
<title>index</title>
<div>
<form id="form" enctype="multipart/form-data">
<fieldset>
<legend>上传文件</legend>
文件1:<input type="file" id="file1" name="file"/> <br/>
文件2:<input type="file" id="file2" name="file"/> <br/>
备注:<input type="text" id="remark" name="remark"><input type="button" onclick="upload()" value="上传">
</fieldset>
</form>
</div>
<div class='progress'>
<div class="step"></div>
</div>
<div id="progressPercent">0%</div>
</body>
</html>
Apache Commons FileUpload 1.3.3
Apache Commons FileUpload提供了两组API,传统API和流式API。传统API假定文件项必须在用户实际可访问之前存储在某个位置,这种方法很方便,但是它占用内存且耗时比较长。
传统API文件在被用户读取前,必须等待被保存在内存或者硬盘中(临时文件),这种方法非常简单,但是另一方面却带来了内存和时间上的额外开销。流式API更佳轻量级,它可以让你牺牲一点便利性以换得理想的性能,文件可以直接从网络输入流中获取。
传统API
web.xml
<web-app>
<display-name>Archetype Created Web Application</display-name>
<listener>
<!--创建临时文件清理跟踪器,用于跟踪临时文件,并且在垃圾回收器回收DiskFileItem时删除-->
<listener-class>
org.apache.commons.fileupload.servlet.FileCleanerCleanup
</listener-class>
</listener>
</web-app>
CommonsFileUploadServlet.java
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.ProgressListener;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.FileCleanerCleanup;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FileCleaningTracker;
import org.apache.commons.io.FileUtils;
import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.List;
@WebServlet(name = "fileUploadServlet", urlPatterns = {"/upload"})
public class CommonsFileUploadServlet extends GenericServlet {
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) req;
HttpServletResponse httpServletResponse = (HttpServletResponse) res;
httpServletResponse.setContentType("text/html; charset=UTF-8");
try {
//进度监听器
ProgressListener progressListener = new ProgressListener() {
private long megaBytes = -1;
/**
* @param pBytesRead 已经读取的字节数
* @param pContentLength 总字节数
* @param pItems 字段编号
*/
@Override
public void update(long pBytesRead, long pContentLength, int pItems) {
//减少通知次数,每接收1M通知一次
long mBytes = pBytesRead / (1024 * 1024);
if (megaBytes == mBytes) {
return;
}
megaBytes = mBytes;
System.out.println("正在读取item:" + pItems);
if (pContentLength == -1) {
System.out.println("到目前为止," + pBytesRead + "字节已读");
} else {
System.out.println("到目前为止," + pBytesRead + "/" + pContentLength
+ "字节已读");
}
}
};
//获取临时文件清理跟踪器
FileCleaningTracker fileCleaningTracker = FileCleanerCleanup.getFileCleaningTracker(httpServletRequest.getServletContext());
//为基于磁盘的文件项创建工厂
DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
//设置尺寸阈值,单位字节,超过设定值则文件临时写入磁盘,否则保存在内存
diskFileItemFactory.setSizeThreshold(1024 * 1024);
//设置文件写入磁盘时临时保存的目录
diskFileItemFactory.setRepository(new File("E:/文件上传测试"));
//设置临时文件清理跟踪器,如果设置为null,将不再对临时文件进行跟踪
diskFileItemFactory.setFileCleaningTracker(fileCleaningTracker);
ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);
//设置允许上传的单个文件最大尺寸,单位字节,-1代表不限制,参数类型是long
servletFileUpload.setFileSizeMax(1024 * 1024 * 10L);
//设置整个请求的大小的最大值,单位字节,-1代表不限制,参数类型是long
servletFileUpload.setSizeMax(1024 * 1024 * 100L);
//设置进度监听器
servletFileUpload.setProgressListener(progressListener);
//设置读取每个部分的请求头的字符集
servletFileUpload.setHeaderEncoding("UTF-8");
boolean isMultipartContent = ServletFileUpload.isMultipartContent(httpServletRequest);
if (isMultipartContent) {
//开始上传文件并解析请求,按顺序获取各个表单项
List<FileItem> fileItems = servletFileUpload.parseRequest(httpServletRequest);
if (fileItems != null) {
for (FileItem fileItem : fileItems) {
//如果当前表单项是字段
if (fileItem.isFormField()) {
//获取字段名
String fieldName = fileItem.getFieldName();
//获取字段值
String value = fileItem.getString("UTF-8");
System.out.println("请求字段:" + fieldName + "=" + value);
}
//如果当前表单项是文件
else {
DiskFileItem diskFileItem = (DiskFileItem) fileItem;
//获取字段名
String fieldName = diskFileItem.getFieldName();
//获取浏览器提供的原始文件名
String fileName = diskFileItem.getName();
//获取文件MIME类型
String contentType = diskFileItem.getContentType();
//获取文件大小,单位字节
long sizeInBytes = diskFileItem.getSize();
//判断文件是否存储在内存中
boolean isInMemory = diskFileItem.isInMemory();
File storeLocation = diskFileItem.getStoreLocation();
System.out.println("请求字段:" + fieldName);
System.out.println("原始文件名:" + fileName);
System.out.println("MIME类型:" + contentType);
System.out.println("文件大小(字节):" + sizeInBytes);
System.out.println("文件临时保存在内存中?:" + isInMemory);
System.out.println("文件临时保存的路径:" + storeLocation.getAbsolutePath());
//等同于diskFileItem.write(new File("E:/文件上传测试/" + fileName))
try (InputStream inputStream = diskFileItem.getInputStream()) {
FileUtils.copyInputStreamToFile(inputStream, new File("E:/文件上传测试/" + fileName));
}
//手动删除临时文件
diskFileItem.delete();
}
}
}
try (PrintWriter printWriter = httpServletResponse.getWriter()) {
printWriter.println("上传成功");
}
}
} catch (Exception e) {
try (PrintWriter printWriter = httpServletResponse.getWriter()) {
printWriter.println(e.getMessage());
}
e.printStackTrace();
}
}
}
需要注意几点:
1.临时文件可以手动删除,也创建临时文件清理跟踪器自动删除,或者同时使用这两者方式。
2.进度监听器可能会降低性能,比如update方法中发送网络数据包等,所以最好还是降低通知频率。
3.如果上传大文件,需要配置tomcat的Connector的maxSwallowSize属性为负数,否则客户端不会接收到响应。
流式API
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.ProgressListener;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
@WebServlet(name = "fileUploadServlet", urlPatterns = {"/upload"})
public class CommonsFileUploadStreamServlet extends GenericServlet {
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) req;
HttpServletResponse httpServletResponse = (HttpServletResponse) res;
httpServletResponse.setContentType("text/html; charset=UTF-8");
try {
//进度监听器
ProgressListener progressListener = new ProgressListener() {
private long megaBytes = -1;
/**
* @param pBytesRead 已经读取的字节数
* @param pContentLength 总字节数
* @param pItems 字段编号
*/
@Override
public void update(long pBytesRead, long pContentLength, int pItems) {
//减少通知次数,每接收1M通知一次
long mBytes = pBytesRead / (1024 * 1024);
if (megaBytes == mBytes) {
return;
}
megaBytes = mBytes;
System.out.println("正在读取item:" + pItems);
if (pContentLength == -1) {
System.out.println("到目前为止," + pBytesRead + "字节已读");
} else {
System.out.println("到目前为止," + pBytesRead + "/" + pContentLength
+ "字节已读");
}
}
};
//为基于磁盘的文件项创建工厂
DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);
//设置允许上传的单个文件最大尺寸,单位字节,-1代表不限制,参数类型是long
servletFileUpload.setFileSizeMax(1024 * 1024 * 10L);
//设置整个请求的大小的最大值,单位字节,-1代表不限制,参数类型是long
servletFileUpload.setSizeMax(1024 * 1024 * 100L);
//设置进度监听器
servletFileUpload.setProgressListener(progressListener);
//设置读取每个部分的请求头的字符集
servletFileUpload.setHeaderEncoding("UTF-8");
boolean isMultipartContent = ServletFileUpload.isMultipartContent(httpServletRequest);
if (isMultipartContent) {
//开始上传文件并解析请求,按顺序获取各个表单项
FileItemIterator fileItemIterator = servletFileUpload.getItemIterator(httpServletRequest);
while (fileItemIterator.hasNext()) {
FileItemStream fileItemStream = fileItemIterator.next();
String name = fileItemStream.getFieldName();
if (fileItemStream.isFormField()) {
try (InputStream inputStream = fileItemStream.openStream()) {
System.out.println("表单字段:" + name + ",值:"
+ Streams.asString(inputStream, "utf-8"));
}
} else {
System.out.println("请求字段:" + fileItemStream.getFieldName());
System.out.println("原始文件名:" + fileItemStream.getName());
System.out.println("MIME类型:" + fileItemStream.getContentType());
try (
//文件输出流
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(
new FileOutputStream("E:/文件上传测试/" + fileItemStream.getName()));
//文件输入流
InputStream inputStream = new BufferedInputStream(fileItemStream.openStream());) {
byte[] bytes = new byte[1024 * 8];
int n;
while (-1 != (n = inputStream.read(bytes))) {
bufferedOutputStream.write(bytes, 0, n);
}
}
}
}
try (PrintWriter printWriter = httpServletResponse.getWriter()) {
printWriter.println("上传成功");
}
}
} catch (Exception e) {
try (PrintWriter printWriter = httpServletResponse.getWriter()) {
printWriter.println(e.getMessage());
}
e.printStackTrace();
}
}
}
Servlet
import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
/**
* 必须使用@MultipartConfig注解标注Servlet
* maxFileSize表示允许上传的文件大小的最大值,单位字节,-1表示无限制
* maxRequestSize表示整个请求大小的最大值,单位字节,-1表示无限制
* fileSizeThreshold表示尺寸阈值,单位字节,超过设定值则文件临时写入磁盘,否则保存在内存
* location表示临时文件的目录
*/
@WebServlet(name = "servletFileUploadServlet", urlPatterns = {"/upload"})
@MultipartConfig(maxFileSize = 1024 * 1024 * 10, maxRequestSize = 1024 * 1024 * 100, fileSizeThreshold = 1024 * 1024, location = "E:/文件上传测试/")
public class ServletFileUploadServlet extends GenericServlet {
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest httpServletRequest = (HttpServletRequest) req;
httpServletRequest.setCharacterEncoding("UTF-8");
HttpServletResponse httpServletResponse = (HttpServletResponse) res;
httpServletResponse.setContentType("text/html; charset=UTF-8");
Collection<Part> parts = httpServletRequest.getParts();
try {
parts.forEach(part -> {
//如果part.getContentType() == null,就代表这是个表单字段,否则就是文件
if (part.getContentType() == null) {
//获取字段名
String fieldName = part.getName();
//获取字段值
String value = httpServletRequest.getParameter(fieldName);
System.out.println("请求字段:" + fieldName + "=" + value);
} else {
System.out.println("请求字段:" + part.getName());
System.out.println("原始文件名:" + part.getSubmittedFileName());
System.out.println("MIME类型:" + part.getContentType());
System.out.println("文件大小(字节):" + part.getSize());
try {
part.write("E:/文件上传测试/" + part.getSubmittedFileName());
//手动删除临时文件
part.delete();
} catch (IOException e) {
e.printStackTrace();
}
}
});
try (PrintWriter printWriter = httpServletResponse.getWriter()) {
printWriter.println("上传成功");
}
} catch (Exception e) {
try (PrintWriter printWriter = httpServletResponse.getWriter()) {
printWriter.println(e.getMessage());
}
e.printStackTrace();
}
}
}