一、异步请求处理的相关配置
Servlet容器配置
对于那些使用web.xml
配置文件的应用,请确保web.xml
的版本更新到3.0:
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
...
</web-app>
异步请求必须在web.xml
将DispatcherServlet
下的子元素<async-supported>true</async-supported>
设置为true。此外,所有可能参与异步请求处理的过滤器Filter
都必须配置为支持ASYNC类型的请求分派。在Spring框架中为过滤器启用支持ASYNC类型的请求分派应是安全的,因为这些过滤器一般都继承了基类OncePerRequestFilter
,后者在运行时会检查该过滤器是否需要参与到异步分派的请求处理中。
以下是一个例子,展示了web.xml
的配置:
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<filter>
<filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
<filter-class>org.springframework.~.OpenEntityManagerInViewFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>
</web-app>
如果应用使用的是Servlet 3规范基于Java编程的配置方式,比如通过WebApplicationInitializer
,那么你也需要设置"asyncSupported"标志和ASYNC分派类型的支持,就像你在web.xml
中所配置的一样。你可以考虑直接继承AbstractDispatcherServletInitializer
或AbstractAnnotationConfigDispatcherServletInitializer
来简化配置,它们都自动地为你设置了这些配置项,并使得注册Filter
过滤器实例变得非常简单。
注意过滤器的地址。
二、使用HandlerInterceptor拦截请求
Spring的处理器映射机制包含了处理器拦截器。拦截器在你需要为特定类型的请求应用一些功能时可能很有用,比如,检查用户身份等。
处理器映射处理过程配置的拦截器,必须实现 org.springframework.web.servlet
包下的 HandlerInterceptor
接口。这个接口定义了三个方法: preHandle(..)
,它在处理器实际执行 之前 会被执行; postHandle(..)
,它在处理器执行 完毕 以后被执行; afterCompletion(..)
,它在 整个请求处理完成 之后被执行。这三个方法为各种类型的前处理和后处理需求提供了足够的灵活性。
preHandle(..)
方法返回一个boolean值。你可以通过这个方法来决定是否继续执行处理链中的部件。当方法返回 true
时,处理器链会继续执行;若方法返回 false
, DispatcherServlet
即认为拦截器自身已经完成了对请求的处理(比如说,已经渲染了一个合适的视图),那么其余的拦截器以及执行链中的其他处理器就不会再被执行了。
拦截器可以通过interceptors
属性来配置,该选项在所有继承了AbstractHandlerMapping
的处理器映射类HandlerMapping
都提供了配置的接口。如下面代码样例所示:
<beans>
<bean id="handlerMapping" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="interceptors">
<list>
<ref bean="officeHoursInterceptor"/>
</list>
</property>
</bean>
<bean id="officeHoursInterceptor" class="samples.TimeBasedAccessInterceptor">
<property name="openingTime" value="9"/>
<property name="closingTime" value="18"/>
</bean>
<beans>
package samples;
public class TimeBasedAccessInterceptor extends HandlerInterceptorAdapter {
private int openingTime;
private int closingTime;
public void setOpeningTime(int openingTime) {
this.openingTime = openingTime;
}
public void setClosingTime(int closingTime) {
this.closingTime = closingTime;
}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
Calendar cal = Calendar.getInstance();
int hour = cal.get(HOUR_OF_DAY);
if (openingTime <= hour && hour < closingTime) {
return true;
}
response.sendRedirect("http://host.com/outsideOfficeHours.html");
return false;
}
}
在上面的例子中,所有被此处理器处理的请求都会被TimeBasedAccessInterceptor
拦截器拦截。如果当前时间在工作时间以外,那么用户就会被重定向到一个HTML文件提示用户,比如显示“你只有在工作时间才可以访问本网站”之类的信息。
使用
RequestMappingHandlerMapping
时,实际的处理器是一个处理器方法HandlerMethod
的实例,它标识了一个将被用于处理该请求的控制器方法。
如你所见,Spring的拦截器适配器HandlerInterceptorAdapter
让继承HandlerInterceptor
接口变得更简单了。
上面的例子中,所有控制器方法处理的请求都会被配置的拦截器先拦截到。如果你想进一步缩小拦截的URL范围,你可以通过MVC命名空间或MVC Java编程的方式来配置,或者,声明一个
MappedInterceptor
类型的bean实例来处理。
需要注意的是,HandlerInterceptor
的后拦截postHandle
方法不一定总是适用于注解了@ResponseBody
或ResponseEntity
的方法。这些场景中,HttpMessageConverter
会在拦截器的postHandle
方法被调之前就把信息写回响应中。这样拦截器就无法再改变响应了,比如要增加一个响应头之类的。如果有这种需求,请让你的应用实现ResponseBodyAdvice
接口,并将其定义为一个@ControllerAdvice
bean或直接在RequestMappingHandlerMapping
中配置。
三、视图解析
所有web应用的MVC框架都提供了视图相关的支持。Spring提供了一些视图解析器,它们让你能够在浏览器中渲染模型,并支持你自由选用适合的视图技术而不必与框架绑定到一起。Spring原生支持JSP视图技术、Velocity模板技术和XSLT视图等。
有两个接口在Spring处理视图相关事宜时至关重要,分别是视图解析器接口ViewResolver
和视图接口本身View
。视图解析器ViewResolver
负责处理视图名与实际视图之间的映射关系。视图接口View
负责准备请求,并将请求的渲染交给某种具体的视图技术实现。
21.5.1 使用ViewResolver接口解析视图
Spring MVC中所有控制器的处理器方法都必须返回一个逻辑视图的名字,无论是显式返回(比如返回一个String
、View
或者ModelAndView
)还是隐式返回(比如基于约定的返回)。Spring中的视图由一个视图名标识,并由视图解析器来渲染。Spring有非常多内置的视图解析器。下表列出了大部分,表后也给出了一些例子。
表21.3 视图解析器
视图解析器 | 描述 |
---|---|
AbstractCachingViewResolver | 一个抽象的视图解析器类,提供了缓存视图的功能。通常视图在能够被使用之前需要经过准备。继承这个基类的视图解析器即可以获得缓存视图的能力。 |
XmlViewResolver | 视图解析器接口ViewResolver 的一个实现,该类接受一个XML格式的配置文件。该XML文件必须与Spring XML的bean工厂有相同的DTD。默认的配置文件名是/WEB-INF/views.xml 。 |
ResourceBundleViewResolver | 视图解析器接口ViewResolver 的一个实现,采用bundle根路径所指定的ResourceBundle 中的bean定义作为配置。一般bundle都定义在classpath路径下的一个配置文件中。默认的配置文件名为views.properties 。 |
UrlBasedViewResolver | ViewResolver 接口的一个简单实现。它直接使用URL来解析到逻辑视图名,除此之外不需要其他任何显式的映射声明。如果你的逻辑视图名与你真正的视图资源名是直接对应的,那么这种直接解析的方式就很方便,不需要你再指定额外的映射。 |
InternalResourceViewResolver | UrlBasedViewResolver 的一个好用的子类。它支持内部资源视图(具体来说,Servlet和JSP)、以及诸如JstlView 和TilesView 等类的子类。You can specify the view class for all views generated by this resolver by using setViewClass(..) 。更多的细节,请见UrlBasedViewResolver 类的java文档。 |
VelocityViewResolver / FreeMarkerViewResolver | UrlBasedViewResolver 下的实用子类,支持Velocity视图VelocityView (Velocity模板)和FreeMarker视图FreeMarkerView 以及它们对应子类。 |
ContentNegotiatingViewResolver | 视图解析器接口ViewResolver 的一个实现,它会根据所请求的文件名或请求的Accept 头来解析一个视图。 |
我们可以举个例子,假设这里使用的是JSP视图技术,那么我们可以使用一个基于URL的视图解析器UrlBasedViewResolver
。这个视图解析器会将URL解析成一个视图名,并将请求转交给请求分发器来进行视图渲染。
<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
若返回一个test
逻辑视图名,那么该视图解析器会将请求转发到RequestDispatcher
,后者会将请求交给/WEB-INF/jsp/test.jsp
视图去渲染。
如果需要在应用中使用多种不同的视图技术,你可以使用ResourceBundleViewResolver
:
<bean id="viewResolver"
class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"/>
<property name="defaultParentView" value="parentView"/>
</bean>
ResourceBundleViewResolver
会检索由bundle根路径下所配置的ResourceBundle
,对于每个视图而言,其视图类由[viewname].(class)
属性的值指定,其视图url由[viewname].url
属性的值指定。下一节将详细讲解视图技术,你可以在那里找到更多例子。你还可以看到,视图还允许有基视图,即properties文件中所有视图都“继承”的一个文件。通过继承技术,你可以为众多视图指定一个默认的视图基类。
AbstractCachingViewResolver
的子类能够缓存已经解析过的视图实例。关闭缓存特性也是可以的,只需要将cache
属性设置为false
即可。另外,如果实在需要在运行时刷新某个视图(比如修改了Velocity模板时),你可以使用removeFromCache(String viewName, Locale loc)
方法。`
为啥没有HTML的页面视图。因为其它都是包含HTML元素的,并没有纯粹的HTML页面,都需要注入视图相关的东西。这个链接http://docs.spring.io/spring-framework/docs/4.2.4.RELEASE/spring-framework-reference/html/view.html详细介绍了各种视图,例如jsp、xml、freemark等。那怎么解决这个问题呢?前后端分离,就可以解决。
3.2视图链
Spring支持同时使用多个视图解析器。因此,你可以配置一个解析器链,并做更多的事比如,在特定条件下覆写一个视图等。你可以通过把多个视图解析器设置到应用上下文(application context)中的方式来串联它们。如果需要指定它们的次序,那么设置order
属性即可。请记住,order属性的值越大,该视图解析器在链中的位置就越靠后。
在下面的代码例子中,视图解析器链中包含了两个解析器:一个是InternalResourceViewResolver
,它总是自动被放置在解析器链的最后;另一个是XmlViewResolver
,它用来指定Excel视图。InternalResourceViewResolver
不支持Excel视图。
<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
<bean id="excelViewResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
<property name="order" value="1"/>
<property name="location" value="/WEB-INF/views.xml"/>
</bean>
<!-- in views.xml -->
<beans>
<bean name="report" class="org.springframework.example.ReportExcelView"/>
</beans>
如果一个视图解析器不能返回一个视图,那么Spring会继续检查上下文中其他的视图解析器。此时如果存在其他的解析器,Spring会继续调用它们,直到产生一个视图返回为止。如果最后所有视图解析器都不能返回一个视图,Spring就抛出一个ServletException
。
视图解析器的接口清楚声明了,一个视图解析器是可以返回null值的,这表示不能找到任何合适的视图。并非所有的视图解析器都这么做,但是也存在不得不如此的场景,即解析器确实无法检测对应的视图是否存在。比如,InternalResourceViewResolver
在内部使用了RequestDispatcher
,并且进入分派过程是检测一个JSP视图是否存在的唯一方法,但这个过程仅可能发生唯一一次。同样的VelocityViewResolver
和部分其他的视图解析器也存在这样的情况。具体的请查阅某个特定的视图解析器的Java文档,看它是否会report不存在的视图。因此,如果不把InternalResourceViewResolver
放置在解析器链的最后,将可能导致解析器链无法完全执行,因为InternalResourceViewResolver
永远都会 返回一个视图。
重定向前缀——redirect:
尽管使用RedirectView
来做重定向能工作得很好,但如果控制器自身还是需要创建一个RedirectView
,那无疑控制器还是了解重定向这么一件事情的发生。这还是有点不尽完美,不同范畴的耦合还是太强。控制器其实不应该去关心响应会如何被渲染。In general it should operate only in terms of view names that have been injected into it.
一个特别的视图名前缀能完成这个解耦:redirect:
。如果返回的视图名中含有redirect:
前缀,那么UrlBasedViewResolver
(及它的所有子类)就会接受到这个信号,意识到这里需要发生重定向。然后视图名剩下的部分会被解析成重定向URL。
这种方式与通过控制器返回一个重定向视图RedirectView
所达到的效果是一样的,不过这样一来控制器就可以只专注于处理并返回逻辑视图名了。如果逻辑视图名是这样的形式:redirect:/myapp/some/resource
,他们重定向路径将以Servlet上下文作为相对路径进行查找,而逻辑视图名如果是这样的形式:redirect:http://myhost.com/some/arbitrary/path
,那么重定向URL使用的就是绝对路径。
注意的是,如果控制器方法注解了@ResponseStatus
,那么注解设置的状态码值会覆盖RedirectView
设置的响应状态码值。
重定向前缀——forward:
对于最终会被UrlBasedViewResolver
或其子类解析的视图名,你可以使用一个特殊的前缀:forward:
。这会导致一个InternalResourceView
视图对象的创建(它最终会调用RequestDispatcher.forward()
方法),后者会认为视图名剩下的部分是一个URL。因此,这个前缀在使用InternalResourceViewResolver
和InternalResourceView
时并没有特别的作用(比如对于JSP来说)。但当你主要使用的是其他的视图技术,而又想要强制把一个资源转发给Servlet/JSP引擎进行处理时,这个前缀可能就很有用(或者,你也可能同时串联多个视图解析器)。
与redirect:
前缀一样,如果控制器中的视图名使用了forward:
前缀,控制器本身并不会发觉任何异常,它关注的仍然只是如何处理响应的问题。
@RequestMapping(path = "/files/{path}", method = RequestMethod.POST)
public String upload(...) {
// ...
return "redirect:files/{path}";
}
Spring内置对多路上传的支持,专门用于处理web应用中的文件上传。你可以通过注册一个可插拔的MultipartResolver
对象来启用对文件多路上传的支持。该接口在定义于org.springframework.web.multipart
包下。Spring为一般的文件上传提供了MultipartResolver
接口的一个实现,为Servlet 3.0多路请求的转换提供了另一个实现。
默认情况下,Spring的多路上传支持是不开启的,因为有些开发者希望由自己来处理多路请求。如果想启用Spring的多路上传支持,你需要在web应用的上下文中添加一个多路传输解析器。每个进来的请求,解析器都会检查是不是一个多部分请求。若发现请求是完整的,则请求按正常流程被处理;如果发现请求是一个多路请求,则你在上下文中注册的MultipartResolver
解析器会被用来处理该请求。之后,请求中的多路上传属性就与其他属性一样被正常对待了。【最后一句翻的不好,multipart翻译成多路还是多部分还在斟酌中。望阅读者注意此处。】
21.10.2 使用MultipartResolver与Commons FileUpload传输文件
下面的代码展示了如何使用一个通用的多路上传解析器CommonsMultipartResolver
:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 支持的其中一个属性,支持的最大文件大小,以字节为单位 -->
<property name="maxUploadSize" value="100000"/>
</bean>
当然,要让多路解析器正常工作,你需要在classpath路径下准备必须的jar包。如果使用的是通用的多路上传解析器CommonsMultipartResolver
,你所需要的jar包是commons-fileupload.jar
。
当Spring的DispatcherServlet
检测到一个多部分请求时,它会激活你在上下文中声明的多路解析器并把请求交给它。解析器会把当前的HttpServletRequest
请求对象包装成一个支持多路文件上传的请求对象MultipartHttpServletRequest
。有了MultipartHttpServletRequest
对象,你不仅可以获取该多路请求中的信息,还可以在你的控制器中获得该多路请求的内容本身。
四、文件上传 Servlet 3.0下的MultipartResolver
要使用基于Servlet 3.0的多路传输转换功能,你必须在web.xml
中为DispatcherServlet
添加一个multipart-config
元素,或者通过Servlet编程的方法使用javax.servlet.MultipartConfigElement
进行注册,或你自己定制了自己的Servlet类,那你必须使用javax.servlet.annotation.MultipartConfig
对其进行注解。其他诸如最大文件大小或存储位置等配置选项都必须在这个Servlet级别进行注册,因为Servlet 3.0不允许在解析器MultipartResolver的层级配置这些信息。
当你通过以上任一种方式启用了Servlet 3.0多路传输转换功能,你就可以把一个StandardServletMultipartResolver
解析器添加到你的Spring配置中去了:
<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
</bean>
21.10.4 处理表单中的文件上传
当解析器MultipartResolver
完成处理时,请求便会像其他请求一样被正常流程处理。首先,创建一个接受文件上传的表单将允许用于直接上传整个表单。编码属性(enctype="multipart/form-data"
)能让浏览器知道如何对多路上传请求的表单进行编码(encode)。
<html>
<head>
<title>Upload a file please</title>
</head>
<body>
<h1>Please upload a file</h1>
<form method="post" action="/form" enctype="multipart/form-data">
<input type="text" name="name"/>
<input type="file" name="file"/>
<input type="submit"/>
</form>
</body>
</html>
下一步是创建一个能处理文件上传的控制器。这里需要的控制器与一般注解了@Controller
的控制器基本一样,除了它接受的方法参数类型是MultipartHttpServletRequest
,或MultipartFile
。
@Controller
public class FileUploadController {
@RequestMapping(path = "/form", method = RequestMethod.POST)
public String handleFormUpload(@RequestParam("name") String name, @RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
byte[] bytes = file.getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
请留意@RequestParam
注解是如何将方法参数对应到表单中的定义的输入字段的。在上面的例子中,我们拿到了byte[]
文件数据,只是没对它做任何事。在实际应用中,你可能会将它保存到数据库、存储在文件系统上,或做其他的处理。特别注意这个,有的时候缺失这个,可能导致file为null。
当使用Servlet 3.0的多路传输转换时,你也可以使用javax.servlet.http.Part
作为方法参数:
@Controller
public class FileUploadController {
@RequestMapping(path = "/form", method = RequestMethod.POST)
public String handleFormUpload(@RequestParam("name") String name, @RequestParam("file") Part file) {
InputStream inputStream = file.getInputStream();
// store bytes from uploaded file somewhere
return "redirect:uploadSuccess";
}
}
21.10.5 处理客户端发起的文件上传请求
在使用了RESTful服务的场景下,非浏览器的客户端也可以直接提交多路文件请求。上一节讲述的所有例子与配置在这里也都同样适用。但与浏览器不同的是,提交的文件和简单的表单字段,客户端发送的数据可以更加复杂,数据可以指定为某种特定的内容类型(content type)——比如,一个多路上传请求可能第一部分是个文件,而第二部分是个JSON格式的数据:
POST /someUrl
Content-Type: multipart/mixed
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit
{
"name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...
对于名称为meta-data
的部分,你可以通过控制器方法上的@RequestParam("meta-data") String metadata
参数来获得。但对于那部分请求体中为JSON格式数据的请求,你可能更想通过接受一个对应的强类型对象,就像@RequestBody
通过HttpMessageConverter
将一般请求的请求体转换成一个对象一样。
这是可能的,你可以使用@RequestPart
注解来实现,而非@RequestParam
。该注解将使得特定多路请求的请求体被传给HttpMessageConverter
,并且在转换时考虑多路请求中不同的内容类型参数'Content-Type'
:
@RequestMapping(path = "/someUrl", method = RequestMethod.POST)
public String onSubmit(@RequestPart("meta-data") MetaData metadata, @RequestPart("file-data") MultipartFile file) {
// ...
}
请注意MultipartFile
方法参数是如何能够在@RequestParam
或@RequestPart
注解下互用的,两种方法都能拿到数据。但,这里的方法参数@RequestPart("meta-data") MetaData
则会因为请求中的内容类型请求头'Content-Type'
被读入成为JSON数据,然后再通过MappingJackson2HttpMessageConverter
被转换成特定的对象。
五、异常
异常 | HTTP状态码 |
---|---|
BindException | 400 (无效请求) |
ConversionNotSupportedException | 500 (服务器内部错误) |
HttpMediaTypeNotAcceptableException | 406 (不接受) |
HttpMediaTypeNotSupportedException | 415 (不支持的媒体类型) |
HttpMessageNotReadableException | 400 (无效请求) |
HttpMessageNotWritableException | 500 (服务器内部错误) |
HttpRequestMethodNotSupportedException | 405 (不支持的方法) |
MethodArgumentNotValidException | 400 (无效请求) |
MissingServletRequestParameterException | 400 (无效请求) |
MissingServletRequestPartException | 400 (无效请求) |
NoHandlerFoundException | 404 (请求未找到) |
NoSuchRequestHandlingMethodException | 404 (请求未找到) |
TypeMismatchException | 400 (无效请求) |
MissingPathVariableException | 500 (服务器内部错误) |
NoHandlerFoundException | 404 (请求未找到) |
总结:
Spring的视图解析也是设计得异常灵活。控制器一般负责准备一个Map
模型、填充数据、返回一个合适的视图名等,同时它也可以直接将数据写到响应流中。视图名的解析高度灵活,支持多种配置,包括通过文件扩展名、Accept
内容头、bean、配置文件等的配置,甚至你还可以自己实现一个视图解析器ViewResolver
。模型(MVC中的M,model)其实是一个Map
类型的接口,彻底地把数据从视图技术中抽象分离了出来。你可以与基于模板的渲染技术直接整合,如JSP、Velocity和Freemarker等,或者你还可以直接生成XML、JSON、Atom以及其他多种类型的内容。Map
模型会简单地被转换成合适的格式,比如JSP的请求属性(attribute),一个Velocity模板的模型等。