3 springMVC详解(高级技术)

1 DispatcherServlet新的配置

1.1 自定义DispatcherServlet配置

在SpittrWebAppInitializer中我们所编写的三个方法仅仅是必须要重载的abstract方法。但实际上还有更多的方法可以进行重载,从而实现额外的配置。此类的方法之一就是customizeRegistration()。在AbstractAnnotationConfigDispatcherServletInitializer 将DispatcherServlet注册到Servlet容器中之后,就会调用customizeRegistration(),并将Servlet注册后得到的Registration.Dynamic 传递进来。通过重载customizeRegistration()方法,我们可以对DispatcherServlet进行额外的配置。例如配置Multipart的属性。
3 springMVC详解(高级技术)

 1.2 使用web.xml配置DispatcherServlet

在典型的Spring MVC应用中,我们会需要DispatcherServlet和ContextLoader Listener。AbstractAnnotationConfigDispatcherServletInitializer会自动注册它们,但是如果需要在web.xml中注册的话,那就需要我们自己来完成这项任务了。如下是一个基本的web.xml文件,它按照传统的方式搭建了DispatcherServlet和ContextLoaderListener。

3 springMVC详解(高级技术)

ContextLoaderListener和DispatcherServlet各自都会加载一个Spring应用上下文。上下文参数contextConfigLocation指定了一个XML 文件的地址,这个文件定义了根应用上下文,它会被ContextLoaderListener加载。DispatcherServlet会根据Servlet的名字找到一个文件,并基于该文件加载应用上下文。在上图所示,Servlet的名字是appServlet,因此DispatcherServlet会从“WEB-INFappServlet-context.xml”文件中加载其应用上下文。

2 处理multipart形式的数据

一般表单提交所形成的请求结果是很简单的,就是以“&”符分割的多个name-value对。例如,当在Spittr应用中提交注册表单时,请求会如下所示:

3 springMVC详解(高级技术)

尽管这种编码形式很简单,并且对于典型的基于文本的表单提交也足够满足要求,但是对于传送二进制数据,如上传图片,就显得力不从心了。与之不同的是,multipart格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中, 它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制,下面展现了multipart的请求体:

3 springMVC详解(高级技术)

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的StandardServletMultipartResolver没有构造器参数,也没有要设置的属性。这样,在Spring应用上下文中,将其声明为bean就会非常简单,如下所示:

3 springMVC详解(高级技术)

既然这个@Bean方法如此简单,你可能就会怀疑我们到底该如何限制StandardServletMultipartResolver的工作方式呢。如果我们想要限制用户上传文件的大小,该怎么实现?如果我们想要指定文件在上传时,临时写入目录在什么位置的话,该如何实现?因为没有属性和构造器参数,StandardServletMultipartResolver的功能看起来似乎有些受限。我们可以通过重载customizeRegistration()方法(它会得到一个Dynamic  作为参数)来配置multipart的具体细节:

3 springMVC详解(高级技术)

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

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

3 springMVC详解(高级技术)

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

3 springMVC详解(高级技术)

2.2 处理multipart请求

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

假设我们允许用户在注册Spittr应用的时候上传一张图片,那么我们需要修改表单,以允许用户选择要上传的图片,同时还需要修改SpitterController 中的processRegistration()方法来接收上传的图片。如下的代码片段来源于Thymeleaf注册表单视图(registrationForm.html),着重强调了表单所需的修改:

3 springMVC详解(高级技术)

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

3 springMVC详解(高级技术)

2.3 接受MultipartFile

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

3 springMVC详解(高级技术)

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

除此之外,MultipartFile还提供了一个便利的transferTo()方法,它能够帮助我们将上传的文件写入到文件系统中。作为样例,我们可以在processRegistration()方法中添加如下的几行代码,从而将上传的图片文件写入到文件系统中:

3 springMVC详解(高级技术)

2.4 将文件保存到Amazon S3

另外一种方案就是让别人来负责处理这些事情。多加几行代码,我们就能将图片保存到云端。例如,如下的程序清单所展现的saveImage()方法能够将上传的文件保存到Amazon S3 中,我们在processRegistration()中可以调用该方法。

3 springMVC详解(高级技术)

3 异常处理

Spring提供了多种方式将异常转换为响应:

  • 特定的Spring异常将会自动映射为指定的HTTP状态码;
  • 异常上可以添加@ResponseStatus注解,从而将其映射为某一个HTTP状态码;
  • 在方法上可以添加@ExceptionHandler注解,使其用来处理异常。

3.1 将异常映射为HTTP状态码

为了阐述这项功能,请参考SpittleController中如下的请求处理方法,它可能会产生HTTP 404状态(但目前还没有实现):

3 springMVC详解(高级技术)

上面代码会从SpittleRepository中,通过ID检索Spittle对象。如果findOne()方法能够返回Spittle对象的话,那么会将Spittle放到模型中,然后名为spittle的视图会负责将其渲染到响应之中。但是如果findOne()方法返回null的话,那么将会抛出SpittleNotFoundException异常。现在SpittleNotFoundException就是一个简单的非检查型异常,如下所示:

3 springMVC详解(高级技术)

如果调用spittle()方法来处理请求,并且给定ID获取到的结果为空,那么SpittleNotFoundException(默认)将会产生500状态码(Internal Server Error)的响应。实际上,如果出现任何没有映射的异常,响应都会带有500状态码(springMVC自己提供的),但是,我们可以通过映射SpittleNotFoundException对这种默认行为进行变更。当抛出SpittleNotFoundException异常时,这是一种请求资源没有找到的场景。如果资源没有找到的话,HTTP状态码404是最为精确的响应状态码。所以,我们要使用@ResponseStatus注解将SpittleNotFoundException映射为HTTP状态码404。@ResponseStatus注解:将异常映射为特定的状态码

