继 Spring 2.0 对 Spring MVC 进行重大升级后,Spring 2.5 又为 Spring MVC 引入了注解驱动功能。如今你无须让 Controller 继承任何接口,无需在 XML 配置文件中定义请求和 Controller 的映射关系,仅仅使用注解就可让一个 POJO 具备 Controller 的绝大部分功能 —— Spring MVC 框架的易用性获得了进一步的加强.在框架灵活性、易用性和扩展性上,Spring MVC 已经全面超越了其它的 MVC 框架,伴随着 Spring 一路高唱猛进,能够预见 Spring MVC 在 MVC 市场上的吸引力将愈来愈不可抗拒。web
本文将介绍 Spring 2.5 新增的 Sping MVC 注解功能,讲述如何使用注解配置替换传统的基于 XML 的 Spring MVC 配置。spring
回页首数组
使用太低版本 Spring MVC 的读者都知道:当建立一个 Controller 时,咱们须要直接或间接地实现 org.springframework.web.servlet.mvc.Controller 接口。通常状况下,咱们是经过继承 SimpleFormController 或 MultiActionController 来定义本身的 Controller 的。在定义 Controller 后,一个重要的事件是在 Spring MVC 的配置文件中经过 HandlerMapping 定义请求和控制器的映射关系,以便将二者关联起来。mvc
来看一下基于注解的 Controller 是如何定义作到这一点的,下面是使用注解的 BbtForumController:app
清单 1. BbtForumController.java框架
package com.baobaotao.web; import com.baobaotao.service.BbtForumService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import java.util.Collection; @Controller //<——① @RequestMapping("/forum.do") public class BbtForumController { @Autowired private BbtForumService bbtForumService; @RequestMapping //<——② public String listAllBoard() { bbtForumService.getAllBoard(); System.out.println("call listAllBoard method."); return "listBoard"; } } |
从上面代码中,咱们能够看出 BbtForumController 和通常的类并无区别,它没有实现任何特殊的接口,于是是一个地道的 POJO。让这个 POJO 不同凡响的魔棒就是 Spring MVC 的注解!jsp
在 ① 处使用了两个注解,分别是 @Controller 和 @RequestMapping。在“使用 Spring 2.5 基于注解驱动的 IoC”这篇文章里,笔者曾经指出过 @Controller、@Service 以及 @Repository 和 @Component 注解的做用是等价的:将一个类成为 Spring 容器的 Bean。因为 Spring MVC 的 Controller 必须事先是一个 Bean,因此 @Controller 注解是不可缺乏的。编辑器
真正让 BbtForumController 具有 Spring MVC Controller 功能的是 @RequestMapping 这个注解。@RequestMapping 能够标注在类定义处,将 Controller 和特定请求关联起来;还能够标注在方法签名处,以便进一步对请求进行分流。在 ① 处,咱们让 BbtForumController 关联“/forum.do”的请求,而 ② 处,咱们具体地指定 listAllBoard() 方法来处理请求。因此在类声明处标注的 @RequestMapping 至关于让 POJO 实现了 Controller 接口,而在方法定义处的 @RequestMapping 至关于让 POJO 扩展 Spring 预约义的 Controller(如 SimpleFormController 等)。
为了让基于注解的 Spring MVC 真正工做起来,须要在 Spring MVC 对应的 xxx-servlet.xml 配置文件中作一些手脚。在此以前,仍是先来看一下 web.xml 的配置吧:
清单 2. web.xml:启用 Spring 容器和 Spring MVC 框架
web.xml 中定义了一个名为 annomvc 的 Spring MVC 模块,按照 Spring MVC 的契约,须要在 WEB-INF/annomvc-servlet.xml 配置文件中定义 Spring MVC 模块的具体配置。annomvc-servlet.xml 的配置内容以下所示:
由于 Spring 全部功能都在 Bean 的基础上演化而来,因此必须事先将 Controller 变成 Bean,这是经过在类中标注 @Controller 并在 annomvc-servlet.xml 中启用组件扫描机制来完成的,如 ① 所示。
在 ② 处,配置了一个 AnnotationMethodHandlerAdapter,它负责根据 Bean 中的 Spring MVC 注解对 Bean 进行加工处理,使这些 Bean 变成控制器并映射特定的 URL 请求。
而 ③ 处的工做是定义模型视图名称的解析规则,这里咱们使用了 Spring 2.5 的特殊命名空间,即 p 命名空间,它将原先须要经过 <property> 元素配置的内容转化为 <bean> 属性配置,在必定程度上简化了 <bean> 的配置。
启动 Tomcat,发送 http://localhost/forum.do URL 请求,BbtForumController 的 listAllBoard() 方法将响应这个请求,并转向 WEB-INF/jsp/listBoard.jsp 的视图页面。
在低版本的 Spring MVC 中,咱们能够经过继承 MultiActionController 让一个 Controller 处理多个 URL 请求。使用 @RequestMapping 注解后,这个功能更加容易实现了。请看下面的代码:
package com.baobaotao.web; import com.baobaotao.service.BbtForumService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class BbtForumController { @Autowired private BbtForumService bbtForumService; @RequestMapping("/listAllBoard.do") // <—— ① public String listAllBoard() { bbtForumService.getAllBoard(); System.out.println("call listAllBoard method."); return "listBoard"; } @RequestMapping("/listBoardTopic.do") // <—— ② public String listBoardTopic(int topicId) { bbtForumService.getBoardTopics(topicId); System.out.println("call listBoardTopic method."); return "listTopic"; } } |
在这里,咱们分别在 ① 和 ② 处为 listAllBoard() 和 listBoardTopic() 方法标注了 @RequestMapping 注解,分别指定这两个方法处理的 URL 请求,这至关于将 BbtForumController 改造为 MultiActionController。这样 /listAllBoard.do 的 URL 请求将由 listAllBoard() 负责处理,而 /listBoardTopic.do?topicId=1 的 URL 请求则由 listBoardTopic() 方法处理。
对于处理多个 URL 请求的 Controller 来讲,咱们倾向于经过一个 URL 参数指定 Controller 处理方法的名称(如 method=listAllBoard),而非直接经过不一样的 URL 指定 Controller 的处理方法。使用 @RequestMapping 注解很容易实现这个经常使用的需求。来看下面的代码:
清单 4. 一个 Controller 对应一个 URL,由请求参数决定请求处理方法
package com.baobaotao.web; import com.baobaotao.service.BbtForumService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/bbtForum.do") // <—— ① 指定控制器对应URL请求 public class BbtForumController { @Autowired private BbtForumService bbtForumService; // <—— ② 若是URL请求中包括"method=listAllBoard"的参数,由本方法进行处理 @RequestMapping(params = "method=listAllBoard") public String listAllBoard() { bbtForumService.getAllBoard(); System.out.println("call listAllBoard method."); return "listBoard"; } // <—— ③ 若是URL请求中包括"method=listBoardTopic"的参数,由本方法进行处理 @RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(int topicId) { bbtForumService.getBoardTopics(topicId); System.out.println("call listBoardTopic method."); return "listTopic"; } } |
在类定义处标注的 @RequestMapping 让 BbtForumController 处理全部包含 /bbtForum.do 的 URL 请求,而 BbtForumController 中的请求处理方法对 URL 请求的分流规则在 ② 和 ③ 处定义分流规则按照 URL 的 method 请求参数肯定。因此分别在类定义处和方法定义处使用 @RequestMapping 注解,就能够很容易经过 URL 参数指定 Controller 的处理方法了。
@RequestMapping 注解中除了 params 属性外,还有一个经常使用的属性是 method,它可让 Controller 方法处理特定 HTTP 请求方式的请求,如让一个方法处理 HTTP GET 请求,而另外一个方法处理 HTTP POST 请求,以下所示:
package com.baobaotao.web; import com.baobaotao.service.BbtForumService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller @RequestMapping("/bbtForum.do") public class BbtForumController { @RequestMapping(params = "method=createTopic",method = RequestMethod.POST) public String createTopic(){ System.out.println("call createTopic method."); return "createTopic"; } } |
这样只有当 /bbtForum.do?method=createTopic 请求以 HTTP POST 方式提交时,createTopic() 方法才会进行处理。
Controller 的方法标注了 @RequestMapping 注解后,它就能处理特定的 URL 请求。咱们不由要问:请求处理方法入参是如何绑定 URL 参数的呢?在回答这个问题以前先来看下面的代码:
@RequestMapping(params = "method=listBoardTopic") //<—— ① topicId入参是如何绑定URL请求参数的? public String listBoardTopic(int topicId) { bbtForumService.getBoardTopics(topicId); System.out.println("call listBoardTopic method."); return "listTopic"; } |
当咱们发送 http://localhost//bbtForum.do?method=listBoardTopic&topicId=10 的 URL 请求时,Spring 不但让 listBoardTopic() 方法处理这个请求,并且还将 topicId 请求参数在类型转换后绑定到 listBoardTopic() 方法的 topicId 入参上。而 listBoardTopic() 方法的返回类型是 String,它将被解析为逻辑视图的名称。也就是说 Spring 在如何给处理方法入参自动赋值以及如何将处理方法返回值转化为 ModelAndView 中的过程当中存在一套潜在的规则,不熟悉这个规则就不可能很好地开发基于注解的请求处理方法,所以了解这个潜在规则无疑成为理解 Spring MVC 框架基于注解功能的核心问题。
咱们不妨从最多见的开始提及:请求处理方法入参的类型能够是 Java 基本数据类型或 String 类型,这时方法入参按参数名匹配的原则绑定到 URL 请求参数,同时还自动完成 String 类型的 URL 请求参数到请求处理方法参数类型的转换。下面给出几个例子:
特别的,若是入参是基本数据类型(如 int、long、float 等),URL 请求参数中必定要有对应的参数,不然将抛出 TypeMismatchException 异常,提示没法将 null 转换为基本数据类型。
另外,请求处理方法的入参也能够一个 JavaBean,以下面的 User 对象就能够做为一个入参:
package com.baobaotao.web; public class User { private int userId; private String userName; //省略get/setter方法 public String toString(){ return this.userName +","+this.userId; } } |
下面是将 User 做为 listBoardTopic() 请求处理方法的入参:
@RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(int topicId,User user) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:"+topicId); System.out.println("user:"+user); System.out.println("call listBoardTopic method."); return "listTopic"; } |
这时,若是咱们使用如下的 URL 请求:http://localhost/bbtForum.do?method=listBoardTopic&topicId=1&userId=10&userName=tom
topicId URL 参数将绑定到 topicId 入参上,而 userId 和 userName URL 参数将绑定到 user 对象的 userId 和 userName 属性中。和 URL 请求中不容许没有 topicId 参数不一样,虽然 User 的 userId 属性的类型是基本数据类型,但若是 URL 中不存在 userId 参数,Spring 也不会报错,此时 user.userId 值为 0。若是 User 对象拥有一个 dept.deptId 的级联属性,那么它将和 dept.deptId URL 参数绑定。
若是咱们想改变这种默认的按名称匹配的策略,好比让 listBoardTopic(int topicId,User user) 中的 topicId 绑定到 id 这个 URL 参数,那么能够经过对入参使用 @RequestParam 注解来达到目的:
package com.baobaotao.web; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; … @Controller @RequestMapping("/bbtForum.do") public class BbtForumController { @RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(@RequestParam("id") int topicId,User user) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:"+topicId); System.out.println("user:"+user); System.out.println("call listBoardTopic method."); return "listTopic"; } … } |
这里,对 listBoardTopic() 请求处理方法的 topicId 入参标注了 @RequestParam("id") 注解,因此它将和 id 的 URL 参数绑定。
Spring 2.0 定义了一个 org.springframework.ui.ModelMap 类,它做为通用的模型数据承载对象,传递数据供视图所用。咱们能够在请求处理方法中声明一个 ModelMap 类型的入参,Spring 会将本次请求模型对象引用经过该入参传递进来,这样就能够在请求处理方法内部访问模型对象了。来看下面的例子:
清单 9. 使用 ModelMap 访问请示对应的隐含模型对象
@RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(@RequestParam("id")int topicId, User user,ModelMap model) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:" + topicId); System.out.println("user:" + user); //① 将user对象以currUser为键放入到model中 model.addAttribute("currUser",user); return "listTopic"; } |
对于当次请求所对应的模型对象来讲,其全部属性都将存放到 request 的属性列表中。象上面的例子,ModelMap 中的 currUser 属性将放到 request 的属性列表中,因此能够在 JSP 视图页面中经过 request.getAttribute(“currUser”) 或者经过 ${currUser} EL 表达式访问模型对象中的 user 对象。从这个角度上看, ModelMap 至关因而一个向 request 属性列表中添加对象的一条管道,借由 ModelMap 对象的支持,咱们能够在一个不依赖 Servlet API 的 Controller 中向 request 中添加属性。
在默认状况下,ModelMap 中的属性做用域是 request 级别是,也就是说,当本次请求结束后,ModelMap 中的属性将销毁。若是但愿在多个请求中共享 ModelMap 中的属性,必须将其属性转存到 session 中,这样 ModelMap 的属性才能够被跨请求访问。
Spring 容许咱们有选择地指定 ModelMap 中的哪些属性须要转存到 session 中,以便下一个请求属对应的 ModelMap 的属性列表中还能访问到这些属性。这一功能是经过类定义处标注 @SessionAttributes 注解来实现的。请看下面的代码:
清单 10. 使模型对象的特定属性具备 Session 范围的做用域
package com.baobaotao.web; … import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.SessionAttributes; @Controller @RequestMapping("/bbtForum.do") @SessionAttributes("currUser") //①将ModelMap中属性名为currUser的属性 //放到Session属性列表中,以便这个属性能够跨请求访问 public class BbtForumController { … @RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(@RequestParam("id")int topicId, User user, ModelMap model) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:" + topicId); System.out.println("user:" + user); model.addAttribute("currUser",user); //②向ModelMap中添加一个属性 return "listTopic"; } } |
咱们在 ② 处添加了一个 ModelMap 属性,其属性名为 currUser,而 ① 处经过 @SessionAttributes 注解将 ModelMap 中名为 currUser 的属性放置到 Session 中,因此咱们不但能够在 listBoardTopic() 请求所对应的 JSP 视图页面中经过 request.getAttribute(“currUser”) 和 session.getAttribute(“currUser”) 获取 user 对象,还能够在下一个请求所对应的 JSP 视图页面中经过 session.getAttribute(“currUser”) 或 ModelMap#get(“currUser”) 访问到这个属性。
这里咱们仅将一个 ModelMap 的属性放入 Session 中,其实 @SessionAttributes 容许指定多个属性。你能够经过字符串数组的方式指定多个属性,如 @SessionAttributes({“attr1”,”attr2”})。此外,@SessionAttributes 还能够经过属性类型指定要 session 化的 ModelMap 属性,如 @SessionAttributes(types = User.class),固然也能够指定多个类,如 @SessionAttributes(types = {User.class,Dept.class}),还能够联合使用属性名和属性类型指定:@SessionAttributes(types = {User.class,Dept.class},value={“attr1”,”attr2”})。
上面讲述了如何往ModelMap中放置属性以及如何使ModelMap中的属性拥有Session域的做用范围。除了在JSP视图页面中经过传统的方法 访问ModelMap中的属性外,读者朋友可能会问:是否能够将ModelMap中的属性绑定到请求处理方法的入参中呢?答案是确定的。Spring为此 提供了一个@ModelAttribute的注解,下面是使用@ModelAttribute注解的例子:
清单 11. 使模型对象的特定属性具备 Session 范围的做用域
package com.baobaotao.web; import com.baobaotao.service.BbtForumService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.annotation.ModelAttribute; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @Controller @RequestMapping("/bbtForum.do") @SessionAttributes("currUser") //①让ModelMap的currUser属性拥有session级做用域 public class BbtForumController { @Autowired private BbtForumService bbtForumService; @RequestMapping(params = "method=listBoardTopic") public String listBoardTopic(@RequestParam("id")int topicId, User user, ModelMap model) { bbtForumService.getBoardTopics(topicId); System.out.println("topicId:" + topicId); System.out.println("user:" + user); model.addAttribute("currUser",user); //②向ModelMap中添加一个属性 return "listTopic"; } @RequestMapping(params = "method=listAllBoard") //③将ModelMap中的 public String listAllBoard(@ModelAttribute("currUser") User user) { //currUser属性绑定到user入参中。 bbtForumService.getAllBoard(); System.out.println("user:"+user); return "listBoard"; } } |
在 ② 处,咱们向 ModelMap 中添加一个名为 currUser 的属性,而 ① 外的注解使这个 currUser 属性拥有了 session 级的做用域。因此,咱们能够在 ③ 处经过 @ModelAttribute 注解将 ModelMap 中的 currUser 属性绑定以请求处理方法的 user 入参中。
因此当咱们先调用如下 URL 请求: http://localhost/bbtForum.do?method=listBoardTopic&id=1&userName=tom&dept.deptId=12
以执行listBoardTopic()请求处理方法,而后再访问如下URL: http://localhost/sample/bbtForum.do?method=listAllBoard
你将能够看到 listAllBoard() 的 user 入参已经成功绑定到 listBoardTopic() 中注册的 session 级的 currUser 属性上了。
咱们知道标注了 @RequestMapping 注解的 Controller 方法就成为了请求处理方法,Spring MVC 容许极其灵活的请求处理方法签名方式。对于方法入参来讲,它容许多种类型的入参,经过下表进行说明:
说明 | |
---|---|
Java 基本数据类型和 String | 默认状况下将按名称匹配的方式绑定到 URL 参数上,能够经过 @RequestParam 注解改变默认的绑定规则 |
request/response/session | 既能够是 Servlet API 的也能够是 Portlet API 对应的对象,Spring 会将它们绑定到 Servlet 和 Portlet 容器的相应对象上 |
org.springframework.web.context.request.WebRequest | 内部包含了 request 对象 |
java.util.Locale | 绑定到 request 对应的 Locale 对象上 |
java.io.InputStream/java.io.Reader | 能够借此访问 request 的内容 |
java.io.OutputStream / java.io.Writer | 能够借此操做 response 的内容 |
任何标注了 @RequestParam 注解的入参 | 被标注 @RequestParam 注解的入参将绑定到特定的 request 参数上。 |
java.util.Map / org.springframework.ui.ModelMap | 它绑定 Spring MVC 框架中每一个请求所建立的潜在的模型对象,它们能够被 Web 视图对象访问(如 JSP) |
命令/表单对象(注:通常称绑定使用 HTTP GET 发送的 URL 参数的对象为命令对象,而称绑定使用 HTTP POST 发送的 URL 参数的对象为表单对象) | 它们的属性将以名称匹配的规则绑定到 URL 参数上,同时完成类型的转换。而类型转换的规则能够经过 @InitBinder 注解或经过 HandlerAdapter 的配置进行调整 |
org.springframework.validation.Errors / org.springframework.validation.BindingResult | 为属性列表中的命令/表单对象的校验结果,注意检验结果参数必须紧跟在命令/表单对象的后面 |
rg.springframework.web.bind.support.SessionStatus | 能够经过该类型 status 对象显式结束表单的处理,这至关于触发 session 清除其中的经过 @SessionAttributes 定义的属性 |
Spring MVC 框架的易用之处在于,你能够按任意顺序定义请求处理方法的入参(除了 Errors 和 BindingResult 必须紧跟在命令对象/表单参数后面之外),Spring MVC 会根据反射机制自动将对应的对象经过入参传递给请求处理方法。这种机制让开发者彻底能够不依赖 Servlet API 开发控制层的程序,当请求处理方法须要特定的对象时,仅仅须要在参数列表中声明入参便可,不须要考虑如何获取这些对象,Spring MVC 框架就象一个大管家同样“任劳任怨”地为咱们准备好了所需的一切。下面演示一下使用 SessionStatus 的例子:
清单 12. 使用 SessionStatus 控制 Session 级别的模型属性
@RequestMapping(method = RequestMethod.POST) public String processSubmit(@ModelAttribute Owner owner, BindingResult result, SessionStatus status) {//<——① new OwnerValidator().validate(owner, result); if (result.hasErrors()) { return "ownerForm"; } else { this.clinic.storeOwner(owner); status.setComplete();//<——② return "redirect:owner.do?ownerId=" + owner.getId(); } } |
processSubmit() 方法中的 owner 表单对象将绑定到 ModelMap 的“owner”属性中,result 参数用于存放检验 owner 结果的对象,而 status 用于控制表单处理的状态。在 ② 处,咱们经过调用 status.setComplete() 方法,该 Controller 全部放在 session 级别的模型属性数据将从 session 中清空。
在低版本的 Spring MVC 中,请求处理方法的返回值类型都必须是 ModelAndView。而在 Spring 2.5 中,你拥有多种灵活的选择。经过下表进行说明: