Spring实战(七)-Spring MVC的高级技术
本文基于《Spring实战(第4版)》所写。
Spring MVC配置的替代方案
自定义DispatcherServlet配置
除了我们之前在SpitterWebAppInitializer中所编写的三个方法仅仅是必须要重载的abstract方法,AbstractAnnotationConfigDispatcherServletInitializer所完成的事情其实比看上去的要多。
此类的方法之一就是customizeRegistration()。
在AbstractAnnotationConfigDispatcherServletInitializer将DispatcherServlet注册到Servlet容器中之后,就会调用customizeRegistration(),并将Servlet注册后得到的Registration.Dynamic传递进来。通过重载customizeRegistration()方法,我们可以对DispatcherServlet进行额外的配置。
例如,可以在Spring MVC中处理请求和文件上传。如果计划使用Servlet 3.0对multipart配置的支持,那么需要使用DispatcherServlet的registration来启动multipart请求。我们可以重载customizeRegistration()方法来设置MultipartConfigElement,如下所示:
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// 存入文件路径;单个文件大小不超过2MB;整个请求不超过4MB;所有的文件都要写入磁盘中
registration.setMultipartConfig(new MultipartConfigElement("/Users/Shangnan/Documents/service_upload/springinaction/spittr"
,2097152,4194304, 0));
}
借助customizeRegistration()方法中的ServletRegistration.Dynamic,我们能够完成多项任务,包括通过调用setLoadOnStartup()设置load-on-startup优先级,通过setInitParameter()设置初始化参数,通过调用setMultipartConfig()配置Servlet 3.0对multipart的支持。
添加其他的Servlet和Filter
基于Java的初始化器(initializer)的一个好处就在于我们可以任意数量的初始化器类。因此,如果我们想往Web容器中注册其他组件的话,只需要创建一个新的初始化器就可以了。最简单的方式就是实现Spring的WebApplicationInitializer接口。
例如,如下的程序展示了如何创建WebApplicationInitializer实现并注册一个Servlet。
package com.myapp.config;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
import com.myapp.MyServlet;
public class MyServletInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class); // 注册 Servlet
myServlet.addMapping("/custom/**"); // 映射 Servlet
}
}
这个初始化器注册了一个Servlet并将其映射到一个路径上。
类似地,我们还可以创建新的WebApplicationInitializer实现来注册Listener和Filter。例如,如下程序展示了注册Filter的WebApplicationInitializer
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class); // 注册 Filter
filter.addMappingForUrlPatterns(null, false, "/custom/**"); // 添加Filter的映射路径
}
如果只是注册Filter,并且该Filter只会映射到DispatcherServlet上的话,那么在还有一种快捷方式,所需要做的仅仅是重载AbstractAnnotationConfigDispatcherServletInitializer的getServetFilters()方法。例如:
@Override
protected Filter[] getServletFilters(){
return new Filter[] { new MyFilter() };
}
这个方法返回的是一个javax.servlet.Filter的数组,说明可以返回任意数量的Filter。
在web.xml中声明DispatcherServlet
在典型的Spring MVC应用中,我们会需要DispatcherServlet和ContextLoaderListener。如下是一个基本的web.xml文件,它按照传统的方式搭建了DispatcherServlet和ContextLoaderListener。
<?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>
<!--注册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>
</servlet>
<!--将DispatcherServlet映射到“/”-->
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
ContextLoaderListener和DispatcherServlet各自都会加载一个Spring应用上下文。上下文参数contextConfigLocation指定了一个XML文件的地址,这个文件定义了根应用上下文,它会被ContextLoaderListener加载。本例中,根上下文会从“/WEB-INF/spring/root-context.xml”中加载bean定义。
DispatcherServlet会根据Servlet的名字找到一个文件,并基于该文件加载应用上下文。本例中,Servlet的名字是appServlet,因此DispatcherServlet会从“/WEB-INF/appServlet-context.xml”文件中加载其应用上下文。
也可以指定DispatcherServlet配置文件的位置,需要在Servlet上指定一个contextConfigLocation初始化参数。例如,如下的配置,DispatcherServlet会从“/WEB-INF/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>
要在Spring MVC中使用基于Java的配置,我们需要告诉DispatcherServlet和ContextLoaderListener使用AnnotationConfigWebApplicationContext,这是一个WebApplicationContext的实现类,它会加载Java配置类,而不是使用XML,要实现这种配置,我们可以设置contextClass上下文参数以及DispatcherServlet的初始化参数。如下的程序清单展现了一个新的web.xml,在这个文件中,它所搭建的Spring MVC使用基于Java的Spring配置:
<?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>contextClass</param-name>
<!--使用Java配置-->
<param-value>
org.springframework.web.context.support.
AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<!--指定根配置类-->
<param-value>com.habuma.spitter.config.RootConfig</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>
<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>
com.habuma.spittr.config.WebConfigConfig
</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>
处理multipart形式的数据
Spittr应用在两个地方需要文件上传。当新用户注册应用的时候,我们希望他们能够上传一张图片,从而与他们的个人信息相关联。当用户提交新的Spittle时,除了文本消息以外,他们可能还会上传一张照片。
对于传送二进制数据,如上传图片,与典型的基于文本的表单提交有所不同,multipart格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分是二进制,下面展示了multipart的请求体:
multipart的请求体在这个multipart的请求中,我们可以看到profilePicture部分与其他部分明显不同。除了其他内容以外,它还有自己的Content-type头,表明它是一个JPEG图片。尽管不一定那么明显,但profilePicture部分的请求体是二进制数据,而不是简单的文本。
在编写控制器方法之前,我们必须要配置一个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所提供的功能支持,并不需要依赖任何其他的项目。如果我们需要将应用部署到Servlet 3.0之前的容器中,或者还没有使用Spring 3.1或者更高版本,那就需要使用CommonsMultipartResolver了。
StandardServletMultipartResolver没有构造器参数,也没有要设置的属性,这样,在Spring应用上下文中,将其声明为bean就会非常简单,如下所示:
@Bean
public MultipartResolver multipartResolver() throws IOException {
return new StrandardServletMultipartResolver();
}
如果想配置StrandardServletMultipartResolver的限制条件,不在Spring中配置,而是要在Servlet中指定multipart的配置。至少,我们必须要指定在文件上传的过程中,所写入的临时文件路径。如果不设定这个最基本配置的话,就无法正常工作了。具体来讲,我们必须要在web.xml或Servlet初始化类中,将multipart的具体细节作为DispatcherServlet配置的一部分。
如果我们采用Servlet初始化类的方式来配置DispatcherServlet的话,这个初始化类应该已经实现了WebApplicationInitializer,那我们可以在Servlet registration上调用setMultipartConfig()方法,传入一个MultipartConfigElement实例。我们可以通过重载customizeRegistration()方法来配置multipart的具体细节。
MultipartConfigElement的构造器所能接受的参数如下:
- 上传文件的临时路径。
- 上传文件的最大容量(以字节为单位)。默认是没有限制的。
- 整个multipart请求的最大容量(以字节为单位),不会关心有多少个part以及每个part的大小。默认是没有限制的。
- 在上传的过程中,如果文件大小达到了一个指定最大容量(以字节单位),将会写入到临时文件路径中。默认值为0,也就是所上传的文件都会写入到磁盘上。
例如,假设我们想限制文件的大小不超过2MB,整个请求不超过4MB,而且所有的文件都要写到磁盘中。下面的代码使用MultipartConfigElement设置了这些临界值:
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads", 2097152, 4194394, 0));
}
如果我们使用更为传统的web.xml来配置MultipartConfigElement的话,那么可以使用<servlet>中<multipart-config>元素,如下所示:
<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>4194394</max-request-size>
</multipart-config>
</servlet>
除了StrandardServletMultipartResolver,我们还可以使用Spring内置的CommonsMultipartResolver。声明如下:
@Bean
public MultipartResolver multipartResolver() {
return new CommonsMultipartResulver();
}
CommonsMultipartResulver不会强制要求设置临时文件路径。默认情况下,这个路径就是Servlet容器的临时目录。不过,通过设置uploadTempDir属性,我们可以将其指定为一个不同的位置。实际上,我们还可以按照这种方式指定其他的multipart上传细节,也就是设置CommonsMultipartResolver的属性。例如,如下的配置就等价于我们通过MultipartConfigElement所配置的StrardardServletMultipartResolver:
@Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
multipartResolver.setUploadTempDir(new FileSystemResource("/tmp/spittr/uploads"));
multipartResolver.setMaxUploadSize(2097152);
multipartResolver.setMaxInMemorySize(0);
return multipartResolver;
}
在这里,我们将最大的文件容量设置为2MB,最大的内存大小设置为0字节,表明不能上传超过2MB的文件,并不管文件的大小如何,所有的文件都会写到磁盘中。但是与MultipartConfigElement有所不同,我们无法设定multipart请求整体的最大容量。
处理multipart请求
要实现控制器方法来接收上传的文件,最常见的方式就是在某个控制器方法参数上添加@RequestPart注解。
假设我们允许用户在注册Spittr应用的时候上传一张图片,那么我们需要修改表单,以允许用户选择要上传的图片,同时还需要修改SpitterController中的processRegistration() 方法来接收上传的图片。如下的代码片段来源于JSP注册表单视图:
<sf:form method="POST" commandName="spitter" enctype="multipart/form-data">
...
<label>Profile Picture</label>:
<input type="file"
name="profilePicture"
accept="image/jpeg,image/png,image/gif" /><br/>
...
</sf:form>
<form>标签现在将enctype属性设置为multipart/form-data,这会告诉浏览器以multipart数据的形式提交表单,而不是以表单数据的形式进行提交。在multipart中,每个输入域都会对应一个part。
除了注册表单中已有的输入域,我们还要添加了一个新的<input>域,其type为file。这能够让用户选择要上传的图片文件。accept属性用来将文件类型限制为JPEG、PNG以及GIF图片。根据其name属性,图片数据将会发送到multipart请求中的profilePicture part之中。
现在我们需要修改processRegistration()方法,使其能够接受上传的图片。其中一种方式是添加byte数组参数,并为其添加@RequestPart注解。如下为示例:
@RequestMappting(value="/register", method=POST)
public String processRegistration(
@RequestPart("profilePicture") byte[] profilePicture,
@Valid Spitter spitter,
Errors errors) {
...
}
当注册表单提交的时候,profilePicture属性将会给定一个byte数组,这个数组中包含了请求中对应part的数据(通过@RequestPart指定)。如果用户提交表单的时候没有选择文件,那么这个数组会是空(而不是null)。获取到图片数据后,processRegistration() 方法剩下的任务就是将文件保存到某个位置。
使用上传文件的原始byte比较简单但是功能有限。因此,Spring还提供了MultipartFile接口,它为处理multipart数据提供了内容更为丰富的对象。如下的程序清单展示了MultipartFile接口的概况。
package org.springframework.web.multipart;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.core.io.InputStreamSource;
public interface MultipartFile extends InputStreamSource {
String getName();
String getOriginalFilename();
String getContentType();
boolean isEmpty();
long getSize();
byte[] getBytes() throws IOException;
InputStream getInputStream() throws IOException;
void transferTo(File var1) throws IOException, IllegalStateException;
}
它提供了获取上传文件byte的方式,还能获得原始的文件名、大小以及内容类型。它还提供了一个InputStream,用来将文件数据以流的方式进行读取。
除此之外,MultipartFile还提供了一个便利的transferTo()方法,它能够帮助我们将上传的文件写入到文件系统中。作为样例,我们可以在processRegistration() 方法中添加如下的几行代码,从而将上传的图片文件写入到文件系统中:
@RequestMapping(value="/register", method=POST)
public String processRegistration(
@Valid SpitterForm spitterForm, // 校验 Spitter输入
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("/tmp/spittr/" + profilePicture.getOriginalFilename()));
return "redirect:/spitter/" + spitter.getUsername();
}
其中用到的SpitterForm类,如下所示:
package spittr.web;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.Email;
import org.springframework.web.multipart.MultipartFile;
import spittr.model.Spitter;
public class SpitterForm {
@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);
}
}
如果需要将应用部署到Servlet 3.0的容器中,那么会有MultipartFile的一个替代方案。Spring MVC也能接受javax.servlet.http.Part作为控制器方法的参数。如果使用Part来替换MultipartFile的话,那么processRegistration()的方法签名将会变成如下的形式:
@RequestMappting(value="/register", method=POST)
public String processRegistration(
@RequestPart("profilePicture") Part profilePicture,
@Valid Spitter spitter,
Errors errors) {
...
}
那么将上传的文件写入文件系统中的代码为:
profilePicture.write(new File("/tmp/spittr/" + profilePicture.getSubmittedFileName()));
值得一提的是,如果在编写控制器方法的时候,通过Part参数的形式接受文件上传,那么就没有必要配置MultipartResolver了。只有使用MultipartFile的时候,我们才需要MultipartResolver。
处理异常
不管发生什么事情,不管是好的还是坏的,Servlet请求的输出都是一个Servlet响应。如果在请求处理的时候,出现了异常,那它的输出依然会是Servlet响应。异常必须要以某种方式转换为响应。
Spring提供了多种方式将异常转换为响应:
- 特定的Spring异常将会自动映射为指定的HTTP状态码;
- 异常上可以添加@ResponseStatus注解,从而将其映射为某一个HTTP状态码;
- 在方法上可以添加@ExceptionHandler注解,使其用来处理异常。
将异常映射为HTTP状态码
在默认情况下,Spring会将自身的一些异常自动转换为合适的状态码。下表列出了这些映射关系。
Spring异常 | HTTP状态码 |
---|---|
BindException | 400 - Bad Request |
ConversionNotSupportedException | 500 - Internal Server Error |
HttpMediaTypeNotAcceptableException | 406 - Not Acceptable |
HttpMediaTypeNotSupportedException | 415 - Unsupported Media Type |
HttpMessageNotWritableException | 500 - Internal Server Error |
HttpRequestMethodNotSupportedException | 405 - Method Not Allowed |
MethodArgumentNotValidException | 400 - Bad Request |
MissingServletRequestParameterException | 400 - Bad Request |
MissingServletRequestPartException | 400 - Bad Request |
NoSuchRequestHandlingMethodException | 404 - Not Found |
TypeMismatchException | 400 - Bad Request |
以上的异常一般会由Spring自身抛出,作为DispatcherServlet处理过程中或执行校验时出现问题的结果。例如,如果DispatcherServlet无法找到适合处理请求的控制器方法,那么将会抛出NoSuchRequestHandlingMethodException异常,最终的结果就是产生404状态码的响应(Not Found)。
对于应用所抛出的异常,这些内置的映射就无能为力了。幸好,Spring提供了一种机制,能够通过@RequestStatus注解将异常映射为HTTP状态码。
为了阐述这项功能,请参考SpittleController中如下的请求处理方法,它可能会产生HTTP 404状态(但目前还没有实现):
@RequestMapping(value="/{spittleId}" , method=RequestMethod.GET)
public String spittle(
@PathVariable("spittleId") long spittleId,
Model model) {
Spittle spittle = spittleRepository.findOne(spittleId);
if (spittle == null) {
throw new SpittleNotFoundException();
}
model.addAttribute(spittle);
return "spittle";
}
现在SpittleNotFoundException就是一个简单的非检查型异常,如下所示:
package spittr.web;
public class SpittleNotFoundException extends RuntimeException {
}
SpittleNotFoundException默认会产生500状态码(Internal Server Error)的响应。实际上,如果出现任何没有映射的异常,响应都会带有500状态码,但是,我们可以通过映射SpittleNotFoundException对这种默认行为进行变更。
当抛出SpittleNotFoundException异常时,这是一种请求资源没有找到的场景。我们要使用@ResponseStatus注解将SpittleNotFoundException映射为HTTP状态码404。
package spittr.web;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_FOUND,
reason = "Spittle Not Found")
public class SpittleNotFoundException extends RuntimeException {
}
编写异常处理的方法
作为样例,假设用户试图创建的Spittle与已创建的Spittle文本完全相同,那么SpittleRepository的save()方法将会抛出DuplicateSpittleException异常。这意味着SpittleController的saveSpittle()方法可能需要处理这个异常。如下面的程序所示,saveSpittle()方法可以只关注成功保存Spittle的情况。
@RequestMapping(method=RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) throws Exception {
spittleRepository.save(new Spittle(null, form.getMessage(), new Date(),
form.getLongitude(), form.getLatitude()));
return "redirect:/spittles";
}
然后,我们为SpittleController添加一个新的方法,它会处理抛出DuplicateSpittleException的情况:
@ExceptionHandler(DuplicateSpittleException.class)
public String handleDuplicateSpittle(){
return "error/duplicate";
}
对于@ExceptionHandler注解标注的方法来说,比较有意思的一点在于它能处理同一个控制器中所有处理器方法所抛出的异常。所有,我们不用在每一个可能抛出DuplicateSpittleException的方法中添加异常处理代码,这一个方法就涵盖了所有的功能。
为控制器添加通知
如果控制器类的特定切面能够运用到整个应用程序的所有控制器中,那么这将会便利很多。举例说明,如果要在多个控制器中处理异常,那@ExceptionHandler注解所标注的方法是很有用的。不过,如果多个控制器类中都会抛出某个特定的异常,那么你可能会发现要在所有的控制器方法中重复相同的@ExceptionHandler方法。或者,为了避免重复,我们会创建一个基础的控制器类,所有控制器类要扩展这个类,从而继承通用的@ExceptionHandler方法。
Spring 3.2为这类问题引入了一个新的解决方案:控制器通知。
控制器通知(controller advice)是任意带有@ControllerAdvice注解的类,这个类会包含一个或多个如下类型的方法:
- @ExceptionHandler注解标注的方法;
- @InitBinder注解标注的方法;
- @ModelAttribute注解标注的方法;
在带有@ControllerAdvice注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有@RequestMapping注解的方法上。
@ControllerAdvice注解本身已经使用@Component,因此@ControllerAdvice注解所标注的类将会自动被组件扫描获取到,就像带有@Component注解的类一样。
@ControllerAdvice最为实用的一个场景就是将所有的@ExceptionHandler方法收集到一个类中,这样所有控制器的异常就能在一个地方进行一致的处理。例如,我们想将DuplicateSpittleException的处理方法用到整个应用程序的所有控制器上。
package spittr.web;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class AppWideExceptionHandler {
@ExceptionHandler(DuplicateSpittleException.class)
public String duplicateSpittleHandler(){
return "error/duplicate";
}
}
现在,如果任意的控制器方法抛出了DuplicateSpittleException,不管这个方法位于哪个控制器中,都会调用这个duplicateSpittleHandler()方法来处理异常。
跨重定向请求传递数据
在处理完POST请求后,通常来讲一个最佳实践就是执行一下重定向。除了其他的一些因素外,这样做能够防止用户点击浏览器的刷新按钮或后退箭头时,客户端重新执行危险的POST请求。
“redirect:”前缀能够让重定向功能变得非常简单。Spring为重定向功能还提供了一些其他的辅助功能。
一般来讲,当一个处理器方法完成之后,该方法所指定的模型数据将会复制到请求中,并作为请求中的属性,请求会转发(forward)到视图上进行渲染。因为控制器方法和视图所处理的是同一个请求,所以在转发的过程中,请求属性能够得以保存。
但是,当控制器的结果是重定向的话,原始的请求就结束了,并且会发起一个新的GET请求。原始请求中所带有的模型数据也就随着请求一起消亡了。在新的请求属性中,没有任何的模型数据,这个请求必须要自己计算数据。
显然,对于重定向来说,模型并不能用来传递数据。但是我们也有一些其他的方案,能够从发起重定向的方法传递数据给处理重定向方法中:
- 使用URL模板以路径变量和/或查询参数的形式传递数据;
- 通过flash属性发送数据。
通过URL模板进行重定向
通过路径变量和查询参数传递数据看起来非常简单。例如
return "redirect:/spitter/" + spitter.getUsername();
这能够正常运行,但是还远远不能说没有问题。当构建URL或SQL查询语句的时候,使用String连接是很危险的。
除了连接String的方式来构建重定向URL,Spring还提供了使用模板的方式来定义重定向URL。例如
@RequestMapping(value="/register", method=POST)
public String processRegistration(
Spitter spitter, Model model) {
spitterRepository.save(spitter);
model.addAttribute("username", spitter.getUsername());
return "redirect:/spitter/{username}";
}
现在,username作为占位符填充到了URL模板中,而不是直接连接到重定向String中,所以username中所有的不安全字符都会进行转义。这样会更加安全,这里允许用户输入任何想要的内容作为username,并会将其附加在路径上。
除此之外,模型中所有其他的原始类型值都可以添加到URL中作为查询参数。作为样例,假设除了username以外,模型中还要包含新创建Spitter对象的id属性,那processRegistration()方法可以改为如下写法:
@RequestMapping(value="/register", method=POST)
public String processRegistration(
Spitter spitter, Model model) {
spitterRepository.save(spitter);
model.addAttribute("username", spitter.getUsername());
model.addAttribute("spitterId", spitter.getId());
return "redirect:/spitter/{username}";
}
所返回的重定向String并没有太大的变化。但是,因为模型中的spitterId属性没有匹配重定向URL中的任何占位符,所以它会自动以查询参数的形式附加到重定向URL上。
如果username属性的值是habuma并且spitterId属性的值是42,那么结果得到的重定向URL路径将会是“/spitter/habuma?spitterId=42”。
使用flash属性
如果在重定向的时候,需要实际发送对象。例如前面的例子中,我们需要重定向的时候传递Spitter对象。Spitter对象要比String和int更为复杂。因此,我们不能想路径变量或查询参数那么容易地发送Spitter对象。
正如我们前面讨论的那样,模型数据最终是以请求参数的形式复制到请求中的,当重定向发生的时候,这些数据就会丢失。因此,我们需要将Spitter对象放到一个位置,使其能够在重定向的过程中存活下来。
有个方案是将Spitter放到会话中,Spring也认为将跨重定向存活的数据放到会话中是一个很不错的方式。但是,Spring认为我们并不需要管理这些数据,相反,Spring提供了将数据发送为flash属性(flash attribute)的功能。
按照定义,flash属性会一直携带这些数据直到下一次请求,然后才会消失。
Spring提供了通过RedirectAttributes设置flash属性的方法,这是Spring 3.1引入的Model的一个子接口。RedirectAttributes提供了Model的所有功能,除此之外,还有几个方法是用来设置flash属性的。
具体来讲,RedirectAttributes提供了一组addFlashAttribute()方法来添加flash属性。重现看一下processRegistration()方法,我们可以使用addFlashAttribute()将Spitter对象添加到模型中:
@RequestMapping(value="/register", method=POST)
public String processRegistration(
Spitter spitter, RedirectAttributes model) {
spitterRepository.save(spitter);
model.addAttribute("username", spitter.getUsername());
model.addFlashAttribute("spitter",spitter);
return "redirect:/spitter/{username}";
}
在这里,我们调用了addFlashAttribute() 方法,并将spitter作为key,Spitter对象作为值。另外,我们还可以不设置key参数,让key根据值的类型自行推断得出:
model.addFlashAttribute(spitter);
因为我们传递了一个Spitter对象给addFlashAttribute()方法,所以推断得到的key将会是spitter。
在重定向执行之前,所有的flash属性都会复制到会话中。在重定向后,存在会话中flash属性会被取出,并从会话转移到模型之中。处理重定向的方法就能从模型中访问Spitter对象了,就像获取其他的模型对象一样。下图阐述了它是如何运行的
flash属性保存在会话中,然后再放到模型中,因此能够在重定向的过程中存活为了完成flash属性的流程,如下展现了更新版本的showSpitterProfile()方法,在从数据库中查找之前,它会首先从模型中检查Spitter对象:
@RequestMapping(value="/{username}", method=GET)
public String showSpitterProfile(@PathVariable String username, Model model) {
if (!model.containsAttribute("spitter")){
Spitter spitter = spitterRepository.findByUsername(username);
model.addAttribute(spitter);
}
return "profile";
}
可以看到showSpitterProfile() 方法所做的第一件事就是检查是否存有key为spitter的model属性。如果模型中包含spitter属性,那就什么都不用做了。这里面包含的Spitter对象将会传到视图中进行渲染。但是如果模型中不包含spitter属性的话,那么showSpitterProfile()将会从Repository中查找Spitter,并将其存放到模型中。