Spring学习(七)--Spring MVC的高级技术
一.Spring MVC配置的替代方案
我们已经了解如何通过AbstractAnnotationConfigDispatcherServlet- Initializer快速搭建了Spring MVC环境。在这个便利的基础类中,假设我们需要基本的DispatcherServlet和 ContextLoaderListener环境,并且Spring配置是使用Java的,而不是XML。尽管对很多Spring应用来说,这是一种安全的假设,但是并不一定总 能满足我们的要求。除了DispatcherServlet以外,我们可能还 需要额外的Servlet和Filter;我们可能还需要对 DispatcherServlet本身做一些额外的配置;或者,如果我们需 要将应用部署到Servlet 3.0之前的容器中,那么还需要 将DispatcherServlet配置到传统的web.xml中。
--自定义DispatcherServlet
1 package com.sky.config;
2
3 import org.springframework.web.filter.CharacterEncodingFilter;
4 import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
5
6 import javax.servlet.ServletContext;
7 import javax.servlet.ServletException;
8
9 /**
10 * @author : S K Y
11 * @version :0.0.1
12 */
13 public class SpringWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
14 @Override
15 protected Class<?>[] getRootConfigClasses() {
16 return new Class[]{
17 RootConfig.class
18 };
19 }
20
21 @Override
22 protected Class<?>[] getServletConfigClasses() {
23 return new Class[]{ //指定配置类
24 WebConfig.class
25 };
26 }
27 @Override
28 public void onStartup(ServletContext servletContext) throws ServletException {
29 super.onStartup(servletContext);
30 servletContext.addFilter("characterEncodingFilter"
31 , new CharacterEncodingFilter("UTF-8", true)) //设置编码
32 .addMappingForUrlPatterns(null, false, "/*"); //设置拦截的请求
33 }
34
35 @Override
36 protected String[] getServletMappings() {
37 return new String[]{"/"}; //将DispatcherServlet映射到"/"
38 }
39 }
--AbstractAnnotationConfigDispatcherServletInitializer所完成的事情其实比看上去要多。在SpittrWebAppInitializer中我们 所编写的三个方法仅仅是必须要重载的abstract方法,还有对于onStartup()方法的重写(主要是为了解决中文乱码的问题,定义字符集).但实际上还 有更多的方法可以进行重载,从而实现额外的配置。
--此类的方法之一就是customizeRegistration()。 在AbstractAnnotation- ConfigDispatcherServletInitializer 将DispatcherServlet注册到Servlet容器中之后,就会调 用customizeRegistration(),并将Servlet注册后得到的 Registration.Dynamic传递进来。通过重 载customizeRegistration()方法,我们可以对 DispatcherServlet进行额外的配置。我们将会看到如何在Spring MVC中处理multipart请求和文件上传。如果计划使用Servlet 3.0对 multipart配置的支持,那么需要使用DispatcherServlet的 registration来启用multipart请求。我们可以重 载customizeRegistration()方法来设 置MultipartConfigElement:
1 @Override
2 protected void customizeRegistration(ServletRegistration.Dynamic registration) {
3 registration.setMultipartConfig( //启用multipart请求
4 new MultipartConfigElement("/resources/uploads") //设置文件上传的存储目录
5 );
6 }
--借助customizeRegistration()方法中的 ServletRegistration.Dynamic,我们能够完成多项任务,包 括通过调用setLoadOnStartup()设置load-on-startup优先级,通过 setInitParameter()设置初始化参数,通过调 用setMultipartConfig()配置Servlet 3.0对multipart的支持。在前 面的样例中,我们设置了对multipart的支持,将上传文件的临时存储 目录设置在“resources/uploads”中。
--现在我们希望新用户在注册时候能够上传图片资源作为我们的个人头像,我们知道一般表单提交所形成的请求结果是很简单的,就是以“&”符分割的多 个name-value对。例如,当在Spittr应用中提交注册表单时,请求会如下所示:username=zhangssan&password=123548&email=1556152@qq.com;尽管这种编码形式很简单,并且对于典型的基于文本的表单提交也足 够满足要求,但是对于传送二进制数据,如上传图片,就显得力不从 心了。与之不同的是,multipart格式的数据会将一个表单拆分为多个 部分(part),每个部分对应一个输入域。在一般的表单输入域中, 它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所 对应的部分可以是二进制,下面展现了multipart的请求体:
--在这个multipart的请求中,我们可以看到profilePicture部分与其 他部分明显不同。除了其他内容以外,它还有自己的Content- Type头,表明它是一个JPEG图片。尽管不一定那么明显,但profilePicture部分的请求体是二进制数据,而不是简单的文 本。尽管multipart请求看起来很复杂,但在Spring MVC中处理它们却很容 易。在编写控制器方法处理文件上传之前,我们必须要配置一个 multipart解析器,通过它来告诉DispatcherServlet该如何读取 multipart请求。
--配置multipart解析器
DispatcherServlet并没有实现任何解析multipart请求数据的功 能。它将该任务委托给了Spring中MultipartResolver策略接口的 实现,通过这个实现类来解析multipart请求中的内容。从Spring 3.1开 始,Spring内置了两个MultipartResolver的实现供我们选择:
1.CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求;
2.StandardServletMultipartResolver:依赖于Servlet 3.0 对multipart请求的支持(始于Spring 3.1)。
一般来讲,在这两者之 间,StandardServletMultipartResolver可能会是优选的方 案。它使用Servlet所提供的功能支持,并不需要依赖任何其他的项 目。如果我们需要将应用部署到Servlet 3.0之前的容器中,或者还没 有使用Spring 3.1或更高版本,那么可能就需要 CommonsMultipartResolver了。
--StandardServletMultipartResolver没有构 造器参数,也没有要设置的属性。这样,在Spring应用上下文中,将 其声明为bean就会非常简单,如下所示:
1 @Bean
2 public MultipartResolver multipartResolver() throws IOException {
3 return new StandardServletMultipartResolver();
4 }
--既然这个@Bean方法如此简单,你可能就会怀疑我们到底该如何限 制StandardServletMultipartResolver的工作方式呢。如果 我们想要限制用户上传文件的大小,该怎么实现?如果我们想要指定 文件在上传时,临时写入目录在什么位置的话,该如何实现?因为没有属性和构造器参数,StandardServletMultipartResolver 的功能看起来似乎有些受限。其实并不是这样,我们是有办法配 置StandardServletMultipartResolver的限制条件的。只不 过不是在Spring中配置StandardServletMultipartResolver, 而是要在Servlet中指定multipart的配置。至少,我们必须要指定在文 件上传的过程中,所写入的临时文件路径。如果不设定这个最基本配 置的话,StandardServlet-MultipartResolver就无法正常工 作。具体来讲,我们必须要在web.xml或Servlet初始化类中,将 multipart的具体细节作为DispatcherServlet配置的一部分。
--如果我们配置DispatcherServlet的Servlet初始化类继承了 Abstract AnnotationConfigDispatcherServletInitializer 或AbstractDispatcher-ServletInitializer的话,那么我 们不会直接创建DispatcherServlet实例并将其注册到Servlet上下 文中。这样的话,将不会有对Dynamic Servlet registration的引用供我 们使用了。但是,我们可以通过重载customizeRegistration() 方法(它会得到一个Dynamic作为参数)来配置multipart的具体细 节:
1 @Override
2 protected void customizeRegistration(ServletRegistration.Dynamic registration) {
3 registration.setMultipartConfig( //启用multipart请求
4 new MultipartConfigElement("/resources/uploads") //设置文件上传的存储目录
5 );
6 }
--到目前为止,我们所使用是只有一个参数的 MultipartConfigElement构造器,这个参数指定的是文件系统 中的一个绝对目录,上传文件将会临时写入该目录中。但是,我们还可以通过其他的构造器来限制上传文件的大小。除了临时路径的位 置,其他的构造器所能接受的参数如下:
1.上传文件的最大容量(以字节为单位)。默认是没有限制的。
2.整个multipart请求的最大容量(以字节为单位),不会关心有多 少个part以及每个part的大小。默认是没有限制的。
3.在上传的过程中,如果文件大小达到了一个指定最大容量(以字 节为单位),将会写入到临时文件路径中。默认值为0,也就是 所有上传的文件都会写入到磁盘上。
--例如,假设我们想限制文件的大小不超过2MB,整个请求不超过 4MB,而且所有的文件都要写到磁盘中。下面的代码使 用MultipartConfigElement设置了这些临界值(需要质注意的是我们在配置缓存路径的时候,需要保证当前的这个文件路径是存在的):
1 @Override
2 protected void customizeRegistration(ServletRegistration.Dynamic registration) {
3 registration.setMultipartConfig( //启用multipart请求
4 new MultipartConfigElement("d;//temp/uploads", 2097152
5 , 4194304, 0) //设置文件上传的存储目录
6 );
7 }
--如果我们使用更为传统的web.xml来配 置MultipartConfigElement的话,那么可以使用<servlet>中 的<multipart-config>元素,如下所示:
--<multipart-config>的默认值与MultipartConfigElement相 同。与MultipartConfigElement一样,必须要配置的 是<location>。
1 <!DOCTYPE html>
2 <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:th="http://www.thymeleaf.org">
4 <head>
5 <meta charset="UTF-8">
6 <title>title</title>
7 <!--到样式表的th:href链接-->
8 <!-- <link rel="stylesheet" type="text/css"
9 th:href="@{/resources/style.css}"/>-->
10 </head>
11 <style type="text/css">
12 .error {
13 color: red;
14 }
15
16 .error_text {
17 border: 1px solid red;
18 }
19
20 .title {
21 color: green;
22 width: 20px;
23 height: 10px;
24 border: 1px solid black;
25 }
26 </style>
27 <body>
28 <form method="post" th:object="${user}" enctype="multipart/form-data">
29 <label class="title" th:class="${#fields.hasErrors('username')} ? 'error'">用户名:</label>
30 <input type="text" th:class="${#fields.hasErrors('username')}?'error_text'"
31 th:field="*{username}"/>
32 <br/>
33 <label class="title" th:class="${#fields.hasErrors('password')} ? 'error'">密码:</label>
34 <input type="password" th:class="${#fields.hasErrors('password')}?'error_text'"
35 th:field="*{password}"/>
36 <br/>
37 <label class="title" th:class="${#fields.hasErrors('email')} ? 'error'">邮箱:</label>
38 <input type="email" th:class="${#fields.hasErrors('email')}?'error_text'"
39 th:field="*{email}"/>
40 <br/>
41 <input type="file" name="picture" accept="image/jpeg,image/png,image/gif"/>
42 <div th:if="${#fields.hasErrors('*')}">
43 <ul>
44 <li th:each="error:${#fields.errors('*')}"
45 th:text="${error}"
46 class="error"
47 ></li>
48 </ul>
49 </div>
50 <button>提交</button>
51 </form>
52 </body>
53 </html>
--修改我们的的页面代码,需要注意两件事情:
1.<form>标签现在将enctype属性设置为multipart/form- data,这会告诉浏览器以multipart数据的形式提交表单,而不是以表 单数据的形式进行提交。在multipart中,每个输入域都会对应一个 part。
2.除了注册表单中已有的输入域,我们还添加了一个新的<input> 域,其type为file。这能够让用户选择要上传的图片文件。accept 属性用来将文件类型限制为JPEG、PNG以及GIF图片。根据其name 属性,图片数据将会发送到multipart请求中的picture part 之中。
--现在,我们需要修改registerPost()方法,使其能够接 受上传的图片。其中一种方式是添加byte数组参数,并为其添 加@RequestPart注解。如下为示例:
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerPost(@RequestPart("picture") byte[] picture, @Validated User user, Errors errors) throws UnsupportedEncodingException {
3 if (errors.hasErrors()) {
4 return "register";
5 } else {
6 String username = user.getUsername();
7 String encode = URLEncoder.encode(username, "UTF-8");
8 return "redirect:/home?username=" + encode;
9 }
10 }
--当注册表单提交的时候,picture属性将会给定一个byte 数组,这个数组中包含了请求中对应part的数据(通过 @RequestPart指定)。如果用户提交表单的时候没有选择文件, 那么这个数组会是空(而不是null)。获取到图片数据后,registerPost()方法剩下的任务就是将文件保存到 某个位置。
--我们将会稍后讨论如何保存文件。但首先,想一下,对于提交的图片 数据我们都了解哪些信息呢。或者,更为重要的是,我们还不知道些 什么呢?尽管我们已经得到了byte数组形式的图片数据,并且根据 它能够得到图片的大小,但是对于其他内容我们就一无所知了。我们 不知道文件的类型是什么,甚至不知道原始的文件名是什么。你需要 判断如何将byte数组转换为可存储的文件。
--使用上传文件的原始byte比较简单但是功能有限。因此,Spring还提 供了MultipartFile接口,它为处理multipart数据提供了内容更为 丰富的对象。如下的程序清单展现了MultipartFile接口的概况。
1 package org.springframework.web.multipart;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.io.InputStream;
6
7 import org.springframework.core.io.InputStreamSource;
8
9 public interface MultipartFile extends InputStreamSource {
10
11 String getName();
12
13 String getOriginalFilename();
14
15 String getContentType();
16
17 boolean isEmpty();
18
19 long getSize();
20
21 byte[] getBytes() throws IOException;
22
23 @Override
24 InputStream getInputStream() throws IOException;
25
26 void transferTo(File dest) throws IOException, IllegalStateException;
27
28 }
--我们可以看到,MultipartFile提供了获取上传文件byte的方式, 但是它所提供的功能并不仅限于此,还能获得原始的文件名、大小以 及内容类型。它还提供了一个InputStream,用来将文件数据以流 的方式进行读取。除此之外,MultipartFile还提供了一个便利的transferTo()方 法,它能够帮助我们将上传的文件写入到文件系统中。作为样例,我 们可以在registerPost()方法中添加如下的几行代码:
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerPost(@RequestPart("picture") MultipartFile pictureFile, @Validated User user, Errors errors) throws IOException {
3 if (errors.hasErrors()) {
4 return "register";
5 } else {
6 pictureFile.transferTo(new File(pictureFile.getOriginalFilename()));
7 String username = user.getUsername();
8 String encode = URLEncoder.encode(username, "UTF-8");
9 return "redirect:/thyme/home?username=" + encode;
10 }
11 }
--在之前我已经指定了文件上传的本地缓存地址为d://temp/uploads,因此我们可以很顺利的在我们的文件夹中找到上传的图片。此时我们利用了pictureFile.getOriginalFilename()方法获取到了文件上传时原始的文件名称,同样我们也可以使用UUID来用随机的名称进行存储
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerPost(@RequestPart("picture") MultipartFile pictureFile, @Validated User user, Errors errors) throws IOException {
3 if (errors.hasErrors()) {
4 return "register";
5 } else {
6 String filename = pictureFile.getOriginalFilename(); //获取到原始的名称
7 String suffix = filename.substring(filename.lastIndexOf(".")); //获取文件的后缀名
8 pictureFile.transferTo(new File(UUID.randomUUID().toString() + suffix)); //产生UUID随机文件名
9 String username = user.getUsername();
10 String encode = URLEncoder.encode(username, "UTF-8");
11 return "redirect:/thyme/home?username=" + encode;
12 }
13 }
--可以发现将文件保存到本地文件系统中是非常简单的,但是这需要我们对这些 文件进行管理。我们需要确保有足够的空间,确保当出现硬件故障时,文件进行了备份,还需要在集群的多个服务器之间处理这些图片文件的同步。我们可以将图片进一步的保存的云服务器中进行存储,在这里直接给出原代码中所提供的保存到Amazon S3的示例代码:
--saveImage()方法所做的第一件事就是构建Amazon Web Service(AWS)凭证。为了完成这一点,你需要有一个S3 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的话,那么registerPost() 的方法将会变成如下的形式:
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerPost(@RequestPart("picture") Part pictureFile, @Validated User user, Errors errors) throws IOException {
3 if (errors.hasErrors()) {
4 return "register";
5 } else {
6 String fileName = pictureFile.getSubmittedFileName();//获取到原始的名称
7 String suffix = fileName.substring(fileName.lastIndexOf(".")); //获取文件的后缀名
8 pictureFile.write(UUID.randomUUID().toString() + suffix); //产生UUID随机文件名
9 String username = user.getUsername();
10 String encode = URLEncoder.encode(username, "UTF-8");
11 return "redirect:/thyme/home?username=" + encode;
12 }
13 }
--就主体来言,Part接口与MultipartFile并没有 太大的差别。在如下的程序清单中,我们可以看到Part接口的有一些 方法其实是与MultipartFile相对应的。getSubmittedFileName()对应于getOriginalFilename()。 类似地,write()对应于transferTo(),借助该方法我们能够将 上传的文件写入文件系统中;值得一提的是,如果在编写控制器方法的时候,通过Part参数的形式 接受文件上传,那么就没有必要配置MultipartResolver了。只 有使用MultipartFile的时候,我们才需要 MultipartResolver。
--添加其他的Servlet和Filter
按照AbstractAnnotationConfigDispatcherServletInitializer 的定义,它会创建DispatcherServlet和 ContextLoaderListener。但是,如果你想注册其他的Servlet、 Filter或Listener的话,那该怎么办呢?基于Java的初始化器(initializer)的一个好处就在于我们可以定义任 意数量的初始化器类。因此,如果我们想往Web容器中注册其他组件 的话,只需创建一个新的初始化器就可以了。最简单的方式就是实现 Spring的WebApplicationInitializer接口。
--通过实现WebApplicationInitializer来注册Servlet
1 public class MyServletInitializer implements WebApplicationInitializer {
2 @Override
3 public void onStartup(ServletContext servletContext) throws ServletException {
4 ServletRegistration.Dynamic myServlet =
5 servletContext.addServlet("myServlet", MyServlet.class);
6 myServlet.addMapping("/custom/**");
7 }
8 }
--MyServletInitializer注册了一个 Servlet并将其映射到一个路径上。我们也可以通过这种方式来手动注 册DispatcherServlet。(但这并没有必要,因 为AbstractAnnotationConfigDispatcherServletInitializer 没用太多代码就将这项任务完成得很漂亮。) 类似地,我们还可以创建新的WebApplicationInitializer实现 来注册Listener和Filter。例如,如下的程序清单展现了如何注册 Filter。
--注册Filter的WebApplicationInitializer
1 @Override
2 public void onStartup(ServletContext servletContext) throws ServletException {
3 /* ServletRegistration.Dynamic myServlet =
4 servletContext.addServlet("myServlet", MyServlet.class);
5 myServlet.addMapping("/custom/**");*/
6 //注册Filter
7 FilterRegistration.Dynamic myFilter = servletContext.addFilter("myFilter", MyFilter.class);
8 myFilter.addMappingForUrlPatterns(null, false, "/custom/*");
9
10 }
--如果要将应用部署到支持Servlet 3.0的容器中,那 么WebApplicationInitializer提供了一种通用的方式,实现在 Java中注册Servlet、Filter和Listener。不过,如果你只是注册Filter, 并且该Filter只会映射到DispatcherServlet上的话,那么 在AbstractAnnotationConfigDispatcherServletInitializer 中还有一种快捷方式。为了注册Filter并将其映射到DispatcherServlet,所需要做的仅 仅是重 载AbstractAnnotationConfigDispatcherServletInitializer的getServlet-Filters()方法。例如,在如下的代码中,重载了 AbstractAnnotationConfigDispatcherServletInitializer的getServletFilters() 方法以注册Filter:
1 @Override
2 protected Filter[] getServletFilters() {
3 return new Filter[]{new MyFilter()};
4 }
--我们可以看到,这个方法返回的是一个javax.servlet.Filter的 数组。在这里它只返回了一个Filter,但它实际上可以返回任意数量 的Filter。在这里没有必要声明它的映射路 径,getServletFilters()方法返回的所有Filter都会映射 到DispatcherServlet上。 如果要将应用部署到Servlet 3.0容器中,那么Spring提供了多种方式来 注册Servlet(包括DispatcherServlet)、Filter和Listener,而不 必创建web.xml文件。但是,如果你不想采取以上所述方案的话,也 是可以的。假设你需要将应用部署到不支持Servlet 3.0的容器中(或 者你只是希望使用web.xml文件),那么我们完全可以按照传统的方 式,通过web.xml配置Spring MVC。让我们看一下该怎么做。
--在web.xml中声明DispatcherServlet
在典型的Spring MVC应用中,我们会需要DispatcherServlet和 ContextLoader Listener。AbstractAnnotationConfigDispatcherServletInitializer 会自动注册它们,但是如果需要在web.xml中注册的话,那就需要我 们自己来完成这项任务了。 如下是一个基本的web.xml文件,它按照传统的方式搭建了 DispatcherServlet和ContextLoaderListener:
--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:
--当然,上面阐述的都是如何让DispatcherServlet和 ContextLoaderListener从XML中加载各自的应用上下文。但 是,我们都更倾向于使用Java配置而不是 XML配置。因此,我们需要让Spring MVC在启动的时候,从带 有@Configuration注解的类上加载配置。 要在Spring MVC中使用基于Java的配置,我们需要告诉 DispatcherServlet和ContextLoaderListener使 用AnnotationConfigWebApplicationContext,这是一 个WebApplicationContext的实现类,它会加载Java配置类,而 不是使用XML。要实现这种配置,我们可以设置contextClass上下文参 数以及DispatcherServlet的初始化参数。如下的程序清单展现 了一个新的web.xml,在这个文件中,它所搭建的Spring MVC使用基 于Java的Spring配置:
二.异常处理
到现在为止,在Spittr应用中,我们假设所有的功能都正常运行。但 是如果某个地方出错的话,该怎么办呢?当处理请求的时候,抛出异 常该怎么处理呢?如果发生了这样的情况,该给客户端什么响应呢? 不管发生什么事情,不管是好的还是坏的,Servlet请求的输出都是一 个Servlet响应。如果在请求处理的时候,出现了异常,那它的输出依 然会是Servlet响应。异常必须要以某种方式转换为响应。Spring提供了多种方式将异常转换为响应:
1.特定的Spring异常将会自动映射为指定的HTTP状态码;
2.异常上可以添加@ResponseStatus注解,从而将其映射为某一 个HTTP状态码;
3.在方法上可以添加@ExceptionHandler注解,使其用来处理 异常。
--处理异常的最简单方式就是将其映射到HTTP状态码上,进而放到响 应之中。接下来,我们看一下如何将异常映射为某一个HTTP状态码。 在默认情况下,Spring会将自身的一些异常自动转换为合适的状态码。列出这些映射关系:
--的异常一般会由Spring自身抛出,作为DispatcherServlet 处理过程中或执行校验时出现问题的结果。例如,如果 DispatcherServlet无法找到适合处理请求的控制器方法,那么 将会抛出NoSuchRequestHandlingMethodException异常,最 终的结果就是产生404状态码的响应(Not Found)。
--尽管这些内置的映射是很有用的,但是对于应用所抛出的异常它们就无能为力了。幸好,Spring提供了一种机制,能够通过 @ResponseStatus注解将异常映射为HTTP状态码。为了阐述这项功能,请参考SpittleController中如下的请求处理方法,它可能会产生HTTP 404状态(但目前还没有实现):
1 @RequestMapping(value = "/home/{username}", method = RequestMethod.GET)
2 public String homeWithUsername(@PathVariable String username, Model model) {
3 model.addAttribute("username", username);
4 return "home";
5 }
--如果我们在还没有定义该controller方法的时候尝试访问,那么得到的结果必然是404Not Found页面:
--那么现在如果我们在实现了这个controller方法的前提下去访问这个方法,我们想要实现这样的需求:我们知道root是管理员账号,我们并不希望一些小伙伴通过"/home/root"地址来访问我们的界面,那么我们所能做的就是在方法中进行判断,并当我们的路径参数为"root"时,给出一个异常(最好是能实现404页面的展示,并希望这个404界面是我们自行定义的),那么我们现在自定义一个异常映射我的HTTP状态码,该过程我们可以使用@ResponseStatus注解来实现:
1 package com.sky.error;
2
3 import org.springframework.http.HttpStatus;
4 import org.springframework.web.bind.annotation.ResponseStatus;
5
6 /**
7 * @author : S K Y
8 * @version :0.0.1
9 */
10 @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "很抱歉,您要访问的页面不存在。")
11 public class WebNotFoundException extends RuntimeException {
12 }
1 @RequestMapping(value = "/home/{username}", method = RequestMethod.GET)
2 public String homeWithUsername(@PathVariable String username, Model model) {
3 try {
4 if (username.equals("root")) {
5 throw new WebNotFoundException();
6 } else {
7 model.addAttribute("username", username);
8 return "home";
9 }
10 } catch (WebNotFoundException e) {
11 return "error/notfound";
12 }
13 }
1 <!DOCTYPE html>
2 <html lang="en">
3 <head>
4 <meta charset="UTF-8">
5 <title>啊哦,您的访问出错了呢</title>
6 </head>
7 <style type="text/css">
8 .error {
9 color: red;
10 }
11 </style>
12 <body>
13 <label class="error">哎呀,您的访问页面不存在呢?是不是哪里出错了?</label>
14 </body>
15 </html>
--此时我们试图访问"/home/root"页面就会得到一个错误页面的展示,这个页面很简单,那么如果我们不进行异常的捕获,会是什么样的状况呢:
1 @RequestMapping(value = "/home/{username}", method = RequestMethod.GET)
2 public String homeWithUsername(@PathVariable String username, Model model) {
3 if (username.equals("root")) {
4 throw new WebNotFoundException();
5 } else {
6 model.addAttribute("username", username);
7 return "home";
8 }
9 }
--可以发现,在页面展示中的Message信息就是我们在@ResponseStatus注解中所定义的错误信息,这样一来,对于我们来说,就能很好的解决对于非法地址访问的处理.但是现在实际上在抛出该异常之后,我们还是希望能做一些其他处理的,但是如果将我们的代码一层层的包的try catch模块中,对于后期的维护,以及代码结构和美观,都是一种破坏,现在我们为该Controller新增一个方法作为我们的异常处理方法,并且可以使用@ExceptionHandler注解来声明:
1 @ExceptionHandler(WebNotFoundException.class)
2 public String handleWebNotFound() {
3 return "error/notfound";
4 }
--那么此时如果我们的方法抛出了该异常,就能有效的跳转到handleWebNotFound()方法中,目前在我们的方法中并没有进行其他的操作,而是直接跳转到了我们自定义的异常页面.handleDuplicateSpittle()方法上添加了 @ExceptionHandler注解,当抛出 DuplicateSpittleException异常的时候,将会委托该方法来处 理。它返回的是一个String,这与处理请求的方法是一致的,指定 了要渲染的逻辑视图名.
--对于@ExceptionHandler注解标注的方法来说,比较有意思的一 点在于它能处理同一个控制器中所有处理器方法所抛出的异常。所 以,尽管我们从saveSpittle()中抽取代码创建了 handleDuplicateSpittle()方法,但是它能够处 理SpittleController中所有方法所抛出的 DuplicateSpittleException异常。我们不用在每一个可能抛出 DuplicateSpittleException的方法中添加异常处理代码,这一 个方法就涵盖了所有的功能。
--既然@ExceptionHandler注解所标注的方法能够处理同一个控制 器类中所有处理器方法的异常,那么你可能会问有没有一种方法能够 处理所有控制器中处理器方法所抛出的异常呢。从Spring 3.2开始,这 肯定是能够实现的,我们只需将其定义到控制器通知类中即可。
--为控制器添加通知
如果控制器类的特定切面能够运用到整个应用程序的所有控制器中, 那么这将会便利很多。举例来说,如果要在多个控制器中处理异常, 那@ExceptionHandler注解所标注的方法是很有用的。不过,如 果多个控制器类中都会抛出某个特定的异常,那么你可能会发现要在 所有的控制器方法中重复相同的@ExceptionHandler方法。或 者,为了避免重复,我们会创建一个基础的控制器类,所有控制器类 要扩展这个类,从而继承通用的@ExceptionHandler方法。
Spring 3.2为这类问题引入了一个新的解决方案:控制器通知。控制器 通知(controller advice)是任意带有@ControllerAdvice注解的 类,这个类会包含一个或多个如下类型的方法:
1.@ExceptionHandler注解标注的方法;
2.@InitBinder注解标注的方法;
3.@ModelAttribute注解标注的方法。
--在带有@ControllerAdvice注解的类中,以上所述的这些方法会 运用到整个应用程序所有控制器中带有@RequestMapping注解的方法上。@ControllerAdvice注解本身已经使用了@Component,因 此@ControllerAdvice注解所标注的类将会自动被组件扫描获取 到,就像带有@Component注解的类一样。
--@ControllerAdvice最为实用的一个场景就是将所有的 @ExceptionHandler方法收集到一个类中,这样所有控制器的异 常就能在一个地方进行一致的处理。例如,我们想将DuplicateSpittleException的处理方法用到整个应用程序的 所有控制器上:
1 package com.sky.web;
2
3 import com.sky.error.WebNotFoundException;
4 import org.springframework.web.bind.annotation.ControllerAdvice;
5 import org.springframework.web.bind.annotation.ExceptionHandler;
6
7 /**
8 * @author : S K Y
9 * @version :0.0.1
10 */
11 @ControllerAdvice
12 public class WebExceptionHandler {
13 @ExceptionHandler(WebNotFoundException.class)
14 public String handleWebNotFound() {
15 return "error/notfound";
16 }
17 }
--现在,如果任意的控制器方法抛出了 WebNotFoundException,不管这个方法位于哪个控制器中,都会调用这个handleWebNotFound()方法来处理异 常。我们可以像编写@RequestMapping注解的方法那样来编写 @ExceptionHandler注解的方法。如程序清单7.10所示,它返 回“error/notfound”作为逻辑视图名,因此将会为用户展现一个友好的出错页面。
三.跨重定向请求传递数据
在处理完POST请求后,通常来讲一个最佳实践就是执行一下重定向。除了其他的一些因素外,这样做能够防止用户点击 浏览器的刷新按钮或后退箭头时,客户端重新执行危险的POST请求。我们借助 了“redirect:”前缀的力量。当控制器方法返回的String值 以“redirect:”开头的话,那么这个String不是用来查找视图的, 而是用来指导浏览器进行重定向的路径。
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerPost(@RequestPart("picture") Part pictureFile, @Validated User user, Errors errors) throws IOException {
3 if (errors.hasErrors()) {
4 return "register";
5 } else {
6 String fileName = pictureFile.getSubmittedFileName();//获取到原始的名称
7 String suffix = fileName.substring(fileName.lastIndexOf(".")); //获取文件的后缀名
8 pictureFile.write(UUID.randomUUID().toString() + suffix); //产生UUID随机文件名
9 String username = user.getUsername();
10 String encode = URLEncoder.encode(username, "UTF-8");
11 return "redirect:/thyme/home?username=" + encode;
12 }
13 }
--“redirect:”前缀能够让重定向功能变得非常简单。你可能会想 Spring很难再让重定向功能变得更简单了。但是,请稍 等:Spring为重定向功能还提供了一些其他的辅助功能。正在发起重定向功能的方法该如何发送数据给重定向的目 标方法呢?一般来讲,当一个处理器方法完成之后,该方法所指定的 模型数据将会复制到请求中,并作为请求中的属性,请求会转发 (forward)到视图上进行渲染。因为控制器方法和视图所处理的是同一个请求,所以在转发的过程中,请求属性能够以保存。
--但是,如图所示,当控制器的结果是重定向的话,原始的请求就 结束了,并且会发起一个新的GET请求。原始请求中所带有的模型数 据也就随着请求一起消亡了。在新的请求属性中,没有任何的模型数 据,这个请求必须要自己计算数据。
--显然,对于重定向来说,模型并不能用来传递数据。但是我们也有一 些其他方案,能够从发起重定向的方法传递数据给处理重定向方法:
1.使用URL模板以路径变量和/或查询参数的形式传递数据;
2.通过flash属性发送数据。
--通过URL模板进行重定向
通过路径变量和查询参数传递数据看起来非常简单,我们以路径变量的形式传递了 username。但是按照现在的写法,username的值是直接连接到重 定向String上的。这能够正常运行,但是还远远不能说没有问题。 当构建URL或SQL查询语句的时候,使用String连接是很危险的:"/home/{username}",除了连接String的方式来构建重定向URL,Spring还提供了使用模板的 方式来定义重定向URL。
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerPost(@RequestPart("picture") Part pictureFile, @Validated User user, Errors errors, Model model) throws IOException {
3 if (errors.hasErrors()) {
4 return "register";
5 } else {
6 /*String fileName = pictureFile.getSubmittedFileName();//获取到原始的名称
7 String suffix = fileName.substring(fileName.lastIndexOf(".")); //获取文件的后缀名
8 pictureFile.write(UUID.randomUUID().toString() + suffix); //产生UUID随机文件名
9 String username = user.getUsername();
10 String encode = URLEncoder.encode(username, "UTF-8");*/
11 model.addAttribute("username", user.getUsername());
12 return "redirect:/thyme/home/{username}";
13 }
14 }
--现在,username作为占位符填充到了URL模板中,而不是直接连接 到重定向String中,所以username中所有的不安全字符都会进行 转义。这样会更加安全,这里允许用户输入任何想要的内容作 为username,并会将其附加到路径上。值得一提的是,对于中文,也能够作出很好的处理.
--除此之外,模型中所有其他的原始类型值都可以添加到URL中作为查 询参数。作为样例,假设除了username以外,模型中还要包含新创建 user对象的email属性,那registerPost()方法可以改 写为如下的形式:
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerPost(@RequestPart("picture") Part pictureFile, @Validated User user, Errors errors, Model model) throws IOException {
3 if (errors.hasErrors()) {
4 return "register";
5 } else {
6 /*String fileName = pictureFile.getSubmittedFileName();//获取到原始的名称
7 String suffix = fileName.substring(fileName.lastIndexOf(".")); //获取文件的后缀名
8 pictureFile.write(UUID.randomUUID().toString() + suffix); //产生UUID随机文件名
9 String username = user.getUsername();
10 String encode = URLEncoder.encode(username, "UTF-8");*/
11 model.addAttribute("username", user.getUsername());
12 model.addAttribute("email", user.getEmail());
13 return "redirect:/thyme/home/{username}";
14 }
15 }
--所返回的重定向String并没有太大的变化。但是,因为模型中的 email属性没有匹配重定向URL中的任何占位符,所以它会自动以查询参数的形式附加到重定向URL上:
--通过路径变量和查询参数的形式跨重定向传递数据是很简单直接的方 式,但它也有一定的限制。它只能用来发送简单的值,如String和 数字的值。在URL中,并没有办法发送更为复杂的值,但这正是flash属性能够提供帮助的领域。
--使用flash属性
假设我们不想在重定向中发送username或email了,而是要发送实际的 User对象。而且如果我们只发送User.id的话,那么处理重定向的方法还需要从数据库中查找才能得到User对象。但是,在重定向之前,我们其实已经得到了User对象。为什么不将其发送给处理重定向的方法,并将其展现出来呢?
User对象要比String和其他基本类型更为复杂。因此,我们不能像路径变量或查询参数那么容易地发送User对象。它只能设置为模型中的属性。但是,正如我们前面所讨论的那样,模型数据最终是以请求参数的形 式复制到请求中的,当重定向发生的时候,这些数据就会丢失。因 此,我们需要将User对象放到一个位置,使其能够在重定向的过程中存活下来。有个方案是将User放到会话中。会话能够长期存在,并且能够 跨多个请求。所以我们可以在重定向发生之前将User放到会话 中,并在重定向后,从会话中将其取出。当然,我们还要负责在重定 向后在会话中将其清理掉。
实际上,Spring也认为将跨重定向存活的数据放到会话中是一个很 不错的方式。但是,Spring认为我们并不需要管理这些数据,相反,Spring提供了将数据发送为flash属性(flash attribute)的功能。 按照定义,flash属性会一直携带这些数据直到下一次请求,然后才会消失。Spring提供了通过RedirectAttributes设置flash属性的方法,这 是Spring 3.1引入的Model的一个子接口。RedirectAttributes提 供了Model的所有功能,除此之外,还有几个方法是用来设置flash属性的。具体来讲,RedirectAttributes提供了一组 addFlashAttribute()方法来添加flash属性:
1 @RequestMapping(value = "/register", method = RequestMethod.POST)
2 public String registerPost(@RequestPart("picture") Part pictureFile, @Validated User user, Errors errors, RedirectAttributes model) throws IOException {
3 if (errors.hasErrors()) {
4 return "register";
5 } else {
6 model.addAttribute("username", user.getUsername());
7 model.addFlashAttribute("user", user);
8 return "redirect:/thyme/home/{username}";
9 }
10 }
--在重定向执行之前,所有的flash属性都会复制到会话中。在重定向 后,存在会话中的flash属性会被取出,并从会话转移到模型之中。处 理重定向的方法就能从模型中访问User对象了,就像获取其他的模 型对象一样。
--为了完成flash属性的流程,如下展现了更新版本的 registerPost()方法,在从数据库中查找之前,它会首先从模型中检查User对象:
1 @RequestMapping(value = "/home/{username}", method = RequestMethod.GET)
2 public String homeWithUsername(@PathVariable String username, Model model) {
3 if (username.equals("root")) {
4 throw new WebNotFoundException();
5 } else {
6 model.addAttribute("username", username);
7 boolean containsAttribute = model.containsAttribute("user");
8 if (!containsAttribute) {
9 model.addAttribute("user", new User());
10 }
11 return "home";
12 }
13 }
--可以看到,registerPost()方法所做的第一件事就是检查 是否存有key为user的model属性。如果模型中包含user属性,那就什么都不用做了。这里面包含的User对象将会传递到 视图中进行渲染。但是如果模型中不包含user属性的话,那 么registerPost()将会给出一个全新的User对象,并将其存放到模型中。