一、SpringMVC配置代替方案
1自定DispatcherServlet
按照AbstractAnnotationConfigDispatcherServletInitializer的定义,它会创建DispatcherServlet和ContextLoaderListener。
AbstractAnnotationConfigDispatcherServletInitializer有三个方法是必须要重载的abstract方法。但是实际上还有更多的方法可以进行重载,从而实现额外的配置。
此类的方法之一就是customizeRegistration()。在AbstractAnnotationConfigDispatcherServletInitializer将DispatcherServlet注册到Servlet容器中之后,就会调用customizeRegistration(),并将Servlet注册后得到的Registration.Dynamic传递进来。通过重载customizeRegistration()方法,我们可以对DispatcherServlet进行额外的配置。
2、添加其他的Servlet和Filter
如果你想注册其他的Servlet、Filter或Listener的话,基于Java的初始化器(initializer)的一个好处就在于我们可以定义任意数量的初始化器类。因此,如果我们想往Web容器中注册其他组件的话,只需创建一个新的初始化器就可以了。最简单的方式就是实现Spring的WebApplicationInitializer接口。
如何创建WebApplicationInitializer实现并注册一个Servlet。
import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRegistration.Dynamic;import org.springframework.web.WebApplicationInitializer;import spittr.servlet.MyServlet;public class MySevletInitializer implements WebApplicationInitializer {@Overridepublic void onStartup(ServletContext servletContext) throws ServletException {Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class);//注册servletmyServlet.addMapping("/custom/**"); //添加映射Servlet } }
它注册了一个Servlet并将其映射到一个路径上。
还可以创建新的WebApplicationInitializer实现来注册Listener和Filter。
import javax.servlet.FilterRegistration.Dynamic; import javax.servlet.ServletContext; import javax.servlet.ServletException;import org.springframework.web.WebApplicationInitializer;import spittr.servlet.MyFilter;public class MyFilterInitializer implements WebApplicationInitializer {@Overridepublic void onStartup(ServletContext servletContext) throws ServletException {Dynamic filter = servletContext.addFilter("MyFilter", MyFilter.class); 注册filter.addMappingForUrlPatterns(null, false, "/custom/*"); 添加Filter的映射路径}}
如果你只是注册Filter,并且该Filter只会映射到DispatcherServlet上的话,那么在AbstractAnnotationConfigDispatcherServletInitializer中还有一种快捷方式。
为了注册Filter并将其映射到DispatcherServlet,所需要做的仅仅是重载AbstractAnnotationConfigDispatcherServletInitializer的getServletFilters()方法。
@Overrideprotected Filter[] getServletFilters() { return new Filter[]{new MyFilter()};}
这个方法返回的是一个javax.servlet.Filter的数组。在这里它只返回了一个Filter,但它实际上可以返回任意数量的Filter。在这里没有必要声明它的映射路径,getServletFilters()方法返回的所有Filter都会映射到DispatcherServlet上。
3、在web.xml中声明DispatcherServlet
在典型的Spring MVC应用中,我们会需要DispatcherServlet和ContextLoaderListener。AbstractAnnotationConfigDispatcherServletInitializer会自动注册它们,但是如果需要在web.xml中注册的话,那就需要我们自己来完成这项任务了。
3.1让DispatcherServlet和ContextLoaderListener从XML中加载各自的应用上下文。
搭建DispatcherServlet和ContextLoaderListener。
<?xml version="1.0" encoding="UTF-8"?> <web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1"><!-- 设置root上下文配置文件位置 --> <context-param><param-name>contextConfigLocation</param-name><param-value>/WEB-INF/spring/root-context.xml</param-value> </context-param><!-- 注册ContextLoaderListener --> <listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener><!-- 注册DispatcherServlet --> <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><servlet-mapping><servlet-name>appServlet</servlet-name><url-pattern>/</url-pattern> </servlet-mapping> </web-app>
其中对于ContextLoaderListener,上下文参数contextConfigLocation指定了一个XML文件的地址,这个文件定义了根应用上下文,它会被ContextLoaderListener加载。
DispatcherServlet会根据Servlet的名字找到一个文件,并基于该文件加载应用上下文。Servlet的名字是appServlet,因此DispatcherServlet会从“/WEB-INF/appServlet-context.xml”文件中加载其应用上下文。但是上面改变了加载的配置文件的路劲(在Servlet上指定一个contextConfigLocation初始化参数。)。
3.2在java配置类中加载
要在Spring MVC中使用基于Java的配置,需要告诉DispatcherServlet和ContextLoaderListener使用AnnotationConfigWebApplicationContext,这是一个WebApplicationContext的实现类,它会加载Java配置类,而不是使用XML。要实现这种配置,可以设置contextClass上下文参数以及DispatcherServlet的初始化参数。在下面文件中,它所搭建的Spring MVC使用基于Java的Spring配置:
<?xml version="1.0" encoding="UTF-8"?> <web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1"><context-param><param-name>contextClass</param-name> <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value> </context-param><!-- 设置root上下文配置文件位置 --> <context-param> <param-name>contextConfigLocation</param-name><param-value>spittr.config.RootConfig</param-value> </context-param><!-- 注册ContextLoaderListener --> <listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener><!-- 注册DispatcherServlet --> <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>
二、处理multipart形式数据
multipart对于处理文件的上传有很好的处理。
multipart格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型数据,但是如果上传文件的话,它所对应的部分可以是二进制,下面展现了multipart的请求体:
在这个multipart的请求中,我们可以看到profilePicture部分与其他部分明显不同。除了其他内容以外,它还有自己的Content-Type头,表明它是一个JPEG图片。profilePicture部分的请求体是二进制数据,而不是简单的文本。
在编写控制器方法处理文件上传之前,我们必须要配置一个multipart解析器,通过它来告诉DispatcherServlet该如何读取multipart请求。
DispatcherServlet没有实现任何解析multipart请求数据的功能。它将该任务委托给了Spring中MultipartResolver策略接口的实现,通过这个实现类来解析multipart请求中的内容。从Spring 3.1开始,Spring内置了两个MultipartResolver的实现供我们选择:
CommonsMultipartResolver:使用Jakarta Commons FileUpload解析multipart请求;
StandardServletMultipartResolver:依赖于Servlet 3.0对multipart请求的支持(始于Spring 3.1)。
2.1使用StandardServletMultipartResolver
其没有构造器参数,也没有要设置的属性。
在Spring应用上下文中配置:
public class WebConfig extends WebMvcConfigurerAdapter{
@Bean
public MultipartResolver multipartResolver()throws IOException{
return new StandardServletMultipartResolver();
}
}
虽然没办法直接通过StandardServletMultipartResolver配置限制条件的。但在Servlet中能指定multipart的配置。具体来讲,我们必须要在web.xml或Servlet初始化类中,将multipart的具体细节作为DispatcherServlet配置的一部分。
方法一:(没用过)
如果采用Servlet初始化类的方式来配置DispatcherServlet的话,这个初始化类应该已经实现了WebApplicationInitializer,那我们可以在Servlet registration上调用setMultipartConfig()方法,传入一个MultipartConfig-Element实例。如下是最基本的DispatcherServlet multipart配置,它将临时路径设置为“/tmp/spittr/uploads”:
方法二:
如果配置DispatcherServlet的Servlet初始化类继承了Abstract AnnotationConfigDispatcherServletInitializer或AbstractDispatcherServletInitializer的话,那么我们不会直接创建DispatcherServlet实例并将其注册到Servlet上下文中。这样的话,将不会有对Dynamic Servlet registration的引用供我们使用了。但是,我们可以通过重载customizeRegistration()方法(它会得到一个Dynamic作为参数)来配置multipart的具体细节:
public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer{
@Overrideprotected void customizeRegistration(Dynamic registration) {registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads"));}
}
MultipartConfigElement构造器,这个参数指定的是文件系统中的一个绝对目录,上传文件将会临时写入该目录中。还可以通过其他的构造器来限制上传文件的大小。除了临时路径的位置,其他的构造器所能接受的参数如下:
上传文件的最大容量(以字节为单位)。默认是没有限制的。
整个multipart请求的最大容量(以字节为单位),不会关心有多少个part以及每个part的大小。默认是没有限制的。
在上传的过程中,如果文件大小达到了一个指定最大容量(以字节为单位),将会写入到临时文件路径中。默认值为0,也就是所有上传的文件都会写入到磁盘上。
假设我们想限制文件的大小不超过2MB,整个请求不超过4MB,而且所有的文件都要写到磁盘中。
@Overrideprotected void customizeRegistration(Dynamic registration) {registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads",2097152,4194304,0));}
或者在xml中配置:
使用<servlet>中的<multipart-config>元素
2.2处理multipart请求
例如传一张图片:
<body><div id="content"><h1>Register</h1><form method="POST" enctype="multipart/form-data" action="upload/up"><label>Profile Picture</label>:<input type="file"name="profilePicture"accept="image/jpeg,image/png,image/gif" /><br/><input type="submit" value="Register" /></form></div></body>
注意:<form>标签现在将enctype属性设置为multipart/form-data,这会告诉浏览器以multipart数据的形式提交表单,而不是以表单数据的形式进行提交。在multipart中,每个输入域都会对应一个part。
添加了一个新的<input>域,其type为file。这能够让用户选择要上传的图片文件。accept属性用来将文件类型限制为JPEG、PNG以及GIF图片。根据其name属性,图片数据将会发送到multipart请求中的profilePicture part之中。
2.2.1MultipartFile接收
Spring还提供了MultipartFile接口,它为处理multipart数据提供了内容更为丰富的对象。
Spring所提供的MultipartFile接口,用来处理上传的文件
MultipartFile提供了获取上传文件byte的方式,还能获得原始的文件名、大小以及内容类型。它还提供了一个InputStream,用来将文件数据以流的方式进行读取。
MultipartFile还提供了一个便利的transferTo()方法,它能够帮助我们将上传的文件写入到文件系统中。
在processRegistration()方法中添加如下的几行代码,从而将上传的图片文件写入到文件系统中:
@RequestMapping(value="/up", method=RequestMethod.POST)public String processRegistration(@RequestPart("profilePicture")MultipartFile profilePicture,Model model) throws Exception{ // 文件保存路径 String filePath = "E:/spittr/image/";System.out.println(filePath+profilePicture.getSize()+".jpg");profilePicture.transferTo(new File(filePath+profilePicture.getSize()+".jpg"));model.addAttribute("imgSrc","/spittr/image/"+profilePicture.getSize()+".jpg");return "displayImg";}
读取存储的内容:
<body><img src="${imgSrc}"></body>
2.3以part形式处理
将应用部署到Servlet 3.0的容器中,那么会有MultipartFile的一个替代方案。Spring MVC也能接受javax.servlet.http.Part作为控制器方法的参数。
Part接口与MultipartFile并没有太大的差别。
Part方法的名称与MultipartFile方法的名称是完全相同的。有一些比较类似,但是稍有差异,比如getSubmittedFileName()对应于getOriginalFilename()。类似地,write()对应于transferTo(),借助该方法我们能够将上传的文件写入文件系统中.
如果在编写控制器方法的时候,通过Part参数的形式接受文件上传,那么就没有必要配置MultipartResolver了。只有使用MultipartFile的时候,我们才需要MultipartResolver。
@RequestMapping(value="/up", method=RequestMethod.POST)public String processRegistration(@RequestPart("profilePicture") Part profilePicture,Model model) throws Exception{ // 文件保存路径 String filePath = "E:/spittr/image/";System.out.println(filePath+profilePicture.getSize()+".jpg"); profilePicture.write(filePath+profilePicture.getSize()+".jpg");model.addAttribute("imgSrc","/spittr/image/"+profilePicture.getSize()+".jpg");return "displayImg";}
三、处理异常
不管发生什么事情,不管是好的还是坏的,Servlet请求的输出都是一个Servlet响应。如果在请求处理的时候,出现了异常,那它的输出依然会是Servlet响应。异常必须要以某种方式转换为响应。
Spring提供了多种方式将异常转换为响应:
特定的Spring异常将会自动映射为指定的HTTP状态码;
异常上可以添加@ResponseStatus注解,从而将其映射为某一个HTTP状态码;
在方法上可以添加@ExceptionHandler注解,使其用来处理异常。
3.1将异常映射为HTTP状态码
在默认情况下,Spring会将自身的一些异常自动转换为合适的状态码。
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 |
MissingServletRequestParameterException | 400 - Bad Request |
MissingServletRequestPartException | 400 - Bad Request |
NoSuchRequestHandlingMethodException | 404 - Not Found |
TypeMismatchException | 400 - Bad Request |
HttpMessageNotWritableException | 500 - Internal Server Error |
HttpRequestMethodNotSupportedException | 405 - Method Not Allowed |
异常一般会由Spring自身抛出,作为DispatcherServlet处理过程中或执行校验时出现问题的结果。
如果DispatcherServlet无法找到适合处理请求的控制器方法,那么将会抛出NoSuchRequestHandlingMethodException异常,最终的结果就是产生404状态码的响应(Not Found)。
3.2@ResponseStatus注解
Spring提供了一种机制,能够通过@ResponseStatus注解将异常映射为HTTP状态码。
public String spittle(@PathVariable("spittleId") long spittledId ,Model model){Spittle spittle = sipttleRepository.findOne(spittleId);if(spittle == null){throw new SpittleNotFoundException();}model.addAttribute(spittle);return "spittle";}
通过ID检索Spittle对象。如果findOne()方法能够返回Spittle对象的话,那么会将Spittle放到模型中,然后名为spittle的视图会负责将其渲染到响应之中。但是如果findOne()方法返回null的话,那么将会抛出SpittleNotFoundException异常。
public class SpittleNotFoundException extends RuntimeException {}
如果调用spittle()方法来处理请求,并且给定ID获取到的结果为空,那么SpittleNotFoundException(默认)将会产生500状态码(Internal Server Error)的响应。实际上,如果出现任何没有映射的异常,响应都会带有500状态码,故返回的不精确,可以修改。
使用@ResponseStatus注解将SpittleNotFoundException映射为HTTP状态码404。
@ResponseStatus(value=HttpStatus.NOT_FOUND,reason="Spittle Not Found") public class SpittleNotFoundException extends RuntimeException {}
在引入@ResponseStatus注解之后,如果控制器方法抛出SpittleNotFoundException异常的话,响应将会具有404状态码,这是因为Spittle Not Found。
3.3异常处理的方法
若在响应中不仅要包括状态码,还要包含所产生的错误,此时的话,就不能将异常视为HTTP错误了,而是要按照处理请求的方式来处理异常了。
假设用户试图创建的Spittle与已创建的Spittle文本完全相同,那么SpittleRepository的save()方法将会抛出DuplicateSpittle Exception异常。这意味着SpittleController的saveSpittle()方法可能需要处理这个异常。
程序的处理:
@RequestMapping(method=RequestMethod.POST)public String saveSpittle(SpittleForm form,Model model){try{spittleRepository.save(new Spittle(null,form,getMessage(),new Date()));return "redirect:/spittles";}catch(DuplicateSpittleException e){return "err/duplicate";}}
如果能让saveSpittle()方法只关注正确的路径,而让其他方法处理异常的话,那么它就能简单一些。
修改:
@RequestMapping(method=RequestMethod.POST)public String saveSpittle(SpittleForm form,Model model){spittleRepository.save(new Spittle(null,form,getMessage(),new Date()));return "redirect:/spittles";}@ExceptionHandler(DuplicateSpittleException.class)public String handlerDuplicateSpittle(){return "err/duplicate";}
handleDuplicateSpittle()方法上添加了@ExceptionHandler注解,当抛出DuplicateSpittleException异常的时候,将会委托该方法来处理。它返回的是一个String,这与处理请求的方法是一致的,指定了要渲染的逻辑视图名,它能够告诉用户他们正在试图创建一条重复的条目。
对于@ExceptionHandler注解标注的方法来说,比较有意思的一点在于它能处理同一个控制器中所有处理器方法所抛出的异常。所以,尽管我们从saveSpittle()中抽取代码创建了handleDuplicateSpittle()方法,但是它能够处理SpittleController中所有方法所抛出的DuplicateSpittleException异常。我们不用在每一个可能抛出DuplicateSpittleException的方法中添加异常处理代码,这一个方法就涵盖了所有的功能。
3.4为控制器添加通知
如果多个控制器类中都会抛出某个特定的异常,那么你可能会发现要在所有的控制器方法中重复相同的@ExceptionHandler方法。或者,为了避免重复,我们会创建一个基础的控制器类,所有控制器类要扩展这个类,从而继承通用的@ExceptionHandler方法。
但是:Spring 3.2为这类问题引入了一个新的解决方案:控制器通知。控制器通知(controller advice)是任意带有@ControllerAdvice注解的类,这个类会包含一个或多个如下类型的方法:
@ExceptionHandler注解标注的方法;
@InitBinder注解标注的方法;
@ModelAttribute注解标注的方法。
在带有@ControllerAdvice注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有@RequestMapping注解的方法上。
@ControllerAdvice注解本身已经使用了@Component,因此@ControllerAdvice注解所标注的类将会自动被组件扫描获取到,就像带有@Component注解的类一样。
@ControllerAdvice最为实用的一个场景就是将所有的@ExceptionHandler方法收集到一个类中,这样所有控制器的异常就能在一个地方进行一致的处理。
如果任意的控制器方法抛出了DuplicateSpittleException,不管这个方法位于哪个控制器中,都会调用这个duplicateSpittleHandler()方法来处理异常。
四:跨重定向请求传递数据
当控制器方法返回的String值以“redirect:”开头的话,那么这个String不是用来查找视图的,而是用来指导浏览器进行重定向的路径。
具体来讲,正在发起重定向功能的方法该如何发送数据给重定向的目标方法呢?一般来讲,当一个处理器方法完成之后,该方法所指定的模型数据将会复制到请求中,并作为请求中的属性,请求会转发(forward)到视图上进行渲染。同一个请求,所以在转发的过程中,请求属性能够得以保存。
当控制器的结果是重定向的话,原始的请求就结束了,并且会发起一个新的GET请求。原始请求中所带有的模型数据也就随着请求一起消亡了。在新的请求属性中,没有任何的模型数据,这个请求必须要自己计算数据。
有一些其他方案,能够从发起重定向的方法传递数据给处理重定向方法中:
使用URL模板以路径变量和/或查询参数的形式传递数据;
通过flash属性发送数据。
4.1通过URL模板进行重定向
通过路径变量和查询参数传递数据看起来非常简单。以路径变量的形式传递了新创建Spitter的username。但是按照现在的写法,username的值是直接连接到重定向String上的。这能够正常运行,但是还远远不能说没有问题。当构建URL或SQL查询语句的时候,使用String连接是很危险的。
Spring还提供了使用模板的方式来定义重定向URL。
username作为占位符填充到了URL模板中,而不是直接连接到重定向String中,所以username中所有的不安全字符都会进行转义。这样会更加安全,这里允许用户输入任何想要的内容作为username,并会将其附加到路径上。
模型中所有其他的原始类型值都可以添加到URL中作为查询参数。作为样例,假设除了username以外,模型中还要包含新创建Spitter对象的id属性,那processRegistration()方法可以改写为如下的形式:
所返回的重定向String并没有太大的变化。但是,因为模型中的spitterId属性没有匹配重定向URL中的任何占位符,所以它会自动以查询参数的形式附加到重定向URL上。
如果username属性的值是habuma并且spitterId属性的值是42,那么结果得到的重定向URL路径将会是“/spitter/habuma?spitterId=42”。
通过路径变量和查询参数的形式跨重定向传递数据是很简单直接的方式,但它也有一定的限制。它只能用来发送简单的值,如String和数字的值。
4.2使用flash属性
Spitter对象要比String和int更为复杂。因此,我们不能像路径变量或查询参数那么容易地发送Spitter对象。它只能设置为模型中的属性。
模型数据最终是以请求参数的形式复制到请求中的,当重定向发生的时候,这些数据就会丢失。因此,我们需要将Spitter对象放到一个位置,使其能够在重定向的过程中存活下来。有个方案是将Spitter放到会话中。会话能够长期存在,并且能够跨多个请求。所以我们可以在重定向发生之前将Spitter放到会话中,并在重定向后,从会话中将其取出。当然,我们还要负责在重定向后在会话中将其清理掉。
Spring认为我们并不需要管理这些数据,相反,Spring提供了将数据发送为flash属性(flash attribute)的功能。按照定义,flash属性会一直携带这些数据直到下一次请求,然后才会消失。
Spring提供了通过RedirectAttributes设置flash属性的方法,这是Spring 3.1引入的Model的一个子接口。RedirectAttributes提供了Model的所有功能。
具体来讲,RedirectAttributes提供了一组addFlashAttribute()方法来添加flash属性。重新看一下processRegistration()方法
调用了addFlashAttribute()方法,并将spitter作为key,Spitter对象作为值。另外,我们还可以不设置key参数,让key根据值的类型自行推断得出:因为我们传递了一个Spitter对象给addFlashAttribute()方法,所以推断得到的key将会是spitter
在重定向执行之前,所有的flash属性都会复制到会话中。在重定向后,存在会话中的flash属性会被取出,并从会话转移到模型之中。
showSpitterProfile()方法所做的第一件事就是检查是否存有key为spitter的model属性。如果模型中包含spitter属性,那就什么都不用做了。这里面包含的Spitter对象将会传递到视图中进行渲染。但是如果模型中不包含spitter属性的话,那么showSpitterProfile()将会从Repository中查找Spitter,并将其存放到模型中。