7.1 Spring MVC 配置的替代方案
7.1.1 自定义DispatcherServlet配置
在SpittrWebAppInitializer中我们所编写的三个方法仅仅是必须要重载的abstract方法。但实际上还有更多的方法可以进行重载,从而实现额外的配置。
此类的方法之一就是customizeRegistration()
。在AbstractAnnotationConfigDispatcherServletInitializer将DispatcherServlet注册到Servlet容器中之后,就会调用customizeRegistration()
,并将Servlet注册后得到的Registration.Dynamic传递进来。通过重载customizeRegistration()
方法,我们可以对DispatcherServlet进行额外的配置。
我们将在Spring MVC中处理multipart
请求和文件上传。如果计划使用Servlet 3.0对multipart
配置的支持,那么需要使用DispatcherServlet的registration
来启用multipart
请求。我们可以重载customizeRegistration()
方法来设置MultipartConfigElement,如下所示:
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
}
借助customizeRegistration()
方法中的ServletRegistration.Dynamic,我们能够完成多项任务,包括通过调用setLoadOnStartup()
设置load-on-startup
优先级,通过setInitParameter()
设置初始化参数,通过调用setMultipartConfig()
配置Servlet 3.0对multipart
的支持。在前面的样例中,我们设置了对multipart
的支持,将上传文件的临时存储目录设置在/tmp/spittr/uploads
中。
7.1.2 添加其他的Servlet和Filter
按照AbstractAnnotationConfigDispatcherServletInitializer的定义,它会创建DispatcherServlet和ContextLoaderListener。基于Java的初始化器(initializer)的一个好处就在于我们可以定义任意数量的初始化器类。因此,如果我们想往Web容器中注册其他组件的话,只需创建一个新的初始化器就可以了。最简单的方式就是实现Spring的WebApplicationInitializer接口。
例如,如下的程序清单展现了如何创建WebApplicationInitializer实现并注册一个Servlet。
package spittr.config;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
public class MyServletInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
Dynamic myServlet= servletContext.addServlet("myServlet", MyServlet.class);
myServlet.addMapping("/custom/**");
}
}
类似的,我们还可以创建新的WebApplicationInitializer实现来注册Listener和Filter。
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class);
filter.addMappingForUrlPatterns(null, false, "/custom/*");
}
如果要将应用部署到支持Servlet 3.0的容器中,那么WebApplicationInitializer提供了一种通用的方式,实现在Java中注册Servlet、Filter和Listener。不过,如果你只是注册Filter,并且该Filter只会映射到DispatcherServlet上的话,那么在AbstractAnnotationConfigDispatcherServletInitializer中还有一种快捷方式。
为了注册Filter并将其映射到DispatcherServlet,所需要做的仅仅是重载AbstractAnnotationConfigDispatcherServletInitializer的getServletFilters()
方法。例如,在如下的代码中,重载了AbstractAnnotationConfigDispatcherServletInitializer的getServletFilters()
方法以注册Filter:
@Override
protected Filter[] getServletFilters() {
return new Filter[] { new MyFilter() };
}
7.1.3 在web.xml中声明DispatcherServlet
<?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-content.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>
</servlet>
<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>contextConfigLocation</param-name>
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>spittr.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>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
解析器,通过它来告诉DispatcherServlet该如何读取
multipart
请求。
7.2.1 配置multipart解析器
从Spring 3.1开始,Spring内置了两个MultipartResolver的实现供我们选择:
- CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求
- StandardServletMultipartResolver:依赖于Servlet 3.0对multipart请求的支持(始于Spring 3.1)
使用Servlet 3.0解析multipart请求
兼容Servlet 3.0的StandardServletMultipartResolver没有构造器参数,也没有要设置的属性。这样,在Spring应用上下文中,将其声明为bean就会非常简单,如下所示:
@Bean
public MultipartResolver multipartResolver() throws IOException {
return new StandardServletMultipartResolver();
}
既然这个@Bean方法如此简单,你可能就会怀疑我们到底该如何限制StandardServletMultipartResolver的工作方式呢。如果我们想要限制用户上传文件的大小,该怎么实现?如果我们想要指定文件在上传时,临时写入目录在什么位置的话,该如何实现?因为没有属性和构造器参数,StandardServletMultipartResolver的功能看起来似乎有些受限。
其实并不是这样,我们是有办法配置StandardServletMultipartResolver的限制条件的。只不过不是在Spring中配置StandardServletMultipartResolver,而是要在Servlet中指定multipart的配置。至少,我们必须要指定在文件上传的过程中,所写入的临时文件路径。如果不设定这个最基本配置的话,StandardServletMultipartResolver就无法正常工作。具体来讲,我们必须要在web.xml或Servlet初始化类中,将multipart的具体细节作为DispatcherServlet配置的一部分。
如果我们采用Servlet初始化类的方式来配置DispatcherServlet的话,这个初始化类应该已经实现了WebApplicationInitializer,那我们可以在Servlet registration上调用setMultipartConfig()
方法,传入一个MultipartConfigElement实例。如下是最基本的DispatcherServlet multipart配置,它将临时路径设置为/tmp/spittr/uploads
:
DispatcherServlet ds = new DispatcherServlet();
Dynamic registration = context.addServlet("appServlet", ds);
registration.addMapping("/");
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));
如果我们配置DispatcherServlet的Servlet初始化类继承了AbstractAnnotationConfigDispatcherServletInitializer或AbstractDispatcherServletInitializer的话,那么我们不会直接创建DispatcherServlet实例并将其注册到Servlet上下文中。这样的话,将不会有对Dynamic Servlet registration的引用供我们使用了。但是,我们可以通过重载customizeRegistration()
方法(它会得到一个Dynamic作为参数)来配置multipart的具体细节:
@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) {
registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads", 2097152, 4194304, 0));
}
web.xml配置如下:
<multipart-config>
<location>/tmp/spittr/uploads</location>
<max-file-size>2097152</max-file-size>
<max-request-size>4194304</max-request-size>
</multipart-config>
配置Jakarta Commons FileUpload multipart解析器
Spring内置了CommonsMultipartResolver,可以作为StandardServletMultipartResolver的替代方案。
将CommonsMultipartResolver声明为Spring bean的最简单方式如下:
@Bean
public MultipartResolver multipartResolver() throws IOException {
return new CommonsMultipartResolver();
}
与StandardServletMultipartResolver有所不同,CommonsMultipart-Resolver不会强制要求设置临时文件路径。默认情况下,这个路径就是Servlet容器的临时目录。不过,通过设置uploadTempDir属性,我们可以将其指定为一个不同的位置:
@Bean
public MultipartResolver multipartResolver() throws IOException {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
multipartResolver.setUploadTempDir(new FileSystemResource("/tmp/spittr/uploads"));
return multipartResolver;
}
我们还可以设置其他上传的细节。
7.2.2 处理multipart请求
现在已经在Spring中(或Servlet容器中)配置好了对mutipart请求的处理,那么接下来我们就可以编写控制器方法来接收上传的文件。要实现这一点,最常见的方式就是在某个控制器方法参数上添加@RequestPart
注解。
假设我们允许用户在注册Spittr应用的时候上传一张图片,那么我们需要修改表单,以允许用户选择要上传的图片,同时还需要修改SpitterController 中的processRegistration()
方法来接收上传的图片。如下的代码片段来源于Thymeleaf注册表单视图(registrationForm.html),着重强调了表单所需的修改:
<form method="POST" th:object="${spitter}"
enctype="multipart/form-data">
<div class="error" th:if="${#fields.hasErrors('*')}">
<ul>
<li th:each="err : ${#fields.errors('*')}" th:text="${ err }">Input
is incorrect</li>
</ul>
</div>
<label>Profile Picture</label>: <input type="file"
name="profilePicture" accept="image/jpeg,image/png,image/gif" /><br />
<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('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 /> <input
type="submit" value="Register" />
</form>
对应的控制器改为:
@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture") byte[] profilePicture, @Valid Spitter spitter, Errors errors) {
if (errors.hasErrors()) {
return "registerForm";
}
spitterRepository.save(spitter);
return "redirect:/spitter/" + spitter.getUsername();
}
当注册表单提交的时候,profilePicture
属性将会给定一个byte
数组,这个数组中包含了请求中对应part
的数据(通过@RequestPart
指定)。如果用户提交表单的时候没有选择文件,那么这个数组会是空(而不是null)。获取到图片数据后,processRegistration()
方法剩下的任务就是将文件保存到某个位置。
接受MultipartFile
使用上传文件的原始byte比较简单但是功能有限。因此,Spring还提供了MultipartFile接口,它为处理multipart数据提供了内容更为丰富的对象。如下的程序清单展现了MultipartFile接口的概况
public interface MultipartFile {
String getName();
String getOriginalFilename();
String getContentType();
boolean isEmpty();
long getSize();
byte[] getBytes() throws IOException;
InputStream getInputStream() throws IOException;
void transferTo(File dest) throws IOException;
}
我们可以看到,MultipartFile提供了获取上传文件byte的方式,但是它所提供的功能并不仅限于此,还能获得原始的文件名、大小以及内容类型。它还提供了一个InputStream,用来将文件数据以流的方式进行读取。除此之外,MultipartFile还提供了一个便利的transferTo()
方法,它能够帮助我们将上传的文件写入到文件系统中。作为样例,我们可以在processRegistration()
方法中添加如下的几行代码,从而将上传的图片文件写入到文件系统中:
profilePicture.transferTo(new File("/data/spittr" + profilePicture.getOriginalFilename()));
将文件保存到Amazon S3中
将文件保存到本地文件系统中是非常简单的,但是这需要我们对这些文件进行管理。我们需要确保有足够的空间,确保当出现硬件故障时,文件进行了备份,还需要在集群的多个服务器之间处理这些图片文件的同步。
另外一种方案就是让别人来负责处理这些事情。多加几行代码,我们就能将图片保存到云端。例如,如下的程序清单所展现的saveImage()
方法能够将上传的文件保存到Amazon S3中,我们在processRegistration()
中可以调用该方法:
saveImage()
方法所做的第一件事就是构建Amazon Web Service(AWS)凭证。为了完成这一点,你需要有一个S2 Access Key和S3 Secret Access Key。当注册S3服务的时候,Amazon会将其提供给你。它们会通过值注入的方式提供给Spitter-Controller。
AWS凭证准备好后,saveImage()方法创建了一个JetS3t的RestS3Service实例,可以通过它来操作S3文件系统。它获取spitterImages bucket的引用并创建用来包含图片的S3Object对象,接下来将图片数据填充到S3Object。
在调用putObject()
方法将图片数据写到S3之前,saveImage()
方法设置了S3Object的权限,从而允许所有的用户查看它。这是很重要的——如果没有它的话,这些图片对我们应用程序的用户就是不可见的。最后,如果出现任何问题的话,将会抛出ImageUploadException异常。
以Part的形式接受上传的文件
如果你需要将应用部署到Servlet 3.0的容器中,那么会有MultipartFile的一个替代方案。Spring MVC也能接受javax.servlet.http.Part作为控制器方法的参数。如果使用Part来替换MultipartFile的话,那么processRegistration()
的方法签名将会变成如下的形式:
@RequestMapping(value = "/register", method = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture") Part profilePicture, @Valid Spitter spitter, Errors errors) {
...
}
如果在编写控制器方法的时候,通过Part参数的形式接受文件上传,那么就没有必要配置MultipartResolver了。只有使用MultipartFile的时候,我们才需要MultipartResolver。
7.3 处理异常
Spring提供了多种方式将异常转换为响应:
- 特定的Spring异常将会自动映射为指定的HTTP状态码
- 异常上可以添加
@ResponseStatus
注解,从而将其映射为某一个HTTP状态码 - 在方法上可以添加
@ExceptionHandler
注解,使其用来处理异常
7.3.1 将异常映射为HTTP状态码
表7.1 Spring的一些异常会默认映射为HTTP状态码
Spring异常 | HTTP状态码 |
---|---|
BindException | 400 - Bad Request |
ConversionNotSupportedException | 500 - Internal Server Error |
HttpMediaTypeNotAcceptableException | 406 - Not Acceptable |
HttpMediaTypeNotSupportedException | 415 - Unsupported Media Type |
HttpMessageNotReadableException | 400 - Bad Request |
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提供了一种机制,能够通过@ResponseStatus
注解将异常映射为HTTP状态码。
为了阐述这项功能,请参考SpittleController中如下的请求处理方法,它可能会产生HTTP 404状态(但目前还没有实现):
@RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
public String spittle(@PathVariable("spittleId") long spittleId, Model model) {
model.addAttribute(spittleRepository.findOne(spittleId));
return "spittle";
}
在这里,会从SpittleRepository中,通过ID检索Spittle对象。如果findOne()
方法能够返回Spittle对象的话,那么会将Spittle放到模型中,然后名为spittle
的视图会负责将其渲染到响应之中。但是如果findOne()
方法返回null
的话,那么将会抛出SpittleNotFoundException异常。现在SpittleNotFoundException就是一个简单的非检查型异常,如下所示:
package spittr.web;
public class SpittleNotFoundException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = -3719986060631817252L;
}
如果调用spittle()
方法来处理请求,并且给定ID获取到的结果为空,那么SpittleNotFoundException(默认)将会产生500状态码(Internal Server Error)的响应。实际上,如果出现任何没有映射的异常,响应都会带有500状态码,但是,我们可以通过映射SpittleNotFoundException对这种默认行为进行变更。
当抛出SpittleNotFoundException异常时,这是一种请求资源没有找到的场景。如果资源没有找到的话,HTTP状态码404是最为精确的响应状态码。所以,我们要使用@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 {
/**
*
*/
private static final long serialVersionUID = -3719986060631817252L;
}
7.3.2 编写异常处理的方法
假设用户试图创建的Spittle与已创建的Spittle文本完全相同,那么SpittleRepository的save()
方法将会抛出DuplicateSpittle Exception异常。这意味着SpittleController的saveSpittle()
方法可能需要处理这个异常。如下面的程序清单所示,saveSpittle()
方法可以直接处理这个异常。
@RequestMapping(method=RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
try {
spittleRepository
.save(new Spittle(null, form.getMessage(), new Date(), form.getLongitude(), form.getLatitude()));
return "redirect:/spittles";
} catch (Exception e) {
return "error/duplicate";
}
}
不过这样会显得过于复杂,所以我们采用下面的方式:
@RequestMapping(method = RequestMethod.POST)
public String saveSpittle(SpittleForm form, Model model) {
spittleRepository
.save(new Spittle(null, form.getMessage(), new Date(), form.getLongitude(), form.getLatitude()));
return "redirect:/spittles";
}
@ExceptionHandler(Exception.class)
public String handelException() {
return "error/duplicate";
}
handleDuplicateSpittle()
方法上添加了@ExceptionHandler
注解,当抛出Exception异常的时候,将会委托该方法来处理。它返回的是一个String,这与处理请求的方法是一致的,指定了要渲染的逻辑视图名,它能够告诉用户他们正在试图创建
一条重复的条目。
对于@ExceptionHandler
注解标注的方法来说,比较有意思的一点在于它能处理同一个控制器中所有处理器方法所抛出的异常。所以,尽
管我们从saveSpittle()
中抽取代码创建了handleDuplicateSpittle()
方法,但是它能够处理SpittleController中所有方法所抛出的Exception异常。我们不用在每一个可能抛出Exception的方法中添加异常处理代码,这一个方法就涵盖了所有的功能。
既然@ExceptionHandler
注解所标注的方法能够处理同一个控制器类中所有处理器方法的异常,那么你可能会问有没有一种方法能够处理所有控制器中处理器方法所抛出的异常呢。从Spring 3.2开始,这肯定是能够实现的,我们只需将其定义到控制器通知类中即可。
7.4 为控制器添加通知
如果控制器类的特定切面能够运用到整个应用程序的所有控制器中,那么这将会便利很多。举例来说,如果要在多个控制器中处理异常,那@ExceptionHandler
注解所标注的方法是很有用的。不过,如果多个控制器类中都会抛出某个特定的异常,那么你可能会发现要在所有的控制器方法中重复相同的@ExceptionHandler
方法。或者,为了避免重复,我们会创建一个基础的控制器类,所有控制器类要扩展这个类,从而继承通用的@ExceptionHandler
方法。
Spring 3.2为这类问题引入了一个新的解决方案:控制器通知。控制器通知(controller advice)是任意带有@ControllerAdvice
注解的类,这个类会包含一个或多个如下类型的方法:
- @ExceptionHandler注解标注的方法
- @InitBinder注解标注的方法
- @ModelAttribute注解标注的方法
在带有@ControllerAdvice
注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有@RequestMapping
注解的方法上。
@ControllerAdvice
注解本身已经使用了@Component
,因此@ControllerAdvice
注解所标注的类将会自动被组件扫描获取到,就像带有@Component
注解的类一样。
@ControllerAdvice
最为实用的一个场景就是将所有的@ExceptionHandler
方法收集到一个类中,这样所有控制器的异常就能在一个地方进行一致的处理。例如,我们想将DuplicateSpittleException的处理方法用到整个应用程序的所有控制器上。如下的程序清单展现的AppWideExceptionHandler就能完成这一任务,这是一个带有@ControllerAdvice
注解的类。
package spittr.web;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class AppWideExceptionHandler {
@ExceptionHandler(SpittleNotFoundException.class)
public String spittleNotFoundExceptionHandler() {
return "error/spittle_not_found";
}
}
7.5 跨重定向请求传递数据
在处理完POST请求后,通常来讲一个最佳实践就是执行一下重定向。除了其他的一些因素外,这样做能够防止用户点击浏览器的刷新按钮或后退箭头时,客户端重新执行危险的POST请求。
redirect:
前缀能够让重定向功能变得非常简单。你可能会想Spring很难再让重定向功能变得更简单了。但是,请稍等:Spring为重定向功能还提供了一些其他的辅助功能。
具体来讲,正在发起重定向功能的方法该如何发送数据给重定向的目标方法呢?一般来讲,当一个处理器方法完成之后,该方法所指定的模型数据将会复制到请求中,并作为请求中的属性,请求会转发forward
到视图上进行渲染。因为控制器方法和视图所处理的是同一个请求,所以在转发的过程中,请求属性能够得以保存。
但是,当控制器的结果是重定向的话,原始的请求就结束了,并且会发起一个新的GET请求。原始请求中所带有的模型数据也就随着请求一起消亡了。在新的请求属性中,没有任何的模型数据,这个请求必须要自己计算数据。
显然,对于重定向来说,模型并不能用来传递数据。但是我们也有一些其他方案,能够从发起重定向的方法传递数据给处理重定向方法中:
- 使用URL模板以路径变量和/或查询参数的形式传递数据
- 通过flash属性发送数据
7.5.1 通过URL模版进行重定向
通过路径变量和查询参数传递数据看起来非常简单。我们以路径变量的形式传递了新创建Spitter的username
。但是按照现在的写法,username
的值是直接连接到重定向String上的。这能够正常运行,但是还远远不能说没有问题。当构建URL或SQL查询语句的时候,使用String连接是很危险的:
return "redirect:/spittr/{username}";
除了连接String的方式来构建重定向URL,Spring还提供了使用模版的方式来定义重定向URL。
model.addAttribute("username", spitter.getUsername());
return "redirect:/spitter/{username}";
现在,username
作为占位符填充到了URL模板中,而不是直接连接到重定向String中,所以username
中所有的不安全字符都会进行转义。这样会更加安全,这里允许用户输入任何想要的内容作为username
,并会将其附加到路径上。
除此之外,模型中所有其他的原始类型值都可以添加到URL中作为查询参数。作为样例,假设除了username
以外,模型中还要包含新创建Spitter对象的id属性,那processRegistration()
方法可以改写为如下的形式:
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
。
7.5.2 使用flash属性
Spitter对象要比String和int更为复杂。因此,我们不能像路径变量或查询参数那么容易地发送Spitter对象。它只能设置为模型中的属性。
但是,正如我们前面所讨论的那样,模型数据最终是以请求参数的形式复制到请求中的,当重定向发生的时候,这些数据就会丢失。因此,我们需要将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 = RequestMethod.POST)
public String processRegistration(@RequestPart("profilePicture") MultipartFile profilePicture,
@Valid Spitter spitter, Errors errors, RedirectAttributes model) {
if (errors.hasErrors()) {
return "registerForm";
}
spitterRepository.save(spitter);
model.addAttribute("username", spitter.getUsername());
model.addFlashAttribute("spitter", spitter);
return "redirect:/spitter/{username}";
}
在重定向执行之前,所有的flash属性都会复制到会话中。在重定向后,存在会话中的flash属性会被取出,并从会话转移到模型之中。处理重定向的方法就能从模型中访问Spitter对象了,就像获取其他的模型对象一样。
为了完成flash属性的流程,如下展现了更新版本的showSpitterProfile()
方法,在从数据库中查找之前,它会首先从模型中检查Spitter对象:
@RequestMapping(value = "/{username}", method = RequestMethod.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,并将其存放到模型中。