概述
继 Spring 2.0 对 Spring MVC 进行重大升级后,Spring 2.5 又为 Spring MVC 引入了注解驱动功能。现在你无须让 Controller 继承任何接口,无需在 XML 配置文件中定义请求和 Controller 的映射关系,仅仅使用注解就可以让一个 POJO 具有 Controller 的绝大部分功能 —— Spring MVC 框架的易用性得到了进一步的增强.在框架灵活性、易用性和扩展性上,Spring MVC 已经全面超越了其它的 MVC 框架,伴随着 Spring 一路高唱猛进,可以预见 Spring MVC 在 MVC 市场上的吸引力将越来越不可抗拒。
本文将介绍 Spring 2.5 新增的 Sping MVC 注解功能,讲述如何使用注解配置替换传统的基于 XML 的 Spring MVC 配置。
使用过低版本 Spring MVC 的读者都知道:当创建一个 Controller 时,我们需要直接或间接地实现 org.springframework.web.servlet.mvc.Controller 接口。一般情况下,我们是通过继承 SimpleFormController 或 MultiActionController 来定义自己的 Controller 的。在定义 Controller 后,一个重要的事件是在 Spring MVC 的配置文件中通过 HandlerMapping 定义请求和控制器的映射关系,以便将两者关联起来。
来看一下基于注解的 Controller 是如何定义做到这一点的,下面是使用注解的 BbtForumController:
清单 1. BbtForumController.java
<span style="color:#333333;">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"; } }</span>
<span style="color:#333333;"><?xml version="1.0" encoding="UTF-8"?> <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_2_5.xsd" version="2.5"> <display-name>Spring Annotation MVC Sample</display-name> <!-- Spring 服务层的配置文件 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <!-- Spring 容器启动监听器 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <!-- Spring MVC 的Servlet,它将加载WEB-INF/annomvc-servlet.xml 的 配置文件,以启动Spring MVC模块--> <servlet> <servlet-name>annomvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet </servlet-class> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>annomvc</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> </web-app></span>web.xml 中定义了一个名为 annomvc 的 Spring MVC 模块,按照 Spring MVC 的契约,需要在 WEB-INF/annomvc-servlet.xml 配置文件中定义
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <!-- ①:对web包中的所有类进行扫描,以完成Bean创建和自动依赖注入的功能 --> <context:component-scan base-package="com.baobaotao.web"/> <!-- ②:启动Spring MVC的注解功能,完成请求和注解POJO的映射 --> <bean class="org.springframework.web.servlet.mvc.annotation. AnnotationMethodHandlerAdapter"/> <!-- ③:对模型视图名称的解析,即在模型视图名称添加前后缀 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/jsp/" p:suffix=".jsp"/> </beans>
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
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 中的请求处理方
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"; } }
@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() 方法处理这
特别的,如果入参是基本数据类型(如 int、long、float 等),URL 请求参数中一定要有对应的参数,否则将抛出 TypeMismatchException 异常
,提示无法将 null 转换为基本数据类型。
另外,请求处理方法的入参也可以一个 JavaBean,如下面的 User 对象就可以作为一个入参:
清单 6. User.java:一个 JavaBean
<span style="font-size:12px;">package com.baobaotao.web; public class User { private int userId; private String userName; //省略get/setter方法 public String toString(){ return this.userName +","+this.userId; } }</span>下面是将 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"; }
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"; } … }
@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 属性将放到
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 的属性
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"; } }
@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 中,你拥有多种灵活的选择。通过下表进行说明:
请求处理方法入参的可选类型 | 说明 |
void | 此时逻辑视图名由请求处理方法对应的 URL 确定,如以下的方法:@RequestMapping("/welcome.do") public void welcomeHandler() { }对应的逻辑视图名为“welcome” |
String | 此时逻辑视图名为返回的字符,如以下的方法:@RequestMapping(method = RequestMethod.GET) public String setupForm(@RequestParam("ownerId") int ownerId, ModelMap model) { Owner owner = this.clinic.loadOwner(ownerId); model.addAttribute(owner); return "ownerForm"; }对应的逻辑视图名为“ownerForm” |
org.springframework.ui.ModelMap | 和返回类型为 void 一样,逻辑视图名取决于对应请求的 URL,如下面的例子:@RequestMapping("/vets.do") public ModelMap vetsHandler() { return new ModelMap(this.clinic.getVets()); }对应的逻辑视图名为“vets”,返回的 ModelMap 将被作为请求对应的模型对象,可以在 JSP 视图页面中访问到。 |
ModelAndView | 当然还可以是传统的 ModelAndView。 |
应该说使用 String 作为请求处理方法的返回值类型是比较通用的方法,这样返回的逻辑视图名不会和请求 URL 绑定,具有很大的灵活性,而模型数据又可以通过 ModelMap 控制。当然直接使用传统的 ModelAndView 也不失为一个好的选择。
注册自己的属性编辑器
Spring MVC 有一套常用的属性编辑器,这包括基本数据类型及其包裹类的属性编辑器、String 属性编辑器、JavaBean 的属性编辑器等。但有时我
们还需要向 Spring MVC 框架注册一些自定义的属性编辑器,如特定时间格式的属性编辑器就是其中一例。
Spring MVC 允许向整个 Spring 框架注册属性编辑器,它们对所有 Controller 都有影响。当然 Spring MVC 也允许仅向某个 Controller 注册属
性编辑器,对其它的 Controller 没有影响。前者可以通过 AnnotationMethodHandlerAdapter 的配置做到,而后者则可以通过 @InitBinder 注解
实现。
下面先看向整个 Spring MVC 框架注册的自定义编辑器:
清单 13. 注册框架级的自定义属性编辑器
<bean class="org.springframework.web.servlet.mvc.annotation. AnnotationMethodHandlerAdapter"> <property name="webBindingInitializer"> <bean class="com.baobaotao.web.MyBindingInitializer"/> </property> </bean>MyBindingInitializer 实现了 WebBindingInitializer 接口,在接口方法中通过 binder 注册多个自定义的属性编辑器,其代码如下所示:
package org.springframework.samples.petclinic.web; import java.text.SimpleDateFormat; import java.util.Date; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.propertyeditors.CustomDateEditor; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.samples.petclinic.Clinic; import org.springframework.samples.petclinic.PetType; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.context.request.WebRequest; public class MyBindingInitializer implements WebBindingInitializer { public void initBinder(WebDataBinder binder, WebRequest request) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); binder.registerCustomEditor(String.class, new StringTrimmerEditor(false)); } }
@Controller public class MyFormController { @InitBinder public void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); } … }注意被标注 @InitBinder 注解的方法必须拥有一个 WebDataBinder 类型的入参,以便 Spring MVC 框架将注册属性编辑器的 WebDataBinder 对象
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.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.SessionAttributes; import java.util.ArrayList; import java.util.List; import java.util.Set; @Controller @RequestMapping("/bbtForum.do") public class BbtForumController { @Autowired private BbtForumService bbtForumService; @ModelAttribute("items")//<——①向模型对象中添加一个名为items的属性 public List<String> populateItems() { List<String> lists = new ArrayList<String>(); lists.add("item1"); lists.add("item2"); return lists; } @RequestMapping(params = "method=listAllBoard") public String listAllBoard(@ModelAttribute("currUser")User user, ModelMap model) { bbtForumService.getAllBoard(); //<——②在此访问模型中的items属性 System.out.println("model.items:" + ((List<String>)model.get("items")).size()); return "listBoard"; } }
小结
Spring 2.5 对 Spring MVC 进行了很大增强,现在我们几乎完全可以使用基于注解的 Spring MVC 完全替换掉原来基于接口 Spring MVC 程序。基
于注解的 Spring MVC 比之于基于接口的 Spring MVC 拥有以下几点好处:
方便请求和控制器的映射;
方便请求处理方法入参绑定URL参数;
Controller 不必继承任何接口,它仅是一个简单的 POJO。
但是基于注解的 Spring MVC 并不完美,还存在优化的空间,因为在某些配置上它比基于 XML 的配置更繁琐。比如对于处理多个请求的 Controller
来说,假设我们使用一个 URL 参数指定调用的处理方法(如 xxx.do?method=listBoardTopic),当使用注解时,每个请求处理方法都必须使用
@RequestMapping() 注解指定对应的 URL 参数(如 @RequestMapping(params = "method=listBoardTopic")),而在 XML 配置中我们仅需要配置一
个 ParameterMethodNameResolver 就可以了。
参考资料
学习
Spring 系列:Spring 框架简介:优秀的 Spring 框架入门系列,了解 Spring 框架的基本概念。
轻量级开发的成功秘诀,第 3 部分: Spring 露出水面:介绍了在 Spring 框架的轻量级 Ioc 容器。
Spring Framework 和 IBM WebSphere Application Server:Interface21 的首席执行官 Rod Johnson 和 IBM 的 WebSphere Open Source 主
管 Paul Buck 讨论了 Spring Framework 通过 IBM WebSphere Application Server 认证对 Spring 和 WebSphere 产品系列的开发人员和客户有何
重要意义。
Tiger 中的注释,第 1 部分: 向 Java 代码中添加元数据:解释了元数据如此有用的原因,向您介绍了 Java 语言中的注释,并研究了 Tiger
的内置注释。
Tiger 中的注释,第 2 部分: 定制注释:说明了如何创建定制注释,如何用自己的注释注解文档,并进一步定制代码。
获得产品和技术
Springframework 网站:下载 Spring 框架。
转自:http://blog.csdn.net/lufeng20/article/details/7598801