赞
踩
整理自《Spring实战》
在第5章中,我们通过扩展AbstractAnnotationConfigDispatcherServletInitializer
快速搭建了Spring MVC环境。在这个便利的基础类中,假设我们需要基本的DispatcherServlet
和ContextLoaderListener
环境,并且Spring配置是使用Java的,而不是XML。
尽管对很多Spring应用来说,这是一种安全的假设,但是并不一定总能满足我们的要求。除了DispatcherServlet以外,我们可能还需要额外的Servlet和Filter;我们可能还需要对DispatcherServlet本身做一些额外的配置;或者,如果我们需要将应用部署到Servlet 3.0之前的容器中,那么还需要将DispatcherServlet配置到传统的web.xml中。
首先回顾第五章中的程序清单5.1 :配置DispatcherServlet
AbstractAnnotationConfigDispatcherServletInitializer所完成的事情实际其实比看上去要多。在SpittrWebAppInitializer中我们所编写的三个方法仅仅是必须要重载的abstract方法。但实际上还有更多的方法可以进行重载,从而实现额外配置。
此类的方法之一就是customizeRegistration()
。在AbstractAnnotationConfigDispatcherServletInitializer将DispatcherServlet注册到Servlet容器中之后,就会调用customizeRegistration(),并将Servlet注册后得到的Registration.Dynamic传递进来。通过重载customizeRegistration()方法,我们可以对DispatcherServlet进行额外的配置
。借助customizeRegistration()方法中的ServletRegistration.Dynamic,我们能够完成多项任务,包括通过调用setLoadOnStartup()设置load-on-startup优先级,通过setInitParameter()设置初始化参数,通过调用setMultipartConfig()配置Servlet 3.0对multipart的支持。在下面样例中,我们设置了对multipart的支持,将上传文件的临时存储目录设置在“/tmp/spittr/uploads”中。
按照AbstractAnnotationConfigDispatcherServletInitializer定义,它会创建DispatcherServlet和ContextLoaderListener
。但是,如果你想注册其他的Servlet、Filter或Listener的话,那该怎么办呢?
基于Java的初始化器(initializer)的一个好处就在于我们可以定义任意数量的初始化器类。因此,如果我们想往Web容器中注册其他组件的话,只需创建一个新的初始化器就可以了。最简单的方式就是实现Spring的WebApplicationInitializer接口
。下面通过实现WebApplicationInitializer来注册Servlet:
上面程序是相当基础的Servlet注册初始化器类。它注册了一个Servlet并将其映射到一个路径上。我们也可以通过这种方式来手动注册DispatcherServlet。(但这并没有必要,因为AbstractAnnotationConfigDispatcherServletInitializer没用太多代码就将这项任务完成得很漂亮。)
类似地,我们还可以创建新的WebApplicationInitializer实现来注册Listener和Filter。例如,如下的程序清单展现了如何注册Filter。
如果要将应用部署到支持Servlet 3.0的容器中,那么WebApplicationInitializer提供了一种通用的方式,实现在Java中注册Servlet、Filter和Listener。不过,如果你只是注册Filter,并且该Filter只会映射到DispatcherServlet上的话,那么在AbstractAnnotationConfigDispatcherServletInitializer中还有一种快捷方式。
为了注册Filter并将其映射到DispatcherServlet,所需要做的仅仅是重载AbstractAnnotationConfigDispatcherServletInitializer的getServlet-Filters()方法
。例如,在如下的代码中,重载了AbstractAnnotationConfigDispatcherServletInitializer的getServletFilters()方法以注册Filter:
我们可以看到,这个方法返回的是一个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和ContextLoaderListener。
在web.xml中搭建Spring MVC:
前面说过,ContextLoaderListener和DispatcherServlet各自都会加载一个Spring应用上下文。
上下文参数contextConfigLocation指定了一个XML文件的地址,这个文件定义了根应用上下文,它会被ContextLoaderListener加载
。如上面程序所示,根上下文会从“/WEB-INF/spring/rootcontext.xml”中加载bean定义。
DispatcherServlet会根据Servlet的名字找到一个文件,并基于该文件加载应用上下文
。在上面程序中,Servlet的名字是appServlet,因此DispatcherServlet会从“/WEBINF/appServlet-context.xml”文件中加载其应用上下文。
如果你希望指定DispatcherServlet配置文件的位置的话,那么可以在Servlet上指定一个contextConfigLocation初始化参数
。例如,如下的配置中,DispatcherServlet会从“/WEBINF/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配置:
在Web应用中,允许用户上传内容是很常见的需求。Spittr应用在两个地方需要文件上传。当新用户注册应用的时候,我们希望他们能够上传一张图片,从而与他们的个人信息相关联。当用户提交新的Spittle时,除了文本消息以外,他们可能还会上传一张照片。
一般表单提交所形成的请求结果是很简单的,就是以“&”符分割的多个name-value对。尽管这种编码形式很简单,并且对于典型的基于文本的表单提交也足够满足要求,但是对于传送二进制数据,如上传图片,就显得力不从心了。与之不同的是,multipart格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制,下面展现了multipart的请求体:
在这个multipart的请求中,我们可以看到profilePicture部分与其他部分明显不同。除了其他内容以外,它还有自己的Content-Type头,表明它是一个JPEG图片。尽管不一定那么明显,但profilePicture部分的请求体是二进制数据,而不是简单的文本。
尽管multipart请求看起来很复杂,但在Spring MVC中处理它们却很容易。在编写控制器方法处理文件上传之前,我们必须要配置一个multipart解析器
,通过它来告诉DispatcherServlet该如何读取multipart请求。
DispatcherServlet并没有实现任何解析multipart请求数据的功能。它将该任务委托给了Spring中MultipartResolver策略接口的实现,通过这个实现类来解析multipart请求中的内容
。从Spring 3.1开始,Spring内置了两MultipartResolver的实现供我们选择:
一般来讲,在这两者之间,StandardServletMultipartResolver可能会是优选的方案
。它使用Servlet所提供的功能支持,并不需要依赖任何其他的项目。如果我们需要将应用部署到Servlet 3.0之前的容器中,或者还没有使用Spring 3.1或更高版本,那么可能就需要CommonsMultipartResolver了。
使用Servlet 3.0解析multipart请求
兼容Servlet 3.0的StandardServletMultipartResolver没有构造器参数,也没有要设置的属性。这样,在Spring应用上下文中,将其声明为bean就会非常简单,如下所示:
接下来我们不是在Spring中配置StandardServletMultipartResolver,而是要在Servlet中指定multipart的配置
。StandardServletMultipartResolver,而是要在Servlet中指定multipart的配置。至少,我们必须要指定在文件上传的过程中,所写入的临时文件路径。如果不设定这个最基本配置的话,StandardServletMultipartResolver就无法正常工作。具体来讲,我们必须要在web.xml或Servlet初始化类中,将multipart的具体细节作为DispatcherServlet配置的一部分
。
如果我们采用Servlet初始化类的方式来配DispatcherServlet的话,这个初始化类应该已经实现了WebApplicationInitializer,那我们可以在Servlet registration上调用setMultipartConfig()方法,传入一个MultipartConfig-Element实例。如下是最基本的DispatcherServlet multipart配置,它将临时路径设置为“/tmp/spittr/uploads”:
如果我们配置DispatcherServlet的Servlet初始化类继承了AbstractAnnotationConfigDispatcherServletInitializer或AbstractDispatcher-ServletInitializer的话,那么我们不会直接创建DispatcherServlet实例并将其注册到Servlet上下文中。这样的话,将不会有对Dynamic Servlet registration的引用供我们使用了。但是,我们可以通过重载customizeRegistration()方法(它会得到一个Dynamic作为参数)来配置multipart的具体细节:
到目前为止,我们所使用是只有一个参数的MultipartConfigElement构造器,这个参数指定的是文件系统中的一个绝对目录,上传文件将会临时写入该目录中。但是,我们还可以通过其他的构造器来限制上传文件的大小。除了临时路径的位置,其他的构造器所能接受的参数如下:
例如,假设我们想限制文件的大小不超过2MB,整个请求不超过4MB,而且所有的文件都要写到磁盘中。下面的代码使用MultipartConfigElement设置了这些临界值:
如果我们使用更为传统的web.xml来配置MultipartConfigElement的话,那么可以使用<servlet>中的<multipart-config>元素,如下所示:
<multipart-config>的默认值与MultipartConfigElement相同。与MultipartConfigElement一样,必须要配置的是<location>。
配置Jakarta Commons FileUpload multipart解析器
通常来讲,StandardServletMultipartResolver会是最佳的选择,但是如果我们需要将应用部署到非Servlet 3.0的容器中,那么就得需要替代的方案。如果喜欢的话,我们可以编写自己的MultipartResolver实现。不过,除非想要在处理multipart请求的时候执行特定的逻辑,否则的话,没有必要这样做。Spring内置了CommonsMultipartResolver,可以作为StandardServletMultipartResolver的替代方案。
将CommonsMultipartResolver声明为Spring bean的最简单方式:
与StandardServletMultipartResolver有所不同,CommonsMultipart-Resolver不会强制要求设置临时文件路径。默认情况下,这个路径就是Servlet容器的临时目录。不过,通过设置uploadTempDir属性,我们可以将其指定为一个不同的位置:
实际上,我们可以按照相同的方式指定其他的multipart上传细节,也就是设置CommonsMultipartResolver的属性。例如,如下的配置就等价于我们在前文通过MultipartConfigElement所配置的StandardServletMultipartResolver:
这里表明不能上传超过2MB的文件,并且不管文件的大小如何,所有的文件都会写到磁盘中。但是与MultipartConfigElement有所不同,我们无法设定multipart请求整体的最大容量。
在编写控制器方法来接收上传的文件时,最常见的方式就是在某个控制器方法参数上添加@RequestPart注解。
假设我们允许用户在注册Spittr应用的时候上传一张图片,那么我们需要修改表单,以允许用户选择要上传的图片,同时还需要修改SpitterController 中的processRegistration()方法来接收上传的图片。如下的代码片段来源于Thymeleaf注册表单视图(registrationForm.html),着重强调了表单所需的修改:
<form>标签现在将enctype属性设置为multipart/formdata,这会告诉浏览器以multipart数据的形式提交表单,而不是以表单数据的形式进行提交。在multipart中,每个输入域都会对应一个part
。
除了注册表单中已有的输入域,我们还添加了一个新的<input>域,其type为file。这能够让用户选择要上传的图片文件。accept属性用来将文件类型限制为JPEG、PNG以及GIF图片。根据其name属性,图片数据将会发送到multipart请求中的profilePicture part之中。
现在,我们需要修改processRegistration()方法,使其能够接受上传的图片。其中一种方式是添加byte数组参数,并为其添加@RequestPart注解。如下为示例:
这种方式不细讲了,比较简单但功能有限。
此外,Spring还提供了MultipartFile接口
,它为处理multipart数据提供了内容更为丰富的对象。如下的程序清单展现了MultipartFile接口的概况。
我们可以看到,MultipartFile提供了获取上传文件byte的方式,但是它所提供的功能并不仅限于此,还能获得原始的文件名、大小以及内容类型。它还提供了一个InputStream,用来将文件数据以流的方式进行读取。
除此之外,MultipartFile还提供了一个便利的transferTo()方法,它能够帮助我们将上传的文件写入到文件系统中。作为样例,我们可以在process-Registration()方法中添加如下的几行代码,从而将上传的图片文件写入到文件系统中:
将文件保存到本地文件系统中是非常简单的,但是这需要我们对这些文件进行管理。我们需要确保有足够的空间,确保当出现硬件故障时,文件进行了备份,还需要在集群的多个服务器之间处理这些图片文件的同步。
将文件保存到Amazon S3中略
以Part的形式接受上传的文件:
如果你需要将应用部署到Servlet 3.0的容器中,那么会有MultipartFile的一个替代方案。Spring MVC也能接受javax.servlet.http.Part作为控制器方法的参数。如果使用Part来替换MultipartFile的话,那么processRegistration()的方法签名将会变成如下的形式:
就主体来言,Part接口与MultipartFile并没有太大的差别。在如下的程序清单中,我们可以看到Part接口的有一些方法其实是与MultipartFile相对应的。
在很多情况下,Part方法的名称与MultipartFile方法的名称是完全相同的。有一些比较类似,但是稍有差异,比如getSubmittedFileName()对应于getOriginalFilename()。类似地,write()对应于transferTo(),借助该方法我们能够将上传的文件写入文件系统中。
值得一提的是,如果在编写控制器方法的时候,通过Part参数的形式接受文件上传,那么就没有必要配置MultipartResolver了。只有使用MultipartFile的时候,我们才需要MultipartResolver。
在应用中,不管发生什么事情,不管是好的还是坏的,Servlet请求的输出都是一个Servlet响应。如果在请求处理的时候,出现了异常,那它的输出依然会是Servlet响应。异常必须要以某种方式转换为响应
。
Spring提供了多种方式将异常转换为响应:
处理异常的最简单方式就是将其映射到HTTP状态码上,进而放到响应之中。接下来,我们看一下如何将异常映射为某一个HTTP状态码。
在默认情况下,Spring会将自身的一些异常自动转换为合适的状态码。下面列出了这些映射关系。
这些异常一般会由Spring自身抛出
,作为DispatcherServlet处理过程中或执行校验时出现问题的结果。例如,如果DispatcherServlet无法找到适合处理请求的控制器方法,那么将会抛出NoSuchRequestHandlingMethodException异常,最终的结果就是产生404状态码的响应(Not Found)。
尽管这些内置的映射是很有用的,但是对于应用所抛出的异常它们就无能为力了
。幸好,Spring提供了一种机制,能够通过@ResponseStatus注解将异常映射为HTTP状态码。
为了阐述这项功能,请参考SpittleController中如下的请求处理方法,它可能会产生HTTP 404状态(但目前还没有实现):
SpittleNotFoundException是一个简单的非检查型异常:
如果调用spittle()方法来处理请求,并且给定ID获取到的结果为空,那么SpittleNotFoundException(默认)将会产生500状态码(Internal Server Error)的响应。实际上,如果出现任何没有映射的异常,响应都会带有500状态码
,但是,我们可以通过映射SpittleNotFoundException对这种默认行为进行变更。
我们可以使用@ResponseStatus注解将SpittleNotFoundException映射为HTTP状态码404。
在引入@ResponseStatus注解之后,如果控制器方法抛出SpittleNotFound-Exception异常的话,响应将会具有404状态码,这是因为Spittle Not Found。
在很多的场景下,将异常映射为状态码是很简单的方案,并且就功能来说也足够了。但是如果我们想在响应中不仅要包括状态码,还要包含所产生的错误,那该怎么办呢?此时的话,我们就不能将异常视为HTTP错误了,而是要按照处理请求的方式来处理异常了。
上面程序运行起来没什么问题,但是这个方法有些复杂。该方法可以有两个路径,每个路径会有不同的输出。如果能让saveSpittle()方法只关注正确的路径,而让其他方法处理异常的话,那么它就能简单一些。
首先,让我们首先将saveSpittle()方法中的异常处理方法剥离掉:
然后为SpittleController添加一个新的方法,它会处理抛出DuplicateSpittleException的情况:
handleDuplicateSpittle()方法上添加了@ExceptionHandler注解,当抛出DuplicateSpittleException异常的时候,将会委托该方法来处理。它返回的是一个String,这与处理请求的方法是一致的,指定了要渲染的逻辑视图名,它能够告诉用户他们正在试图创建一条重复的条目。
对于@ExceptionHandler注解标注的方法来说,它能处理同一个控制器中所有处理器方法所抛出的异常
。所以,尽管我们从saveSpittle()中抽取代码创建了handleDuplicateSpittle()方法,但是它能够处理SpittleController中所有方法所抛出的DuplicateSpittleException异常。我们不用在每一个可能抛出DuplicateSpittleException的方法中添加异常处理代码,这一个方法就涵盖了所有的功能。
既然@ExceptionHandler注解所标注的方法能够处理同一个控制器类中所有处理器方法的异常,那么你可能会问有没有一种方法能够处理所有控制器中处理器方法所抛出的异常呢。从Spring 3.2开始,这肯定是能够实现的,我们只需将其定义到控制器通知类
中即可。
如果控制器类的特定切面能够运用到整个应用程序的所有控制器中,那么这将会便利很多。举例来说,如果要在多个控制器中处理异常,那@ExceptionHandler注解所标注的方法是很有用的。不过,如果多个控制器类中都会抛出某个特定的异常,那么你可能会发现要在所有的控制器方法中重复相同的@ExceptionHandler方法。或者,为了避免重复,我们会创建一个基础的控制器类,所有控制器类要扩展这个类,从而继承通用的@ExceptionHandler方法。
Spring 3.2为这类问题引入了一个新的解决方案:控制器通知。控制器通知(controller advice)是任意带有@ControllerAdvice注解的类
,这个类会包含一个或多个如下类型的方法:
在带有@ControllerAdvice注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有@RequestMapping注解的方法上。
@ControllerAdvice注解本身已经使用了@Component,因此@ControllerAdvice注解所标注的类将会自动被组件扫描获取到,就像带有@Component注解的类一样。
@ControllerAdvice最为实用的一个场景就是将所有的@ExceptionHandler方法收集到一个类中,这样所有控制器的异常就能在一个地方进行一致的处理。
在处理完POST请求后,通常来讲一个最佳实践就是执行一下重定向。除了其他的一些因素外,这样做能够防止用户点击浏览器的刷新按钮或后退箭头时,客户端重新执行危险的POST请求。
“redirect:”前缀能够让重定向功能变得非常简单。但是,请稍等:Spring为重定向功能还提供了一些其他的辅助功能。
具体来讲,正在发起重定向功能的方法该如何发送数据给重定向的目标方法呢?一般来讲,当一个处理器方法完成之后,该方法所指定的模型数据将会复制到请求中,并作为请求中的属性,请求会转发(forward)到视图上进行渲染。因为控制器方法和视图所处理的是同一个请求,所以在转发的过程中,请求属性能够得以保存。
但是,当控制器的结果是重定向的话,原始的请求就结束了,并且会发起一个新的GET请求。原始请求中所带有的模型数据也就随着请求一起消亡了。在新的请求属性中,没有任何的模型数据,这个请求必须要自己计算数据。
显然,对于重定向来说,模型并不能用来传递数据
。但是我们也有一些其他方案,能够从发起重定向的方法传递数据给处理重定向方法中:
通过路径变量和查询参数传递数据看起来非常简单。例如,在程序清单5.19中,我们以路径变量的形式传递了新创建Spitter的username。但是按照现在的写法,username的值是直接连接到重定向String上的。这能够正常运行,但是还远远不能说没有问题。当构建URL或SQL查询语句的时候,使用String连接是很危险的。
除了连接String的方式来构建重定向URL,Spring还提供了使用模板
的方式来定义重定向URL。例如,在程序清单5.19中,processRegistration()方法的最后一行可以改写为如下的形式:
现在,username作为占位符填充到了URL模板中,而不是直接连接到重定向String中,所以username中所有的不安全字符都会进行转义。这样会更加安全,这里允许用户输入任何想要的内容作为username,并会将其附加到路径上。
除此之外,模型中所有其他的原始类型值
都可以添加到URL中作为查询参数。作为样例,假设除了username以外,模型中还要包含新创建Spitter对象的id属性,那processRegistration()方法可以改写为如下的形式:
所返回的重定向String并没有太大的变化。但是,因为模型中的spitterId属性没有匹配重定向URL中的任何占位符,所以它会自动以查询参数的形式附加到重定向URL上。如果username属性的值是habuma并且spitterId属性的值是42,那么结果得到的重定向URL路径将会是“/spitter/habuma?spitterId=42”。
通过路径变量和查询参数的形式跨重定向传递数据是很简单直接的方式,但它也有一定的限制。它只能用来发送简单
的值,如String和数字的值
。在URL中,并没有办法发送更为复杂的值,但这正是flash属性能够提供帮助的领域。
假设我们不想在重定向中发送username或ID了,而是要发送实际的Spitter对象。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对象添加到模型中:
在这里,我们调用了addFlashAttribute()方法,并将spitter作为key,Spitter对象作为值。另外,我们还可以不设置key参数,让key根据值的类型自行推断得出:如下,因为我们传递了一个Spitter对象给addFlashAttribute()方法,所以推断得到的key将会是spitter。
在重定向执行之前,所有的flash属性都会复制到会话中。在重定向后,存在会话中的flash属性会被取出,并从会话转移到模型之中
。处理重定向的方法就能从模型中访问Spitter对象了,就像获取其他的模型对象一样。下图阐述了它是如何运行的。
更新版本的showSpitterProfile()方法如下示例:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。