笔记34 Spring MVC的高级技术——处理multipart形式的数据

一、需求介绍:

Spittr应用在两个地方需要文件上传。当新用户注册应用的时候,我 们希望他们能够上传一张图片,从而与他们的个人信息相关联。当用 户提交新的Spittle时,除了文本消息以外,他们可能还会上传一 张照片。

二、multipart介绍

一般表单提交所形成的请求结果是很简单的,就是以“&”符分割的多 个name-value对。但是当上传二进制数据时,如上传图片,就出现问题。与之不同的是,multipart格式的数据会将一个表单拆分为多个 部分(part),每个部分对应一个输入域。在一般的表单输入域中, 它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所 对应的部分可以是二进制,下面展现了multipart的请求体:

笔记34 Spring MVC的高级技术——处理multipart形式的数据

在这个multipart的请求中,我们可以看到profilePicture部分与其 他部分明显不同。除了其他内容以外,它还有自己的ContentType头,表明它是一个JPEG图片。尽管不一定那么明显,但profilePicture部分的请求体是二进制数据,而不是简单的文 本。

尽管multipart请求看起来很复杂,但在Spring MVC中处理它们却很容 易。在编写控制器方法处理文件上传之前,必须要配置一个 multipart解析器,通过它来告诉DispatcherServlet该如何读取 multipart请求。

三、 配置multipart解析器 

DispatcherServlet并没有实现任何解析multipart请求数据的功 能。它将该任务委托给了Spring中MultipartResolver策略接口的 实现,通过这个实现类来解析multipart请求中的内容。从Spring 3.1开 始,Spring内置了两个MultipartResolver的实现可以选择: 

    • CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求;
    • StandardServletMultipartResolver:依赖于Servlet 3.0 对multipart请求的支持(始于Spring 3.1)。

一般来讲,在这两者之 间,StandardServletMultipartResolver可能会是优选的方 案。它使用Servlet所提供的功能支持,并不需要依赖任何其他的项 目。

1.使用Servlet 3.0解析multipart请求 

兼容Servlet 3.0的StandardServletMultipartResolver没有构 造器参数,也没有要设置的属性。这样,在Spring应用上下文中,将 其声明为bean就会非常简单,如下所示:

1     @Bean
2     public MultipartResolver multipartResolver() throws IOException {
3         return new StandardServletMultipartResolver();
4     }

如果 我们想要限制用户上传文件的大小,该怎么实现?如果我们想要指定 文件在上传时,临时写入目录在什么位置的话,该如何实现?因为没有属性和构造器参数,StandardServletMultipartResolver 的功能看起来似乎有些受限。

其实并不是这样,我们是有办法配 置StandardServletMultipartResolver的限制条件的。只不 过不是在Spring中配置StandardServletMultipartResolver, 而是要在Servlet中指定multipart的配置。至少,我们必须要指定在文 件上传的过程中,所写入的临时文件路径。如果不设定这个最基本配 置的话,StandardServlet-MultipartResolver就无法正常工 作。具体来讲,我们必须要在web.xml或Servlet初始化类中,将 multipart的具体细节作为DispatcherServlet配置的一部分。 

因为我们一直使用的配置是DispatcherServlet的Servlet初始化类继承了 Abstract AnnotationConfigDispatcherServletInitializer ,所以就不会直接创建DispatcherServlet实例并将其注册到Servlet上下 文中。这样的话,将不会有对Dynamic Servlet registration的引用供我 们使用了。但是,我们可以通过重载customizeRegistration() 方法(它会得到一个Dynamic作为参数)来配置multipart的具体细 节:

1     @Override
2     protected void customizeRegistration(Dynamic registration) {
3         // TODO Auto-generated method stub
4         // super.customizeRegistration(registration);
5         registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
6     }

代码中使用的是只有一个参数的 MultipartConfigElement构造器,这个参数指定的是文件系统 中的一个绝对目录,上传文件将会临时写入该目录中。但是,还可以通过其他的构造器来限制上传文件的大小。除了临时路径的位 置,其他的构造器所能接受的参数如下:

    • 上传文件的最大容量(以字节为单位)。默认是没有限制的。
    • 整个multipart请求的最大容量(以字节为单位),不会关心有多 少个part以及每个part的大小。默认是没有限制的。
    • 在上传的过程中,如果文件大小达到了一个指定最大容量(以字 节为单位),将会写入到临时文件路径中。默认值为0,也就是 所有上传的文件都会写入到磁盘上。

