在第5章中,我们通过扩 展AbstractAnnotationConfigDispatcherServletInitializer快速搭建了Spring MVC环境。在这个便利的基础类中,假设我们需要基本的DispatcherServlet和ContextLoaderListener环境,并且Spring配置是使用Java的,而不是XML。
AbstractAnnotationConfigDispatcherServletInitializer剖析:
在Servlet 3.0环境中,容器会在类路径中查找实现 javax.servlet.ServletContainerInitializer接口的类, 如果能发现的话,就会用它来配置Servlet容器。 Spring提供了这个接口的实现,名为SpringServletContainerInitializer,这个类反过来又会查找实现WebApplicationInitializer的类并将配置的任务交给它们来完成。Spring 3.2引入了一个便利的WebApplicationInitializer基础实现,也就 是AbstractAnnotationConfigDispatcherServletInitializer 。因为我们的Spittr-WebAppInitializer扩展了 AbstractAnnotationConfigDispatcherServletInitializer(同时也就实现了 WebApplicationInitializer),因此当部署到Servlet 3.0容器 中的时候,容器自动发现它,并用它来配置Servlet上下文。
7.1 Spring MVC配置的替代方案
7.1.1 自定义DispatcherServlet配置
在SpittrWebAppInitializer中我们 所编写的三个方法仅仅是必须要重载的abstract方法。但实际上还有更多的方法可以进行重载,从而实现额外的配置。
此类的方法之一就是customizeRegistration()。在AbstractAnnotationConfigDispatcherServletInitializer将DispatcherServlet注册到Servlet容器中之后,就会调用customizeRegistration(),并将Servlet注册后得到的Registration.Dynamic传递进来。通过重载customizeRegistration()方法,我们可以对DispatcherServlet进行额外的配置。
7.1.2 添加其他的Servlet和Filter
如果你想注册其他的Servlet、Filter或Listener的话,基于Java的初始化器(initializer)的一个好处就在于我们可以定义任意数量的初始化器类。因此,如果我们想往Web容器中注册其他组件的话,只需创建一个新的初始化器就可以了。最简单的方式就是实现Spring的WebApplicationInitializer接口。
程序清单7.1 通过实现WebApplicationInitializer来注册Servlet
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
import spittr.servlet.MyServlet;
public class MySevletInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);//注册servlet
myServlet.addMapping("/custom/**"); //添加映射Servlet
}
}
类似地,还可以创建新的WebApplicationInitializer实现来注册Listener和Filter。
程序清单7.2 注册Filter的WebApplicationInitializer
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.springframework.web.WebApplicationInitializer;
import spittr.servlet.MyFilter;
public class MyFilterInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
Dynamic filter = servletContext.addFilter("MyFilter", MyFilter.class); //注册
filter.addMappingForUrlPatterns(null, false, "/custom/*"); //添加Filter的映射路径
}
}
如果要将应用部署到支持Servlet 3.0的容器中,那么WebApplicationInitializer提供了一种通用的方式,实现在 Java中注册Servlet、Filter和Listener。不过,如果你只是注册Filter, 并且该Filter只会映射到DispatcherServlet上的话,那么在AbstractAnnotationConfigDispatcherServletInitializer 中还有一种快捷方式。
为了注册Filter并将其映射到DispatcherServlet,所需要做的仅仅是重载AbstractAnnotationConfigDispatcherServletInitializer的getServlet-Filters()方法,来注册Filter。
@Override
protected Filter[] getServletFilters() {
return new Filter[]{new MyFilter()};
}
这个方法返回的是一个javax.servlet.Filter的数组。在这里它只返回了一个Filter,但它实际上可以返回任意数量的Filter。在这里没有必要声明它的映射路径,getServletFilters()方法返回的所有Filter都会映射到DispatcherServlet上。
7.1.3 在web.xml中声明DispatcherServlet
在典型的Spring MVC应用中,我们会需要DispatcherServlet和ContextLoaderListener。AbstractAnnotationConfigDispatcherServletInitializer会自动注册它们,但是如果需要在web.xml中注册的话,那就需要我们自己来完成这项任务了。
程序清单7.3 在web.xml中搭建Spring MVC
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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" >
<!-- 设置root上下文配置文件位置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- 注册ContextLoaderListener -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!-- 注册DispatcherServlet -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!--将DispatcherServlet映射到"/"-->
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
ContextLoaderListener和 DispatcherServlet各自都会加载一个Spring应用上下文。上下文 参数contextConfigLocation指定了一个XML文件的地址,这个文件定义了根应用上下文,它会被ContextLoaderListener加载。如程序清单7.3所示,根上下文会从“/WEB-INF/spring/rootcontext.xml”中加载bean定义。
DispatcherServlet会根据Servlet的名字找到一个文件,并基于该文件加载应用上下文。在程序清单7.3中,Servlet的名字 是appServlet,因此DispatcherServlet会从“/WEBINF/appServlet-context.xml”文件中加载其应用上下文。 如果你希望指定DispatcherServlet配置文件的位置的话,那么可以在Servlet上指定一个contextConfigLocation初始化参数。 如下的配置中,DispatcherServlet会从“/WEBINF/spring/appServlet/servlet-context.xml”加载它的bean:
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name> <!-- 指定的路径上加载应用上下文-->
<param-value>
/WEB-INF/spring/appServlet/servlet-context.xml
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
在web.xml中使用基于Java的配置
要在Spring MVC中使用基于Java的配置,需要告诉DispatcherServlet和ContextLoaderListener使用AnnotationConfigWebApplicationContext,这是一个WebApplicationContext的实现类,它会加载Java配置类,而不是使用XML。要实现这种配置,可以设置contextClass上下文参数以及DispatcherServlet的初始化参数。在下面文件中,它所搭建的Spring MVC使用基于Java的Spring配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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">
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<!-- 设置root上下文配置文件位置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>spittr.config.RootConfig</param-value>
</context-param>
<!-- 注册ContextLoaderListener -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!-- 注册DispatcherServlet -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<init-param>
<param-name>contextConfigLocation</param-name> <!-- 指定应用上下文的加载路径 -->
<param-value>
spittr.config.WebConfig
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
7.2 处理multipart形式的数据
尽管multipart请求看起来很负责,但在Spring MVC中处理它们却很容易。在编写控制器方法处理文件上传之前,我们必须要配置一个multipart解析器,通过它来告诉DispatcherServlet该如何读取multipart请求。
7.2.1 配置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所提供的功能支持,并不需要依赖任何其他的项 目。如果我们需要将应用部署到Servlet 3.0之前的容器中,或者还没 有使用Spring 3.1或更高版本,那么可能就需要 CommonsMultipartResolver了。
使用Servlet 3.0解析multipart请求
兼容Servlet 3.0的StandardServletMultipartResolver没有构造器参数,也没有要设置的属性。这样,在Spring应用上下文中,将其声明为bean就会非常简单,如下所示:
public class WebConfig extends WebMvcConfigurerAdapter{
@Bean
public MultipartResolver multipartResolver()throws IOException{ return new StandardServletMultipartResolver();
}}
虽然没办法直接通过StandardServletMultipartResolver配置限制条件的。但在Servlet中能指定multipart的配置。具体来讲,我们必须要在web.xml或Servlet初始化类中,将multipart的具体细节作为DispatcherServlet配置的一部分。
方法一:(没用过)
如果采用Servlet初始化类的方式来配置DispatcherServlet的话,这个初始化类应该已经实现了WebApplicationInitializer,那我们可以在Servlet registration上调用setMultipartConfig()方法,传入一个MultipartConfig-Element实例。如下是最基本的DispatcherServlet multipart配置,它将临时路径设置为“/tmp/spittr/uploads”:
方法二:
如果配置DispatcherServlet的Servlet初始化类继承了Abstract AnnotationConfigDispatcherServletInitializer或AbstractDispatcherServletInitializer的话,那么我们不会直接创建DispatcherServlet实例并将其注册到Servlet上下文中。这样的话,将不会有对Dynamic Servlet registration的引用供我们使用了。但是,我们可以通过重载customizeRegistration()方法(它会得到一个Dynamic作为参数)来配置multipart的具体细节:
public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
}
}
到目前为止,我们所使用是只有一个参数的 MultipartConfigElement构造器,这个参数指定的是文件系统 中的一个绝对目录,上传文件将会临时写入该目录中。但是,我们还可以通过其他的构造器来限制上传文件的大小。除了临时路径的位 置,其他的构造器所能接受的参数如下:
- 上传文件的最大容量(以字节为单位)。默认是没有限制的。
- 整个multipart请求的最大容量(以字节为单位),不会关心有多 少个part以及每个part的大小。默认是没有限制的。
- 在上传的过程中,如果文件大小达到了一个指定最大容量(以字节为单位),将会写入到临时文件路径中。默认值为0,也就是所有上传的文件都会写入到磁盘上。
例如,假设我们想限制文件的大小不超过2MB,整个请求不超过 4MB,而且所有的文件都要写到磁盘中。下面的代码使 用MultipartConfigElement设置了这些临界值:
@Override
protected void customizeRegistration(Dynamic registration) {
// TODO Auto-generated method stub
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads", 2097152,4194304,0));
}
注意:在这里设置了临时存储的路径,但是在tomcat中并没有该文件目录,所以后面执行程序时会报错。解决办法就是:在tomcat所在目录\work\Catalina\localhost\spittr 目录下新建tmp\spittr\uploads。
或者在xml中配置:使用<servlet>中的<multipart-config>元素:
<multipart-config>的默认值与MultipartConfigElement相同。与MultipartConfigElement一样,必须要配置的是<location>。
7.2.2 处理multipart请求
接下来就可以编写控制器方法来接收上传的文件。要实现这一点,最常见的方式就是在某个控制器方法参数上添 加@RequestPart注解。
需要修改表单,以允许用户选择要上传的图片,同时还需要修 改SpitterController 中的processRegistration()方法来接 收上传的图片。
程序清单1 SpitterForm.java:
package spittr.form;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Email;
import org.springframework.web.multipart.MultipartFile;
import spittr.Spitter;
public class SpitterForm {
//非空,5到16个字符
@NotNull
@Size(min=5, max=16, message="{username.size}")
private String username;
@NotNull
@Size(min=5, max=25, message="{password.size}")
private String password;
@NotNull
@Size(min=2, max=30, message="{firstName.size}")
private String firstName;
@NotNull
@Size(min=2, max=30, message="{lastName.size}")
private String lastName;
@NotNull
@Email
private String email;
private MultipartFile profilePicture;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public MultipartFile getProfilePicture() {
return profilePicture;
}
public void setProfilePicture(MultipartFile profilePicture) {
this.profilePicture = profilePicture;
}
public Spitter toSpitter() {
return new Spitter(username, password, firstName, lastName, email);
}
}
程序清单2 registerForm.html:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spitter</title>
<link rel="stylesheet" type="text/css"
th:href="http://blog.163.com/yetong1985@126/blog/@{/resources/style.css}"></link>
</head>
<body>
<div id="header" th:include="page :: header"></div>
<div id="content">
<h1>Register</h1>
<form method="POST" th:object="${${spitterForm}}" enctype="multipart/form-data">
<div class="errors" th:if="${#fields.hasErrors('*')}">
<ul>
<li th:each="err : ${#fields.errors('*')}"
th:text="${err}">Input is incorrect</li>
</ul>
</div>
<label th:class="${#fields.hasErrors('firstName')}? 'error'">First Name</label>:
<input type="text" th:field="*{firstName}"
th:class="${#fields.hasErrors('firstName')}? 'error'" /><br/>
<label th:class="${#fields.hasErrors('lastName')}? 'error'">Last Name</label>:
<input type="text" th:field="*{lastName}"
th:class="${#fields.hasErrors('lastName')}? 'error'" /><br/>
<label th:class="${#fields.hasErrors('email')}? 'error'">Email</label>:
<input type="text" th:field="*{email}"
th:class="${#fields.hasErrors('email')}? 'error'" /><br/>
<label th:class="${#fields.hasErrors('username')}? 'error'">Username</label>:
<input type="text" th:field="*{username}"
th:class="${#fields.hasErrors('username')}? 'error'" /><br/>
<label th:class="${#fields.hasErrors('password')}? 'error'">Password</label>:
<input type="password" th:field="*{password}"
th:class="${#fields.hasErrors('password')}? 'error'" /><br/>
<label>Profile Picture</label>:
<input type="file"
name="profilePicture"
accept="image/jpeg,image/png,image/gif" /><br/>
<input type="submit" value="Register" />
</form>
</div>
<div id="footer" th:include="page :: copy"></div>
</body>
</html>
<form>标签现在将enctype属性设置为multipart/formdata,这会告诉浏览器以multipart数据的形式提交表单,而不是以表 单数据的形式进行提交。在multipart中,每个输入域都会对应一个 part。
除了注册表单中已有的输入域,我们还添加了一个新的<input> 域,其type为file。这能够让用户选择要上传的图片文件。accept 属性用来将文件类型限制为JPEG、PNG以及GIF图片。根据其name 属性,图片数据将会发送到multipart请求中的profilePicture part 之中。
现在,我们需要修改processRegistration()方法,使其能够接 受上传的图片。其中一种方式是添加byte数组参数,并为其添 加@RequestPart注解。如下为示例:
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(
@RequestPart("profilePicture") byte[] profilePicture,
@Valid Spitter spitter,
Errors errors) {
...
}
当注册表单提交的时候,profilePicture属性将会给定一个byte 数组,这个数组中包含了请求中对应part的数据(通过 @RequestPart指定)。如果用户提交表单的时候没有选择文件,那么这个数组会是空(而不是null)。获取到图片数据 后,processRegistration()方法剩下的任务就是将文件保存到 某个位置。
接受MultipartFile
使用上传文件的原始byte比较简单但是功能有限。因此,Spring还提供了MultipartFile接口,它为处理multipart数据提供了内容更为丰富的对象。
MultipartFile提供了获取上传文件byte的方式,还能获得原始的文件名、大小以及内容类型。它还提供了一个InputStream,用来将文件数据以流的方式进行读取。
MultipartFile还提供了一个便利的transferTo()方法,它能够帮助我们将上传的文件写入到文件系统中。
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(
@Valid SpitterForm spitterForm,
Errors errors) throws IllegalStateException, IOException {
if (errors.hasErrors()) {
return "registerForm";
}
Spitter spitter = spitterForm.toSpitter();
spitterRepository.save(spitter);
MultipartFile profilePicture = spitterForm.getProfilePicture();
profilePicture.transferTo(new File(spitter.getUsername() + ".jpg"));
return "redirect:/spitter/" + spitter.getUsername();
}
上述代码将图片保存到 本地tomcat位置\work\Catalina\localhost\spittr\tmp\spittr\uploads 下。
以Part的形式接受上传的文件
如果需要将应用部署到Servlet 3.0的容器中,Spring MVC也能接受 javax.servlet.http.Part作为控制器方法的参数。如果使用 Part来替换MultipartFile的话,那么processRegistration() 的方法签名将会变成如下的形式:
@RequestMapping(value="/register", method=RequestMethod.POST)
public String processRegistration(
@RequestPart("profilePicture") byte[] profilePicture,
@Valid Spitter spitter,
Errors errors) {
...
}
Part接口与MultipartFile并没有 太大的差别:
如果在编写控制器方法的时候,通过Part参数的形式 接受文件上传,那么就没必要配置MultipartResolver了。只有使用MultipartFile的时候,我们才需要 MultipartResolver。