|
0.提示
1)Spring发行版本附带了PetClinic示例,它是一个在简单的表单处理的上下文中,利用了本节中说明的注解支持的Web应用程序。可以在“samples/petclinic”目录中找到PetClinic应用程序。
2)另外一个建立在基于注解的WebMVC上的示例应用程序,请见imagedb。
这个示例集中在无状态的multi-action控制器,包括多段文件上传的处理。可以在“samples/imagedb”目录找到imagedb应用程序。
只有对应的HandlerMapping(为了实现类型级别的注解)和/或HandlerAdapter(为了实现方法级别的注解)出现在dispatcher中时,@RequestMapping才会被处理。这在DispatcherServlet和DispatcherPortlet中都是缺省的行为。
然而,如果是在定义自己的HandlerMappings或HandlerAdapters,就需要确保一个对应的自定义的DefaultAnnotationHandlerMapping和/或AnnotationMethodHandlerAdapter同样被定义——假设想要使用@RequestMapping。
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-2.5.xsd">
DefaultAnnotationHandlerMapping"/>
AnnotationMethodHandlerAdapter"/>
...(controllerbeandefinitions)...
例1雁联zfpt-servlet.xml配置DefaultAnnotationHandlerMapping和/或AnnotationMethodHandlerAdapter
DefaultAnnotationHandlerMapping">
1
AnnotationMethodHandlerAdapter">
class="com.ylink.zfpt.web.intercepor.SuperUserAccessInterceptor">
例2:web.xml
xmlns=http://java.sun.com/xml/ns/j2eexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> SpringPetClinic
SpringPetClinicsampleapplication
2.1webAppRootKey
webAppRootKey
petclinic.root
2.3log4jConfigLocation
log4jConfigLocation
/WEB-INF/classes/log4j.properties
2.4contextConfigLocation
contextConfigLocation
/WEB-INF/spring/applicationContext-jdbc.xml
/WEB-INF/applicationContext-security.xml
2.5springSecurityFilterChain
springSecurityFilterChain
org.springframework.web.filter.DelegatingFilterProxy
springSecurityFilterChain
/
2.6Log4jConfigListener
org.springframework.web.util.Log4jConfigListener
2.7ContextLoaderListener
org.springframework.web.context.ContextLoaderListener
2.8DispatcherServlet
petclinic
org.springframework.web.servlet.DispatcherServlet
2
petclinic
/
2.9exception.java
java.lang.Exception
/WEB-INF/jsp/uncaughtException.jsp
例3:雁联如果你想要自定义映射策略,显式的定义一个DefaultAnnotationHandlerMapping和/或AnnotationMethodHandlerAdapter也有实际意义。例如,指定一个自定义的PathMatcher或者WebBindingInitializer
一个简单的基于注解的Controller
启动Tomcat,发送http://localhost/forum.doURL请求,BbtForumController的listAllBoard()方法将响应这个请求,并转向WEB-INF/jsp/listBoard.jsp的视图页面。
清单1.BbtForumController.javapackagecom.baobaotao.web;
importcom.baobaotao.service.BbtForumService;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Controller;
importorg.springframework.web.bind.annotation.ModelAttribute;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RequestMethod;
importjava.util.Collection; @Controller//<——①
@RequestMapping("/forum.do")
publicclassBbtForumController{
@Autowired
privateBbtForumServicebbtForumService;
@RequestMapping//<——②
publicStringlistAllBoard(){
bbtForumService.getAllBoard();
System.out.println("calllistAllBoardmethod.");
return"listBoard";
}
}
在①处使用了两个注解,分别是@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等)。
清单2.web.xml:启用Spring容器和SpringMVC框架
为了让基于注解的SpringMVC真正工作起来,需要在SpringMVC对应的xxx-servlet.xml配置文件中做一些手脚。在此之前,还是先来看一下web.xml的配置吧
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">
SpringAnnotationMVCSample
contextConfigLocation
classpath:applicationContext.xml
org.springframework.web.context.ContextLoaderListener
annomvc
org.springframework.web.servlet.DispatcherServlet
2
annomvc
.do
清单3.annomvc-servlet.xml
web.xml中定义了一个名为annomvc的SpringMVC模块,按照SpringMVC的契约,需要在WEB-INF/annomvc-servlet.xml配置文件中定义SpringMVC模块的具体配置。annomvc-servlet.xml的配置内容如下所示:
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">
AnnotationMethodHandlerAdapter"/>
p:prefix="/WEB-INF/jsp/"p:suffix=".jsp"/>
因为Spring所有功能都在Bean的基础上演化而来,所以必须事先将Controller变成Bean,这是通过在类中标注@Controller并在annomvc-servlet.xml中启用组件扫描机制来完成的,如①所示。
在②处,配置了一个AnnotationMethodHandlerAdapter,它负责根据Bean中的SpringMVC注解对Bean进行加工处理,使这些Bean变成控制器并映射特定的URL请求。
而③处的工作是定义模型视图名称的解析规则,这里我们使用了Spring2.5的特殊命名空间,即p命名空间,它将原先需要通过元素配置的内容转化为属性配置,在一定程度上简化了的配置。
2.让一个Controller处理多个URL请求
在低版本的SpringMVC中,我们可以通过继承MultiActionController让一个Controller处理多个URL请求。使用@RequestMapping注解后,这个功能更加容易实现了。请看下面的代码:
清单3.每个请求处理参数对应一个URL
packagecom.baobaotao.web;
importcom.baobaotao.service.BbtForumService;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Controller;
importorg.springframework.web.bind.annotation.RequestMapping;
@Controller
publicclassBbtForumController{
@Autowired
privateBbtForumServicebbtForumService;
@RequestMapping("/listAllBoard.do")//<——①
publicStringlistAllBoard(){
bbtForumService.getAllBoard();
System.out.println("calllistAllBoardmethod.");
return"listBoard";
}
@RequestMapping("/listBoardTopic.do")//<——②
publicStringlistBoardTopic(inttopicId){
bbtForumService.getBoardTopics(topicId);
System.out.println("calllistBoardTopicmethod.");
return"listTopic";
}
}
在这里,我们分别在①和②处为listAllBoard()和listBoardTopic()方法标注了@RequestMapping注解,分别指定这两个方法处理的URL请求,这相当于将BbtForumController改造为MultiActionController。这样/listAllBoard.do的URL请求将由listAllBoard()负责处理,而/listBoardTopic.do?topicId=1的URL请求则由listBoardTopic()方法处理。
清单4.一个Controller对应一个URL,由请求参数决定请求处理方法
对于处理多个URL请求的Controller来说,我们倾向于通过一个URL参数指定Controller处理方法的名称(如method=listAllBoard),而非直接通过不同的URL指定Controller的处理方法。使用@RequestMapping注解很容易实现这个常用的需求。来看下面的代码
packagecom.baobaotao.web;
importcom.baobaotao.service.BbtForumService;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Controller;
importorg.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/bbtForum.do")//<——①指定控制器对应URL请求
publicclassBbtForumController{
@Autowired
privateBbtForumServicebbtForumService;
//<——②如果URL请求中包括"method=listAllBoard"的参数,由本方法进行处理
@RequestMapping(params="method=listAllBoard")
publicStringlistAllBoard(){
bbtForumService.getAllBoard();
System.out.println("calllistAllBoardmethod.");
return"listBoard";
}
//<——③如果URL请求中包括"method=listBoardTopic"的参数,由本方法进行处理
@RequestMapping(params="method=listBoardTopic")
publicStringlistBoardTopic(inttopicId){
bbtForumService.getBoardTopics(topicId);
System.out.println("calllistBoardTopicmethod.");
return"listTopic";
}
} 在类定义处标注的@RequestMapping让BbtForumController处理所有包含/bbtForum.do的URL请求,而BbtForumController中的请求处理方法对URL请求的分流规则在②和③处定义分流规则按照URL的method请求参数确定。所以分别在类定义处和方法定义处使用@RequestMapping注解,就可以很容易通过URL参数指定Controller的处理方法了。
清单5.让请求处理方法处理特定的HTTP请求方法
@RequestMapping注解中除了params属性外,还有一个常用的属性是method,它可以让Controller方法处理特定HTTP请求方式的请求,如让一个方法处理HTTPGET请求,而另一个方法处理HTTPPOST请求,如下所示:
packagecom.baobaotao.web;
importcom.baobaotao.service.BbtForumService;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Controller;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping("/bbtForum.do")
publicclassBbtForumController{
@RequestMapping(params="method=createTopic",method=RequestMethod.POST)
publicStringcreateTopic(){
System.out.println("callcreateTopicmethod.");
return"createTopic";
}
}
这样只有当/bbtForum.do?method=createTopic请求以HTTPPOST方式提交时,createTopic()才会进行处理。
3.处理方法入参如何绑定URL参数
3.1按契约绑定
Controller的方法标注了@RequestMapping注解后,它就能处理特定的URL请求。
我们不禁要问:请求处理方法入参是如何绑定URL参数的呢?在回答这个问题之前先来看下面的代码
清单5.按参数名匹配进行绑定
@RequestMapping(params="method=listBoardTopic")
//<——①topicId入参是如何绑定URL请求参数的?
publicStringlistBoardTopic(inttopicId){
bbtForumService.getBoardTopics(topicId);
System.out.println("calllistBoardTopicmethod.");
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转换为基本数据类型。
清单6.User.java:一个JavaBean
另外,请求处理方法的入参也可以一个JavaBean,如下面的User对象就可以作为一个入参
packagecom.baobaotao.web;
publicclassUser{
privateintuserId;
privateStringuserName;
//省略get/setter方法
publicStringtoString(){
returnthis.userName+","+this.userId;
}
} 7.使用JavaBean作为请求处理方法的入参
下面是将User作为listBoardTopic()请求处理方法的入参:
@RequestMapping(params="method=listBoardTopic")
publicStringlistBoardTopic(inttopicId,Useruser){
bbtForumService.getBoardTopics(topicId);
System.out.println("topicId:"+topicId);
System.out.println("user:"+user);
System.out.println("calllistBoardTopicmethod.");
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注解指定
这里,对listBoardTopic()请求处理方法的topicId入参标注了@RequestParam("id")注解,所以它将和id的URL参数绑定。
packagecom.baobaotao.web;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RequestParam;
…
@Controller
@RequestMapping("/bbtForum.do")
publicclassBbtForumController{
@RequestMapping(params="method=listBoardTopic")
publicStringlistBoardTopic(@RequestParam("id")inttopicId,
Useruser){
bbtForumService.getBoardTopics(topicId);
System.out.println("topicId:"+topicId);
System.out.println("user:"+user);
System.out.println("calllistBoardTopicmethod.");
return"listTopic";
}
…
}
这里@RequestParam注解可以用来提取名为“topicId”的int类型的参数,并将之作为输入参数传入。@RequestParam支持类型转换,还有必需和可选参数。类型转换目前支持所有的基本Java类型,你可通过定制的PropertyEditors来扩展它的范围。下面是一些例子,其中包括了必需和可选参数:
@RequestParam(value="number",required=false)Stringnumber
@RequestParam("id")Longid
@RequestParam("balance")doublebalance
@RequestParamdoubleamount 注意,最后一个例子没有提供清晰的参数名。当且仅当代码带调试符号编译时,结果会提取名为“amount”的参数,否则,将抛出IllegalStateException异常,因为当前的信息不足以从请求中提取参数。由于这个原因,在编码时最好显式的指定参数名。
3.3绑定模型对象中某个属性—ModelMap
Spring2.0定义了一个org.springframework.ui.ModelMap类,它作为通用的模型数据承载对象,传递数据供视图所用。我们可以在请求处理方法中声明一个ModelMap类型的入参,Spring会将本次请求模型对象引用通过该入参传递进来,这样就可以在请求处理方法内部访问模型对象了。来看下面的例子:
@RequestMapping(params="method=listBoardTopic")
publicStringlistBoardTopic(@RequestParam("id")inttopicId,
Useruser,
ModelMapmodel){
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对象。packagecom.baobaotao.web;
…
importorg.springframework.ui.ModelMap;
importorg.springframework.web.bind.annotation.SessionAttributes;
@Controller
@RequestMapping("/bbtForum.do")
@SessionAttributes("currUser")//①将ModelMap中属性名为currUser的属性
//放到Session属性列表中,以便这个属性可以跨请求访问
publicclassBbtForumController{
…
@RequestMapping(params="method=listBoardTopic")
publicStringlistBoardTopic(@RequestParam("id")inttopicId,Useruser,
ModelMapmodel){
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”)访问到这个属性。
1)这里我们仅将一个ModelMap的属性放入Session中,其实@SessionAttributes允许指定多个属性:
你可以通过字符串数组的方式指定多个属性,如@SessionAttributes({“attr1”,”attr2”})。
2)此外,@SessionAttributes还可以通过属性类型指定要session化的ModelMap属性:
如@SessionAttributes(types=User.class),
当然也可以指定多个类,如@SessionAttributes(types={User.class,Dept.class}),
3)还可以联合使用属性名和属性类型指定:
@SessionAttributes(types={User.class,Dept.class},value={“attr1”,”attr2”})。
清单11.使模型对象的特定属性具有Session范围的作用域
上面讲述了如何往ModelMap中放置属性以及如何使ModelMap中的属性拥有Session域的作用范围。除了在JSP视图页面中通过传统的方法访问ModelMap中的属性外,读者朋友可能会问:是否可以将ModelMap中的属性绑定到请求处理方法的入参中呢?答案是肯定的。Spring为此提供了一个@ModelAttribute的注解,下面是使用@ModelAttribute注解的例子
@Controller
@RequestMapping("/bbtForum.do")
@SessionAttributes("currUser")//①让ModelMap的currUser属性拥有session级作用域
publicclassBbtForumController{
@Autowired
privateBbtForumServicebbtForumService;
@RequestMapping(params="method=listBoardTopic")
publicStringlistBoardTopic(@RequestParam("id")inttopicId,
Useruser,
ModelMapmodel){
bbtForumService.getBoardTopics(topicId);
System.out.println("topicId:"+topicId);
System.out.println("user:"+user);
model.addAttribute("currUser",user);//②向ModelMap中添加一个属性
return"listTopic";
}
//③将ModelMap中的currUser属性绑定到user入参中。currUser是session级别的
@RequestMapping(params="method=listAllBoard")
publicStringlistAllBoard(@ModelAttribute("currUser")Useruser){
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属性上了
请求处理方法入参的可选类型 说明 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 为属性列表中的命令/表单对象的校验结果,注意检验结果参数必须紧跟在命令/表单对象的后面 org.springframework.web.bind.support.SessionStatus 可以通过该类型status对象显式结束表单的处理,这相当于触发session清除其中的通过@SessionAttributes定义的属性 SpringMVC框架的易用之处在于,你可以按任意顺序定义请求处理方法的入参(除了Errors和BindingResult必须紧跟在命令对象/表单参数后面以外),SpringMVC会根据反射机制自动将对应的对象通过入参传递给请求处理方法。这种机制让开发者完全可以不依赖ServletAPI开发控制层的程序,当请求处理方法需要特定的对象时,仅仅需要在参数列表中声明入参即可,不需要考虑如何获取这些对象,SpringMVC框架就象一个大管家一样“不辞辛苦”地为我们准备好了所需的一切。下面演示一下使用SessionStatus的例子:
@RequestMapping(method=RequestMethod.POST)
publicStringprocessSubmit(@ModelAttributeOwnerowner,
BindingResultresult,
SessionStatusstatus){//<——①
newOwnerValidator().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中清空。
packagecom.baobaotao.web;
…
importorg.springframework.ui.ModelMap;
importorg.springframework.web.bind.annotation.SessionAttributes;
@Controller
@RequestMapping("/bbtForum.do")
@SessionAttributes("currUser")//①将ModelMap中属性名为currUser的属性
//放到Session属性列表中,以便这个属性可以跨请求访问
publicclassBbtForumController{
…
@RequestMapping(params="method=listBoardTopic")
publicStringlistBoardTopic(@RequestParam("id")inttopicId,Useruser,
ModelMapmodel){
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”)访问到这个属性。
1)这里我们仅将一个ModelMap的属性放入Session中,其实@SessionAttributes允许指定多个属性:
你可以通过字符串数组的方式指定多个属性,如@SessionAttributes({“attr1”,”attr2”})。
2)此外,@SessionAttributes还可以通过属性类型指定要session化的ModelMap属性:
如@SessionAttributes(types=User.class),
当然也可以指定多个类,如@SessionAttributes(types={User.class,Dept.class}),
3)还可以联合使用属性名和属性类型指定:
@SessionAttributes(types={User.class,Dept.class},value={“attr1”,”attr2”})。
清单11.使模型对象的特定属性具有Session范围的作用域
上面讲述了如何往ModelMap中放置属性以及如何使ModelMap中的属性拥有Session域的作用范围。除了在JSP视图页面中通过传统的方法访问ModelMap中的属性外,读者朋友可能会问:是否可以将ModelMap中的属性绑定到请求处理方法的入参中呢?答案是肯定的。Spring为此提供了一个@ModelAttribute的注解,下面是使用@ModelAttribute注解的例子
@Controller
@RequestMapping("/bbtForum.do")
@SessionAttributes("currUser")//①让ModelMap的currUser属性拥有session级作用域
publicclassBbtForumController{
@Autowired
privateBbtForumServicebbtForumService;
@RequestMapping(params="method=listBoardTopic")
publicStringlistBoardTopic(@RequestParam("id")inttopicId,
Useruser,ModelMapmodel){
bbtForumService.getBoardTopics(topicId);
System.out.println("topicId:"+topicId);
System.out.println("user:"+user);
model.addAttribute("currUser",user);//②向ModelMap中添加一个属性
return"listTopic";
}
//③将ModelMap中的currUser属性绑定到user入参中。@RequestMapping(params="method=listAllBoard")
publicStringlistAllBoard(@ModelAttribute("currUser")Useruser){
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属性上了
void 此时逻辑视图名由请求处理方法对应的URL确定,如以下的方法:
@RequestMapping("/welcome.do")
publicvoidwelcomeHandler(){
}”welcome” String 此时逻辑视图名为返回的字符,如以下的方法:
@RequestMapping(method=RequestMethod.GET)
publicStringsetupForm(@RequestParam("ownerId")intownerId,
ModelMapmodel){
Ownerowner=this.clinic.loadOwner(ownerId);
model.addAttribute(owner);
return"ownerForm";
}
对应的逻辑视图名为“ownerForm” org.springframework.ui.ModelMap 和返回类型为void一样,逻辑视图名取决于对应请求的URL,如下面的例子:
@RequestMapping("/vets.do")
publicModelMapvetsHandler(){
returnnewModelMap(this.clinic.getVets());
} ModelAndView 当然还可以是传统的ModelAndView。 应该说使用String作为请求处理方法的返回值类型是比较通用的方法,这样返回的逻辑视图名不会和请求URL绑定,具有很大的灵活性,而模型数据又可以通过ModelMap控制。当然直接使用传统的ModelAndView也不失为一个好的选择。
6.如何准备数据--@ModelAttribute
在编写Controller时,常常需要在真正进入请求处理方法前准备一些数据,以便请求处理或视图渲染时使用。在传统的SimpleFormController里,是通过复写其referenceData()方法来准备引用数据的。
在Spring2.5时,可以将任何一个拥有返回值的方法标注上@ModelAttribute,使其返回值将会进入到模型对象的属性列表中。来看下面的例子:
importorg.springframework.web.bind.annotation.SessionAttributes;
importjava.util.ArrayList;
importjava.util.List;
importjava.util.Set;
@Controller
@RequestMapping("/bbtForum.do")
publicclassBbtForumController{
@Autowired
privateBbtForumServicebbtForumService;
@ModelAttribute("items")//<——①向模型对象中添加一个名为items的属性
publicListpopulateItems(){
Listlists=newArrayList();
lists.add("item1");
lists.add("item2");
returnlists;
}
@RequestMapping(params="method=listAllBoard")
publicStringlistAllBoard(
@ModelAttribute("currUser")Useruser,ModelMapmodel){
bbtForumService.getAllBoard();
//<——②在此访问模型中的items属性
System.out.println("model.items:"+((List)model.get("items")).size());
return"listBoard";
}
}
说明:
在①处,通过使用@ModelAttribute注解,populateItem()方法将在任何请求处理方法执行前调用,SpringMVC会将该方法返回值以“items”为名放入到隐含的模型对象属性列表中。
所以在②处,我们就可以通过ModelMap入参访问到items属性,当执行listAllBoard()请求处理方法时,②处将在控制台打印出“model.items:2”的信息。当然我们也可以在请求的视图中访问到模型对象中的items属性。
一个典型的表单处理场景包括:获得可编辑对象,在编辑模式下显示它持有的数据、允许用户提交并最终进行验证和保存变化数据。
SpringMVC提供下列几个特性辅助进行上述所有活动:
数据绑定机制,完全用从请求参数中获得的数据填充一个对象;
支持错误处理和验证;
JSP表单标记库;
基类控制器。
使用@MVC,除了由于@ModelAttribute、@InitBinder和@SessionAttributes这些注解的存在而不再需要基类控制器外,其它一切都不需要改变。
@RequestMapping(method=RequestMethod.GET)
publicAccountsetupForm(){
...
} @RequestMapping(method=RequestMethod.POST)
publicvoidonSubmit(Accountaccount){
...
} 它们是非常有效的请求处理方法签名。
第一个方法处理初始的HTTPGET请求,准备被编辑的数据,返回一个Account对象供SpringMVC表单标签使用。
第二个方法在用户提交更改时处理随后的HTTPPOST请求,并接收一个Account对象作为输入参数,它是SpringMVC的数据绑定机制用请求中的参数自动填充的。这是一个非常简单的程序模型。
1.xxx.jsp
Account对象中含有要被编辑的数据。
在SpringMVC的术语当中,Account被称作是表单模型对象。这个对象必须通过某个名称让表单标签(还有数据绑定机制)知道它的存在。下面是从JSP页面中截取的部分代码,引用了一个名为“account”的表单模型对象:
AccountNumber:
...
2.xxxController.java
即使我们没有在任何地方指定“account”的名称,这段JSP程序也会和上面所讲的方法签名协作的很好。这是因为@MVC用返回对象的类型名称作为默认值,因此一个Account类型的对象默认的就对应一个名为“account”的表单模型对象。如果默认的不合适,我们就可以用@ModelAttribute来改变它的名称,如下所示:
@RequestMapping(method=RequestMethod.GET)
public@ModelAttribute("account")SpecialAccountsetupForm(){
...
} @RequestMapping(method=RequestMethod.POST)
publicvoidupdate(@ModelAttribute("account")SpecialAccountaccount){
...
}
此处setupForm()不是一个请求处理方法,而是任何请求处理方法被调用之前,用来准备表单项模型对象的一个方法。对那些熟悉SpringMVC的老用户来说,这和SimpleFormController的formBackingObject()方法是非常相似的。
最初的GET方法中我们得到一次表单模型对象,在随后的POST方法中当我们依靠数据绑定机制用用户所做的改变覆盖已有的Account对象时,我们会第二次得到它,在这种表单处理场景中把@ModelAttribute放在方法上是很有用的。当然,作为一种两次获得对象的替换方案,我们也可以在两次请求过程中将它保存进HTTP的会话(session),这就是我们下面将要分析的情况。
@Controller
@SessionAttributes("account")
publicclassAccountFormController{
...
} @Controller
@SessionAttributes(types=Account.class)
publicclassAccountFormController{
...
}
根据上面的注解,AccountFormController会在初始的GET方法和随后的POST方法之间,把名为“account”的表单模型对象(或者象第二个例子中的那样,把所有Account类型的表单模型对象)存入HTTP会话(session)中。不过,当有改变连续发生的时候,就应当把属性对象从会话中移除了。我们可以借助SessionStatus实例来做这件事,如果把它添加进onSubmit的方法签名中,@MVC会完成这个任务:
@RequestMapping(method=RequestMethod.POST)
publicvoidonSubmit(Accountaccount,SessionStatussessionStatus){
...
sessionStatus.setComplete();//Clears@SessionAttributes
}
7.3雁联例子
1.效果图:
在“subProcess.jsp”页面,点击“修改”按钮,进入“修改页面(modifyProcess.jsp)”,
“修改页面(modifyProcess.jsp)”上会显示该条记录的信息(根据ID从数据库读取记录,然后显示在页面上)
(modifyProcess.jsp)”修改了信息后,
用户再次点击“修改按钮”进入到“修改页面(modifyProcess.jsp)”时,“修改页面(modifyProcess.jsp)”上显示的不是“修改后”的信息,而还是“修改前”的信息。
解决方案:此问题是由于浏览器的缓存引起的,在“修改页面(modifyProcess.jsp)”的head处加入如下语句:
src=" value="/static/js/public.js"/>">
value="/static/css/cssCn.css"/>"
rel="stylesheet"
type="text/css"/>
“subProcess.jsp页面”中,关于“修改”按钮的代码如下:
'''');"class="button">
|
3.xxxController.java
在“subProcess.jsp页面”中点击“修改”按钮,程序运行到xxxController.java.
@Controller
@RequestMapping("/procAdmin/subSysProc")
publicclassSubSysProController{ privateValidatorbeanValidator; @RequestMapping(params="method=modifyProcUI",method=RequestMethod.GET)
publicStringmodifyProcUI(Processprocess,BindingResultresult,Modelmodel){ beanValidator.validate(process,result);
if(result.hasErrors()){
returnaddProcUI();
}
//根据要修改记录ID,得到要修改的记录的信息
process=processService.getProcessById(process.getId());
System.out.println("1.prcsDescription="+process.getPrcsDescription());
//获得子系统列表
ListsubSysList=subSysService.getAllSubSysList();
model.addAttribute("process",process);
model.addAttribute("subSysList",subSysList);
return"processManage/processModify";
} }
4.zfpt-servlet.xml
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:webflow="http://www.springframework.org/schema/webflow-config"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/webflow-confighttp://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd
http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"> AnnotationMethodHandlerAdapter">
/WEB-INF/validator-rules.xml
/WEB-INF/validator.xml
5.modifyProcess.jsp
//禁止浏览器缓存,本例子中有用
action=""method="PUT">
进 程 名
|
描 述
cssStyle="WIDTH:180px"maxlength="200"/>
|
工作目录
|
子 系 统
itemLabel="code"itemValue="id"/>
|
8.定制数据绑定--@InitBinder注解
有时数据绑定需要定制,例如我们也许需要指定必需填写的域,或者需要为日期、货币金额等类似事情注册定制的PropertyEditors。用@MVC实现这些功能是非常容易的:
@InitBinder
publicvoidinitDataBinder(WebDataBinderbinder){
binder.setRequiredFields(newString[]{"number","name"});
} @InitBinder注解的方法可以访问@MVC用来绑定请求参数的DataBinder实例,它允许我们为每个控制器定制必须项。
SpringMVC有一套常用的属性编辑器,这包括基本数据类型及其包裹类的属性编辑器、String属性编辑器、JavaBean的属性编辑器等。但有时我们还需要向SpringMVC框架注册一些自定义的属性编辑器,如特定时间格式的属性编辑器就是其中一例。
1)SpringMVC允许向整个Spring框架注册属性编辑器,它们对所有Controller都有影响。
2)当然SpringMVC也允许仅向某个Controller注册属性编辑器,对其它的Controller没有影响。
前者可以通过AnnotationMethodHandlerAdapter的配置做到,而后者则可以通过@InitBinder注解实现。
下面先看向整个SpringMVC框架注册的自定义编辑器:
清单13.注册框架级的自定义属性编辑器
清单14.自定义属性编辑器--配置一个定制的WebBindingInitializer
为了外化数据绑定初始化的过程,可以提供一个WebBindingInitializer接口的自定义实现。通过为一个AnnotationMethodHandlerAdapter提供一个定制的bean配置可以使它启用,这样就覆盖了默认配置。
MyBindingInitializer实现了WebBindingInitializer接口,在接口方法中通过binder注册多个自定义的属性编辑器其代码如下所示:
packageorg.springframework.samples.petclinic.web;
importjava.text.SimpleDateFormat;
importjava.util.Date;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.beans.propertyeditors.CustomDateEditor;
importorg.springframework.beans.propertyeditors.StringTrimmerEditor;
importorg.springframework.samples.petclinic.Clinic;
importorg.springframework.samples.petclinic.PetType;
importorg.springframework.web.bind.WebDataBinder;
importorg.springframework.web.bind.support.WebBindingInitializer;
importorg.springframework.web.context.request.WebRequest; publicclassMyBindingInitializerimplementsWebBindingInitializer{ publicvoidinitBinder(WebDataBinderbinder,WebRequestrequest){
SimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(
Date.class,newCustomDateEditor(dateFormat,false));
binder.registerCustomEditor(
String.class,newStringTrimmerEditor(false));
}
}
例2:看注册的自定义编辑器:如果希望某个属性编辑器仅作用于特定的Controller,可以在Controller中定义一个标注@InitBinder注解的方法,可以在该方法中向Controller了注册若干个属性编辑器.
使用@InitBinder注解控制器方法,可以在控制器类内部直接配置Web数据绑定。@InitBinder指定初始化WebDataBinder的方法,后者被用于填充注解的句柄方法的命令和表单对象参数。
这个init-binder方法支持@RequestMapping支持的全部参数,除了命令/表单对象和对应的验证结果对象。Init-binder方法必须没有返回值。因此,它们常被声明为void。典型的参数,包括WebDataBinder以及WebRequest或者java.util.Locale,允许代码注册上下文特定的编辑器。
注意:被标注@InitBinder注解的方法必须拥有一个WebDataBinder类型的入参,以便SpringMVC框架将注册属性编辑器的WebDataBinder对象传递进来。
@Controller
publicclassMyFormController{@InitBinder
publicvoidinitBinder(WebDataBinderbinder){
SimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(
Date.class,newCustomDateEditor(dateFormat,false));
//binder.setRequiredFields(newString[]{"number","name"});
}
…
}
9.数据绑定结果和验证--BindingResult
数据绑定也许会导致类似于类型转换或域缺失的错误。不管发生什么错误,我们都希望能返回到编辑的表单,让用户自行更正。要想实现这个目的,我们可直接在方法签名的表单模型对象后面追加一个BindingResult对象注意检验结果参数必须紧跟在命令/表单对象的后面,:@RequestMapping(method=RequestMethod.POST)
publicModelAndViewonSubmit(Accountaccount,BindingResultbindingResult){
if(bindingResult.hasErrors()){
ModelAndViewmav=newModelAndView();
mav.getModel().putAll(bindingResult.getModel());
returnmav;
}
//Savethechangesandredirecttothenextview...
}
发生错误时我们返回到出现问题的视图,并把从BindingResult得到的属性增加到模型上,这样特定域的错误就能够反馈给用户。要注意的是,我们并没有指定一个显式的视图名,而是允许DispatcherServlet依靠与入口URI路径信息匹配的默认视图名。
调用Validator对象并把BindingResult传给它,仅这一行代码就可实现验证操作。这允许我们在一个地方收集绑定和验证错误:
0)xxxController.java
@RequestMapping(method=RequestMethod.POST)
publicModelAndViewonSubmit(Accountaccount,BindingResultbindingResult){
newAccountValidator().validate(account,bindingResult);
if(bindingResult.hasErrors()){
ModelAndViewmav=newModelAndView();
mav.getModel().putAll(bindingResult.getModel());
returnmav;
}
//Savethechangesandredirecttothenextview...
}
1)OwnerValidator.java
publicclassAccountValidator{
publicvoidvalidate(Ownerowner,Errorserrors){
if(!StringUtils.hasLength(owner.getFirstName())){
errors.rejectValue("firstName","required","required");
}
if(!StringUtils.hasLength(owner.getLastName())){
errors.rejectValue("lastName","required","required");
}
if(!StringUtils.hasLength(owner.getAddress())){
errors.rejectValue("address","required","required");
}
if(!StringUtils.hasLength(owner.getCity())){
errors.rejectValue("city","required","required");
}
Stringtelephone=owner.getTelephone();
if(!StringUtils.hasLength(telephone)){
errors.rejectValue("telephone","required","required");
}else{
for(inti=0;i if((Character.isDigit(telephone.charAt(i)))==false){
errors.rejectValue("telephone","nonNumeric","non-numeric");
break;
}
}
}
}
}
2)xxx.jsp
|
现在是时候结束我们的Spring2.5Web层注解(非正式称法为@MVC)之旅了。
Web层注解频遭诟病是有事实依据的,那就是嵌入源代码的URI路径。
这个问题很好矫正,URI路径和控制器类之间的匹配关系用XML配置文件去管理,只在方法级的映射中使用@RequestMapping注解。
我们将配置一个ControllerClassNameHandlerMapping,它使用依赖控制器类名字的惯例,将URI映射到控制器:
现在“/accounts/”这样的请求都被匹配到AccountsController上,它与方法级别上的@RequestMapping注解协作的很好,只要添加上方法名就能够完成上述映射。此外,既然我们的方法并不会返回视图名称,我们现在就可以依据惯例匹配类名、方法名、URI路径和视图名。
当@Controller被完全转换为@MVC后,程序的写法如下:
@Controller
publicclassAccountsController{
privateAccountRepositoryaccountRepository;
@Autowired
publicAccountsController(AccountRepositoryaccountRepository){
this.accountRepository=accountRepository;
}
@RequestMapping(method=RequestMethod.GET)
publicvoidshow(@RequestParam("number")Stringnumber,
Mapmodel){
model.put("account",accountRepository.findAccount(number));
}
...
对应的XML配置文件如下:
ControllerClassNameHandlerMapping"/>
InternalResourceViewResolver">
你可以看出这是一个最精减的XML。程序里注解中没有嵌入URI路径,也没有显式指定视图名,请求处理方法也只有很简单的一行,方法签名与我们的需求精准匹配,其它的请求处理方法也很容易添加。不需要基类,也不需要XML(至少也是没有直接配置控制器),我们就能获得上述所有优势。
也许接下来你就可以看到,这种程序设计模型是多么有效了。
11.@RequestMapping简介
11.1灵活的请求处理方法签名
我们曾经承诺过要提供灵活的方法签名,现在来看一下成果。
输入的参数中移除了响应对象,增加了一个代表模型的Map;
返回的不再是ModelAndView,而是一个字符串,指明呈现响应时要用的视图名字:
@RequestMapping("/accounts/show")
publicStringshow(HttpServletRequestrequest,Mapmodel)
throwsException{
Stringnumber=ServletRequestUtils.getStringParameter(request,"number");
model.put("account",accountRepository.findAccount(number));
return"/WEB-INF/views/accounts/show.jsp";
}
Map输入参数是一个“隐式的”模型,对于我们来说在调用方法前创建它很方便,其中添加的键—值对数据便于在视图中解析应用。本例视图为show.jsp页面。
11.2当方法没有指定视图时
有种令人感兴趣的情形是当方法没有指定视图时(例如返回类型为void)会有什么事情发生,按照惯例DispatcherServlet要再使用请求URI的路径信息,不过要移去前面的斜杠和扩展名。让我们把返回类型改为void:
@RequestMapping("/accounts/show")
publicvoidshow(HttpServletRequestrequest,Mapmodel)
throwsException{
Stringnumber=ServletRequestUtils.getStringParameter(request,"number");
model.put("account",accountRepository.findAccount(number));
} 对于给定的请求处理方法和“/accounts/show”的请求映射,我们可以期望DispatcherServlet能够获得“accounts/show”的默认视图名称,当它与如下适当的视图解析器结合共同作用时,会产生与前面指明返回视图名同样的结果:
强烈推荐视图名称依赖惯例的方式,因为这样可以从控制器代码中消除硬编码的视图名称。如果你想定制DispatcherServlet获取默认视图名的方式,就在servlet上下文环境中配置一个你自己的RequestToViewNameTranslator实现,并为其beanid赋名为“viewNameTranslator”。
11.3继续@RequestMapping的讨论
RequestMapping("/accounts/")
11.2方法级别:
@RequestMapping(value="delete",method=RequestMethod.POST)
@RequestMapping(value="index",method=RequestMethod.GET,params="type=checking")
@RequestMapping
第一个方法级的请求映射和类级别的映射结合,当HTTP方法是POST时与路径“/accounts/delete”匹配;
第二个添加了一个要求,就是名为“type”的请求参数和其值“checking”都需要在请求中出现;
第三个根本就没有指定路径,这个方法匹配所有的HTTP方法,如果有必要的话可以用它的方法名。
下面改写我们的方法,使它可以依靠方法名进行匹配,程序如下:
@Controller
@RequestMapping("/accounts/")
publicclassAccountsController{
@RequestMapping(method=RequestMethod.GET)
publicvoidshow(@RequestParam("number")Stringnumber,
Mapmodel){
model.put("account",accountRepository.findAccount(number));
}
...
方法匹配的请求是“/accounts/show”,依据的是类级别的@RequestMapping指定的匹配路径“/accounts/”和方法名“show”。
13.@PathVariable
@PathVariable是用来对指定请求的URL路径里面的变量
@RequestMapping(value?=?"form/{id}/apply",?
method?=?{RequestMethod.PUT,?RequestMethod.POST})?? {id}在这个请求的URL里就是个变量,可以使用@PathVariable来获取@PathVariable和@RequestParam的区别就在于:@RequestParam用来获得静态的URL请求入参channelManage.jsp
'',null,''dialogleft=200px;dialogtop=260px;dialogwidth=800px;dialogheight=400px'');location.reload();">
2.ChannelManageController.java
@RequestMapping(value="/channel/{channelId}/start",method=RequestMethod.GET)
publicStringstart(@PathVariableIntegerchannelId)
throwsIOException,MalformedObjectNameException,NullPointerException,
InstanceNotFoundException,MBeanException,ReflectionException,
BusinessException{
//前面要加上权限控制
|
|