文件上传知识体系

如下图

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" 类型, 用于实现文件选择;

扩充 enctype="multipart/form-data" 编码形式,用于支持文件传输;代码语言:javascript复制<%@page pageEncoding="UTF-8" %>

enctype="multipart/form-data">





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 parts = request.getParts();

Iterator itor = parts.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复制

org.apache.commons

commons-lang3

3.9

commons-io

commons-io

2.6

commons-fileupload

commons-fileupload

1.4

代码示例:

代码语言: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 items = null;

try {

items = upload.parseRequest(request);

} catch (FileUploadException e) {

throw new RuntimeException(e);

}

Iterator iter = items.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复制

org.springframework

spring-webmvc

5.1.8.RELEASE

org.apache.commons

commons-lang3

3.9

commons-io

commons-io

2.6

commons-fileupload

commons-fileupload

1.4

代码示例: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">

xupload

xupload

org.springframework.web.servlet.DispatcherServlet

contextConfigLocation

classpath:springmvc.xml

1

xupload

/mvc/*

index.jsp

代码示例:springmvc.xml

代码语言: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" />

代码示例:SpringMVCApacheCommonsUploadController

代码语言: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 fileNames = multipartRequest.getFileNames();

while (fileNames.hasNext()) {

String fieldName = fileNames.next();

List files = multipartRequest.getFiles(fieldName);

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">

xupload

xupload

org.springframework.web.servlet.DispatcherServlet

contextConfigLocation

classpath:springmvc.xml

1

d:/temp

1024

2048

512

xupload

/mvc/*

index.jsp

代码示例:springmvc.xml

代码语言: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. 文件上传请求发起

3.1. Server 端发起(HttpClient)

应用场景:Server 端请求转发;

关键依赖:

代码语言:javascript复制

org.apache.httpcomponents

httpclient

4.5.9

org.apache.httpcomponents

httpmime

4.5.9

代码示例:

代码语言: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" %>

效果展示:

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"%>

enctype="multipart/form-data"

method="POST"

target="formSubmitResult"

>

‍代码示例:form_upload_callback_trigger.jsp

代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>

submitted files info

${submittedFilesInfo}

代码示例: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 parts = request.getParts();

Iterator itor = parts.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"%>

效果展示:

例2:获取用户拖拽的文件(FileList、File)

核心逻辑:

代码示例:

代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>

WebJ2EE FileAPI

效果展示:

例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"%>

WebJ2EE FileAPI

效果展示:

例4:用 onprogress 事件监听文件上传进度

XMLHttpRequest Level 2,支持 onprogress 事件,可以监听文件上传或下载中的进度。

核心逻辑:

代码示例:

代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>

select a file to upload.


FileInfo:

fileName:

fileSize:

fileType:

progress:


效果展示:

例5:FormData + XMLHttpRequest 上传文件

核心逻辑:

代码示例:

代码语言:javascript复制<%@ page language="java" pageEncoding="UTF-8"%>

select a file to upload.

id="myfile"

multiple="multiple"

accept="image/*"

onchange="onFileSelected()"

/>


selected files info


效果展示:

参考:

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 入门经典》