文件上传知识体系
如下图
1. 协议规范(RFC 1867)
HTML 表单最初只支持 application/x-www-form-urlencoded 形式编码(key=value&key=value...),但它不适合用于传输二进制数据(文件)或者包含非ASCII字符的数据。所以 multipart/form-data 就诞生了,专门用于传输文件。
HTML 的二进制文件传输特性,最初在《RFC 1867:Form-based File Upload in HTML》中定义。 扩充 type="file" 类型, 用于实现文件选择;
2. 文件上传请求响应
2.1. Servlet 3.x(MultipartConfig)
Servlet 3.x 大法好,无需插件,就能处理上传的文件。
代码语言:javascript复制import org.apache.commons.io.IOUtils;
import javax.servlet.ServletException;
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.nio.charset.Charset;
import java.util.Collection;
import java.util.Iterator;
@MultipartConfig(
fileSizeThreshold = 512, // 超过这个值,就暂存到磁盘
location = "d:/temp", // 暂存区
maxFileSize = 1024, // 请求中单个文件的最大尺寸
maxRequestSize = 2048 // 请求的最大尺寸
)
@WebServlet(name = "RecvFile", urlPatterns = {"/recvfile"})
public class RecvFile extends javax.servlet.http.HttpServlet {
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
Collection
Iterator
while (itor.hasNext()) {
Part part = itor.next();
String fieldName = part.getName(); // 字段名
String fileName = part.getSubmittedFileName(); // 文件名(注:此接口Servlet 3.1才有)
String fileType = part.getContentType();
String fileContent = new String(
IOUtils.toByteArray(
part.getInputStream()
),
Charset.forName("UTF-8")
);
System.out.format(
"field:%s, fileName:%s, type:%s, content:%s\n",
fieldName, fileName, fileType, fileContent);
}
}
}代码语言:javascript复制field:textfield, fileName:null, type:null, content:琦玉
field:single, fileName:杰洛斯.txt, type:text/plain, content:谢谢您为我解惑,老师的战斗为我指明了道路,强大的象征,终极目标的所在,我也要到那里去。
field:multi, fileName:琦玉1.txt, type:text/plain, content:没有什么是一拳解决不了的,如果有,那就两拳。
field:multi, fileName:琦玉2.txt, type:text/plain, content:咱回吧。
field:empty, fileName:, type:application/octet-stream, content:2.2. Apache Commons Upload
Servlet 2.x 环境自身无法方便的处理文件上传请求,第三方工具 Apache Commons Upload 则是最好的选择。
关键依赖:
代码语言:javascript复制
代码示例:
代码语言:javascript复制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.IOUtils;
import javax.servlet.ServletException;
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.nio.charset.Charset;
import java.util.Iterator;
import java.util.List;
@WebServlet(name = "ApacheRecvFile", urlPatterns = {"/apacherecvfile"})
public class ApacheRecvFile extends javax.servlet.http.HttpServlet {
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
// 检测是否为文件上传请求
if (!ServletFileUpload.isMultipartContent(request)) {
throw new RuntimeException("不是文件上传请求!");
}
// 配置上传文件缓存策略
// 1. SizeThreshold: 缓存文件大小阈值
// a.上传文件小于此阈值,暂存于内存;
// b.上传文件大于此阈值,暂存于磁盘;
// 2. Repository: 上传文件暂存于磁盘时的目录;
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(512); // 默认值:10240
factory.setRepository(new File("d:/temp"));
// 文件上传相关参数
// 1. FileSizeMax: 限制请求中单个文件大小
// 2. SizeMax: 限制请求的总大小
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(1024); // 默认:-1,无限制
upload.setSizeMax(2048); // 默认:-1,无限制
// 分析请求
List
try {
items = upload.parseRequest(request);
} catch (FileUploadException e) {
throw new RuntimeException(e);
}
Iterator
while (iter.hasNext()) {
FileItem item = iter.next();
if (item.isFormField()) {
// 普通字段
String fieldName = item.getFieldName(); //字段名
String fileContent = item.getString("UTF-8");//字段值
System.out.format(
"field:%s, content:%s\n",
fieldName, fileContent);
} else {
// 文件字段
String fieldName = item.getFieldName(); // 字段名
String fileName = item.getName(); // 文件名
String fileType = item.getContentType();
String fileContent = new String(
IOUtils.toByteArray(
item.getInputStream()
),
Charset.forName("UTF-8")
);
System.out.format(
"field:%s, fileName:%s, type:%s, content:%s\n",
fieldName, fileName, fileType, fileContent);
}
}
}
}代码语言:javascript复制field:textfield, content:琦玉
field:single, fileName:D:\杰洛斯.txt, type:text/plain, content:谢谢您为我解惑,老师的战斗为我指明了道路,强大的象征,终极目标的所在,我也要到那里去。
field:multi, fileName:D:\\琦玉1.txt, type:text/plain, content:没有什么是一拳解决不了的,如果有,那就两拳。
field:multi, fileName:D:\\琦玉2.txt, type:text/plain, content:咱回吧。
field:empty, fileName:, type:application/octet-stream, content:2.3. Spring MVC
Spring MVC 是一个分层的 Java Web 开发框架。Spring MVC的核心元素就是 Dispatcher Servlet,负责处理所有请求,但 DispatcherServlet 并没有实现任何解析 multipart 请求数据的功能。它将该任务委托给了Spring 中 MultipartResolver 策略接口的实现,通过这个实现类来解析 multipart 请求中的内容。Spring内置了两个 MultipartResolver 的实现供:
CommonsMultipartResolver:依赖 Apache Commons Upload 解析 multipart 请求。StandardServletMultipartResolver:依赖 Servlet 3.x 对 multipart 请求的原生支持。2.3.1 CommonsMultipartResolver 示例
关键依赖:
代码语言:javascript复制
代码示例:web.xml
代码语言:javascript复制
xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
代码语言:javascript复制
xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> class="org.springframework.web.multipart.commons.CommonsMultipartResolver" p:defaultEncoding="UTF-8" p:maxInMemorySize="512" p:maxUploadSizePerFile="1048576" p:maxUploadSize="2097152" />
代码语言:javascript复制package webj2ee;
import org.apache.commons.io.IOUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.List;
@RestController
public class SpringMVCApacheCommonsUploadController {
@RequestMapping(value = "/recvfile")
public void handleUpload(HttpServletRequest request) throws IOException {
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
Iterator
while (fileNames.hasNext()) {
String fieldName = fileNames.next();
List
for (MultipartFile file : files) {
String fileName = file.getOriginalFilename();
String fileType = file.getContentType();
String fileContent = new String(
IOUtils.toByteArray(
file.getInputStream()
),
Charset.forName("UTF-8")
);
System.out.format(
"field:%s, fileName:%s, type:%s, content:%s\n",
fieldName, fileName, fileType, fileContent);
}
}
}
}2.3.2 StandardServletMultipartResolver 示例
与 CommonsMultipartResolver 示例相比,
只有 web.xml、springmvc.xml 略有差异;
代码示例:web.xml
代码语言:javascript复制
xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
代码语言:javascript复制
xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> class="org.springframework.web.multipart.support.StandardServletMultipartResolver"/>
3.1. Server 端发起(HttpClient)
应用场景:Server 端请求转发;
关键依赖:
代码语言:javascript复制
代码示例:
代码语言:javascript复制package webj2ee;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
public class SendMultiPartRequest {
public static void main(String[] args) {
// 构造连接池
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(4);
cm.setDefaultMaxPerRoute(2);
// CloseableHttpClient
CloseableHttpClient httpclient = HttpClients.custom().setConnectionManager(cm).build();
// 构造 MultiPart 请求实体
MultipartEntityBuilder mpEntityBuilder = MultipartEntityBuilder.create();
mpEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); // 注意字符集
mpEntityBuilder.setCharset(Charset.forName("UTF-8"));
mpEntityBuilder.addTextBody("text", "这是普通文本字段", ContentType.TEXT_PLAIN.withCharset("UTF-8")); // MultiPart 普通字段
mpEntityBuilder.addBinaryBody("file1", new File("D:/杰洛斯.txt")); // Mulitpart 文件字段
mpEntityBuilder.addBinaryBody("file2", new File("D:/琦玉1.txt"));
HttpEntity entity = mpEntityBuilder.build();
// 构造 POST 请求
HttpPost httpPost = new HttpPost("http://localhost:8080/xupload/apacherecvfile");
httpPost.setEntity(entity);
// 发送请求
try {
CloseableHttpResponse closeableResponse = httpclient.execute(httpPost);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}注意事项:注意设置字符集,小心乱码;
3.2. 客户端发起 —— Flash(Uploadify)
Flash(Uploadify)的唯一价值就是增强了 IE7、8、9 的文件上传能力。如果你没办法甩开IE 这个小垃圾(特别是低版本IE),而且还想实现丰富的文件上传功能,Flash(Uploadify)是你唯一的选择。
优点:
a. √ 兼容IE7、IE8、IE9
b. √ 支持上传完成回调机制;
c. √ 支持多选文件上传;
d. √ 支持筛选上传文件类型;
e. √ 支持限定上传文件尺寸;
f. √ 支持文件上传进度监控;
缺点:
a. 要求客户端安装 Flash 控件;
b. Cookie 在 Safari 环境下不能正常发送;
图:官方对 Session Cookie 问题的说明
代码示例:
代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>
<%@ page session="false" %>
$("#myMultiFileUpload").uploadify({
swf : './uploadify/uploadify.swf', // 指明flash插件路径
uploader : './apacherecvfile', // 文件上传请求地址
width : 120,
height : 30,
fileObjName : "myfile", // 相当于
buttonText : "选择文件",
removeCompleted : false,
fileTypeExts : "*.png;*.jpg;*.txt", // 限制可选的文件类型
onUploadComplete: function(file) { // 每个文件上传成功的回调
console.log('[' + file.name + '] upload complete.');
}
});
效果展示:
3.3. 客户端发起 —— Form 表单上传
用 Form 表单上传文件,是最传统的方法,优点就是兼容性特别好。
优点:兼容性好,不需要插件,浏览器原生支持
老古董(IE7/8/9):
a. √ 支持上传完成回调机制;
b. × 支持多选文件上传;
c. × 支持筛选上传文件类型;
d. × 支持限定上传文件尺寸;
e. × 支持文件上传进度监控;
现代浏览器(>=IE10、Chrome、Firefox):
a. √ 支持上传完成回调机制;
b. √ 支持多选文件上传;
c. √ 支持筛选上传文件类型;
d. × 支持限定上传文件尺寸;
e. × 支持文件上传进度监控;
缺点:低版本浏览器上,能力偏弱;
代码示例:form_upload_ie8_ie9.jsp
代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>
function fileUploadSuccess(params){
alert("fileUploadSuccess called!\n"+params);
}
代码示例:form_upload_callback_trigger.jsp
代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>
parent.${callbackFnName}("${callbackParams}");
代码示例:RecvFilesWithCallback.java
代码语言:javascript复制package webj2ee;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
@MultipartConfig()
@WebServlet("/recvfileswithcallback")
public class RecvFilesWithCallback extends HttpServlet {
private static final long serialVersionUID = 1L;
public RecvFilesWithCallback() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
// 解析上传文件
StringBuilder submittedFilesInfo = new StringBuilder();
Collection
Iterator
while(itor.hasNext()) {
Part part = itor.next();
String name = part.getName();
String submittedFileName = part.getSubmittedFileName();
long size = part.getSize();
String contentType = part.getContentType();
submittedFilesInfo.append(name+": "+submittedFileName+", "+contentType+", "+size/1000 + "K" + "\r\n");
}
// 回调函数名、参数
String callbackFnName = request.getParameter("callbackFnName");
String callbackParams = "来自Servlet的返回值...";
// 返回上传成功页面,触发上传成功回调函数
request.setAttribute("submittedFilesInfo", submittedFilesInfo);
request.setAttribute("callbackFnName", callbackFnName);
request.setAttribute("callbackParams", callbackParams);
request.getRequestDispatcher("./form_upload_callback_trigger.jsp").forward(request, response);
}
}效果展示:
3.4. 客户端发起 —— FileAPI + XMLHttpRequest 上传
这是功能最强大、最灵活的文件上传方案。
优点:功能强大、灵活、定制性强
老古董(IE7/8/9):
× 传统浏览器环境中,不支持 Ajax 文件上传;
现代浏览器(>=IE10、Chrome、Firefox):
a. √ 支持上传完成回调机制;
b. √ 支持多选文件上传;
c. √ 支持筛选上传文件类型;
d. √ 支持限定上传文件尺寸;
e. √ 支持文件上传进度监控;
缺点:只能在现代浏览器环境中使用;
3.4.1 File API
H5 提供了一组简洁有效的文件操作接口:File API
主要涉及:
FileList:用户通过file控件或拖拽选择的一组文件;
File:FileList里面放的就是File;
Blob:代表一段二进制数据,File就是继承自Blob;
FileReader:用于从File、Blob中读取数据;
FormData:用Ajax实现上传、进度显示时会用到;
特别注意:
H5 的 File API 虽然可以让我们访问本地文件系统,但只能被动地读取,也就是说只有用户主动触发了文件读取行为(比如通过file控件选择选择文件或拖拽文件),才能访问到File API;
浏览器兼容性:
3.4.2 XMLHttpRequest Level 2
特别注意,是 XMLHttpRequest Level 2,支持文件上传、上传进度展示等特性。与 H5 的 File API 相结合,可以发挥很大
浏览器兼容性:
例1:获取用户选择的文件(FileList、File)
核心逻辑:
代码示例:
代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>
$("#myfile").bind("change", function(e){
var fileList = this.files;
console.dir(fileList);
for(var i=0; i var file = fileList[i]; console.log(file.name + ": " + file.size/1024 + "KB;"); } }); 效果展示: 例2:获取用户拖拽的文件(FileList、File) 核心逻辑: 代码示例: 代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>
$("#myFileDrop").bind("drop", function(e) {
// 阻止冒泡、阻止浏览器默认行为(浏览器默认会自动打开拖拽的文件)
e.stopPropagation();
e.preventDefault();
// 通过 e.originalEvent.dataTransfer.files 获取拖拽进来的 FileList
// (注: 经jquery包装的事件对象e中不包含dataTransfer对象,
// 所以需要通过e.originalEvent访问浏览器的原始事件对象)
var fileMsg = "";
var fileList = e.originalEvent.dataTransfer.files;
for(var i=0; i var file = fileList[i]; fileMsg += file.name + ": " + file.size/1024 + "KB;\n"; } // 将组装后的文件信息展示在textarea组件中 $(this).val(fileMsg); }); 效果展示: 例3:用 FileReader 预览图片 FileReader 是一种异步文件读取机制,用于读取File、Blob中的文件数据。 常用接口: 代码语言:javascript复制// 用于将 Blob 或 File 对象,按指定的编码(默认UTF-8),转化为字符串形式; readAsText(Blob|File, opt_encoding): // 用于将 Blob 或 File 对象,转换为一个基于base64编码的Data URL对象。(文件上传前预览就是靠这个技术) readAsDataURL(Blob|File):核心逻辑: 代码示例: 代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>
$("#myfile").bind("change", function(e){
let fileList = this.files; // 获取用户选择的文件列表;
for(let file of fileList ){ // 为每个文件,生成一张预览图;
let reader = new FileReader(); // 构建一个FileReader实例;
// FileReader是异步读取数据,注册onload事件,监听文件读取进度;
reader.addEventListener("load", function () {
// FileReader完成读取后,onload事件被触发,
// 属性 result 中包含换后的 Data URL 对象;
let imageDataUrl = reader.result;
// 构造一个Image结构
let image = new Image();
image.src = imageDataUrl;
$("#previewArea").append( image );
}, false);
reader.readAsDataURL(file); // 将图片转换为基于Base64的Data URL对象;
}
});
效果展示:
例4:用 onprogress 事件监听文件上传进度
XMLHttpRequest Level 2,支持 onprogress 事件,可以监听文件上传或下载中的进度。
核心逻辑:
代码示例:
代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>
function onFileSelected() {
// file的非multiple模式,FileList中只会有一个元素;
var file = document.getElementById('myfile').files[0];
// 展示fileName、fileSize、fileType
$('#fileName').text(file.name);
$('#fileSize').text(Math.round(file.size / 1024) + 'KB');
$('#fileType').text(file.type);
$('#progress').text("0%");
}
function uploadFile() {
// 1. 获取用户选取的文件
var file = document.getElementById('myfile').files[0];
// 2. 用FormData组件要发送的表单数据(文件)
var fd = new FormData();
fd.append("myfile", file);
// 3. 使用XMLHttpRequest发送请求
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", function(e) {
let computable = e.lengthComputable; // 进度是否可计算
if( computable ){
let loaded = e.loaded; // 已上传量
let total = e.total; // 数据总量
$('#progress').text( Math.round(loaded / total * 100) + '%');
}else {
$('#progress').text( 'unable to compute progress!');
}
}, false);
xhr.upload.addEventListener("load", function(e) { // 传输完成
console.log("Transfer Complete!");
}, false);
xhr.addEventListener("load", function(e) { // 请求完成
console.log(xhr.responseText);
}, false);
xhr.open("POST", "./apacherecvfile");
xhr.send(fd);
}
效果展示:
例5:FormData + XMLHttpRequest 上传文件
核心逻辑:
代码示例:
代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>
function onFileSelected() {
var files = document.getElementById('myfile').files;
var selectedFilesInfo = "";
for(var i=0; i var file = files[i]; var name = file.name; var contentType = file.type; var size = file.size; selectedFilesInfo += name+": "+contentType+", "+size/1000 + "K" + "\n"; } $("#selectedFilesInfo").text(selectedFilesInfo); } function uploadFile() { // 1. 获取用户选取的文件 var files = document.getElementById('myfile').files; // 2. 用 FormData 组装 Multipart 型请求 var fd = new FormData(); for(var i=0; i fd.append("myfile", files[i], files[i].name); } // 3. 使用XMLHttpRequest Level 2发送请求 // 注:推荐使用 Fetch API 替代 XMLHttpRequest var xhr = new XMLHttpRequest(); xhr.addEventListener("load", function(e) { // 请求完成 alert("上传完成!"); }, false); xhr.open("POST", "./HandleUpload"); xhr.send(fd); } 效果展示: 参考: RFC 1867: https://tools.ietf.org/html/rfc1867 RFC 6532: https://tools.ietf.org/html/rfc6532 https://caniuse.com/ https://developer.mozilla.org/zh-CN/ 《Servlet、JSP 和 Spring MVC 初学指南》 《Spring 入门经典》