例如,假设我们想限制文件的大小不超过2MB,整个请求不超过 4MB,而且所有的文件都要写到磁盘中。下面的代码使 用MultipartConfigElement设置了这些临界值:

1     @Override
2     protected void customizeRegistration(Dynamic registration) {
3         // TODO Auto-generated method stub
4         registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads", 2097152, 4194304, 0));
5     }

如果我们使用更为传统的web.xml来配 置MultipartConfigElement的话,那么可以使用<servlet>中 的<multipart-config>元素,如下所示:

 笔记34 Spring MVC的高级技术——处理multipart形式的数据

2.配置Jakarta Commons FileUpload multipart解析器 

通常来讲,StandardServletMultipartResolver会是最佳的 选择,但是如果我们需要将应用部署到非Servlet 3.0的容器中,那么 就得需要替代的方案。

Spring内置了 CommonsMultipartResolver,可以作 为StandardServletMultipartResolver的替代方案。 

将CommonsMultipartResolver声明为Spring bean的最简单方式如 下:

1 @Bean
2      public MultipartResolver multipartResolver() throws IOException {
3      return new CommonsMultipartResolver();
4     }

与StandardServletMultipartResolver有所不 同,CommonsMultipart-Resolver不会强制要求设置临时文件路 径。默认情况下,这个路径就是Servlet容器的临时目录。不过,通过 设置uploadTempDir属性,我们可以将其指定为一个不同的位置:

笔记34 Spring MVC的高级技术——处理multipart形式的数据

 

实际上,我们可以按照相同的方式指定其他的multipart上传细节,也 就是设置CommonsMultipartResolver的属性。例如,如下的配 置就等价于我们在前文通过MultipartConfigElement所配置的 StandardServletMultipartResolver:

笔记34 Spring MVC的高级技术——处理multipart形式的数据

在这里,我们将最大的文件容量设置为2MB,最大的内存大小设置为 0字节。这两个属性直接对应于MultipartConfigElement的第二 个和第四个构造器参数,表明不能上传超过2MB的文件,并且不管文 件的大小如何,所有的文件都会写到磁盘中。但是 与MultipartConfigElement有所不同,我们无法设定multipart请 求整体的最大容量。

四、处理multipart请求 

现在已经在Spring中(或Servlet容器中)配置好了对mutipart请求的处 理,那么接下来我们就可以编写控制器方法来接收上传的文件。要实 现这一点,最常见的方式就是在某个控制器方法参数上添 加@RequestPart注解。 

假设我们允许用户在注册Spittr应用的时候上传一张图片,那么我们 需要修改表单,以允许用户选择要上传的图片,同时还需要修 改SpitterController 中的processRegistration()方法来接 收上传的图片。

笔记34 Spring MVC的高级技术——处理multipart形式的数据

<form>标签现在将enctype属性设置为multipart/formdata,这会告诉浏览器以multipart数据的形式提交表单,而不是以表 单数据的形式进行提交。在multipart中,每个输入域都会对应一个 part。 

除了注册表单中已有的输入域,我们还添加了一个新的<input> 域,其type为file。这能够让用户选择要上传的图片文件。accept 属性用来将文件类型限制为JPEG、PNG以及GIF图片。根据其name 属性,图片数据将会发送到multipart请求中的profilePicture part 之中。

我们需要修改processRegistration()方法,使其能够接 受上传的图片。其中一种方式是添加byte数组参数,并为其添 加@RequestPart注解。如下示例:

笔记34 Spring MVC的高级技术——处理multipart形式的数据

当注册表单提交的时候,profilePicture属性将会给定一个byte 数组,这个数组中包含了请求中对应part的数据(通过 @RequestPart指定)。如果用户提交表单的时候没有选择文件, 那么这个数组会是空(而不是null)。获取到图片数据后,processRegistration()方法剩下的任务就是将文件保存到 某个位置。

1.接受MultipartFile (如何将byte数组转换为可存储的文件。)

使用上传文件的原始byte比较简单但是功能有限。因此,Spring还提 供了MultipartFile接口,它为处理multipart数据提供了内容更为 丰富的对象。如下的程序清单展现了MultipartFile接口的概况。

笔记34 Spring MVC的高级技术——处理multipart形式的数据

MultipartFile提供了获取上传文件byte的方式, 但是它所提供的功能并不仅限于此,还能获得原始的文件名、大小以 及内容类型。它还提供了一个InputStream,用来将文件数据以流 的方式进行读取。