3 springMVC详解(高级技术)

3.2 编写异常处理的方法

假设用户试图创建的Spittle与已创建的Spittle文本完全相同,那么SpittleRepository的save()方法将会抛出DuplicateSpittle Exception异常。这意味着SpittleController的saveSpittle()方法可能需要处理这个异常。

3 springMVC详解(高级技术)

现在,我们为SpittleController添加一个新的方法,它会处理抛出DuplicateSpittleException的情况:

3 springMVC详解(高级技术)

对于@ExceptionHandler注解标注的方法来说,比较有意思的一点在于它能处理同一个控制器中所有处理器方法所抛出的异常。所以,尽管我们从saveSpittle()中抽取代码创建了handleDuplicateSpittle()方法,但是它能够处理SpittleController中所有方法所抛出的DuplicateSpittleException异常。我们不用在每一个可能抛出DuplicateSpittleException的方法中添加异常处理代码,这一个方法就涵盖了所有的功能。

既然@ExceptionHandler注解所标注的方法能够处理同一个控制器类中所有处理器方法的异常,那么你可能会问有没有一种方法能够处理所有控制器中处理器方法所抛出的异常呢。从Spring 3.2开始,这肯定是能够实现的,我们只需将其定义到控制器通知类中即可

3.3 为控制器添加通知

Spring 3.2为这类问题引入了一个新的解决方案:控制器通知。控制器通知(controller

advice)是任意带有@ControllerAdvice注解的类,这个类会包含一个或多个如下类型的方法:

  • @ExceptionHandler注解标注的方法;
  • @InitBinder注解标注的方法;
  • @ModelAttribute注解标注的方法。

在带有@ControllerAdvice注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有@RequestMapping注解的方法上。

3 springMVC详解(高级技术)

现在,如果任意的控制器方法抛出了DuplicateSpittleException,不管这个方法位于   哪个控制器中,都会调用这个duplicateSpittleHandler()方法来处理异常。我们可以   像编写@RequestMapping注解的方法那样来编写@ExceptionHandler注解的方法。如程序清单7.10所示,它返回“error/duplicate”作为逻辑视图名,因此将会为用户展现一个友好的出错页面。

4 跨重定向请求传递数据

在控制器方法返回的视图名称中,我们借助了“redirect:”前缀的力措。当控制器方法返回的String值以“redirect:”开头的话,那么这个String不是用来查找视图 的,而是用来指导浏览器进行重定向的路径。我们可以回头看一下程序清单5.17,可以看到processRegistration()方法返回的“redirect:String”如下所示:

3 springMVC详解(高级技术)

当控制器的结果是重定向的话,原始的请求就结束了,并且会发起一个新的GET请求。原始请求中所带有的模型数据也就随着请求一起消亡了。在新的请求属性   中,没有任何的模型数据,这个请求必须要自己计算数据。

3 springMVC详解(高级技术)

显然,对于重定向来说,模型并不能用来传递数据。但是我们也有一些其他方案,能够从发   起重定向的方法传递数据给处理重定向方法中:

  • 使用URL模板以路径变措和/或查询参数的形式传递数据;
  • 通过flash属性发送数据。

4.1 通过URL模板进行重定向

4.1.1 连接String的方式来构建重定向URL

3 springMVC详解(高级技术)

这能够正常运行,但是还远远不能说没有问题。当构建URL或SQL查询语句的时候,使用String连接是很危险的。

4.1.2 使用模板的方式来定义重定向 URL

3 springMVC详解(高级技术)

现在,username作为占位符填充到了URL模板中,而不是直接连接到重定向String中, 所以username中所有的不安全字符都会进行转义。这样会更加安全,这里允许用户输入任

除此之外,模型中所有其他的原始类型值都可以添加到URL中作为查询参数。作为样例,假设除了username以外,模型中还要包含新创建Spitter对象的id属性,那processRegistration()方法可以改写为如下的形式:
3 springMVC详解(高级技术)

所返回的重定向String并没有太大的变化。但是,因为模型中的spitterId属性没有匹配   重定向URL中的任何占位符,所以它会自动以查询参数的形式附加到重定向URL上。

通过路径变措和查询参数的形式跨重定向传递数据是很简单直接的方式,但它也有一定的限   制。它只能用来发送简单的值,如String和数字的值。在URL中,并没有办法发送更为复杂的值,但这正是flash属性能够提供帮助的领域。

4.2 使用flash属性

Spring也认为将跨重定向存活的数据放到会话中是一个很不错的方式。但是,Spring认为我们并不需要管理这些数据,相反,Spring提供了将数据发送为flash属性(flash attribute)的功能。按照定义,flash属性会一直携带这些数据直到下一次请求,然后才会消失。

Spring提供了通过RedirectAttributes设置flash属性的方法,这是Spring 3.1引入的 Model的一个子接口。RedirectAttributes提供了Model的所有功能,除此之外,还有   几个方法是用来设置flash属性的。

3 springMVC详解(高级技术)

在重定向执行之前,所有的flash属性都会复制到会话中。在重定向后,存在会话中的flash属性会被取出,并从会话转移到模型之中。处理重定向的方法就能从模型中访问Spitter对象 了,就像获取其他的模型对象一样。图7.2阐述了它是如何运行的。

3 springMVC详解(高级技术)

在从数据库中查找之前,它会首先从模型中检查Spitter对象:

3 springMVC详解(高级技术)