7.2 Processing multipart form data
普通的表单生成的请求中的参数都很简单,就是用&连起来的名-值对.这种方式适用于文本类型的表单提交,但是二进制数据,比如说图片上传,用这种方式提交就不太可靠了.这类表单被称之为multipart form,这种表单将数据分割成不同的部分,每一部分都是一个独立的域,可以拥有自己的类型.
下面显示了multipart请求体的内容
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="firstName"
Charles
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="lastName"
Xavier
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="email"
charles@xmen.com
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="username"
professorx
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="password"
letmein01
------WebKitFormBoundaryqgkaBn8IHJCuNmiW
Content-Disposition: form-data; name="profilePicture"; filename="me.jpg"
Content-Type: image/jpeg
[[ Binary image data goes here ]]
------WebKitFormBoundaryqgkaBn8IHJCuNmiW--
在上面的
multipart请求体中,profilePicture不同于其它部分.它有自己的Content-Type头信息表示自己是一个JPEG文件.虽然看上去并不明显,但是profilePicture的body是二进制数据而不是简单的文本.
虽然multipart请求看上去比较复杂,但是在spring mvc控制器中处理它们是比较简单的.不过在开始写对应的请求的控制器之前,需要先配置一个multipart解析器,让DispatchServlet能够识别DispatchServlet请求.
7.2.1 Configuring a multipart resolver
DispatchServlet本身并没有包含如何解析multipart请求中数据的逻辑.它会将multipart请求委托给一个spring MultipartResolver实现,由对方解析multipart请求中的内容.从spring版本3.1开始,spring发布时即包含了两个MultipartResolver实现
1. CommonsMultipartResolver 使用Jakarta Commons FileUpload解析multipart请求
2. StandardServletMultipartResolver 依赖servlet 3.0对multipart请求的支持
通常来说,应该优先选择StandardServletMultipartResolver. 因为它是servlet3.0容器原生支持的,不需要导入其它项目包.但是如果应用部署在servlet3.0之前的容器里,或者spring的版本是3.1以下,那还是选择CommonsMultipartResolver吧.
RESOLVING MULTIPART REQUESTS WITH SERVLET3.0 使用servlet3.0解析multipart请求
StandardServletMultipartResolver的构造方法没有参数,也不需要额外设置其它的属性.因此声明它的时候很方便,如下所示
@Bean
public MultipartResolver multipartResolver() throws IOException {
return new StandardServletMultipartResolver();
}
如何对上传的文件大小进行限制,如何指定文件存储的路径? 实际上,这样的限制不能在
StandardServletMultipartResolver上设置,而应该在servlet中配置multipart时设置. 至少需要指定文件的临时存储路径,否则StandardServletMultipartResolver无法工作. 更具体点,你必须在web.xml或者servlet初始化类中设置multipart选项,将其做为DispatchServlet配置的一部分.
根据配置multipart选项的不同根据配置dispatchservlet的方法而不同
1. 如果直接通过实现WebApplicationInitializer接口配置了dispatchServlet,那么可以调用setMultipartConfig()来配置multipart.
如下示
DispatcherServlet ds = new DispatcherServlet();
Dynamic registration = context.addServlet("appServlet", ds);
registration.addMapping("/");
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/spittr/uploads"));
2. 如果是通过继承AbstractAnnotationConfigDispatcherServletInitializer或者AbstractDispatcherServletInitializer类来配置dispatchservlet,那么就没有创建dispatchservlet的实例也没有直接在servlet上下文中注册它.这样的话,Dynamic就没有直接的引用了.但是可以直接重写customizeRegistration方法,在方法内部配置multipart.
如下示
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(
new MultipartConfigElement("/tmp/spittr/uploads"));
}
译注:应该是第二种用的比较多吧.
MultipartConfigElement的构造方法有几个参数可以用于设置上传文件的最大值,等等信息,具体如下
The maximum size (in bytes) of any file uploaded. By default there is no limit.
The maximum size (in bytes) of the entire multipart request, regardless of how
many parts or how big any of the parts are. By default there is no limit.
The maximum size (in bytes) of a file that can be uploaded without being written to the temporary location. The default is 0, meaning that all uploaded files
will be written to disk.
比如下面的代码就限制上传的文件大小不能超过2MB,整个请求不能超过4MB,请求的所有内容都需要写入磁盘
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement("c:/xxxx", 2097152, 4194304, 0));
}
完整的java配置
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class };
}
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads", 2097152, 4194304, 0));
}
// @Override
// protected Filter[] getServletFilters() {
// return new Filter[] { new MyFilter() };
// }
}
完整的XML配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
<multipart-config>
<location>/tmp/spittr/uploads</location>
<max-file-size>2097152</max-file-size>
<max-request-size>4194304</max-request-size>
</multipart-config>
</servlet>
</web-app>
CONFIGURING AJAKARTACOMMONS FILEUPLOAD MULTIPART RESOLVER
StandardServletMultipartResolver是最好的选择,但是只能在servlet3.0的容器里使用.如你所愿,可以自己写MultipartResolver接口的实现.但是除非是要在处理multipart请求中执行一些特定的需求,否则没必要自己实现MultipartResolver接口.spring发布时原生包含了CommonsMultipartResolver,作为StandardServletMultipartResolver之外的另一个选择.
声明CommonsMultipartResolver的方法如下
@Bean
public MultipartResolver multipartResolver() {
return new CommonsMultipartResolver();
}
与配置
StandardServletMultipartResolver时不同,不需要明确给CommonsMultipartResolver指定文件存储的临时目录.默认情况下,该目录就是servlet容器的临时目录.当然也可以通过设置uploadTempDir属性来指定一个不同的目录.
@Bean
public MultipartResolver multipartResolver() throws IOException {
CommonsMultipartResolver multipartResolver =
new CommonsMultipartResolver();
multipartResolver.setUploadTempDir(
new FileSystemResource("/tmp/spittr/uploads"));
return multipartResolver;
}
通过设置属性值的方式还可以设置
CommonsMultipartResolver的其它选项.比如说下面的设置与上面通过MultipartConfigElement设置StandardServletMultipartResolver实际的功能类似
@Bean
public MultipartResolver multipartResolver() throws IOException {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
multipartResolver.setUploadTempDir(new FileSystemResource("/tmp/spittr/uploads"));
multipartResolver.setMaxUploadSize(2097152);
multipartResolver.setMaxInMemorySize(0);
return multipartResolver;
}
从上面可见,有两个属性值与
MultipartConfigElement的第二个和第四个构造方法参数值一致,表示上传的文件不能走过2MB,并且所有的文件都要写入磁盘.但是与MultipartConfigElement不同,multipartResolver没办法限制multipart请求的总大小.
7.2.2 Handling multipart requests
现在你已经成功配置了Spring对multipart请求的支持,现在需要编写相应的控制器方法以接收上传的文件了.实现该功能的最常用的方法就是给一个控制器方法加上@RequestPart的注解.
下面实现注册时让用户上传头像的功能.
先修改registrationForm.html文件,给个上传文件的入口.
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %>
<%@ page session="false"%>
<html>
<head>
<title>Spittr</title>
<link rel="stylesheet" type="text/css"
href="<c:url value="/resources/style.css" />">
</head>
<body>
<h1>Register</h1>
<sf:form method="POST" commandName="spitter" enctype="multipart/form-data">
<sf:errors path="*" element="div" cssClass="errors" />
First Name: <sf:input path="firstName" />
<!-- <span id="firstName.errors">size must be between 2 and 30</span> -->
<br />
Last Name: <sf:input path="lastName" />
<!-- <span id="lastName.errors">size must be between 2 and 30</span> -->
<br />
Email: <sf:input path="email" />
<!-- <span id="email.errors">size must be between 2 and 30</span> -->
<br />
Username: <sf:input path="username" />
<!-- <span id="username.errors">size must be between 2 and 30</span> -->
<br />
Password: <sf:password path="password" />
<!-- <span id="password.errors">size must be between 2 and 30</span> -->
<br />
<label>Profile Picture</label>:
<input type="file"
name="profilePicture"
accept="image/jpeg,image/png,image/gif" />
<br/>
<input type="submit" value="Register" />
</sf:form>
</body>
</html>
控制器方法
//第7章
@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(
@RequestPart("profilePicture") MultipartFile profilePicture,
@Valid Spitter spitter,
Errors errors) {
if (errors.hasErrors()) {
return "registerForm";
}
try {
//profilePicture.transferTo(new File("/data/spittr/" + profilePicture.getOriginalFilename()));
System.out.println(profilePicture.getOriginalFilename());
profilePicture.transferTo(new File("c:/" + profilePicture.getOriginalFilename()));
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
}
//spitterRepository.save(spitter);
System.out.println(spitter.getEmail());
return "redirect:/spitter/" + spitter.getUsername();
}
这里只是输出了文件的名字,给上传的文件指定了目录,没有做太深入的设置
注意,方法中的参数设置:
@RequestPart("profilePicture") MultipartFile profilePicture
结果