使用 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框架。

相关推荐