先让我们来看一段摘自《Spring 2.5 Reference 中文版》(http://www.redsaga.com/spring_ref/2.5/spring-reference.pdf)的一段关于FileUpload的开场描述:
"Spring支持web应用中的分段文件上传。这种支持是由即插即用的MultipartResolver来实现。这些解析器都定义在org.springframework.web.multipart包里。Sprig提供了现成的MultipartResolver可以支持Commons FileUpload(http://jakarta.apache.org/commons/fileupload)和COS FileUpload(http://www.servlets.ocm/cos)。"
是的,Spring通过配置一个分段上传解析器来完成对文件上传的解析和封装工作,那么Spring是如何完成这一工作的呢:
首先,DispatcherServlet必须找到一个文件上传解析器的实例,使用这个实例来检查本次请求的HttpServletRequest是否是一个分段文件上传的Request,通过下面的Spring 源码可以看到,首先必须保证有一个MultipartResolver的实例,并且由该类的Resolver的isMultipart方法来验证,本次Request是否为文件上传的Request.如果以上条件都满足,那么Spring将其转换为一个继承自HttpServletRequest的MultipartHttpServletRequest返回,这样在你的Controller中就可以使用这个经过转换的request,从中取到MultipartFile信息。
- protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
- if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
- if (request instanceof MultipartHttpServletRequest) {
- logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
- "this typically results from an additional MultipartFilter in web.xml");
- }
- else {
- return this.multipartResolver.resolveMultipart(request);
- }
- }
- // If not returned before: return original request.
- return request;
- }
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (request instanceof MultipartHttpServletRequest) {
logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
"this typically results from an additional MultipartFilter in web.xml");
}
else {
return this.multipartResolver.resolveMultipart(request);
}
}
// If not returned before: return original request.
return request;
}
由以上分析可以看出,我们必须配置一个MultipartResolver,在这里我们使用支持Commons FileUpload的CommonsMultipartResolver:
- <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" p:defaultEncoding="utf-8"/>
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" p:defaultEncoding="utf-8"/>
而且我们可以在该Resolver中定义文件上传的最大长度:
- <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" p:defaultEncoding="utf-8" p:maxUploadSize="100000"/>
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" p:defaultEncoding="utf-8" p:maxUploadSize="100000"/>
当用户选择的上传文件大于maxUploadSize值的时候,commons fileupload会抛出一个异常MaxUploadSizeExceededException表示用户上传的文件超出了最大限制。
当然,我们可以通过Spring MVC中的ExceptionResolver来针对该异常定义一个显示错误的View,但针对有可能存在的多个文件上传Controller中都会发生文件大小超长这个异常的情况,除了我们自定义一个粒度更细的ExceptionResolver,我们还可以把上传文件合法性判断挪到用户自己的Controller中来做。而且我个人更偏向于后一种做法。
除了Spring Configuration之外,我们还需要准备一个页面上传的jsp文件供View视图使用:
- <%@ 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">
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <title>Insert title here</title>
- </head>
- <body style="text-align:left">
- <% if(request.getAttribute("success") != null) {%>
- Upload Successfully!!!<br/>
- <% }%>
- <form id="loginform" name="loginform" method="POST" enctype="multipart/form-data">
- <table width="100%" border="0" cellspacing="0" cellpadding="0">
- <tr>
- <td height="30" align="right">Choose File</td>
- <td align="left">
- <input name="imageFile" type="file"/>
- </td>
- </tr>
- <tr>
- <td align="center" colspan="2">
- <input type="submit" value="submit" name="submit" />
- </td>
- </tr>
- </table>
- </form>
- </body>
- </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">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body style="text-align:left">
<% if(request.getAttribute("success") != null) {%>
Upload Successfully!!!<br/>
<% }%>
<form id="loginform" name="loginform" method="POST" enctype="multipart/form-data">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td height="30" align="right">Choose File</td>
<td align="left">
<input name="imageFile" type="file"/>
</td>
</tr>
<tr>
<td align="center" colspan="2">
<input type="submit" value="submit" name="submit" />
</td>
</tr>
</table>
</form>
</body>
</html>
注意:在文件上传Form表单中,一定要将enctype设置为"multipart/form-data"因为只有这样才能使Spring知道这是一个文件上传的请求。
细心的读者也许能发现Form表单中action为默认值也就是说post到和上传页面同样的URL,因此我们定义了一个Controller分别来处理这个请求的GET和POST请求。下面让我们来看看这个Controller:
1.我们通过@Controller声明这个类为Spring组件,告知Spring容器在初始化的时候需要加载该类实例到Spring Context Container中。
2.通过@RequestMapping("/sec_upload.do")将sec_upload.do的请求指向该Controller处理。
- @Controller
- @RequestMapping("/sec_upload.do")
- public class UploadController {
- //...
- }
@Controller
@RequestMapping("/sec_upload.do")
public class UploadController {
//...
}
3.定义一个处理GET请求的方法,该方法简单的将选择文件Form表单页展现给用户:
- @RequestMapping(method = RequestMethod.GET)
- public String handleUploadShow() {
- return "uploadView";
- }
@RequestMapping(method = RequestMethod.GET)
public String handleUploadShow() {
return "uploadView";
}
4.定义一个处理POST请求的方法,该方法进行用户文件上传处理:
- @RequestMapping(method = RequestMethod.POST)
- public String handleUploadProcess(
- @RequestParam("imageFile") MultipartFile file, Model model)
- throws Exception {
- //具体的业务逻辑操作。。。
- model.addAttribute("success", "true");
- return "uploadView";
- }
@RequestMapping(method = RequestMethod.POST)
public String handleUploadProcess(
@RequestParam("imageFile") MultipartFile file, Model model)
throws Exception {
//具体的业务逻辑操作。。。
model.addAttribute("success", "true");
return "uploadView";
}
通过@RequestParam("imageFile")注解,Spring会将request请求中的imageFile的文件信息自动绑定到MultipartFile对象。
上面的Controller方法解决的文件绑定的问题,但假设我们的Form表单中除了文件选择框还有其他一些用户填写的信息,那么我们怎么处理呢?仿照上面的方法,我们可以为多个参数提供多个@RequestParam注解来完成数据绑定工作,但我们也可以通过MultipartHttpServletRequest对象来获取这些信息,因为在DispatcherServlet中Spring已经将一个普通的HttpServletRequest转换为了一个MultipartHttpServletRequest:
- @RequestMapping(method = RequestMethod.POST)
- public String handleAnotherUploadProcess(
- MultipartHttpServletRequest request, Model model) throws Exception {
- MultipartFile file = request.getFile("imageFile");
- //request.getParameter("xxx");
- //request.getContentType();
- //request.getContentLength();
- //some other processing...
- model.addAttribute("success", "true");
- return "uploadView";
- }
@RequestMapping(method = RequestMethod.POST)
public String handleAnotherUploadProcess(
MultipartHttpServletRequest request, Model model) throws Exception {
MultipartFile file = request.getFile("imageFile");
//request.getParameter("xxx");
//request.getContentType();
//request.getContentLength();
//some other processing...
model.addAttribute("success", "true");
return "uploadView";
}
这种方式还是需要我们不断的通过request.getParameter("xxx")方式来获得参数,了解Spring MVC的同学可能想到了,使用CommandObject绑定-回答正确。假设我们定义了一个POJO对象:
- public class BoUploadFile {
- private MultipartFile imageFile;
- public MultipartFile getImageFile() {
- return imageFile;
- }
- public void setImageFile(MultipartFile imageFile) {
- this.imageFile = imageFile;
- }
- private String name;
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
public class BoUploadFile {
private MultipartFile imageFile;
public MultipartFile getImageFile() {
return imageFile;
}
public void setImageFile(MultipartFile imageFile) {
this.imageFile = imageFile;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
这个对象中不仅包括需要封装的上传文件信息,还包括其他一些用户输入的普通信息。那么有了这个封装对象,我们的Controller可以变成如下的样子:
- @RequestMapping(method = RequestMethod.POST)
- public String handleThirdUploadProcess(BoUploadFile uploadFile, Model model) throws Exception{
- MultipartFile file = uploadFile.getImageFile();
- //这里你可以通过uploadFile.getName()...等等获取用户输入的其他普通信息了。
- model.addAttribute("success", "true");
- return "uploadView";
- }
@RequestMapping(method = RequestMethod.POST)
public String handleThirdUploadProcess(BoUploadFile uploadFile, Model model) throws Exception{
MultipartFile file = uploadFile.getImageFile();
//这里你可以通过uploadFile.getName()...等等获取用户输入的其他普通信息了。
model.addAttribute("success", "true");
return "uploadView";
}
5.自定义一个文件验证类,来验证文件的合法性。
- /**
- * 用户文件上传验证类
- *
- * @author Jacky Lau created at 2008-8-27
- * @since 1.0
- * @version 1.0
- */
- public class MultipartFileValidator {
- private final static long MAX_SIZE = 1024 * 1024;
- /**
- * 文件大小上限
- */
- private long maxSize = MAX_SIZE;
- /**
- * 可接受的文件content-type
- */
- private String[] allowedContentTypes;
- @PostConstruct
- public void afterPropertiesSet() {
- Assert
- .notEmpty(allowedContentTypes,
- "The content types allowed to be uploaded must contain one at least!");
- }
- /**
- * 验证上传文件是否合法,如果不合法那么会抛出异常
- *
- * @param file
- * 用户上传的文件封装类
- */
- public void validate(MultipartFile file) {
- Assert.notNull(file, "The multipart file is null!");
- if (file.getSize() > maxSize)
- throw new FileOutOfMaxLengthException("error.upload.outmaxlen",
- new Object[] { maxSize },
- "The file uploaded is out of max file size!");
- if (!ArrayUtils.contains(allowedContentTypes, file.getContentType()))
- throw new ContentTypeNotSupportException("error.upload.content.notsupported", null,
- "The content type '"+file .getContentType()+"' is not a valid content type !");
- }
- /**
- * 设置文件上传大小上限
- *
- * @param maxSize
- * 文件上传大小上限
- */
- public void setMaxSize(long maxSize) {
- this.maxSize = maxSize;
- }
- /**
- * 设置可接受的文件content-type数组
- *
- * @param allowedContentTypes
- * 可接受的文件content-type数组
- */
- public void setAllowedContentTypes(String[] allowedContentTypes) {
- this.allowedContentTypes = allowedContentTypes;
- }
- }
/**
* 用户文件上传验证类
*
* @author Jacky Lau created at 2008-8-27
* @since 1.0
* @version 1.0
*/
public class MultipartFileValidator {
private final static long MAX_SIZE = 1024 * 1024;
/**
* 文件大小上限
*/
private long maxSize = MAX_SIZE;
/**
* 可接受的文件content-type
*/
private String[] allowedContentTypes;
@PostConstruct
public void afterPropertiesSet() {
Assert
.notEmpty(allowedContentTypes,
"The content types allowed to be uploaded must contain one at least!");
}
/**
* 验证上传文件是否合法,如果不合法那么会抛出异常
*
* @param file
* 用户上传的文件封装类
*/
public void validate(MultipartFile file) {
Assert.notNull(file, "The multipart file is null!");
if (file.getSize() > maxSize)
throw new FileOutOfMaxLengthException("error.upload.outmaxlen",
new Object[] { maxSize },
"The file uploaded is out of max file size!");
if (!ArrayUtils.contains(allowedContentTypes, file.getContentType()))
throw new ContentTypeNotSupportException("error.upload.content.notsupported", null,
"The content type '"+file .getContentType()+"' is not a valid content type !");
}
/**
* 设置文件上传大小上限
*
* @param maxSize
* 文件上传大小上限
*/
public void setMaxSize(long maxSize) {
this.maxSize = maxSize;
}
/**
* 设置可接受的文件content-type数组
*
* @param allowedContentTypes
* 可接受的文件content-type数组
*/
public void setAllowedContentTypes(String[] allowedContentTypes) {
this.allowedContentTypes = allowedContentTypes;
}
}
这样我们可以通过这个validator判断上传的文件是否超出了最大限制,文件格式是否正确等判断。我们可以通过配置文件配置该验证器,在这里为了方便起见在类中我用以下方式来初始化该验证器:
- private MultipartFileValidator validator;
- @PostConstruct
- public void init() {
- validator = new MultipartFileValidator();
- validator.setAllowedContentTypes(new String[] { "image/jpeg",
- "image/pjpeg" });
- }
private MultipartFileValidator validator;
@PostConstruct
public void init() {
validator = new MultipartFileValidator();
validator.setAllowedContentTypes(new String[] { "image/jpeg",
"image/pjpeg" });
}
至此,我们已经完成了文件上传的开发,可以看出这和普通的Controller开发没有任何区别,简单而且灵活。以下是该Controller的全部代码:
- @Controller
- @RequestMapping("/sec_upload.do")
- public class UploadController {
- private MultipartFileValidator validator;
- @PostConstruct
- public void init() {
- validator = new MultipartFileValidator();
- validator.setAllowedContentTypes(new String[] { "image/jpeg",
- "image/pjpeg" });
- }
- @RequestMapping(method = RequestMethod.GET)
- public String handleUploadShow() {
- return "uploadView";
- }
- @RequestMapping(method = RequestMethod.POST)
- public String handleUploadProcess(
- @RequestParam("imageFile") MultipartFile file, Model model)
- throws Exception {
- validator.validate(file);
- String path = "d:\\temp\\ftp\\" + file.getOriginalFilename();
- String resizePath = "d:\\temp\\ftp\\resize\\"
- + file.getOriginalFilename();
- FileHelper.save(path, file.getBytes());
- if (ImageHelper.isJpg(ImageHelper.getImageType(path)))
- ImageHelper.resizeJPG(path, resizePath, 120, 118);
- model.addAttribute("success", "true");
- return "uploadView";
- }
- }
@Controller
@RequestMapping("/sec_upload.do")
public class UploadController {
private MultipartFileValidator validator;
@PostConstruct
public void init() {
validator = new MultipartFileValidator();
validator.setAllowedContentTypes(new String[] { "image/jpeg",
"image/pjpeg" });
}
@RequestMapping(method = RequestMethod.GET)
public String handleUploadShow() {
return "uploadView";
}
@RequestMapping(method = RequestMethod.POST)
public String handleUploadProcess(
@RequestParam("imageFile") MultipartFile file, Model model)
throws Exception {
validator.validate(file);
String path = "d:\\temp\\ftp\\" + file.getOriginalFilename();
String resizePath = "d:\\temp\\ftp\\resize\\"
+ file.getOriginalFilename();
FileHelper.save(path, file.getBytes());
if (ImageHelper.isJpg(ImageHelper.getImageType(path)))
ImageHelper.resizeJPG(path, resizePath, 120, 118);
model.addAttribute("success", "true");
return "uploadView";
}
}
在以后的文章中,我会对Spring进行上传文件特殊处理做一些探究,比如用户上传一个csv的通讯录文件,那么通过Spring的属性编辑器一个custom的Editor来进行数据转换,可以将CSV中的信息转换成其他你所需要的信息:比如从CSV文件中抽取邮件地址放到一个字符串数组中,让你可以进行后续的业务逻辑操作。。。
而在本文中让我们来讨论一下文件上传的另外一种情况,例如一个支持导入你的本地通讯录列表的Web Mail系统。那么这个Mail System可能不仅仅希望你上传的通讯录文件作为一个副本保存在服务器端,他还希望更进一步的了解你上传通讯录文件的内容,以便能够将这些内容添加到你的Web Mail通讯录中。好的,我们了解了这样一个需求,就让我们来看看Spring MVC是如何做到的。
首先,让我们来看一段Spring 2.5 Reference中文版中的话(详情请参考http://www.redsaga.com/spring_ref/2.5/html/mvc.html#mvc-multipart-forms):
也就是说,我们可以通过一个特定的属性编辑器来讲得到的二进制数据转换成我们希望的数据类型,比如一个文本字符串或者提取其中的某些属性组成一个字符串列表,比如Email通讯录列表。
为了启用我们的PropertyEditor,那么我们就需要了解另外一个东东-WebDataBinder,通过它去调用PropertyEditor,我们可以将请求数据绑定成我们希望的数据对象。
初始化自定义WebDataBinder的方法有两种(详情请参考http://www.redsaga.com/spring_ref/2.5/html/mvc.html#mvc-ann-webdatabinder):
1.使用@InitBinder注解;
- @Controller
- ublic class MyFormController {
- @InitBinder
- public void initBinder(WebDataBinder binder) {
- SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
- dateFormat.setLenient(false);
- binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
- }
- // ...
@Controller
public class MyFormController {
@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
2.在配置文件中配置
- <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
- <property name="cacheSeconds" value="0" />
- <property name="webBindingInitializer">
- <bean class="org.springframework.samples.petclinic.web.ClinicBindingInitializer" />
- </property>
- </bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="cacheSeconds" value="0" />
<property name="webBindingInitializer">
<bean class="org.springframework.samples.petclinic.web.ClinicBindingInitializer" />
</property>
</bean>
让我们继续导入通讯录列表的例子,首先让我们准备一个CSV邮件通讯录列表文件contacts.csv:
- 名,姓,全名,昵称,邮箱地址
- Jacky,Lau,Jacky Lau,,jacky@gmail.com
- Grace,Fox,Grace Fox,,grace@gmail.com
- Tom,Hanks,Tom Hanks,,tom@gmail.com
- Charly,White,Charly White,,charly@gmail.com
名,姓,全名,昵称,邮箱地址
Jacky,Lau,Jacky Lau,,jacky@gmail.com
Grace,Fox,Grace Fox,,grace@gmail.com
Tom,Hanks,Tom Hanks,,tom@gmail.com
Charly,White,Charly White,,charly@gmail.com
我们还需要一个类来表示好友邮件信息-Email.class:
- public class Email {
- private String name;
- private String email;
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public String getEmail() {
- return email;
- }
- public void setEmail(String email) {
- this.email = email;
- }
- }
public class Email {
private String name;
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
接下来我们来准备一个自定义的Property Editor: CustomEmailDataEditor
- public class CustomEmailDataEditor extends PropertyEditorSupport {
- public void setValue(Object value) {
- if (value instanceof MultipartFile) {//处理上传文件,此处默认上传的是格式正确的CSV文件
- MultipartFile multipartFile = (MultipartFile) value;
- System.out.println(multipartFile.getContentType());//打印Content-Type
- try {
- //使用第三方开源类库OpenCSV来读取CSV文件
- CSVReader reader = new CSVReader(new InputStreamReader(
- multipartFile.getInputStream()));
- String[] nextLine;
- // 去除第一行header信息
- reader.readNext();
- List<Email> emails = new ArrayList<Email>();
- while ((nextLine = reader.readNext()) != null) {
- Email email = new Email();
- email.setName(nextLine[2]);
- email.setEmail(nextLine[4]);
- emails.add(email);
- }
- //绑定数据列表
- super.setValue(emails);
- } catch (IOException ex) {
- throw new IllegalArgumentException(
- "Cannot read contents of multipart file: "
- + ex.getMessage());
- }
- } else if (value instanceof byte[]) {
- super.setValue(value);
- } else {
- super.setValue(value != null ? value.toString().getBytes() : nu