使用 Spring 2.5 基于注解驱动的 Spring MVC
概述
继Spring2.0对SpringMVC进行重大升级后,Spring2.5又为SpringMVC引入了注解驱动功能。现在你无须让Controller继承任何接口,无需在XML配置文件中定义请求和Controller的映射关系,仅仅使用注解就可以让一个POJO具有Controller的绝大部分功能——SpringMVC框架的易用性得到了进一步的增强.在框架灵活性、易用性和扩展性上,SpringMVC已经全面超越了其它的MVC框架,伴随着Spring一路高唱猛进,可以预见SpringMVC在MVC市场上的吸引力将越来越不可抗拒。
本文将介绍Spring2.5新增的SpingMVC注解功能,讲述如何使用注解配置替换传统的基于XML的SpringMVC配置。
--------------------------------------------------------------------------------
一个简单的基于注解的Controller
使用过低版本SpringMVC的读者都知道:当创建一个Controller时,我们需要直接或间接地实现org.springframework.web.servlet.mvc.Controller接口。一般情况下,我们是通过继承SimpleFormController或MultiActionController来定义自己的Controller的。在定义Controller后,一个重要的事件是在SpringMVC的配置文件中通过HandlerMapping定义请求和控制器的映射关系,以便将两者关联起来。
来看一下基于注解的Controller是如何定义做到这一点的,下面是使用注解的BbtForumController:
- 清单 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与众不同的魔棒就是SpringMVC的注解!
在①处使用了两个注解,分别是@Controller和@RequestMapping。在“使用Spring2.5基于注解驱动的IoC”这篇文章里,笔者曾经指出过@Controller、@Service以及@Repository和@Component注解的作用是等价的:将一个类成为Spring容器的Bean。由于SpringMVC的Controller必须事先是一个Bean,所以@Controller注解是不可缺少的。
真正让BbtForumController具备SpringMVCController功能的是@RequestMapping这个注解。@RequestMapping可以标注在类定义处,将Controller和特定请求关联起来;还可以标注在方法签名处,以便进一步对请求进行分流。在①处,我们让BbtForumController关联“/forum.do”的请求,而②处,我们具体地指定listAllBoard()方法来处理请求。所以在类声明处标注的@RequestMapping相当于让POJO实现了Controller接口,而在方法定义处的@RequestMapping相当于让POJO扩展Spring预定义的Controller(如SimpleFormController等)。
为了让基于注解的SpringMVC真正工作起来,需要在SpringMVC对应的xxx-servlet.xml配置文件中做一些手脚。在此之前,还是先来看一下web.xml的配置吧:
- 清单2.web.xml:启用Spring容器和SpringMVC框架
<?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>
web.xml中定义了一个名为annomvc的SpringMVC模块,按照SpringMVC的契约,需要在WEB-INF/annomvc-servlet.xml配置文件中定义SpringMVC模块的具体配置。annomvc-servlet.xml的配置内容如下所示:
- 清单3.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>
因为Spring所有功能都在Bean的基础上演化而来,所以必须事先将Controller变成Bean,这是通过在类中标注@Controller并在annomvc-servlet.xml中启用组件扫描机制来完成的,如①所示。
在②处,配置了一个AnnotationMethodHandlerAdapter,它负责根据Bean中的SpringMVC注解对Bean进行加工处理,使这些Bean变成控制器并映射特定的URL请求。
而③处的工作是定义模型视图名称的解析规则,这里我们使用了Spring2.5的特殊命名空间,即p命名空间,它将原先需要通过<property>元素配置的内容转化为<bean>属性配置,在一定程度上简化了<bean>的配置。
启动Tomcat,发送http://localhost/forum.doURL请求,BbtForumController的listAllBoard()方法将响应这个请求,并转向WEB-INF/jsp/listBoard.jsp的视图页面。
--------------------------------------------------------------------------------
让一个Controller处理多个URL请求
在低版本的SpringMVC中,我们可以通过继承MultiActionController让一个Controller处理多个URL请求。使用@RequestMapping注解后,这个功能更加容易实现了。请看下面的代码:
- 清单3.每个请求处理参数对应一个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 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请求方式的请求,如让一个方法处理HTTPGET请求,而另一个方法处理HTTPPOST请求,如下所示:
- 清单4.让请求处理方法处理特定的HTTP请求方法
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请求以HTTPPOST方式提交时,createTopic()方法才会进行处理。
--------------------------------------------------------------------------------
处理方法入参如何绑定URL参数
按契约绑定
Controller的方法标注了@RequestMapping注解后,它就能处理特定的URL请求。我们不禁要问:请求处理方法入参是如何绑定URL参数的呢?在回答这个问题之前先来看下面的代码:
- 清单5.按参数名匹配进行绑定
@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中的过程中存在一套潜在的规则,不熟悉这个规则就不可能很好地开发基于注解的请求处理方法,因此了解这个潜在规则无疑成为理解SpringMVC框架基于注解功能的核心问题。
我们不妨从最常见的开始说起:请求处理方法入参的类型可以是Java基本数据类型或String类型,这时方法入参按参数名匹配的原则绑定到URL请求参数,同时还自动完成String类型的URL请求参数到请求处理方法参数类型的转换。下面给出几个例子:
•listBoardTopic(inttopicId):和topicIdURL请求参数绑定;
•listBoardTopic(inttopicId,StringboardName):分别和topicId、boardNameURL请求参数绑定;
特别的,如果入参是基本数据类型(如int、long、float等),URL请求参数中一定要有对应的参数,否则将抛出TypeMismatchException异常,提示无法将null转换为基本数据类型。
另外,请求处理方法的入参也可以一个JavaBean,如下面的User对象就可以作为一个入参:
- 清单6.User.java:一个JavaBean
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()请求处理方法的入参:
- 清单7.使用JavaBean作为请求处理方法的入参
@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
topicIdURL参数将绑定到topicId入参上,而userId和userNameURL参数将绑定到user对象的userId和userName属性中。和URL请求中不允许没有topicId参数不同,虽然User的userId属性的类型是基本数据类型,但如果URL中不存在userId参数,Spring也不会报错,此时user.userId值为0。如果User对象拥有一个dept.deptId的级联属性,那么它将和dept.deptIdURL参数绑定。
通过注解指定绑定的URL参数
如果我们想改变这种默认的按名称匹配的策略,比如让listBoardTopic(inttopicId,Useruser)中的topicId绑定到id这个URL参数,那么可以通过对入参使用@RequestParam注解来达到目的:
- 清单8.通过@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参数绑定。
绑定模型对象中某个属性
Spring2.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对象的支持,我们可以在一个不依赖ServletAPI的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方法就成为了请求处理方法,SpringMVC允许极其灵活的请求处理方法签名方式。对于方法入参来说,它允许多种类型的入参,通过下表进行说明:
请求处理方法入参的可选类型说明
Java基本数据类型和String默认情况下将按名称匹配的方式绑定到URL参数上,可以通过@RequestParam注解改变默认的绑定规则
request/response/session既可以是ServletAPI的也可以是PortletAPI对应的对象,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它绑定SpringMVC框架中每个请求所创建的潜在的模型对象,它们可以被Web视图对象访问(如JSP)
命令/表单对象(注:一般称绑定使用HTTPGET发送的URL参数的对象为命令对象,而称绑定使用HTTPPOST发送的URL参数的对象为表单对象)它们的属性将以名称匹配的规则绑定到URL参数上,同时完成类型的转换。而类型转换的规则可以通过@InitBinder注解或通过HandlerAdapter的配置进行调整
org.springframework.validation.Errors/org.springframework.validation.BindingResult为属性列表中的命令/表单对象的校验结果,注意检验结果参数必须紧跟在命令/表单对象的后面
rg.springframework.web.bind.support.SessionStatus可以通过该类型status对象显式结束表单的处理,这相当于触发session清除其中的通过@SessionAttributes定义的属性
SpringMVC框架的易用之处在于,你可以按任意顺序定义请求处理方法的入参(除了Errors和BindingResult必须紧跟在命令对象/表单参数后面以外),SpringMVC会根据反射机制自动将对应的对象通过入参传递给请求处理方法。这种机制让开发者完全可以不依赖ServletAPI开发控制层的程序,当请求处理方法需要特定的对象时,仅仅需要在参数列表中声明入参即可,不需要考虑如何获取这些对象,SpringMVC框架就象一个大管家一样“不辞辛苦”地为我们准备好了所需的一切。下面演示一下使用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中清空。
方法返回参数
在低版本的SpringMVC中,请求处理方法的返回值类型都必须是ModelAndView。而在Spring2.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也不失为一个好的选择。
--------------------------------------------------------------------------------
注册自己的属性编辑器
SpringMVC有一套常用的属性编辑器,这包括基本数据类型及其包裹类的属性编辑器、String属性编辑器、JavaBean的属性编辑器等。但有时我们还需要向SpringMVC框架注册一些自定义的属性编辑器,如特定时间格式的属性编辑器就是其中一例。
SpringMVC允许向整个Spring框架注册属性编辑器,它们对所有Controller都有影响。当然SpringMVC也允许仅向某个Controller注册属性编辑器,对其它的Controller没有影响。前者可以通过AnnotationMethodHandlerAdapter的配置做到,而后者则可以通过@InitBinder注解实现。
下面先看向整个SpringMVC框架注册的自定义编辑器:
- 清单13.注册框架级的自定义属性编辑器
<bean class="org.springframework.web.servlet.mvc.annotation. AnnotationMethodHandlerAdapter"> <property name="webBindingInitializer"> <bean class="com.baobaotao.web.MyBindingInitializer"/> </property> </bean>
MyBindingInitializer实现了WebBindingInitializer接口,在接口方法中通过binder注册多个自定义的属性编辑器,其代码如下所示:
- 清单14.自定义属性编辑器
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,可以在Controller中定义一个标注@InitBinder注解的方法,可以在该方法中向Controller了注册若干个属性编辑器,来看下面的代码:
- 清单15.注册Controller级的自定义属性编辑器
@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类型的入参,以便SpringMVC框架将注册属性编辑器的WebDataBinder对象传递进来。
--------------------------------------------------------------------------------
如何准备数据
在编写Controller时,常常需要在真正进入请求处理方法前准备一些数据,以便请求处理或视图渲染时使用。在传统的SimpleFormController里,是通过复写其referenceData()方法来准备引用数据的。在Spring2.5时,可以将任何一个拥有返回值的方法标注上@ModelAttribute,使其返回值将会进入到模型对象的属性列表中。来看下面的例子:
- 清单16.定义为处理请求准备数据的方法
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"; } }
在①处,通过使用@ModelAttribute注解,populateItem()方法将在任何请求处理方法执行前调用,SpringMVC会将该方法返回值以“items”为名放入到隐含的模型对象属性列表中。
所以在②处,我们就可以通过ModelMap入参访问到items属性,当执行listAllBoard()请求处理方法时,②处将在控制台打印出“model.items:2”的信息。当然我们也可以在请求的视图中访问到模型对象中的items属性。
--------------------------------------------------------------------------------
小结
Spring2.5对SpringMVC进行了很大增强,现在我们几乎完全可以使用基于注解的SpringMVC完全替换掉原来基于接口SpringMVC程序。基于注解的SpringMVC比之于基于接口的SpringMVC拥有以下几点好处:
•方便请求和控制器的映射;
•方便请求处理方法入参绑定URL参数;
•Controller不必继承任何接口,它仅是一个简单的POJO。
但是基于注解的SpringMVC并不完美,还存在优化的空间,因为在某些配置上它比基于XML的配置更繁琐。比如对于处理多个请求的Controller来说,假设我们使用一个URL参数指定调用的处理方法(如xxx.do?method=listBoardTopic),当使用注解时,每个请求处理方法都必须使用@RequestMapping()注解指定对应的URL参数(如@RequestMapping(params="method=listBoardTopic")),而在XML配置中我们仅需要配置一个ParameterMethodNameResolver就可以了。
参考资料
学习
•Spring系列:Spring框架简介:优秀的Spring框架入门系列,了解Spring框架的基本概念。
•轻量级开发的成功秘诀,第3部分:Spring露出水面:介绍了在Spring框架的轻量级Ioc容器。
•SpringFramework和IBMWebSphereApplicationServer:Interface21的首席执行官RodJohnson和IBM的WebSphereOpenSource主管PaulBuck讨论了SpringFramework通过IBMWebSphereApplicationServer认证对Spring和WebSphere产品系列的开发人员和客户有何重要意义。
•Tiger中的注释,第1部分:向Java代码中添加元数据:解释了元数据如此有用的原因,向您介绍了Java语言中的注释,并研究了Tiger的内置注释。
•Tiger中的注释,第2部分:定制注释:说明了如何创建定制注释,如何用自己的注释注解文档,并进一步定制代码。
获得产品和技术
•Springframework网站:下载Spring框架。