文章目录
Spring MVC配置的替代方案
自定义DispatcherServlet配置
在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”中。
添加其它的Servlet和Filter
类似地,我们还可以创建新的WebApplicationInitializer实现 来注册Listener和Filter。例如,如下的程序清单展现了如何注册Filter。
如果你只是注册Filter, 并且该Filter只会映射到DispatcherServlet上的话,那么 在AbstractAnnotationConfigDispatcherServletInitializer
中还有一种快捷方式。
为了注册Filter并将其映射到DispatcherServlet,所需要做的仅 仅是重载AbstractAnnotationConfigDispatcherServletInitializer的getServlet-Filters()方法。
在这里没有必要声明它的映射路 径,getServletFilters()方法返回的所有Filter都会映射到DispatcherServlet上。
在web.xml中声明DispatcherServlet
在典型的Spring MVC应用中,我们会需要DispatcherServlet和 Context-Loader Listener。AbstractAnnotationConfigDispatcherServletInitiali 会自动注册它们,但是如果需要在web.xml中注册的话,那就需要我们自己来完成这项任务了。
ContextLoaderListener和 DispatcherServlet各自都会加载一个Spring应用上下文。上下文 参数contextConfigLocation指定了一个XML文件的地址,这个 文件定义了根应用上下文,它会被ContextLoaderListener加 载。如程序清单7.3所示,根上下文会从“/WEB-INF/spring/root-
context.xml”中加载bean定义
如果你希望指定DispatcherServlet配置文件的位置的话,那么 可以在Servlet上指定一个contextConfigLocation初始化参数。 例如,如下的配置中,DispatcherServlet会从“/WEB-INF/spring/appServlet/servlet-context.xml”加载它的bean:
处理multipart形式的数据
在编写控制器方法处理文件上传之前,我们必须要配置一个 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所提供的功能支持,并不需要依赖任何其他的项目
至少,我们必须要指定在文 件上传的过程中,所写入的临时文件路径。如果不设定这个最基本配 置的话,StandardServlet-MultipartResolver就无法正常工 作。
具体来讲,我们必须要在web.xml或Servlet初始化类中,将multipart的具体细节作为DispatcherServlet配置的一部分。
如果我们配置DispatcherServlet的Servlet初始化类继承了 Abstract AnnotationConfigDispatcherServletInitializer 或AbstractDispatcher-ServletInitializer的话,那么我 们不会直接创建DispatcherServlet实例并将其注册到Servlet上下 文中。这样的话,将不会有对Dynamic Servlet registration的引用供我 们使用了。但是,我们可以通过重载customizeRegistration() 方法(它会得到一个Dynamic作为参数)来配置multipart的具体细节:
除了临时路径的位 置,其他的构造器所能接受的参数如下:
- 上传文件的最大容量(以字节为单位)。默认是没有限制的。
- 整个multipart请求的最大容量(以字节为单位),不会关心有多 少个part以及每个part的大小。默认是没有限制的。
- 在上传的过程中,如果文件大小达到了一个指定最大容量(以字 节为单位),将会写入到临时文件路径中。默认值为0,也就是
所有上传的文件都会写入到磁盘上。
通常来讲,StandardServletMultipartResolver会是最佳的 选择,但是如果我们需要将应用部署到非Servlet 3.0的容器中,那么
就得需要替代的方案。(配置Jakarta Commons FileUpload multipart解析器)
处理multpart请求
最常见的方式就是在某个控制器方法参数上添 加@RequestPart注解。
当注册表单提交的时候,profilePicture属性将会给定一个byte 数组,这个数组中包含了请求中对应part的数据(通过 @RequestPart指定)。如果用户提交表单的时候没有选择文件,那么这个数组会是空(而不是null)。
接受MultipartFile
使用上传文件的原始byte比较简单但是功能有限。因此,Spring还提 供了MultipartFile接口,它为处理multipart数据提供了内容更为丰富的对象
以Part的形式接受上传的文件
如果你需要将应用部署到Servlet 3.0的容器中,那么会 有MultipartFile的一个替代方案。Spring MVC也能接受
javax.servlet.http.Part作为控制器方法的参数。
在很多情况下,Part方法的名称与MultipartFile方法的名称是完 全相同的。有一些比较类似,但是稍有差异
getSubmittedFileName()对应于getOriginalFilename()。 类似地,write()对应于transferTo()
处理异常
不管发生什么事情,不管是好的还是坏的,Servlet请求的输出都是一 个Servlet响应。如果在请求处理的时候,出现了异常,那它的输出依然会是Servlet响应。异常必须要以某种方式转换为响应。
Spring提供了多种方式将异常转换为响应:
- 特定的Spring异常将会自动映射为指定的HTTP状态码;
- 异常上可以添加@ResponseStatus注解,从而将其映射为某一 个HTTP状态码;
- 在方法上可以添加@ExceptionHandler注解,使其用来处理 异常
处理异常的最简单方式就是将其映射到HTTP状态码上,进而放到响 应之中
将异常映射为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 |
表7.1中的异常一般会由Spring自身抛出,作为DispatcherServlet 处理过程中或执行校验时出现问题的结果。例如,如果 DispatcherServlet无法找到适合处理请求的控制器方法,那么 将会抛出NoSuchRequestHandlingMethodException异常,最终的结果就是产生404状态码的响应(Not Found)。
如果findOne()方法返回null的话,那么将会抛 出SpittleNotFoundException异常。现在SpittleNotFoundException就是一个简单的非检查型异常
SpittleNotFoundException(默认)将会产生500状态 码(Internal Server Error)的响应。实际上,如果出现任何没有映射
的异常,响应都会带有500状态码,
当抛出SpittleNotFoundException异常时,这是一种请求资源 没有找到的场景。如果资源没有找到的话,HTTP状态码404是最为精 确的响应状态码。所以,我们要使用@ResponseStatus注解将SpittleNotFoundException映射为HTTP状态码404。
编写异常处理的方法
它运行起来没什么问题,但是这个方法有些复杂。该方法可以有两个 路径,每个路径会有不同的输出。如果能让saveSpittle()方法只 关注正确的路径,而让其他方法处理异常的话,那么它就能简单一些。
对于@ExceptionHandler注解标注的方法来说,比较有意思的一 点在于它能处理同一个控制器中所有处理器方法所抛出的异常
既然@ExceptionHandler注解所标注的方法能够处理同一个控制 器类中所有处理器方法的异常,那么你可能会问有没有一种方法能够 处理所有控制器中处理器方法所抛出的异常呢。从Spring 3.2开始,这肯定是能够实现的,我们只需将其定义到控制器通知类中即可。
为控制器添加通知
如 果多个控制器类中都会抛出某个特定的异常,那么你可能会发现要在 所有的控制器方法中重复相同的@ExceptionHandler方法。或 者,为了避免重复,我们会创建一个基础的控制器类,所有控制器类要扩展这个类,从而继承通用的@ExceptionHandler方法。
Spring 3.2为这类问题引入了一个新的解决方案:控制器通知。控制器 通知(controller advice)是任意带有@ControllerAdvice注解的
类,这个类会包含一个或多个如下类型的方法:
- @ExceptionHandler注解标注的方法
- @InitBinder注解标注的方法
- @ModelAttribute注解标注的方法
@ControllerAdvice最为实用的一个场景就是将所有的 @ExceptionHandler方法收集到一个类中,这样所有控制器的异常就能在一个地方进行一致的处理。
跨重定向请求传递数据
在第5章,在控制器方法返回的视图名称中,我们借助 了“redirect:”前缀的力量。当控制器方法返回的String值 以“redirect:”开头的话,那么这个String不是用来查找视图的,而是用来指导浏览器进行重定向的路径。
正在发起重定向功能的方法该如何发送数据给重定向的目标方法呢?
一般来讲,当一个处理器方法完成之后,该方法所指定的 模型数据将会复制到请求中,并作为请求中的属性,请求会转发
(forward)到视图上进行渲染
但是,如图7.1所示,当控制器的结果是重定向的话,原始的请求就 结束了,并且会发起一个新的GET请求
模型的属性是以请求属性的形式存放在请求中的,在重定向后无法存活
显然,对于重定向来说,模型并不能用来传递数据。但是我们也有一 些其他方案,能够从发起重定向的方法传递数据给处理重定向方法
中:
- 使用URL模板以路径变量和/或查询参数的形式传递数据;
- 通过flash属性发送数据
通过URL模板进行重定向
现在,username作为占位符填充到了URL模板中,而不是直接拼接到重定向String中,所以username中所有的不安全字符都会进行 转义。这样会更加安全,这里允许用户输入任何想要的内容作为username,并会将其附加到路径上。
除此之外,模型中所有其他的原始类型值都可以添加到URL中作为查 询参数。作为样例,假设除了username以外,模型中还要包含新创建 Spitter对象的id属性,那processRegistration()方法可以改写为如下的形式:
因为模型中的 spitterId属性没有匹配重定向URL中的任何占位符,所以它会自动以查询参数的形式附加到重定向URL上。
通过路径变量和查询参数的形式跨重定向传递数据是很简单直接的方 式,但它也有一定的限制。它只能用来发送简单的值,如String和 数字的值。在URL中,并没有办法发送更为复杂的值,但这正是flash属性能够提供帮助的领域。
使用flash属性
假设我们不想在重定向中发送username或ID了,而是要发送实际的 Spitter对象
有个方案是将Spitter放到会话中。会话能够长期存在,并且能够 跨多个请求。所以我们可以在重定向发生之前将Spitter放到会话 中,并在重定向后,从会话中将其取出。当然,我们还要负责在重定向后在会话中将其清理掉。
实际上,Spring也认为将跨重定向存活的数据放到会话中是一个很 不错的方式。但是,Spring认为我们并不需要管理这些数据,相 反,Spring提供了将数据发送为flash属性(flash attribute)的功能。 按照定义,flash属性会一直携带这些数据直到下一次请求,然后才
会消失。
在这里,我们调用了addFlashAttribute()方法,并将spitter 作为key,Spitter对象作为值。另外,我们还可以不设置key参
数,让key根据值的类型自行推断得出:
在重定向执行之前,所有的flash属性都会复制到会话中。在重定向 后,存在会话中的flash属性会被取出,并从会话转移到模型之中
在从数据库中查找之前,它会首先 从模型中检查Spitter对象,没有的话,再从数据库里面取