参考资料
:
(1)
RFC 1867
(2)Java Servlet Specification 3.1;
(3)《Java Web高级编程》;
1. Multipart FormData
Multipart是HTML中表单文件上传的基本格式,一般通过如下方法可以通过HTTP上传文件:
<form action="_URL_" method="POST" enctype="multipart/form-data">
<input type="text" name="username" />
<input type="file" name="userfile1" />
<input type="submit" value="submit" />
</form>
有两个地方是使用Multipart的关键:
(1)对于POST请求来说,enctype的默认值是application/x-www-form-urlencoded,而这里要是用
multipart/form-data
;
(2)
<input />
的type设置为
file
;
1.1 Multipart的数据格式
基于Multipart,请求的每个部分都有指定的边界分隔开,都有一个值为form-data的
Content-Disposition
和匹配表单输入名称的
name
。
如果是文件类型字段,还将有
filename
,匹配MIME类型的
Content-Type
;
使用下面的表单提交单个文件和其他文本域:
测试1:单文件上传
<form action="/s/upload/1" method="post" enctype="multipart/form-data">
<fieldset>
<legend>测试1:单文件上传</legend>
<p><label for="name">名称 </label><input id="name" type="text" name="name" /></p>
<p><label for="files">文件 </label><input id="files" type="file" name="files" /></p>
<p><label for="location">地区 </label><input id="location" type="text" name="location" /></p>
<input type="submit" />
</fieldset>
</form>
请求数据内容:
------WebKitFormBoundaryBDujyAl87MaTQd9J
Content-Disposition: form-data; name="name"
串个沙
------WebKitFormBoundaryBDujyAl87MaTQd9J
Content-Disposition: form-data; name="files"; filename="说明.txt"
Content-Type: text/plain
------WebKitFormBoundaryBDujyAl87MaTQd9J
Content-Disposition: form-data; name="location"
地球
------WebKitFormBoundaryBDujyAl87MaTQd9J--
可以看到每个部分有边界像分隔,这个边界值由客户端决定,是随机生成的(这里我使用chrome做的实验,因此可以看到“WebKit”)。
测试2:多文件上传
<form action="/s/upload/1" method="post" enctype="multipart/form-data">
<fieldset>
<legend>测试2:多文件上传</legend>
<p><label for="name">名称 </label><input id="name1" type="text" name="name" /></p>
<p><label for="files">文件 </label><input id="files1" type="file" name="files1" multiple="multiple" /></p>
<p><label for="location">地区 </label><input id="location1" type="text" name="location" /></p>
<input type="submit" />
</fieldset>
</form>
请求头Content-Type:
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryZGDkB6nM3LvHX0KI
请求数据内容:
------WebKitFormBoundaryLnO8YOO2DPoP3M6y
Content-Disposition: form-data; name="name"
2个文件
------WebKitFormBoundaryLnO8YOO2DPoP3M6y
Content-Disposition: form-data; name="files1"; filename="说明.txt"
Content-Type: text/plain
------WebKitFormBoundaryLnO8YOO2DPoP3M6y
Content-Disposition: form-data; name="files1"; filename="说明 (copy).txt"
Content-Type: text/plain
------WebKitFormBoundaryLnO8YOO2DPoP3M6y
Content-Disposition: form-data; name="location"
中国
------WebKitFormBoundaryLnO8YOO2DPoP3M6y--
基于chrome得到的结果,没有出现一个Content-type为
multipart/mixed
的嵌套部分,而是用多个文件字段部分表示。
2. Servlet 3.0对Multipart的支持
在Servlet3.0之前对于文件上传的支持,可以通过Commons FileUpload等第三方工具来实现。Java EE 6中Servlet 3.0新增了对它的支持,这样可以不再依赖第三方库。主要通过HttpServletRequest的
getParts()
和
getPart()
方法。
结合前面的测试表单,我用一个Servlet和Jsp来演示处理的具体方法:
UploadServlet2:
@WebServlet (
name = "uploadServlet2",
urlPatterns = "/upload2",
loadOnStartup = 1
)
@MultipartConfig (
fileSizeThreshold = 5_242_880,
maxFileSize = 20_971_520L,
maxRequestSize = 41_943_040L
)
public class UploadServlet2 extends HttpServlet {
private static final Logger logger = LogManager.getLogger();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("/p/upload/upload.jsp").forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Collection<Part> parts = req.getParts();
logger.debug("uploading...");
req.setAttribute("parts", parts);
req.getRequestDispatcher("/p/upload/resolvePart.jsp").forward(req, resp);
}
}
通过ServletContainerInitializer配置:
@Override
public void contextInitialized(ServletContextEvent sce) {
logger.info("ServletContextListener invoked");
//获取ServletContext对象
ServletContext servletContext = sce.getServletContext();
//添加Servlet
ServletRegistration.Dynamic uploadRegistration = servletContext.addServlet("uploadServlet", UploadServlet.class);
uploadRegistration.addMapping("/upload/*");
uploadRegistration.setLoadOnStartup(1);
MultipartConfigElement configElement = new MultipartConfigElement(".", 52428800,
52428800, 0);
uploadRegistration.setMultipartConfig(configElement);
}
必须启用multipart支持,配置一样有三种方法:注解,编程(ServletContextListener/ServletContainerInitializer),XML,但是基本的配置项是一样的:
location
:临时文件目录,一般可以使用默认的由服务器软件决定;
fileSizehreshold
:超过这个阈值放入临时文件目录,否则在内存中有垃圾回收处理;
maxFileSize
和
maxRequestSize
;
location的配置需要注意,Servlet规范中的说明:
location元被解析为一个绝对路径且默认为 javax.servlet.context.tempdir。如果指定了相对地址,它将是相对于 tempdir 位置。绝对路径与相对地址的测试必须使用 java.io.File.isAbsolute。
3. Spring对Multipart的支持
服务端主要就是解析Multipart数据。
启用Multipart,这里通过Spring的WebApplicationInitializer初始化器进行配置,本质是通过ServletContainerInitializer。启用指定DispatcherServlet的Multipart支持。还是通过
Registration
配置的和上面的ServletContextListener一样。这里配置了自定义的临时目录,如果指定的目录不存在或是无法访问,通过”.”配置,因为前面引用的Serlvet规范已经说明了,相对目录是基于容器的临时目录的。
Spring同时兼容了基于Commons FileUpload和Servlet 3.0标准API两种方式的解析。
@Order(1)
public class BootStrap implements WebApplicationInitializer {
/* 略 */
@Override
public void onStartup(ServletContext container) throws ServletException {
/* 略 */
DispatcherServlet dispatcherServlet = new DispatcherServlet(cgContext);
dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
ServletRegistration.Dynamic dispatcherCG = container.addServlet("cgServlet",
dispatcherServlet);
dispatcherCG.setLoadOnStartup(1);
//文件上传支持
//设置临时文件目录
File path = new File(container.getRealPath("/tmp/web_yjh_files/"));
if(path.exists() || path.mkdirs()) {
dispatcherCG.setMultipartConfig(new MultipartConfigElement(
path.getAbsolutePath(), 200_971_520L, 401_943_040L, 0
));
} else {
dispatcherCG.setMultipartConfig(new MultipartConfigElement(
".", 200_971_520L, 401_943_040L, 0
));
}
/* 略 */
}
}
Spring提供了一个Multipart的解析器:MultipartResolver,因此在
@Configuration
类中添加一个Bean,有两种选择
CommonsMultipartResolver
和
StandardServletMultipartResolver
,分别基于Commons File Upload和Servlet 3.0标准API;
@Bean
public MultipartResolver multipartResolver() {
return new CommonsMultipartResolver();
}
编写Controller method,使用Spring的
MultipartFile
:
@RequestMapping(value = "upload", method = RequestMethod.POST, produces = "text/html")
@ResponseBody
public String upload(String username,
@RequestParam(value = "attachment") MultipartFile parts) throws Exception {
return "success " +
"name:" + parts.getName() +
"size:" + parts.getSize() +
"contentType:" + parts.getContentType() +
(parts.getContentType() == null ? "" : ("filename" + parts.getOriginalFilename())) +
"" + IOUtils.toString(parts.getInputStream()) +
"size:" + parts.getSize();
}
使用Part API:
Serlvet Part的标准API包括:
(1)
name
:表单字段名;
(2)
size
:Part数据内容的大小;
(3)
submittedFileName
:Servlet 3.1新增方法,获取文件名,之前必须通过
getHeader("content-disposition")
解析;
(4)
contentType
:如果
<input>
类型是type这个值是
null
,如果是文件,可以获得它的MIME类型;
(5)
getHeader
:获取指定头;
(6)
getInputStream
:获取输入流;
对于表单数据的 Content-Disposition,即使没有文件名,也可使用 part 的名称通过 HttpServletRequest 的
getParameter 和 getParameterValues 方法得到 part 的字符串值。
<h1>Multipart解析</h1>
<c:forEach items="${parts}" var="part">
<p>
<h3>Part: ${part.name}</h3>
<span>Size: ${part.size}</span><br />
<span>SubmittedFileName: ${part.submittedFileName},Servlet 3.1新增方法</span><br />
<span>ContentType: ${part.contentType}</span><br />
<c:choose>
<c:when test="${part.contentType == null || part.contentType eq 'text/plain'}">
<p>
${IOUtils.toString(part.inputStream)}
</p>
</c:when>
<c:when test="${part.contentType eq 'application/octet-stream'}">
<p>
${part.getHeader("content-disposition")}
</p>
</c:when>
</c:choose>
</p>
</c:forEach>
<c:import url="upload.jsp" />