MultipartFile还提供了一个便利的transferTo()方 法,它能够将上传的文件写入到文件系统中。

笔记34 Spring MVC的高级技术——处理multipart形式的数据

2.效果

笔记34 Spring MVC的高级技术——处理multipart形式的数据                      笔记34 Spring MVC的高级技术——处理multipart形式的数据                    

笔记34 Spring MVC的高级技术——处理multipart形式的数据                      笔记34 Spring MVC的高级技术——处理multipart形式的数据

3.具体代码实现

以增加留言为例子,每一条留言可以添加一张图片。

在WebContent目录下创建image文件夹用来存放上传的图片。

<1>修改SpittleControllers类中的addSpittle方法。 

 1     public String addSpittle(HttpServletRequest request, PubSpittle pubSpittle)
 2             throws IllegalStateException, IOException {
 3 
 4         MultipartFile profilePicture = pubSpittle.getSpittlePicture();
 5         String id = Long.toString(GenerateUnID.next());
 6         if (request.getParameter("spittlePicture") != null) {
 7             profilePicture.transferTo(new File(url, id + ".jpg"));
 8             pubSpittle.setSpittlePictureString(id);
 9         } else {
10             pubSpittle.setSpittlePictureString("spitter_logo_50");
11         }
12         spittleRepository.save(pubSpittle);
13         return "redirect:/spittles?username=" + username;
14     }

如果用户不上传图片的话,会默认显示应用logo,图片名字是spitter_logo_50 

<2>PubSpittle.java要做相应的修改 

 1 package myspittr.pubspittle;
 2 
 3 import org.springframework.web.multipart.MultipartFile;
 4 
 5 public class PubSpittle {
 6     private String message;
 7     private String title;
 8     private String username;
 9     private MultipartFile spittlePicture;
10     private String spittlePictureString;
11 
12     public String getMessage() {
13         return message;
14     }
15 
16     public void setMessage(String message) {
17         this.message = message;
18     }
19 
20     public String getTitle() {
21         return title;
22     }
23 
24     public void setTitle(String title) {
25         this.title = title;
26     }
27 
28     public String getUsername() {
29         return username;
30     }
31 
32     public void setUsername(String username) {
33         this.username = username;
34     }
35 
36     public MultipartFile getSpittlePicture() {
37         return spittlePicture;
38     }
39 
40     public void setSpittlePicture(MultipartFile spittlePicture) {
41         this.spittlePicture = spittlePicture;
42     }
43 
44     public String getSpittlePictureString() {
45         return spittlePictureString;
46     }
47 
48     public void setSpittlePictureString(String spittlePictureString) {
49         this.spittlePictureString = spittlePictureString;
50     }
51 }

<3>图片的名字也存储到数据库中所对应留言的条目中,所以Spittle.java也要做相应的修改。

 1 package myspittr.spittle;
 2 
 3 import org.apache.commons.lang3.builder.EqualsBuilder;
 4 import org.apache.commons.lang3.builder.HashCodeBuilder;
 5 
 6 public class Spittle {
 7     private Long id;
 8     private String message;
 9     private String title;
10     private String time;
11     private String username;
12     private String spittlePicture;
13 
14     public Spittle() {
15     }
16 
17     public Spittle(Long id, String message, String title, String time, String username, String spittlePicture) {
18         this.id = id;
19         this.message = message;
20         this.time = time;
21         this.title = title;
22         this.username = username;
23         this.spittlePicture = spittlePicture;
24     }
25 
26     public String getUsername() {
27         return username;
28     }
29 
30     public long getId() {
31         return id;
32     }
33 
34     public String getMessage() {
35         return message;
36     }
37 
38     public String getTime() {
39         return time;
40     }
41 
42     public String getTitle() {
43         return title;
44     }
45 
46     public String getSpittlePicture() {
47         return spittlePicture;
48     }
49 
50     @Override
51     public boolean equals(Object that) {
52         return EqualsBuilder.reflectionEquals(this, that, "id", "time");
53     }
54 
55     @Override
56     public int hashCode() {
57         return HashCodeBuilder.reflectionHashCode(this, "id", "time");
58     }
59 }

<4>显示上传的图片

在spittles.jsp页面合适的位置中添加如下代码:

1 <img src="<s:url value="/image/${spittle.spittlePicture}.jpg" /> " width="200" height="299">