16. Web MVC 框架
16.1 Spring Web MVC 框架介绍
Spring Web 模型-视图-控制器(MVC) 框架是围绕 DispatcherServlet而设计的,其支持可配置的 handler 映射,视图解析,本地化、时区和主题的解析以及文件上传的功能。DispatcherServlet 负责将请求分发到不同的 handler。默认的 handler 通过@Controller 和 @RequestMapping注解,提供多种灵活的处理方法。若加上 @PathVariable 注解和其他辅助功能,你也可用使用 @Controller 机制来创建 RESTful web 站点和应用程序。
用 Spring Web MVC ,你不需要实现框架指定的任何接口或继承任意基类,就可以使用任意对象作为命令对象(或表单对象)。Spring 的数据绑定相当之灵活,比如,Spring可以将不匹配的类型作为应用可识别的验证错误,而不是系统错误,所以,你不需要去重复定义一套属性一致而类型是原始字符串的业务逻辑对象,去处理错误的提交或对字符串进行类型转换。反过来说就是,spring 允许你直接将正确类型的参数绑定到业务逻辑对象。
Spring 的视图解析也相当之灵活。完成一个请求,Controller 通常是负责准备一个数据模型 Map 和选择一个指定的视图,当然,也支持直接将数据写到响应流里。视图名称的解析是高度可配置的,可以通过文件扩展名、accept header 的 Content-Type、bean 的名称、属性文件或自定义的 ViewResolver 实现来解析。模型(Model,MVC 中的 M),是一个 Map 接口,提供对视图数据的完全抽象,可直接与渲染模版集成,如 JSP,Veloctiy,Freemarker;或直接生成原始数据,或xml、json等其他类型的响应内容。模型 Map 接口只是负责将数据转换为合适格式,如 jsp 请求属性,velocity 的 model 等。
16.2 The DispatcherServlet
像其他 web MVC 框架一样, Spring web MVC 框架也是基于请求驱动,围绕一个核心 Servlet 转发请求到对应的 Controller 而设计的,提供对web 程序开发的基础的支持。然而 Spring 的 DispatcherServlet 并不仅仅拥有这些,因为 Spring MVC 框架集成了 Spring IOC 容器,因此,Spring MVC 可以使用 Spring 提供的其他功能。
DispatcherServlet 继承了 HttpServlet ,是一个真实的 Servlet,因此可以在 web.xml 文件声明。另外你需要使用 url 匹配元件指定 DispatcherServlet 处理的请求。如下例子,使用了标准 java EE Servlet 配置,配置了一个 DispatcherServlet的声明和匹配 url 元件:
<web-app>
<servlet>
<servlet-name>example</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>example</servlet-name>
<url-pattern>/example/*</url-pattern>
</servlet-mapping>
</web-app>
在刚才配置的例子中,所有以 /example 开始的请求都会被名为 example 的 DispatcherServlet 所处理。在 Servlet 3.0+ 环境,也可以以编程方式配置上述 DispatcherServlet。如下代码与上述 web.xml 配置例子等效:
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet());
registration.setLoadOnStartup(1);
registration.addMapping("/example/*");
}
}
上述的操作仅仅是开启了 Spring Web MVC 之旅的第一步,现在你需要配置 Spring Web MVC 所使用到的各种 bean(这不在本节讨论范围)。
在Spring里可以获取到 ApplicationContext 实例,在 web MVC 框架,每一个 DispatcherServlet 都拥有自己的 WebApplicationContext,这个 WebApplicationContext 继承了根 WebApplicationContext 定义的所有 bean.
DispatcherServlet 在初始化时,Spring MVC 会查找 web 应用 WEB-INF 目录下的[servlet-name]-servlet.xml 并创建在此文件定义的 bean,若在全局范围里有一个名称相同的 bean,全局范围的 bean 会被覆盖掉。
<web-app>
<servlet>
<servlet-name>golfing</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>golfing</servlet-name>
<url-pattern>/golfing/*</url-pattern>
</servlet-mapping>
</web-app>
上述配置,要求应用程序在 WEB-INF 目录下有一个 golfing-servlet.xml 文件,在这个文件里,会包含 Spring MVC 的所有组件(beans)。你可以通过定义 servlet 初始化参数来改变[servlet-name]-servlet.xml 文件的路径,如下:
<web-app>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
6.2.1 WebApplicationContext 的专用 bean
DispatcherServlet 使用了其专用的 bean 来处理请求和渲染视图。这些 bean 是 Spring 的组成部分之一,你可以选择在 WebApplicationContext配置所使用一个或多个专用的bean。当然,你并不需要一开始就去配置这些专用的 bean,因为在你不配置这些 bean时,Spring 会维护一系列默认的 bean。首先我们看一下 DispatcherServlet 依赖了哪些专用的 bean,后续再作详解。
Bean 类型 | 解释 |
---|---|
HandlerMapping | 将传入的请求映射到处理器,与一系列基于各种条件的 pre- 和 post- 处理器,这些处理器根据 HandlerMapping 实现的不同而会有所差异。 |
HandlerAdapter | 帮助 DispatcherServlet 去调用请求所映射的 handler,不管hadler 最终是否会被调用,这个处理过程都会存在的。比如,调用注解控制器前需要解析各种 annotations。因此,HandlerAdapter 的主要目的就是从 DispatcherServlet 中屏蔽这些处理细节。 |
HandlerExceptionResolver | 将异常映射到指定视图,也支持自定义更加复杂的异常处理流程 |
ViewResolver | 将合理的视图名称解释为真实的视图类型 |
ThemeResolver | 解释 web 程序可用的主题,比如,提供个性化的布局 |
MultipartResolver | 解释 multi-part 请求,比如,在 html form 里支持文件上传 |
16.2.2 默认的 DispatcherServlet 配置
如上一节所说,每一个 DispatcherServlet 都维持了一系列默认的实现。这些默认实现的信息保存在 org.springframework.web.servlet 包里的 DispatcherServlet.properties 文件。
尽管所有专用的 bean 都有其合理的默认值。迟早你也需要根据实际去自定义这些 bean 的中一个或多个属性值。例如一种很常见的自定义应用,配置一个 InternalResourceViewResolver,其 prefix 为视图文件的父文件夹。
不管这些默认细节如何实现,在这里都需要清楚一个概念——一旦在 WebApplicationContext 配置自己专用的 bean,就有效覆盖了原有一系列默认的实现,至少也会作为这个专用 bean 的一个实例。
16.2.3 DispatcherServlet 处理顺序
在你建立一个 DispatcherServlet 之后,并处理一个传进来的请求时,DispatcherServlet 会按照以下顺序年来处理这个请求:
- 寻找 WebApplicationContext,并将 WebApplicationContext作为一个属性绑定到请求里,以便控制器或其他原件在后续中使用。默认会以 DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE 键绑定到请求里。
- 将本地化解析器绑定到请求里,以便在处理这个请求时,原件可以解析到客户端的地区(为了渲染视图,准备日期等)。如果你不需要本地化解析器,可以忽略这个步骤。
- 将主题解析其绑定到请求里,让原件(如视图)决定去使用哪一种主题。如果你不需要使用主题,可以忽略这个步骤。
- 如果你指定一个 multipart file 解析器,会检查这个请求包含 multiparts 请求。当发现了 multiparts,这个请求会被封装为 MultipartHttpServletRequest 对象,提供给后续原件处理。
- 寻找合适的 handler。如何找到这个 handler,执行与这个 handler 关联的执行链,目的是准备一个 model 或 渲染。
- 如果返回一个 model,渲染相对应的视图。反之(可能是因为 pre- 或 post- 处理器拦截了这个请求,也可能是权限问题),便不渲染任何视图,因为这个请求可能已执行完成。
handler 异常解析是在 WebApplicationContext 声明的,接收在上述处理过程抛出的异常。使用异常解析器,你可以根据异常信息自定义其处理方式。
16.3 实现控制器(Controller)逻辑
Controller层面主要是控制web请求的路由和视图解析,Spring对于Controller的支持是十分完善的,我们在定义路由逻辑的时候并要求我们继承和实现某个类,只需要添加Spring的注解信息就可以做到。下面我么你先看一个简单的Controller定义逻辑:
@Controller
public class HelloWorldController {
@RequestMapping("/helloWorld")
public String helloWorld(Model model) {
model.addAttribute("message", "Hello World!");
return "helloWorld";
}
}
如你所见,@Controller 和 @RequestMapping 允许灵活的配置方法签名。在上述例子中,helloWorld 方法接受一个 Model 参数,并返回一个视图名称,当然也允许添加方法入参和返回不同类型的值,这些内容将会在后面解释。@Controller 、@RequestMapping 和其他一些功能注解组成了 Spring MVC 实现的基础,这一节将会谈到这些组成的注解和在 Servlet 环境的普遍用法。
16.3.1 使用 @Controller 定义控制器
@Controller 表明了被注解类的服务角色——控制器。Spring 不需要去继承任何 Controller 的基类或引用任意的 Servlet API。当然了,如何你需要的, 你仍然可以引用 Servlet API。
@Controller 注解定义了被注解类的原型,表明了注解类的服务角色。dispatcher 会扫描这些被 @Controller 标记的类并检测 @RequestMapping 标记的方法(见下一节)。
你可以在 dispatcher 上下文显式定义控制器 bean,不过,为了与 Spring 支持在类路径上检测 bean 并自动注册这些 bean 定义 保持一致,@Controller 也许允许自动检测。
要开启注解控制器的扫描功能,需要在你的配置里添加组件扫描元件。如下 xml 所示,可以使用 spring-context 模式开启此扫描功能:
<context:component-scan base-package="org.springframework.samples.petclinic.web"/>
6.3.2 使用 @RequestMapping 映射请求
你可以在类或指定 handler 方法上,使用 @RequestMapping 注解来映射 URL,如 /appointments。定义在类上以为这该类下的所有的方法级别的@RequestMapping将以类上定义的路径为基础path前缀,如果类上没有定义的话,将直接使用方法级别的value值作为匹配的path。
@Controller
@RequestMapping("/appointments")
public class AppointmentsController {
private final AppointmentBook appointmentBook;
@Autowired
public AppointmentsController(AppointmentBook appointmentBook) {
this.appointmentBook = appointmentBook;
}
@RequestMapping(method = RequestMethod.GET)
public Map<String, Appointment> get() {
return appointmentBook.getAppointmentsForToday();
}
@RequestMapping(value="/new", method = RequestMethod.GET)
public AppointmentForm getNewForm() {
return new AppointmentForm();
}
}
例子中,在多处地方使用 @RequestMapping。第一个用在了类上,表示@RequestMapping 这个控制器下的所有 handler 方法都是相对 /appointments 路径而言的。get() 方法对 @RequestMapping 做了进一步的细化 —— 此方法只接收 GET 请求方式(@RequestMapping 默认匹配所有的 http 方法),换句话说就是 /appointments 的GET 请求会调用这个方法; add() 方法也做一个类似的细化; getNewForm() 方法在 RequestMapping 上组合定义了 http 方法和路径,因此此方法会处理 appointments/new 的 GET 请求。
URI 模版模式
URI 模版是一个类似于 URI 的字符串,其中包含了一个或多个变量。当你将这些变量替换掉市,就变回了 URI可在方法入参上使用注解 @PathVariable 绑定 URI 的模版参数:
@RequestMapping(value="/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable String ownerId, Model model) {
Owner owner = ownerService.findOwner(ownerId);
model.addAttribute("owner", owner);
return "displayOwner";
}
URI 模版 " /owners/{ownerId}" 指定了参数 owernId。当控制器处理这个请求时,会将 URI 中匹配的部分赋值给 owernId 变量。如,当传入 /owners/fred 请求时,owernId 的值就是 fred。
在处理 @PathVariable 注解时,Srping MVC 是根据名称来匹配 URI 模版变量的。你可以在注解里指定这个名称:
@RequestMapping(value="/owners/{ownerId}", method=RequestMethod.GET)
public String findOwner(@PathVariable("ownerId") String theOwner, Model model) {
// implementation omitted
}
如果URI 模版变量名和入参名一致,可以省略这个细节。只要你的代码不是不带调试信息的编译,Spring MVC 将匹配入参名和 URI 变量名。
一个方法可以有任意个 @PathVariable 注解。
@RequestMapping(value="/owners/{ownerId}/pets/{petId}", method=RequestMethod.GET)
public String findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
Owner owner = ownerService.findOwner(ownerId);
Pet pet = owner.getPet(petId);
model.addAttribute("pet", pet);
return "displayPet";
}
URI 模版可以组合类型和参数路径的 @RequestMapping。因此,findPet 可以处理类似 /owners/42/pets/21 的URI 。
@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {
@RequestMapping("/pets/{petId}")
public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
// implementation omitted
}
}
@PathVariable 参数可以是任意的简单类型(如 int,long,Date 等),Spring 会自动将其进行类型转换,转换出错会抛出 TypeMismatchException。你也可以注册支持解析其他数据类型,这个内容后面会涉及到。
在 URI 模版上使用正则表达式
偶尔,在URI 模版变量里,你会需要用到更加精确的控制。比如 "/spring-web/spring-web-3.0.5.jar" 这样的URI,该如何拆分成多个部分?
@RequestMapping 注解支持在 URI 模版变量里使用正则表达式。语法 {变量名:正则表达式},第一个部分定义变量的名称,第二部分是正则表达式。如
@RequestMapping("/spring-web/{symbolicName:[a-z-]}-{version:\\d\\.\\d\\.\\d}{extension:\\.[a-z]}")
public void handle(@PathVariable String version, @PathVariable String extension) {
// ...
}
}
路径模式比较
当一个 URL 与多个模式匹配时,会设法找出最具体的那一个路径。
当模式中的 URI 模版变量和通配符的数量相对较少,会认为其相对具体。如:/hotels/{hotel}/* 相对 /hotels/{hotel}/** 更加合适,因为 /hotels/{hotel}/* 只有一个URI 模版变量和一个通配符,而 hotels/{hotel}/**` 有一个 URI 模版变量和两个通配符。
当两个模式中的 URI 模版变量和通配符数量相同时,更详细的那一个会认为相对适合。如 /foo/bar* 比 /foo/* 更为详细。
一些额外的特别规定:
- 任意模式都比默认全匹配 /** 模式具体。如:/api/{a}/{b}/{c} 比 /** 更加具体。
- 任意不包含两个通配符的模式都比前缀模式(如 /public/) 更加具体。/public/path3/{a}/{b}/{c} 比 /public/ 更加具体。
路径模式的后缀匹配
Spring MVC 默认自动执行 "." 的后缀匹配,所以当一个控制器匹配 /person 时,其也隐式匹配 /person.。这样的设计允许通过文件扩展名来说明内容的类型名比如 /person.pdf, /person.xml 等。然而,这里会有一个常犯的陷阱,当路径最后的片段是 URI 模版变量时(如 /person/{id}),请求 /person/1.json 可以正确匹配路径,变量 id=1,拓展名为 json,可当 id 自身包含 . (如 /person/joe@email.com),那匹配结果就不是我们所期望的,显然 ".com" 不是文件扩展名。
解决这个问题的正确方法是配置 Spring MVC 只对注册的文件扩展名做后缀匹配,这要求内容(扩展名)协商好。
矩阵变量
矩阵变量是我之前完全没有接触过的一种内容,后来读了文档之后才知道竟然还有这种操作,这里也特别记录一下矩阵变量。
矩阵变量可以出现在任何path上面,矩阵变量之间通过";"(英文分号)来区分,举个例子"/cars;color=red;year=2012",矩阵变量如果有多个值的话,值之间可以通过,(英文逗号)来分割,例如"color=red,green,blue"。
如果一个URL中出现矩阵变量的话,那么请求映射模式必须用URI模板表示它们。这确保了无论矩阵变量是否存在,以及它们以什么顺序被提供,请求都能被正确匹配。
// GET /pets/42;q=11;r=22
@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET)
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
由于所有路径段都可能包含矩阵变量,因此在某些情况下,您需要更具体地确定变量的预期位置:
// GET /owners/42;q=11/pets/21;q=22
@RequestMapping(value = "/owners/{ownerId}/pets/{petId}", method = RequestMethod.GET)
public void findPet(
@MatrixVariable(value="q", pathVar="ownerId") int q1,
@MatrixVariable(value="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
-----------------------------------------
// GET /pets/42
@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET)
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
------------------------------------------
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@RequestMapping(value = "/owners/{ownerId}/pets/{petId}", method = RequestMethod.GET)
public void findPet(
@MatrixVariable Map<String, String> matrixVars,
@MatrixVariable(pathVar="petId"") Map<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 11, "s" : 23]
}
默认情况下矩阵变量是不被spring激活的,如果你想使用矩阵变量的话,你需要在配置Spring的时候激活一下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven enable-matrix-variables="true"/>
</beans>
可消费的媒体类型
你可以指定一系列可消费的媒体类型来压缩主要映射。这样只用当 Content-Type 请求头匹配可消费的媒体类型,才认为这个请求是可映射的。如:
@Controller
@RequestMapping(value = "/pets", method = RequestMethod.POST, consumes="application/json")
public void addPet(@RequestBody Pet pet, Model model) {
// 实现省略
}
请求参数和头字段值
你可以通过请求参数条件来压缩请求匹配范围,如使用 "myParam", "!myParam", 或 "myParam=myValue"。前两种情况表示 存在/不存在,第三种指定了参数值。如下给出指定参数值的例子:
@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {
@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET, params="myParam=myValue")
public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
// 省略实现
}
}
类似的,头字段也支持 存在/不存在 和基于指定头字段值的匹配:
@Controller
@RequestMapping("/owners/{ownerId}")
public class RelativePathUriTemplateController {
@RequestMapping(value = "/pets", method = RequestMethod.GET, headers="myHeader=myValue")
public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
// 省略实现
}
}
16.3.3 定义 @RequestMapping 处理方法
@RequestMapping 处理方法允许非常灵活的签名,其支持方法参数和返回值(在这一节谈到)。除了 BindingResult 参数,其他类型参数顺序随意.
如下是可以支持的方法参数:
- Request 或 response 对象 (Servlet API). 选择任意指定的 request 或 response 类型,如ServletRequest o或 HttpServletRequest.
- Session 对象 (Servlet API):需要是 HttpSession 类型. 这种类型的参数会强制合适 session 的存在。因此,这个参数永远不会为 null。
- org.springframework.web.context.request.WebRequest 或 org.springframework.web.context.request.NativeWebRequest.允许通过请求参数访问和 request/session 属性访问
- java.util.Locale 给当前请求本地化,取决于最具体的本地化解析器,实际上取决与是 Servlet 环境配置的 LocaleResolver 。
- java.io.InputStream / java.io.Reader 可访问请求的内容。这是 Servlet API 暴露的原生 InputStream/Reader 。
- java.io.OutputStream / java.io.Writer 用于 产生 response 的内容。这是 Servlet API 暴露的原生 OutputStream/Writer.
- org.springframework.http.HttpMethod 可访问 HTTP 请求方法。
- @PathVariable 注解参数,可访问 URI 模版变量。
- @MatrixVariable 矩阵变量
- @RequestParam 注解参数,可访问指定 Servlet request 参数。参数值会被转换为方法参数的类型。
- @RequestHeader 注解参数,可访问指定 Servlet request 的 HTTP 头字段。参数值会被转换为方法参数的类型。
- @RequestBody 注解参数,可访问 HTTP 请求体。参数值使用 HttpMessageConverter 转换为方法参数类型
- @RequestPart 注解参数,可访问 "multipart/form-data" 请求的内容。
- HttpEntity<?> 参数,可访问 Servlet request 的HTTP 头和内容。
以下是可支持的返回类型
- 一个ModelAndView对象,该模型隐式地丰富了命令对象和@ModelAttribute注解引用数据访问器方法的结果
- 一个Model对象,其中view的名字是通过RequestToViewNameTranslator隐式声明的,该模型隐式地丰富了命令对象和@ModelAttribute注解引用数据访问器方法的结果
- 一个Map对象,内容跟model对象基本一致。
- 一个View对象,其中model的信息隐式地丰富了命令对象和@ModelAttribute注解引用数据访问器方法的结果。
- 一个String对象,内容基本跟View对象的内容差不多。
- 返回void,如果方法内容已经自己处理了response内容或者想依赖RequestToViewNameTranslator来生成对应的view名称的话,这种情况下可以返回void
- 如果方法本身被@ResponseBody注释了,这时候返回的类型会被直接写入到响应体里面,这时候返回信息会根据方法声明的类型,通过HttpMessageConverter来进行转换。
- 一个HttpHeaders对象,可以通过此对象拿到响应头信息。
- 一个Callable<?>对象,一个异步结果信息。
使用 @RequestParam 将请求参数绑定到方法参数
在控制器里,使用 @RequestParam 将请求参数绑定到方法参数。
@Controller
@RequestMapping("/pets")
public class EditPetForm {
@RequestMapping(method = RequestMethod.GET)
public String setupForm(@RequestParam("petId") int petId, ModelMap model) {
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
}
使用 @RequestParam 的参数默认是必须提供的,当然,你可以指定其为可选的,将 @RequestParam 的 reqired 属性设置 false 即可。(如, @RequestParam(value="id", required=false)).
如果方法参数的类型不是 String,类型转换会自动执行,如果将 @RequestParam 用于 Map<String, String> 或 MultiValueMap<String, String> 参数,此参数 map 会填充所有的请求参数。
使用 @RequestBody 映射请求体
@RequestBody 注解参数表示该参数将与 HTTP 请求体绑定。例子:
@RequestMapping(value = "/something", method = RequestMethod.PUT)
public void handle(@RequestBody String body, Writer writer) throws IOException {
writer.write(body);
}
@RequestBody 方法参数可添加 @Valid 注解,被注解的参数会使用配置的 Validator 来验证。当使用 MVC 命名空间或 mvc Java 配置时,应用会自动配置 JSR-303 验证器(前提是在类路径能找到 JSR-303 的实现)。
类似于 @ModelAttribute 参数,Errors 参数也可用来检测错误。当 Errore 参数没有声明时,或抛出 MethodArgumentNotValidException。此异常会被 DefaultHandlerExceptionResolver 处理 —— 向客户端发送 400 错误。
使用 @ResponseBody 映射响应体
@ResponseBody 的使用类似于 @RequestBody。此注解用在方法上,用来表示直接将返回数据写到 HTTP 响应体里。注意,不是将数据放到 Model 中,或解析为视图名称。例子:
@RequestMapping(value = "/something", method = RequestMethod.PUT)
@ResponseBody
public String helloWorld() {
return "Hello World";
}
上述例子会将 Hello World 文本写到 HTTP 响应流中。
使用 @RestController 创建 REST 控制器
一种比较常见的场景,控制器实现 REST API,只会返回 JSON、XML 或其他自定义媒体类型。为了方便,你可以在控制器上添加 @RestController 注解,而不是在每一个 @RequestMapping 上使用 @ResponseBody。
@RestController 是一个结合了 @ResponseBody 和 @Controller 的注解。不仅如此,@RestController 赋予了控制器更多的意义,在未来的版本中可能会携带额外的语义。。
使用 HttpEntity
HttpEntity 的用法类似于 @RequestBody 和 @ResponseBody 注解。除了可以访问请求/响应体,HttpEntity(和特用与响应的子类 ResponseEntity) 还可以访问 request 和 response 的头字段。例子:
@RequestMapping("/something")
public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntity) throws UnsupportedEncodingException {
String requestHeader = requestEntity.getHeaders().getFirst("MyRequestHeader"));
byte[] requestBody = requestEntity.getBody();
// do something with request header and body
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("MyResponseHeader", "MyValue");
return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
}
上述例子获取了 MyRequestHeader 头字段的值,以字节数组的形式读取了请求体,随后将 MyRequestHeader 添加到 response,将 Hello World 写到响应流和设置响应状态码为 201(Created).
在方法上使用 @ModelAttribute
@ModelAttribute 可用欲方法或方法参数中。这一部分将介绍 @ModelAttribute 在方法中的使用,下一部分介绍其在方法啊参数中的使用。
在方法上使用 @ModelAttribute 注解,表示此方法的目的在于添加一个或多个模型属性。这种方法所支持的参数类型与 @RequestMapping 一样,不同的是,其不能直接映射到 request。另外,在同一个控制器里,@ModelAttribute 会在 @RequestMapping 之前调用。
// 添加一个属性
// 方法的返回值会以 "account" 键添加到 model
// 可通过 @ModelAttribute("myAccount") 自定义
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountManager.findAccount(number);
}
// 添加多个属性
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountManager.findAccount(number));
// 再添加多个……
}
第一种,在方法里隐式添加一个属性并返回;第二种,方法里接收 Model 参数,并将任意个属性添加到 Model中。一个控制器可以有多个 @ModelAttribute 方法。在同一个控制器中,所有 @ModelAttribute 方法都会在 @RequestMapping 方法之前调用。
@ModelAttribute 注解也可用在 @RequestMapping 方法中。这种情况下,@RequestMapping 方法的返回值将解析为模型属性,而不是视图名称。相反,视图名称来源于视图名称的约定,就类似于方法返回 void
指定 redirect 和 flash 属性
在重定向 URL 中,所有模型属性默认暴露给 URI 模版变量,剩下的属性(原始类型或原始类型集合/数组)会自动拼接到查询参数中。
然而,在一个带注解的控制器中,模型也许包含了额外的属性(用于渲染,如下拉框属性)。在重定向场景中,要准确控制这些属性,可在 @RequestMapping 方法中声明 RedirectAttributes 类型参数,并往其添加 RedirectView 使用的属性。如果这个控制方法的确发生重定向,将使用 RedirectAttributes 的内容,否则使用默认 Model 的内容。
RequestMappingHandlerAdapter 提供了一个 "ignoreDefaultModelOnRedirect" 标志,用来设置在控制方法重定向时,默认Model 的内容是否从不使用。相反,控制器方法应该声明 RedirectAttributes 类型属性,否则会没有任何属性传递给 RedirectView。为了向后兼容,MVC 命名空间和 MVC Java 配置都将 "ignoreDefaultModelOnRedirect" 设置为 false。可我们还是建议你在新应用里将其设置为 true。
RedirectAttributes 接口也可以用来添加 flash 属性。与其他重定向属性(在重定向 URL 中销毁)不同的是,flash 属性会保存到 HTTP session(因此 flash 属性也不会在 URL 上出现)。作用于重定向 URL 的控制器里的模型会自动接收这些 flash 属性,之后,flash 属性会从 session 中移除。
使用 @CookieValue 映射 cookie 值
@CookieValue 注解允许将方法参数与HTTP cookie 值绑定。
@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@CookieValue("JSESSIONID") String cookie) {
//...
}
如果方法参数不是 String 类型,类型转换会自动执行
使用 @RequestHeader 映射请求头字段属性
@RequestHeader 注解允许将方法参数与请求头字段绑定。
如下一个请求头字段值的样例:
Host localhost:8080
Accept text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding gzip,deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive 300
如下代码演示了如何获取 Accept-Encoding 和 Keep-Alive 头字段值:
@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@RequestHeader("Accept-Encoding") String encoding,
@RequestHeader("Keep-Alive") long keepAlive) {
//...
}
使用 @ControllerAdvice 注解增强控制器
@ControllerAdvice 注解可以让实现类通过类路径自动检测出来。当使用 MVC 命名空间或 MVC Java 配置时,此此功能是默认启动的。
带有 @ControllerAdvice 注解的类,可以包含 @ExceptionHandler、@InitBinder, 和 @ModelAttribute 注解的方法,并且这些注解的方法会通过控制器层次应用到所有 @RequestMapping 方法中,而不用一一在控制器内部声明。
// 应用到所有 @RestController 控制器
@ControllerAdvice(annotations = RestController.class)
public class AnnotationAdvice {}
// 应用到指定包下的控制器
@ControllerAdvice("org.example.controllers")
public class BasePackageAdvice {}
// 应用到指定类型的控制器
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class AssignableTypesAdvice {}
16.3.4 异步请求处理
Spring MVC 引入了基于异步请求的 Servlet 3。在异步请求中,控制器方法通常会返回 java.util.concurrent.Callable 对象后再使用一个独立的线程产生返回值,而不是直接返回一个值。同时释放 Servlet 容器的主线程和允许处理其他请求。Spring MVC 借助 TaskExecutor ,在一个独立线程中调用 Callable,当 Callable 返回时,将请求转发到 Servlet 容器并继续处理 Callable 返回值。例子如下:
@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public String call() throws Exception {
// ...
return "someView";
}
};
}
16.3.4 异步请求处理
Spring MVC 引入了基于异步请求的 Servlet 3。在异步请求中,控制器方法通常会返回 java.util.concurrent.Callable 对象后再使用一个独立的线程产生返回值,而不是直接返回一个值。同时释放 Servlet 容器的主线程和允许处理其他请求。Spring MVC 借助 TaskExecutor ,在一个独立线程中调用 Callable,当 Callable 返回时,将请求转发到 Servlet 容器并继续处理 Callable 返回值。例子如下:
@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public String call() throws Exception {
// ...
return "someView";
}
};
}
异步请求的另外一种方式,是让控制器返回 DeferredResult 实例。这种情况下,依然是从一个独立线程处理并产生返回值。然而,Spring MVC 并不知晓这个线程的后续处理。比如说,这个返回结果可以用来响应某些外部事件(如 JMS 信息,计划任务等)。例子如下:
@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
// 将 deferredResult 保存到内存队列
return deferredResult;
}
// 在其他线程中...
deferredResult.setResult(data);
如果不了解 Servlet 3 异步处理的细节,理解起来可能会一定的难度.
- 一个 ServletRequest 请求可通过调用 request.startAsync() 方法设置为异步模式。此步骤最主要的作用是,在此 Servlet 和其他过滤器退出的情况下,response 依然可以保持打开状态,以便其他线程来完成处理。
- 调用 request.startAsync() 方法返回一个 AsyncContext。在异步处理中,AsyncContext 可以用来做进一步的控制。比如说,AsyncContext 提供的 dispatch 方法,可以在应用线程中调用,将请求转发回 Servlet 容器。异步 dispatch(转发)类似于平时使用的 forward 方法。不同的是,异步 dispatch(转发)是从应用里的一个线程转发到 Servlet 容器中的另一个线程,而 forward 方法则是在 Servlet 容器里的同一个线程间转发。
- ServletRequest 可以定位当前的 DispatcherType(转发类型),此功能可以用于判断 Servlet 或 Filter 是在原始请求线程上处理请求,还是在异步转发线程中处理。
记住以上事实之后,接着了解一下异步请求处理 Callable 的过程:(1) 控制器返回一个 Callable ,(2) Spring MVC 开始异步处理,将 Callable 提交给 TaskExecutor,TaskExecutor 在一个独立线程中处理,(3) DispatcherServlet 和所有过滤器退出请求处理线程,不过保持 response 为打开状态,(4) Callable 产生一个结果之后,Spring MVC 将这个请求转发回 Servlet 容器,(5) 再次调用 DispatcherServlet,并重新处理 Callable 异步产生的结果。(2),(3),(4) 的准确顺序在不同情况下可能有所不同,这个取决于并发线程的处理速度。
异步请求处理 DeferredResult 的事件顺序大体上和处理 Callable 的顺序相同。不同的是,这里是由应用程序的某些线程来处理异步结果:(1) 控制器返回一个 DeferredResult 对象,并将其保存到可访问的内存队列或列表中,(2) Spring MVC 开始异步处理,(3) DispatcherServlet 和所有过滤器退出请求处理线程,不过保持 response 为打开状态,(4) 应用程序在某些线程中设置 DeferredResult,之后 Spring MVC 将这个请求转发回 Servlet 容器,(5) 再次调用 DispatcherServlet,并重新处理异步产生的结果。
当控制器返回的 Callable 在执行时反生了异常,会出现什么情况?这种情况类似于控制器发生异常时的情况。所出现的异常会由同一控制器里的 @ExceptionHandler 方法处理,或由所配置的 HandlerExceptionResolver 实例来处理。如果是执行 DeferredResult 时出现异常,你可以选择调用 DeferredResult 提供的 setErrorResult(Object) 方法,该方法须提供一个异常或其他你设置设置的对象。 当结果是一个 Exception 时,会由同一控制器里的 @ExceptionHandler 方法处理,或由所配置的 HandlerExceptionResolver 实例来处理。
16.4 Handler 映射
16.4.1 使用 HandlerInterceptor 拦截请求
Spring 的 handler 映射机制包含了 handler 拦截器。使用handler 拦截器,可以在某些的请求中应用的特殊的功能,比如说,检查权限。
handler 映射的拦截器必须实现 HandlerInterceptor 接口(此节接口位于 org.springframework .web.servlet 包中)。这个接口定义了三个方法:preHandle(..) 在 handler 执行前调用;postHandle(..) 在handler 执行后调用;afterCompletion(..) 在整一个请求完成后调用。这三个方法基本足够应对各种预处理和后处理的状况。
preHandle(..) 方法返回一个 boolean 值。你可以使用这个方法来中断或继续处理 handler 执行链。当此方法返回 true 时,hadler 执行链会继续执行;反之,DispatcherServlet 会认为此拦截器已处理完成该请求(和渲染一个视图),之后不再执行余下的拦截器,也不在执行 handler 执行链。
可以使用 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>
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;
}
}
注意,HandlerInterceptor 的 postHandle 方法不一定适用于@ResponseBody和ResponseEntity方法。在这种情况下,HttpMessageConverter 实例会在 postHandle 方法执行之前就将数据写到 response 并提交 response,所以 postHandle 方法不可能再处理 response(如添加一个 Header)。相反,应用程序可以实现 ResponseBodyAdvice ,将其声明为 @ControllerAdvice bean 或将其直接在 RequestMappingHandlerAdapter 中配置它。
16.5 视图解析
所有 web 应用的 MVC 框架都会提了视图解析的方案,Spring 提供的视图解析,可以让你在不指定特定视图技术的前提下,便可在浏览器中渲染模型。Spring 支持使用 USP,Veloctiy 模板和 XSLT 视图技术,这些视图技术都是开箱即用的。查看Chapter 17, 视图技术,可以了解到如何集成和使用多种不同的视图技术。
ViewResolver 和 View 是 Spring 处理视图的两个重要接口。当中,ViewResolver 提供了视图名称和真实视图之间的映射,View 则是负责解决某个视图的技术的请求预处理和请求的后续处理。
16.5.1 使用 ViewResolver 接口解析视图
Spring web MVC 中的所有 handler 方法都需要解析某一个逻辑视图名称,可以是显式的,如如返回 String, View, 或 ModelAndView 实例,也可以是隐式的(这个需基于事先约定)。
举个例子,解析 JSP 视图技术,可以使用 UrlBasedViewResolver 解析器。此解析器会将视图名称转换为 url,和传递请求到 RequestDispatcher,以便渲染视图。
<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,接着 RequestDispatcher 将请求发送到 /WEB-INF/jsp/test.jsp。
16.5.2 视图解析器链
Spring 提供多种视图技术。因此,你可以定义解析器链,比如,可在某些情况下覆盖指定视图。可通过在应用上下文中添加多个解析器来定义解析器链,如有需要的,也可指定这些解析器的顺序。记住,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>
<beans>
<bean name="report" class="org.springframework.example.ReportExcelView"/>
</beans>
如果一个视图解析器不能导出一个视图,Spring 会检索上下文,查找其他视图解析器。如果查找到其他视图解析器,Spring 会继续处理,直到有解析器导出一个视图。如果没有解析器返回一个视图,Spring 会抛出 ServletException。
视图解析协议规定视图解析器可以返回 null,表示没有找到指定的视图。然而,不是所有的视图解析器返回null,都表示没有找到视图。因为某些情况下,视图解析器也无法检测视图是否存在。比如,InternalResourceViewResolver 在内部逻辑里使用 RequestDispatcher,如果 JSP 文件存在,那分发是唯一可以找到 JSP 文件的方式,可分发只能执行一次。VelocityViewResolver 和其他解析器也类似。
16.5.4 ContentNegotiatingViewResolver
ContentNegotiatingViewResolver 自身并没有去解析视图,而是将其委派给其他视图解析器,选择指定响应表述返回给客户端。有以下两种策略,允许客户端请求指定表述方式的资源:
- 不同的资源使用不同的 URI 表示:通常使用文件拓展名来表示不同的响应表述,如 http://www.example.com/users/fred.pdf 请求用户 fred 的 pdf 视图表述,http://www.example.com/users/fred.xml 则是请求用户 fred 的 xml 视图表述。
- 使用相同的 uri 加载指定资源,不过使用 Accept HTTP 请求头表示其请求的http://en.wikipedia.org/wiki/Internet_media_type[媒体类型]。如uri http://www.example.com/users/fred,当 Accept 为 application/pdf ,表示请求用户 fred 的pdf视图表述,当 Accept 为 application/xml 表示请求用户 fred 的xml 视图表述。这种策略也可称为 内容协商.
为了支持同一资源的多种表述,Spring 提供了 ContentNegotiatingViewResolver,可根据文件拓展名或 Accept 头字段值选择指定资源的表述.ContentNegotiatingViewResolver 自身并解释视图,而是将其转发给通过 ViewResolvers 属性配置的视图解析器.
如下,是一个 ContentNegotiatingViewResolver 配置样例:
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="mediaTypes">
<map>
<entry key="atom" value="application/atom+xml"/>
<entry key="html" value="text/html"/>
<entry key="json" value="application/json"/>
</map>
</property>
<property name="viewResolvers">
<list>
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</list>
</property>
<property name="defaultViews">
<list>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" />
</list>
</property>
</bean>
<bean id="content" class="com.foo.samples.rest.SampleContentAtomView"/>
InternalResourceViewResolver 处理试图名称的解析和 JSP 页面,另外,BeanNameViewResolver会返回基于 bean 名称的视图(可参考 "使用 ViewResolver 解析视图,了解 Spring 如何寻找和初始化一个视图")。在上述例子中,content bean 是一个继承 AbstractAtomFeedView 的类,这个bean 可以返回一个 RSS 原子.
在上述配置中,当请求是由 .html拓展名组成时,视图解析器会去寻找一个匹配 text/html 的视图。InternalResourceViewResolver 提供了 text/html 的映射。当请求是由 .atom 拓展名组成时,视图解析器会去寻找一个匹配 .atom 的视图。这个视图 BeanNameViewResolver 有提供,若视图名称为 content,则映射到 SampleContentAtomView。当请求是由 .json拓展名组成时,会选择 DefaultViews 提供的 MappingJackson2JsonView 接口,注意这个映射与视图名称无关。另外,客户端的请求不带拓展名,通过 Accept 头字段指定媒体类型时,也会执行和文件拓展名一样的处理逻辑。
可以根据 http://localhost/content.atom 或 http://localhost/content和Accept字段值为 application/atom+xml 两种情况,返回原子视图的控制器代码如下:
@Controller
public class ContentController {
private List<SampleContent> contentList = new ArrayList<SampleContent>();
@RequestMapping(value="/content", method=RequestMethod.GET)
public ModelAndView getContent() {
ModelAndView mav = new ModelAndView();
mav.setViewName("content");
mav.addObject("sampleContentList", contentList);
return mav;
}
}
16.7 构建 URI
Spring MVC 提供了构建和编码 URI 的机制,这种机制的使用需要通过 UriComponentsBuilder 和 UriComponents.
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
"http://example.com/hotels/{hotel}/bookings/{booking}").build();
URI uri = uriComponents.expand("42", "21").encode().toUri();
注意,UriComponents 是不可变的;如果有需要的,expand() 和 encode() 操作会返回一个新的实例。你可以单独使用一个 URI 原件展开和编码 URI 模版字符串:
UriComponents uriComponents = UriComponentsBuilder.newInstance()
.scheme("http").host("example.com").path("/hotels/{hotel}/bookings/{booking}").build()
.expand("42", "21")
.encode();
Servlet 环境中,ServletUriComponentsBuilder 的子类提供了从 Servlet 请求复制 URL 信息的静态方法:
HttpServletRequest request = ...
// 重用 host, scheme, port, path 和 query
// 替换 "accountId" 查询参数
ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}").build()
.expand("123")
.encode();
16.10文件上传
16.10.1文件上传介绍
Spring本身也是支持在web模块式使用文件上传的,如果你想使用Spring的文件上传模块的话,就必须要自己生命一个处理文件上传的MultipartResolver实现类,通常情况洗我们使用的是CommonsMultipartResolver。Spring会检测每一个Http请求,检测请求中是否包含文件上传的内容,如果没有文件上传的内容被检测到的话,Spring会照常处理这个请求,但是如果有上传内容被检测到的话,我们在Spring中声明的MultipartResolver就会爬上用场了,我们在上传文件时候附加的参数,可以像正常参数一样被我们取到。
16.10.2通用的文件上传逻辑
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 其中一个可以配置的属性; 上传文件的最大字节 -->
<property name="maxUploadSize" value="100000"/>
</bean>
当然,为了multipart resolver 能够正常运行,需要在类路径添加一些jar包. 对于 CommonsMultipartResolver 而言, 你需要使用 commons-fileupload.jar.
当 Spring DispatcherServlet 检测到一个 multi-part 请求时, 就激活在上下文定义的resolver 并移交请求. 然后,resolver 包装当前 HttpServletRequest 成支持multipart文件上传的 MultipartHttpServletRequest. 通过 MultipartHttpServletRequest, 你可以获取当前请求所包含multiparts信息,实际上你也可以在controllers获取多个multipart文件.
16.10.4 在表单中处理一个上传文件
在完成添加 MultipartResolver 之后, 这个请求就会和普通请求一样被处理. 首先, 创建一个带上传文件的表单. 设置( enctype="multipart/form-data") 告诉浏览器将表单编码成 multipart request:
<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 和 normal annotated @Controller非常相似:
@Controller
public class FileUploadController {
@RequestMapping(value = "/form", method = RequestMethod.POST)
public String handleFormUpload(@RequestParam("name") String name,
@RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
byte[] bytes = file.getBytes();
// 将bytes保存
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
注意 @RequestParam 将方法参数映射输入元素的声明形式. 在这个例子中, 对 byte[] 并没有做什么操作, 但是在实践中你可以保存在数据库, 存储在文件系统, 等等.
16.11 异常处理
16.11.1 异常处理
Spring的 HandlerExceptionResolver 用来处理包括controller执行期间在内的异常. 一个 HandlerExceptionResolver 有点类似于 web 应用中定义在 web.xml 中的异常映射. 但是,它们提供了更加灵活的方式.比如说,在抛出异常的时候它提供了一些关于哪个异常处理器将被执行的信息.此外,在请求被转发到另外一个URL之前,编程方式的异常处理提供了更多的处理方式.s
实现 HandlerExceptionResolver 接口的话, 只需实现 resolveException(Exception, Handler) 方法并且返回一个 ModelAndView, 也可以使用提供的 SimpleMappingExceptionResolver 或者创建一个 @ExceptionHandler 方法. SimpleMappingExceptionResolver 使你能够获取任何异常的类名,这些异常可以被抛出或者映射到一个视图. 这和Servlet API的异常映射特性是等价的,但是它可以通过不同的异常处理器实现更好的异常处理. 另外 @ExceptionHandler 可以被注解在一个处理异常的方法上. 这个方法可以被定义在包含 @Controller 的类局部区域 或者定义在包含 @ControllerAdvice 的类里面应用于多个 @Controller 类.
16.11.2 @ExceptionHandler
HandlerExceptionResolver
接口 和 SimpleMappingExceptionResolver
实现类允许映射异常到具体的视图,在转发到视图之前可以有Java逻辑代码. 但是, 有些情况下,
尤其是注解 @ResponseBody
的方法而不是一个视图的情况下,它可以更方便的直接设置返回的状态和返回的错误内容.就像我们可以在HandlerExceptionResolver接口实现中在出现异常的情况下返回一个友好的提示页面,对于@ResponseBody这种情况的话我们可以处理成返回一个默认的code值来供前段识别或者返回一个默认的数值等等。
可以通过 @ExceptionHandler 方法. 当它在一个 controller 内部声明时,它将被用于那个controller(或它的子类)的 @RequestMapping 方法抛出的异常.
@Controller
public class SimpleController {
// @RequestMapping methods omitted ...
@ExceptionHandler(IOException.class)
public ResponseEntity<String> handleIOException(IOException ex) {
// prepare responseEntity
return responseEntity;
}
}
@ExceptionHandler
的value可以设置一个需要被处理的异常数组. 如果一个异常被抛出并且包含在这个异常列表中, 然后就会调用 @ExceptionHandler
方法. 如果没有设置value,
那么就会使用参数里面的异常. 和标准controller的 @RequestMapping 方法很相似, @ExceptionHandler 方法的参数值和返回值相当灵活. 比如说, HttpServletRequest 可以在 Servlet 环境中被接收, PortletRequest 在 Portlet 环境中被接收. 返回值可以是 String, 它将解释为一个视图, 可以是 ModelAndView 对象, 可以是 ResponseEntity 对象, 或者你可以添加 @ResponseBody 方法直接返回消息.
16.11.5 处理默认的错误页面
当相应的code是error状态但是response的响应体是空的时候,容器通常需要渲染一个默认的错误页面供用户使用。为来定义这个通用的错误页面,你可以在web.Xml中声明<error-page>标签,在Servlet3之前,你还需要为每个标签声明对应的error code,但是到来Servelt3之后你就不需要这么做了。
<error-page>
<location>/error</location>
</error-page>
@Controller
public class ErrorController {
@RequestMapping(value="/error", produces="application/json")
@ResponseBody
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", request.getAttribute("javax.servlet.error.status_code"));
map.put("reason", request.getAttribute("javax.servlet.error.message"));
return map;
}
}
Spring的灵活配置信息
16.13.2 ModelAndView
ModelMap 类本质上是一个好听一点的 Map,它坚持一个通用的命名约定,把用于显示在 View 上面的对象添加到其中。考虑下面的 Controller 实现;注意添加到 ModelAndView 中的对象没有指定任意关联的名称。
public class DisplayShoppingCartController implements Controller {
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
List cartItems = ***;
User user = ***;
ModelAndView mav = new ModelAndView("displayShoppingCart");
mav.addObject(cartItems);
mav.addObject(user);
return mav;
}
}
ModelAndView 类使用了一个 ModelMap 类,ModelMap 是一个自定义的 Map 实现,它会为添加到其中的对象自动生成一个 key。决定被添加对象名称的策略是,如果是一个 scalar 对象,会使用对象类的简短类名。对于将 scalar 对象添加到 ModelMap 实例的情况,下面的例子展示了生成的名称:
- 添加的 x.y.User 实例会生成名称 user。
- 添加的 x.y.Registration 会生成名称 registration
- 添加的 x.y.Foo 实例会生成名称 foo
- 添加的 java.util.HashMap 实例会生成名称 hashMap。在这种情况下,你可能想要显式指定名称,因为 hashMap 不够直观。
- 添加 null 会导致抛出一个 IllegalArgumentException。如果你要添加的一个对象(或多个对象)为 null,那么你也想要显式指定名称。
在添加一个 Set 或 List 之后,生成名称的策略是,使用集合中第一个对象的简短类名,并在名称后追加 List。对数组使用的也是该策略。下面的例子会让你对集合的名称生成的语义更加清楚:
- 添加一个具有零个或多个 x.y.User 元素的 x.y.User[] 数组,会生成名称 userList。
- 添加一个具有零个或多个 x.y.User 元素的 x.y.Foo[] 数组,会生成名称 fooList。
- 添加一个具有零个或多个 x.y.User 元素的 java.util.ArrayList,会生成名称 userList。
- 添加一个具有零个或多个 x.y.Foo 元素的 java.util.HashSet,会生成名称 fooList。
- 根本不能添加一个空的 java.util.ArrayList(实际上,addObject(..) 调用基本上会是一个无效操作)。