Chapter 13. Web MVC framework Web框架

13.1. 概述

Spring的web框架围绕DispatcherServlet设计。 DispatcherServlet的作用是将请求分发到不同的处理器。 Spring的web框架包括可配置的处理器(handler)映射、视图(view)解析、本地化(local)解析、 主题(theme)解析以及对文件上传的支持。Spring的Web框架中缺省的处理器是Controller 接口,这是一个非常简单的接口,仅包含ModelAndView handleRequest(request, response) 方法。可以通过实现这个接口来创建自己的控制器(也可以称之为处理器),但是更推荐继承Spring提供的一系列控制器, 比如AbstractControllerAbstractCommandControllerSimpleFormController。注意,需要选择正确的基类:如果没有表单,就不需要一个FormController。 这是和Struts的一个主要区别。

[Tip]Tip

从Spring 2.5开始,使用Java 5或者以上版本的用户可以采用基于注解的controller方式。 在实现传统的Controller及其子类时这是一种更好的替换方案, 其中提供了更为灵活的处理multi-action的能力。更多信息请参考Section 13.12, “基于注解的控制器配置”

Spring Web MVC允许使用任何对象作为命令对象(或表单对象)- 不必实现某个特定于框架的接口或从某个基类继承。 Spring的数据绑定相当灵活,例如,它认为类型不匹配这样的错误应该是应用级的验证错误,而不是系统错误。 所以你不需要为了保证表单内容的正确提交,而重复定义一个和业务对象有相同属性的表单对象来处理简单的无 类型字符串或者对字符串进行转换。这也是和Struts相比的另一个重要区别,Struts是围绕 ActionActionForm等基类构建的。

和WebWork相比,Spring将对象细分成更多不同的角色:控制器( Controller)、可选的命令对象(Command Object)或表单对象(Form Object), 以及传递到视图的模型(Model)。模型不仅包含命令对象或表单对象,而且也可以包含任何引用数据。 相比之下,WebWork的Action将所有的这些角色都合并在一个单独的对象里。 虽然WebWork的确允许在表单中使用现有的业务对象,但是必须把它们定义成相应的Action 类的bean属性。更重要的是,在进行视图层(View)运算和表单赋值时,WebWork使用的是同一个处理请求的 Action实例。因此,引用数据也需要被定义成Action 的bean属性。这样一个对象就承担了太多的角色(当然,对于这个观点仍有争议)。

Spring的视图解析相当灵活。一个控制器甚至可以直接向response输出一个视图 (此时控制器返回ModelAndView的值必须是null)。在一般的情况下,一个ModelAndView 实例包含一个视图名字和一个类型为Map的model, 一个model是一些以bean的名字为key,以bean对象(可以是命令或form,也可以是其他的JavaBean) 为value的键值对。对视图名称的解析处理也是高度可配置的,可以通过bean的名字、属性文件或者自定义的 ViewResolver实现来进行解析。实际上基于 Map的model(也就是MVC中的M)是高度抽象的,适用于各种表现层技术。 也就是说,任何表现层都可以直接和Spring集成,无论是JSP、Velocity还是其它表现层技术。 Map model可以被转换成合适的格式,比如JSP request attribute或者Velocity template model。

13.1.1. 与其他MVC实现框架的集成

由于种种原因,许多团队倾向于使用其他的web框架。比如,某些团队已经在其他的技术和工具方面进行了 投入,他们希望充分利用已有的经验。另外,Struts不仅有大量的书籍和工具,而且有许多开发人员都熟悉它。 因此,如果能忍受Struts的架构性缺陷,它仍然是web层一个不错的选择。WebWork和其它的web框架也是这样。

如果不打算使用Spring的web MVC框架,但仍希望使用Spring提供的其它功能, 可以很容易地将所选择的web框架和Spring结合起来。只需通过Spring的 ContextLoadListener启动一个root application context,就可以在 Struts或WebWork的Action中,通过ServletContext属性 (或者Spring提供的相应辅助方法)进行访问。请注意我们没有提到任何具体的“plugins”,因此也你不 需要任何专门的集成工具。从web层的角度看,可以以root application context实例为入口,把Spring作为一个library使用。

即便不使用Spring的web MVC,仍可以使用Spring提供的服务以及所有注册的bean。 在这里,Spring不会和Struts或WebWork竞争,它只是提供这些纯粹的web MVC框架所没有的功能, 例如:bean的配置、数据访问和事务处理。因此可以使用Spring的中间层或者数据访问层来增强你的应用, 比如你可能只需要JDBC或Hibernate事务抽象。

13.1.2. Spring Web MVC框架的特点

Spring的web模块提供了大量独特的功能,包括:

  • 清晰的角色划分:控制器(controller)、验证器(validator)、 命令对象(command object)、表单对象(form object)、模型对象(model object)、 Servlet分发器(DispatcherServlet)、 处理器映射(handler mapping)、视图解析器(view resolver)等等。 每一个角色都可以由一个专门的对象来实现。

  • 强大而直接的配置方式:将框架类和应用程序类都能作为JavaBean配置,支持跨多个context的引用,例如,在web控制器中对业务对象和验证器(validator)的引用。

  • 可适配、非侵入:可以根据不同的应用场景,选择合适的控制器子类 (simple型、command型、form型、wizard型、multi-action型或者自定义),而不是从单一控制器 (比如Action/ActionForm)继承。

  • 可重用的业务代码:可以使用现有的业务对象作为命令或表单对象,而不需要去扩展某个特定框架的基类。

  • 可定制的绑定(binding) 和验证(validation):比如将类型不匹配作为应用级的验证错误, 这可以保存错误的值。再比如本地化的日期和数字绑定等等。在其他某些框架中,你只能使用字符串表单对象, 需要手动解析它并转换到业务对象。

  • 可定制的handler mapping和view resolution:Spring提供从最简单的URL映射, 到复杂的、专用的定制策略。与某些web MVC框架强制开发人员使用单一特定技术相比,Spring显得更加灵活。

  • 灵活的model转换:在Springweb框架中,使用基于Map的 键/值对来达到轻易地与各种视图技术的集成。

  • 可定制的本地化和主题(theme)解析:支持在JSP中可选择地使用Spring标签库、支持JSTL、支持Velocity(不需要额外的中间层)等等。

  • 简单而强大的JSP标签库(Spring Tag Library):支持包括诸如数据绑定和主题(theme) 之类的许多功能。它提供在标记方面的最大灵活性。如欲了解详情,请参阅附录Appendix D, spring.tld

  • JSP表单标签库:在Spring2.0中引入的表单标签库,使得在JSP中编写 表单更加容易。如欲了解标签库详情,请参阅附录Appendix E, spring-form.tld

  • Spring Bean的生命周期可以被限制在当前的HTTP Request或者HTTP Session。 准确的说,这并非Spring MVC框架本身特性,而应归属于Sping MVC使用的WebApplicationContext容器。 该功能在Section 3.4.4, “其他作用域”有详细描述。

13.2. DispatcherServlet

与其它web MVC框架一样,Spring的web MVC框架是一个请求驱动的web框架,其设计围绕一个中心的servlet进行, 它能将请求分发给控制器,并提供其它功能帮助web应用开发。然而,Spring的DispatcherServlet 所做的不仅仅是这些,它和Spring的IoC容器完全集成在一起,从而允许你使用Spring的其它功能。

下图展示了Spring Web MVC DispatcherServlet处理请求的流程。 熟悉设计模式的读者可能会发现DispatcherServlet应用了“ Front Controller”模式(很多其他的主流web框架也都用到了该模式)。

Spring Web MVC请求处理流程

DispatcherServlet实际上是一个Servlet (它继承了HttpServlet)。与其它Servlet一样, DispatcherServlet定义在web应用的web.xml文件中。 DispatcherServlet处理的请求必须在同一个web.xml文件里使用url-mapping定义映射。 下面的例子演示了如何配置DispatcherServlet

<web-app>

    <servlet>
        <servlet-name>example</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>example</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>

</web-app>

在上面的例子里,所有以.form结尾的请求都会由名为 exampleDispatcherServlet处理。这只是配置Spring Web MVC 的第一步。接下来需要配置DispatcherServlet本身和Spring Web MVC 框架用到的其他的bean。

正如在Section 3.8, “The ApplicationContext”中所描述的,Spring中的 ApplicationContext实例可以被限制在不同的作用域(scope)中。在web MVC框架中,每个 DispatcherServlet有它自己的WebApplicationContext ,这个context继承了根 WebApplicationContext 的所有bean定义。这些继承的bean也可以在每个serlvet自己的所属的域中被覆盖(override),覆盖后的bean 可以被设置成只有这个servlet实例自己才可以使用的属性。

Spring Web MVC中的Context体系

DispatcherServlet的初始化过程中,框架会在web应用的 WEB-INF文件夹下寻找名为[servlet-name]-servlet.xml 的配置文件,生成文件中定义的bean。这些bean会覆盖在全局范围(global cope)中定义的同名的bean。

下面这个例子展示了在web.xmlDispatcherServlet的配置:

<web-app>
    ...
    <servlet>
        <servlet-name>golfing</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>golfing</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
</web-app>

上述servlet配置完成后,还需要配置/WEB-INF/ golfing-servlet.xml文件。golfing-servlet.xml这个文件应该声明在 Spring Web MVC 框架中需要的bean。 这个文件的路径也可以通过web.xml中servlet 的初始化参数来更改(详情见下面的例子)。

WebApplicationContext仅仅是一个拥有web应用必要功能的普通 ApplicationContext。它与一个标准的 ApplicationContext的不同之处在于,它能够解析theme (参考Section 13.7, “使用主题”),并且它知道自己与哪个servlet相关联 (通过ServletContext)。WebApplicationContext 被绑定在ServletContext上,需要时,可以使用 RequestContextUtils提供的静态方法找到WebApplicationContext

Spring的DispatcherServlet有一组特殊的bean, 用来处理请求和渲染相应的视图。这些bean包含在Spring的框架里,可以在 WebApplicationContext中配置,配置方式与配置其它bean相同。这些bean 中的每一个都在下文作详细描述。此刻读者只需知道它们的存在,以便我们继续对DispatcherServlet 进行讨论。对大多数bean,Spring都提供了合理的缺省值,所以在开始阶段,你不必担心如何对其进行配置。

Table 13.1. WebApplicationContext中特殊的bean

Bean类型描述
控制器(Controllers)控制器 实现的是MVC中的C
处理器映射(Handler mapping)处理器映射包含预处理器(pre-processor), 后置处理器(post-processor)和控制器的列表,它们在符合某种条件时才被执行(例如符合控制器指定的URL)。
视图解析器(View resolvers)视图解析器 可以将视图名解析为对应的视图。
本地化解析器(Locale resolver)本地化解析器能够解析用户正在使用的本地化设置,以提供国际化视图。
主题解析器(Theme resolver)主题解析器能够解析你的web应用所使用的主题,以提供个性化的布局。
文件上传解析器(Multipart File resolver)文件上传解析器提供HTML表单文件上传功能。
处理器异常解析器(Handler exception resolver(s))处理器异常解析器可以将异常对应到视图,或者实现更加复杂的异常处理逻辑。

DispatcherServlet配置完成后,当相应的请求到达时,处理就开始了。 下面的列表描述了DispatcherServlet处理请求的全过程:

  1. 找到WebApplicationContext并将其绑定到请求的一个属性上, 以便控制器和处理链上的其它处理器能使用WebApplicationContext。 默认的属性名为DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE

  2. 将本地化解析器(localResolver)绑定到请求上,这样使得处理链上的处理器在处理请求(准备数据、显示视图等等) 时能进行本地化处理。若不使用本地化解析器,也不会有任何副作用,因此如果不需要本地化解析,忽略它即可。

  3. 将主题解析器绑定到请求上,这样视图可以决定使用哪个主题。如果你不需要主题,可以忽略它,不会有任何影响。

  4. 如果上传文件解析器被指定,Spring会检查每个接收到的请求是否存在上传文件,如果存在, 这个请求将被封装成MultipartHttpServletRequest以便被处理链中的其它处理器使用 (关于文件上传的更多内容请参考Section 13.8.2, “使用MultipartResolver”)。

  5. 找到合适的处理器,执行和这个处理器相关的执行链(预处理器,后置处理器,控制器),以便为视图准备模型数据(用于渲染)。

  6. 如果模型数据被返回,就使用配置在WebApplicationContext中的视图解析器显示视图, 否则视图不会被显示。有多种原因可以导致返回的数据模型为空,比如预处理器或后处理器可能截取了请求,这可能是出于安全原因, 也可能是请求已经被处理,没有必要再处理一次。

请求处理过程中抛出的异常,可以被任何定义在WebApplicationContext中的异常解析器所获取。 使用这些异常解析器,可以在异常抛出时根据需要定义特定行为。

Spring的DispatcherServlet也支持返回Servlet API定义的last-modification-date。 决定某个请求的最后修改日期很简单:DispatcherServlet会首先寻找一个合适的handler mapping,检查从中取得指定的处理器是否实现了 LastModified接口,如果是,将调用long getLastModified(request)方法,并将结果返回给客户端。

可以通过两种方式定制Spring的DispatcherServlet:在web.xml文件中增加添加context参数, 或servlet初始化参数。下面是可能用到的参数。

Table 13.2. DispatcherServlet初始化参数

参数描述
contextClass 实现WebApplicationContext接口的类,当前的servlet用它来创建上下文。如果这个参数没有指定, 默认使用XmlWebApplicationContext
contextConfigLocation 传给上下文实例(由contextClass指定)的字符串,用来指定上下文的位置。这个字符串可以被分成多个字符串(使用逗号作为分隔符) 来支持多个上下文(在多上下文的情况下,如果同一个bean被定义两次,后面一个优先)。
namespaceWebApplicationContext命名空间。默认值是[server-name]-servlet

13.3. 控制器

控制器的概念是MVC设计模式的一部分(确切地说,是MVC中的C)。应用程序的行为通常被定义为服务接口, 而控制器使得用户可以访问应用所提供的服务。控制器解析用户输入,并将其转换成合理的模型数据,从而可以进一步由视图展示给用户。 Spring以一种抽象的方式实现了控制器概念,这样可以支持不同类型的控制器。Spring本身包含表单控制器、命令控制器、向导型控制器等多种多样的控制器。

Spring控制器架构的基础是org.springframework.mvc.Controller接口,其代码如下:

public interface Controller {

    /**
     * Process the request and return a ModelAndView object which the DispatcherServlet
     * will render.
     */
    ModelAndView handleRequest(
        HttpServletRequest request,
        HttpServletResponse response) throws Exception;

}

可以发现Controller接口仅仅声明了一个方法,它负责处理请求并返回合适的模型和视图。Spring MVC实现的基础 就是这三个概念:Mdel、View(ModelAndView)以及 Controller。虽然 Controller接口是完全抽象的,但Spring也提供了许多你可能会用到的控制器。Controller接口仅仅定义了每个控制器都必须提供的基本功能: 处理请求并返回一个模型和一个视图。

13.3.1. AbstractControllerWebContentGenerator

为提供一套基础设施,所有的Spring控制器都继承了 AbstractControllerAbstractController 提供了诸如缓存支持和mimetype设置这样的功能。

Table 13.3. AbstractController提供的功能

功能描述
supportedMethods 指定这个控制器应该接受什么样的请求方法。通常它被设置成同时支持GET和POST,但是可以选择你想支持的方法。如果控制器不支持请求发送的方法, 客户端会得到通知(通常是抛出一个ServletException)。
requiresSession 表明这个控制器是否需要HTTP session才能正常工作。如果控制器在没有session的情况下接收到请求,客户端会因为抛出ServletException 而得到通知。
synchronizeOnSession 指定controller是否同步用户的HTTP session。
cacheSeconds 指定controller通知客户端对数据内容缓存的秒数,一般为大于零的整数。默认值为-1,即不缓存。
useExpiresHeader 指定Controller在响应请求时是否兼容HTTP 1.0 Expires header。缺省值为true
useCacheHeader 指定Controller在相应请求时是否兼容HTTP 1.1 Cache-Control header。默认值为true

当从AbstractController继承时,只需要实现handleRequestInternal(HttpServletRequest, HttpServletResponse)抽象方法,该方法将用来实现自定义的逻辑,并返回一个ModelAndView对象。下面这个简单的例子演示 了如何从AbstractController继承以及如何在applicationContext.xml中进行配置。

package samples;

public class SampleController extends AbstractController {

    public ModelAndView handleRequestInternal(
        HttpServletRequest request,
        HttpServletResponse response) throws Exception {

        ModelAndView mav = new ModelAndView("hello");
        mav.addObject("message", "Hello World!");
        return mav;        
    }
}
<bean id="sampleController" class="samples.SampleController">
    <property name="cacheSeconds" value="120"/>
</bean>

要让该简单控制器工作, 除创建一个handler mapping(请参考Section 13.4, “处理器映射(handler mapping)”一节)外, 需要的全部就是上面的类和在web application context中的声明。 该controller在再次检查前,通知客户端将响应数据缓存2分钟,并返回使用硬编码的视图名(尽管这样做不好)。

13.3.2. 其它的简单控制器

尽管可以继承AbstractController来实现自己的控制器,不过Spring提供的众多控制器减轻了我们开发简单MVC应用时的负担。 ParameterizableViewController基本上和上面例子中的一样,不同的是,可以在application context中指定返回的视图名称(从而 避免了在Java代码中的硬编码)。

UrlFilenameViewController会检查URL,获取文件请求的文件名,并把它作为视图名加以使用。。例如, http://www.springframework.org/index.html对应的视图文件名是index

13.3.3. MultiActionController

Spring提供了MultiActionController来将多个请求处理方法合并在一个控制器里,这样可以把相关功能组合在一起。 (如果你很熟悉Struts,会发现这与Struts的DispatchAction很像) MultiActionController位于org.springframework.web.mvc.multiaction包中,它可以定义页面请求到控制器方法名的映射, 然后在处理相应请求时调用该方法。当你有很多比较小的且相关的功能时使用MultiActionController很方便,这样就不必为每个小功能创建 一个单独的Controller了。但是一般来说MultiActionController不适合处理复杂逻辑,或者完全不相关 的功能,这时应该坚持使用标准方法,当在一个控制器存在大量公共的行为,但是有多个调用入口时,使用MultiActionController就特别方便。

MultiActionController有两种使用方式:一是创建MultiActionController的子类,并指定将被 MethodNameResolver解析的方法(这种情况下不需要这个delegate参数);二是定义一个委托对象, MethodNameResolver解析出目标方法后将调用该对象的相应方法。这种情况下需要定义MultiActionController 的实例并将委托对象作为协作者注入(可通过构造参数或者setDelegate方法)。

MultiActionController需要一种策略,使其可以通过解析请求信息来获得要调用的方法。这个解析策略由 MethodNameResolver接口定义。MultiActionController提供了'methodNameResolver' 属性使得你可以注入需要的MethodNameResolver。在自己的MultiActionController(或者前面说的委托对象) 上定义的请求处理方法必须符合如下签名:

// 'anyMeaningfulName'指任意方法名
public [ModelAndView | Map | void] anyMeaningfulName(HttpServletRequest, HttpServletResponse [,HttpSession] [,AnyObject])

上述方法的详细信息可参考 MultiActionController类 Javadoc。如果打算使用MultiActionController,那最好看看它的Javadoc。不过,下面提供了 一些关于合法的请求处理方法的基本例子。

标准格式(跟Controller接口定义的一样)。

public ModelAndView displayCatalog(HttpServletRequest, HttpServletResponse)

下面这个方法接收Login参数,该参数中包含从请求中抽取出来的信息。

public ModelAndView login(HttpServletRequest, HttpServletResponse, Login)

下面这个方法要求请求中已经存在合法的session对象。

public ModelAndView viewCart(HttpServletRequest, HttpServletResponse, HttpSession)

下面这个方法接受一个Product参数,这个参数包含从请求中抽取出来的信息,并且要求请求中已经存在一个 合法的session对象。注意参数的顺序很重要:session必须是第三个参数,而绑定参数必须是final的,并位于session之后。

public ModelAndView updateCart(HttpServletRequest, HttpServletResponse, HttpSession, Product)

下面这个方法声明返回void类型,这说明它会直接写response。

public void home(HttpServletRequest, HttpServletResponse)

下面这个方法返回Map,表明视图解析器应该从请求中抽取视图名,而返回数据将被放入model (参考Section 13.11, “惯例优先原则(convention over configuration)”)。

public Map list(HttpServletRequest, HttpServletResponse)

MethodNameResolver负责从请求中解析出需要调用的方法名称。Spring本身已经提供了一系列 MethodNameResolver的实现,当然也可以编写自己的实现。注意,如果没有明确注入自己的实现,Spring默认使用 InternalPathMethodNameResolver

  • InternalPathMethodNameResolver -从请求路径中获取文件名作为方法名

    比如,http://www.sf.net/testing.view的请求会调用testing(HttpServletRequest,HttpServletResponse)方法。

  • ParameterMethodNameResolver - 解析请求参数,并将它作为方法名。

    比如,对应http://www.sf.net/index.view?method=testIt的请求,会调用 testIt(HttpServletRequest, HttpServletResponse)方法)。使用paramName属性定义要使用的请求参数名称。

  • PropertiesMethodNameResolver - 使用用户自定义的属性(Properties)对象,将请求的URL映射到方法名。比如,当属性中包含 /index/welcome.html=doIt时,对/index/welcome.html 的请求会调用 doIt(HttpServletRequest, HttpServletResponse)方法。 PropertiesMethodNameResolver内部使用了 Spring的PathMatcher,所以支持路径通配符,比如上边那个URL写成/**/welcom?.html也是可以的。

可以声明自己的方法来处理请求处理过程中产生的Exceptions。该方法的签名与请求处理方法的签名类似:第一个参数必须是 HttpServletRequest,第二个参数必须是HttpServletResponse。不过与请求处理 方法不同的是,该方法的名字可以任意,具体匹配策略由该方法的第三个参数(参数类型必须是一种Exception)决定。Spring根据最接近的 异常类型进行匹配。下面是一个这种异常处理方法签名的例子:

public ModelAndView processException(HttpServletRequest, HttpServletResponse, IllegalArgumentException)

我们来看一个例子,其中展示了MultiActionControllerParameterMethodNameResolver一同使用的委托方式。

<bean id="paramMultiController"
      class="org.springframework.web.servlet.mvc.multiaction.MultiActionController">

    <property name="methodNameResolver">
        <bean class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
            <property name="paramName" value="method"/>
        </bean>
    </property>

    <property name="delegate">
        <bean class="samples.SampleDelegate"/>
    </property>

</bean>
}
public class SampleDelegate {

    public ModelAndView retrieveIndex(HttpServletRequest req, HttpServletResponse resp) {
        return new ModelAndView("index", "date", new Long(System.currentTimeMillis()));
    }
}

当使用上述的委托方式时,我们需要配置PropertiesMethodNameResolver,来完成与我们定义的方法的任意数量的URL的匹配。

<bean id="propsResolver"
      class="org....mvc.multiaction.PropertiesMethodNameResolver">
    <property name="mappings">
        <value>
/index/welcome.html=retrieveIndex
/**/notwelcome.html=retrieveIndex
/*/user?.html=retrieveIndex
        </value>
    </property>
</bean>

<bean id="paramMultiController" class="org....mvc.multiaction.MultiActionController">

    <property name="methodNameResolver" ref="propsResolver"/>
    <property name="delegate">
        <bean class="samples.SampleDelegate"/>
    </property>

</bean>

13.3.4. 命令控制器

Spring的command controllers是Spring MVC的重要部分。 命令控制器提供了一种和数据对象交互的方式,并动态地将来自HttpServletRequest的参数绑定到指定的数据对象上。 它的功能和Struts中的ActionForm有点像,不过在Spring中,不需要实现任何接口来实现数据绑定。 首先,让我们看一下有哪些可以使用的命令控制器:

  • AbstractCommandController - 可以使用该抽象命令控制器来创建自己的命令控制器,它能够将请求参数绑定到指定的命令对象。 这个类并不提供任何表单功能,但是它提供验证功能,并且让你在控制器中去实现如何处理由请求参数值产生的命令对象。

  • AbstractFormController - 一个支持表单提交的抽象控制器类。 使用这个控制器,可以定义表单,并使用从控制器获取的数据对象构建表单。 当用户输入表单内容,AbstractFormController将用户输入的内容绑定到命令对象,验证表单内容, 并将该对象交给控制器,完成相应的操作。它支持的功能有防止重复提交、表单验证以及一般的表单处理流程。 子类需要实现自己的方法来指定采用哪个视图来显示输入表单,哪个视图显示表单正确提交后的结果。 如果需要表单,但不想在应用上下文中指定显示给用户的视图,可使用该控制器。

  • SimpleFormController - 这是一个form controller,当需要根据命令对象来创建相应的form的时候,该类可以提供更多的支持。 可以为其指定一个命令对象,显示表单的视图名,当表单提交成功后显示给用户的视图名等等。

  • AbstractWizardFormController - 这是一个抽象类,继承这个类需要实现validatePage()processFinish()processCancel() 方法。

    你有可能也需要写一个contractor,它至少需要调用setPages()setCommandName()方法。setPages()的参数是一个String数组,这个数组包含了组成向导的视图名。 setCommandName()的参数是一个String,该参数将用来在视图中调用你的命令对象。

    AbstractFormController的实例一样, 需要使用命令对象(其实就是一个JavaBean, 这个bean中包含了表单的信息)。 这里有两个选择:在构造函数中调用setCommandClass()方法(参数是命令对象的类名),或者实现formBackingObject()方法。

    AbstractWizardFormController 有多个可以覆写(override)的方法。 最有用的一个是referenceData(..)。 这个方法允许把模型数据以Map的格式传递给视图;getTargetPage() 允许动态地更改向导的页面顺序, 或者直接跳过某些页面;onBindAndValidate() 允许覆写内置的绑定和验证流程。

    最后,我们有必要提一下setAllowDirtyBack()setAllowDirtyForward()两个方法。 可以在getTargetPage()中调用这两个方法,这两个方法将决定在当前页面验证失败时,是否允许向导前移和后退。

    AbstractWizardFormController的更完整的方法列表请参考JavaDoc。 在Spring发行版本附带的例子jPetStore中,有一个关于向导实现的例子: org.springframework.samples.jpetstore.web.spring.OrderFormController

13.4. 处理器映射(handler mapping)

通过处理器映射,可以将web请求映射到正确的处理器(handler)上。 Spring内置了很多处理器映射策略,例如:SimpleUrlHandlerMapping或者BeanNameUrlHandlerMapping。 现在我们先来看一下HandlerMapping的基本概念。

HandlerMapping的基本功能是将请求传递到HandlerExecutionChain上。 首先,这个HandlerExecutionChain必须包含一个能处理该请求的处理器。 其次,这个链也可以包含一系列可以拦截请求的拦截器。 当收到请求时,DispatcherServlet将请求交给处理器映射,让它检查请求并找到一个适当的HandlerExecutionChain。 然后,DispatcherServlet执行定义在链中的处理器和拦截器(interceptor)。

在处理器映射中通过配置拦截器(包括处理器执行前、执行后、或者执行前后运行拦截器)将使其功能更强大。 同时也可以通过自定义HandlerMapping来支持更多的功能。 比如,一个自定义的处理器映射不仅可以根据请求的URL,而且还可以根据和请求相关的特定session状态来选择处理器。

下面我们将讲述Spring中最常用的两个处理器映射。 它们都是AbstractHandlerMapping的子类,同时继承了下面这些属性:

  • interceptors: 在映射中使用的拦截器列表。 HandlerInterceptor将在Section 13.4.3, “拦截器(HandlerInterceptor)”这一节讲述。

  • defaultHandler: 缺省的处理器。 当没有合适的处理器可以匹配请求时,该处理器就会被使用。

  • order: 根据每个映射的order属性值 (由org.springframework.core.Ordered 接口定义),Spring 将上下文中可用的映射进行排序,然后选用第一个和请求匹配的处理器。

  • alwaysUseFullPath:如果这个属性被设成true,Spring 将会使用绝对路径在当前的servlet context中寻找合适的处理器。 这个属性的默认值是false,在这种情况下,Spring会使用当前servlet context中的相对路径。 例如,如果一个servlet在servlet-mapping中用的值是/testing/*,当alwaysUseFullPath 设成true时, 处理器映射中的URL格式应该使用/testing/viewPage.html,当这个属性设成false,同一个URL应该写成 /viewPage.html

  • urlDecode:这个属性的默认值是true,和2.5版本一样。 如果想比较编码后的路径,可以把这个属性设为false。 不过,需要注意的是,HttpServletRequest总是返回解码后的servlet路径, 与编码后的格式进行比较时可能不会匹配。

  • lazyInitHandlers:这个属性允许设置是否延迟singleton处理器的初始化工作(prototype处理器的初始化都是延迟的)。 这个属性的默认值是false

注意:最后三个属性只有org.springframework.web.servlet.handler.AbstractUrlHandlerMapping的子类才有。

13.4.1. BeanNameUrlHandlerMapping

BeanNameUrlHandlerMapping是一个简单但很强大的处理器映射,它将收到的HTTP请求映射到bean的名称(这些bean需要在web应用上下文中定义)。 例如,为了实现一个用户新建账号的功能,我们提供了FormController (关于CommandController和FormController请参考Section 13.3.4, “命令控制器”)和显示表单的JSP视图(或Velocity模版)。 当使用BeanNameUrlHandlerMapping时,我们用如下方式将包含http://samples.com/editaccount.form的访问请求映射到指定的FormController上:

<beans>
  <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>

  <bean name="/editaccount.form" class="org.springframework.web.servlet.mvc.SimpleFormController">
    <property name="formView" value="account"/>
    <property name="successView" value="account-created"/>
    <property name="commandName" value="account"/>
    <property name="commandClass" value="samples.Account"/>
  </bean>
<beans>

所有对/editaccount.form的请求就会由上面的FormController处理。 当然我们得在web.xml中定义servlet-mapping,接受所有以.form结尾的请求。

<web-app>
    ...
    <servlet>
        <servlet-name>sample</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

   <!-- maps the sample dispatcher to *.form -->
    <servlet-mapping>
        <servlet-name>sample</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>
    ...
</web-app>
[Note]Note

要使用BeanNameUrlHandlerMapping,无须(如上所示)在web应用上下文中定义它。 缺省情况下,如果在上下文中没有找到处理器映射,DispatcherServlet会为你创建一个BeanNameUrlHandlerMapping

13.4.2. SimpleUrlHandlerMapping

另一个更加强大的处理器映射是SimpleUrlHandlerMapping。 它在应用上下文中可以进行配置,并且有Ant风格的路径匹配功能。 (请参考org.springframework.util.PathMatcher的JavaDoc)。下面几个例子可以帮助理解:

<web-app>
    ...
    <servlet>
        <servlet-name>sample</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- maps the sample dispatcher to *.form -->
    <servlet-mapping>
        <servlet-name>sample</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>

    <!-- maps the sample dispatcher to *.html -->
    <servlet-mapping>
        <servlet-name>sample</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>
    ...
</web-app>

上面的web.xml设置允许所有以.html.form结尾的请求都由这个sample DispatcherServlet处理。

<beans>
        
    <!-- no 'id' required, HandlerMapping beans are automatically detected by the DispatcherServlet -->
    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <value>
                /*/account.form=editAccountFormController
                /*/editaccount.form=editAccountFormController
                /ex/view*.html=helpController
                /**/help.html=helpController
            </value>
        </property>
    </bean>

    <bean id="helpController"
          class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>

    <bean id="editAccountFormController"
          class="org.springframework.web.servlet.mvc.SimpleFormController">
        <property name="formView" value="account"/>
        <property name="successView" value="account-created"/>
        <property name="commandName" value="Account"/>
        <property name="commandClass" value="samples.Account"/>
    </bean>
<beans>

这个处理器映射首先将对所有目录中文件名为help.html的请求传递给helpControllerhelpController是一个UrlFilenameViewController (要了解更多关于控制器的信息,请参阅Section 13.3, “控制器”)。 对ex目录中所有以view开始,以.html 结尾的请求都会被传递给helpController。 同样的,我们也为editAccountFormController定义了两个映射。

13.4.3. 拦截器(HandlerInterceptor

Spring的处理器映射支持拦截器。当你想要为某些请求提供特殊功能时,例如对用户进行身份认证,这就非常有用。

处理器映射中的拦截器必须实现org.springframework.web.servlet包中的HandlerInterceptor接口。 这个接口定义了三个方法,一个在处理器执行前被调用,一个在处理器执行后被调用,另一个在整个请求处理完后调用。 这三个方法提供你足够的灵活度做任何处理前后的操作。

preHandle(..)方法有一个boolean返回值。 使用这个值,可以调整执行链的行为。 当返回true时,处理器执行链将继续执行,当返回false时,DispatcherServlet认为该拦截器已经处理完了请求(比如显示正确的视图),而不继续执行执行链中的其它拦截器和处理器。

下面的例子提供了一个拦截器,它拦截所有请求,如果当前时间不是在上午9点到下午6点,它将用户重定向到某个页面。

<beans>
    <bean id="handlerMapping"
          class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="interceptors">
            <list>
                <ref bean="officeHoursInterceptor"/>
            </list>
        </property>
        <property name="mappings">
            <value>
                /*.form=editAccountFormController
                /*.view=editAccountFormController
            </value>
        </property>
    </bean>

    <bean id="officeHoursInterceptor"
          class="samples.TimeBasedAccessInterceptor">
        <property name="openingTime" value="9"/>
        <property name="closingTime" value="18"/>
    </bean>
<beans>
package samples;

public class TimeBasedAccessInterceptor extends HandlerInterceptorAdapter {

    private int openingTime;
    private int closingTime;

    public void setOpeningTime(int openingTime) {
        this.openingTime = openingTime;
    }

    public void setClosingTime(int closingTime) {
        this.closingTime = closingTime;
    }

    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) throws Exception {

        Calendar cal = Calendar.getInstance();
        int hour = cal.get(HOUR_OF_DAY);
        if (openingTime <= hour < closingTime) {
            return true;
        } else {
            response.sendRedirect("http://host.com/outsideOfficeHours.html");
            return false;
        }
    }
}

所有的请求都将被TimeBasedAccessInterceptor截获, 如果当前时间不在上班时间,用户会被重定向到一个静态html页面,提供诸如只有上班时间才能访问网站之类的告示。

Spring还提供了一个adapter类HandlerInterceptorAdapter让用户更方便的扩展HandlerInterceptor接口。

13.5. 视图与视图解析

所有web应用的MVC框架都有它们定位视图的方式。 Spring提供了视图解析器供你在浏览器显示模型数据,而不必被束缚在特定的视图技术上。 Spring内置了对JSP,Velocity模版和XSLT视图的支持。 Chapter 14, 集成视图技术这一章详细说明了Spring如何与不同的视图技术集成。

ViewResolverView是Spring的视图处理方式中特别重要的两个接口。 ViewResolver提供了从视图名称到实际视图的映射。 View处理请求的准备工作,并将该请求提交给某种具体的视图技术。

13.5.1. 视图解析器(ViewResolver

正如前面(Section 13.3, “控制器”)所讨论的, SpringWeb框架的所有控制器都返回一个ModelAndView实例。 Sprnig中的视图以名字为标识,视图解析器通过名字来解析视图。Spring提供了多种视图解析器。我们将举例加以说明。

Table 13.4. 视图解析器

ViewResolver描述
AbstractCachingViewResolver 抽象视图解析器实现了对视图的缓存。在视图被使用之前,通常需要进行一些准备工作。 从它继承的视图解析器将对要解析的视图进行缓存。
XmlViewResolver XmlViewResolver实现ViewResolver,支持XML格式的配置文件。 该配置文件必须采用与Spring XML Bean Factory相同的DTD。默认的配置文件是 /WEB-INF/views.xml
ResourceBundleViewResolver ResourceBundleViewResolver实现ViewResolver, 在一个ResourceBundle中寻找所需bean的定义。 这个bundle通常定义在一个位于classpath中的属性文件中。默认的属性文件是views.properties
UrlBasedViewResolver UrlBasedViewResolver实现ViewResolver, 将视图名直接解析成对应的URL,不需要显式的映射定义。 如果你的视图名和视图资源的名字是一致的,就可使用该解析器,而无需进行映射。
InternalResourceViewResolver 作为UrlBasedViewResolver的子类, 它支持InternalResourceView(对Servlet和JSP的包装), 以及其子类JstlViewTilesView。 通过setViewClass方法,可以指定用于该解析器生成视图使用的视图类。 更多信息请参考UrlBasedViewResolver的Javadoc。
VelocityViewResolver / FreeMarkerViewResolver 作为UrlBasedViewResolver的子类, 它能支持VelocityView(对Velocity模版的包装)和FreeMarkerView以及它们的子类。

举例来说,当使用JSP作为视图层技术时,就可以使用UrlBasedViewResolver。 这个视图解析器会将视图名解析成URL,并将请求传递给RequestDispatcher来显示视图。

<bean id="viewResolver"
      class="org.springframework.web.servlet.view.UrlBasedViewResolver">
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

当返回的视图名为test时, 这个视图解析器将请求传递给RequestDispatcherRequestDispatcher再将请求传递给/WEB-INF/jsp/test.jsp

当在一个web应用中混合使用不同的视图技术时,可以使用ResourceBundleViewResolver

<bean id="viewResolver"
      class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="basename" value="views"/>
    <property name="defaultParentView" value="parentView"/>
</bean>

ResourceBundleViewResolver通过basename所指定的ResourceBundle解析视图名。 对每个待解析的视图,ResourceBundle里的[视图名].class所对应的值就是实现该视图的类。 同样,[视图名].url所对应的值是该视图所对应的URL。 从上面的例子里能够发现,可以指定一个parent view,其它的视图都可以从parent view扩展。用这种方法,可以声明一个默认的视图。

关于视图缓存的注意事项 - 继承AbstractCachingViewResolver的解析器可以缓存它曾经解析过的视图。 当使用某些视图技术时,这可以大幅度的提升性能。 也可以关掉缓存功能,只要把cache属性设成false就可以了。 而且,如果需要在系统运行时动态地更新某些视图(比如,当一个Velocity模板被修改了), 可以调用removeFromCache(String viewName, Locale loc)方法来达到目的。

13.5.2. 视图解析链

Spring支持多个视图解析器一起使用。可以把它们当作一个解析链。 这样有很多好处,比如在特定情况下重新定义某些视图。 定义视图解析链很容易,只要在应用上下文中定义多个解析器就可以了。 必要时,也可以通过order属性来声明每个解析器的序列。 要记住的是,某个解析器的order越高, 它在解析链中的位置越靠后。

下面这个例子展示了一个包含两个解析器的解析链。 一个是InternalResourceViewResolver,这个解析器总是被自动的放到链的末端。 另一个是XmlViewResolver,它支持解析Excel视图(而InternalResourceViewResolver不可以)。

<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
  <property name="prefix" value="/WEB-INF/jsp/"/>
  <property name="suffix" value=".jsp"/>
</bean>

<bean id="excelViewResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
  <property name="order" value="1"/>
  <property name="location" value="/WEB-INF/views.xml"/>
</bean>

<!-- in views.xml -->

<beans>
  <bean name="report" class="org.springframework.example.ReportExcelView"/>
</beans>

如果某个解析器没有找到合适的视图,Spring会在上下文中寻找是否配置了其它的解析器。 如果有,它会继续进行解析,否则,Srping会抛出一个Exception

要记住,当一个视图解析器找不到合适的视图时,它可能 返回null值。 但是,不是每个解析器都这么做。这是因为,在某些情况下,解析器可能无法侦测出符合要求的视图是否存在。 比如,InternalResourceViewResolver在内部调用了RequestDispatcher。 请求分发是检查一个JSP文件是否存在的唯一方法,不幸的是,这个方法只能用一次。 同样的问题在VelocityViewResolver和其它解析器中也有。 当使用这些解析器时,最好仔细阅读它们的Javadoc,看看需要的解析器是否无法发现不存在的视图。 这个问题产生的副作用是,如果InternalResourceViewResolver解析器没有放在链的末端, InternalResourceViewResolver后面的那些解析器根本得不到使用, 因为InternalResourceViewResolver总是返回一个视图!

13.5.3. 重定向(Rediret)到另一个视图

在前面我们提到过,一个控制器通常会返回视图名,然后由视图解析器解析到某种视图实现。 对于像JSP这样实际上由Servlet/JSP引擎处理的视图, 我们通常使用InternalResourceViewResolverInternalResourceView。 这种视图实现最终会调用Servlet API的RequestDispatcher.forward(..)方法或RequestDispatcher.include()方法将用户指向最终页面。 对于别的视图技术而言(比如Velocity、XSLT等等),视图本身就会生成返回给用户的内容。

有些时候,在视图显示以前,我们可能需要给用户发一个HTTP redirect重定向指令。 比如,一个控制器成功的处理了一个表单提交(数据以HTTP POST的方式发送),它最终可能委托给另一个控制器来完成剩下的工作。 在这种情况下,如果我们使用内部forward,接手工作的那个控制器将会得到所有以POST方式提交的表单数据, 这可能会引起潜在的混淆,干扰那个控制器的正常工作。 另一个在显示视图之前返回HTTP redirect的原因是这可以防止用户重复提交同一表单。 具体一点讲,浏览器先用POST的方式提交表单,然后它接收到重定向的指令,它继续用GET的方式去下载新的页面。 从浏览器的角度看,这个新的页面不是POST的返回结果,而是GET的。 这样,用户不可能在点击刷新的时候不小心再次提交表单,因为刷新的结果是再次用GET 去下载表单提交后的结果页面,而不是重新提交初始的POST数据。

13.5.3.1. RedirectView

在控制器中强制重定向的方法之一是让控制器创建并返回一个Spring的RedirectView的实例。 在这种情况下,DispatcherServlet不会使用通常的视图解析机制, 既然它已经拿到了一个(重定向)视图,它就让这个视图去完成余下的工作。

RedirectView会调用HttpServletResponse.sendRedirect()方法, 其结果是给用户的浏览器发回一个HTTP redirect。所有的模型属性都被转换成以HTTP请求的访问参数。 这意味着这个模型只能包含可以被简便的转换成string形式的HTTP请求访问参数的对象,比如String或者可以被转换成String的类型。

如果使用RedirectView视图,并且它是由控制器创建的, 重定向的URL最好是用Spring所提供的IoC功能注射到控制器中。 这样这个URL就可以和视图名一起在上下文中被声明,而不是固化在控制器内。

13.5.3.2. redirect:前缀

尽管使用RedirectView帮我们达到了目的,但是如果控制器生成RedirectView的话, 控制器不可避免地要知道某个请求的结果是让用户重定向到另一个页面。这不是最佳的实现,因为这使得系统不同模块之间结合得过于紧密。 其实控制器不应该过问返回结果是如何生成的,通常情况下,它应该只关心注入给它的视图名称。

解决上述问题的方法是依靠redirect:前缀。 如果返回的视图名包含redirect:前缀,UrlBasedViewResolver (以及它的子类) 会知道系统要生成一个HTTP redirect。 视图名其余的部分会被当作重定向URL。

这样做的最终结果跟控制器返回RedirectView是一样的,但现在控制器只需要和逻辑上的视图名打交道。 redirect:/my/response/controller.html这个逻辑视图名中的URL是当前servlet context中的相对路径。 与之相比,redirect:http://myhost.com/some/arbitrary/path.html中的URL是绝对路径。 重要的是,只要这个重定向视图名和其他视图名以相同的方式注入到控制器中,控制器根本不知道重定向是否发生。

13.5.3.3. forward:前缀

类似的,我们也可以使用包含有forward:前缀的视图名。 这些视图名会被UrlBasedViewResolver和它的子类正确解析。 解析的内部实现是生成一个InternalResourceView, 这个视图最终会调用RequestDispatcher.forward()方法,将forward视图名的其余部分作为URL。 所以,当使用InternalResourceViewResolver/InternalResourceView, 并且你所用的视图技术是JSP时,你没有必要使用这个前缀。 但是,当你主要使用其它的视图技术,但仍需要对Servlet/JSP engine处理的页面强制forward时, 这个forward前缀还是很有用的(但就这个问题而言,如果不想使用forward前缀,也可以使用视图解析链)。

redirect:前缀一样,如果含有forward前缀的视图名和其他视图名一样被注入控制器, 控制器根本不需要知道在处理响应的过程中是否发生任何特殊情况。

13.6. 本地化解析器

Spring架构的绝大部分都支持国际化,Spring的web MVC框架也不例外。 DispatcherServlet 允许使用客户端本地化信息自动解析消息。 这个工作由LocaleResolver对象完成。

当收到请求时,DispatcherServlet查找一个本地化解析器,如果找到,就使用它设置本地化信息。 通过RequestContext.getLocale()方法,总可以获取由本地化解析器解析的客户端的本地化信息。

除自动的本地化解析以外,还可以将一个拦截器置于处理器映射中(参考 Section 13.4.3, “拦截器(HandlerInterceptor)”), 以便在某种环境下可以改变本地化信息,例如,可以基于请求中的参数变更本地化信息。

本地化解析器和拦截器都定义在org.springframework.web.servlet.i18n包中, 可以在应用的上下文中配置它们。下文介绍了一些Spring提供的本地化解析器。

13.6.1. AcceptHeaderLocaleResolver

这个本地化解析器检查请求中客户端浏览器发送的accept-language头信息, 通常这个HTTP Header包含客户端操作系统的本地化信息。

13.6.2. CookieLocaleResolver

这个本地化解析器检查客户端中的Cookie是否包含本地化信息。 如果有的话,就使用。当配置这个解析器的时候,可以指定cookie名,以及cookie的最长生存期(Max Age)。 下面这个例子定义了一个CookieLocaleResolver

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">

    <property name="cookieName" value="clientlanguage"/>
    
    <!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) -->
    <property name="cookieMaxAge" value="100000">

</bean>

Table 13.5. CookieLocaleResolver的属性

属性缺省值描述
cookieNameclassname + LOCALEcookie的名字
cookieMaxAgeInteger.MAX_INT cookie在客户端存在的最长时间。如果该值是-1,这个cookie只被保留在内存中,当客户关闭浏览器时,这个cookie就不存在了
cookiePath/ 通过这个参数,可以将该cookie的作用限制在一部分特定的。 具体地说,只有该目录(cookiePath)及其子目录下的页面可以使用这个cookie

13.6.3. SessionLocaleResolver

SessionLocaleResolver允许从用户请求相关的session中获取本地化信息。

13.6.4. LocaleChangeInterceptor

可以使用LocaleChangeInterceptor修改本地化信息。 这个拦截器需要被添加到处理器映射中(参考Section 13.4, “处理器映射(handler mapping)”)。 它可以侦测请求中某个特定的参数,然后调用上下文中的LocaleResolver中的 setLocale()方法,相应地修改本地化信息。

<bean id="localeChangeInterceptor"
      class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
    <property name="paramName" value="siteLanguage"/>
</bean>

<bean id="localeResolver"
      class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>

<bean id="urlMapping"
      class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="localeChangeInterceptor"/>
        </list>
    </property>
    <property name="mappings">
        <value>/**/*.view=someController</value>
    </property>
</bean>

在上面这个例子中,所有对*.view资源的请求,只要包含参数siteLanguage, 都会改变本地化信息。比如下面这个请求http://www.sf.net/home.view?siteLanguage=nl会将网站语言修改为荷兰语。

13.7. 使用主题

13.7.1. 简介

Sping的web MVC框架允许你通过主题(theme)来控制网页的风格,这将进一步改善用户的体验。 简单来说,一个主题就是一组静态的资源(比如样式表和图片),它们可以影响应用程序的视觉效果。

13.7.2. 如何定义主题

为了在web应用中使用主题,需要设置org.springframework.ui.context.ThemeSourceWebApplicationContext是从ThemeSource扩展而来, 但是它本身并没有实现ThemeSource定义的方法,它把这些任务转交给别的专用模块。 如果没有明确设置,真正实现ThemeSource的类是org.springframework.ui.context.support.ResourceBundleThemeSource。 这个类在classpath的根部(比如在/WEB-INF/classes目录下)寻找合适的属性文件来完成配置。 如果想自己实现ThemeSource接口, 或者需要配置ResourceBundleThemeSource所需的属性文件的前缀名(basename prefix), 可以在应用上下文中定义一个名为"themeSource"的bean(注意,必须用这个名字)。 web application context会自动检测并使用这个bean。

在使用ResourceBundleThemeSource时,每个主题是用一个属性文件来配置的。 这个属性文件中列举了构成一个主题所需的资源。比如:

styleSheet=/themes/cool/style.css
background=/themes/cool/img/coolBg.jpg

这些属性的名字应该和视图中的某些主题元素(themed element)一一对应。 在JSP视图中,这些元素通常用spring:theme标签声明(这个标签的用法和spring:message很相似)。 下文这个JSP片段使用了我们在前面定义的主题:

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
   <head>
      <link rel="stylesheet" href="<spring:theme code="styleSheet"/>" type="text/css"/>
   </head>
   <body background="<spring:theme code="background"/>">
      ...
   </body>
</html>

除非有特殊配置,当ResourceBundleThemeSource寻找所需的属性文件时,它默认在配置的属性文件名中没有任何前缀, 也就是说,它只会在classpath的根部寻找。举例来说,如果一个主题的定义包含在cool.properties这个属性文件中, 需要把这个文件放在classpath的根部,比如在/WEB-INF/classes目录下。 同时,ResourceBundleThemeSource 使用标准的Java resource bundle管理机制, 这意味着实现主题的国际化是很容易的。 比如,/WEB-INF/classes/cool_nl.properties这个属性文件中可以指向一个显示荷兰文字的图片。

译者注:如果对ResourceBundle和它的属性文件名的规范不熟悉,请参阅JavaDoc中关于ResourceBundle.getBundle(String baseName,Locale locale)这个API。 这个baseName参数和属性文件名有一定关系。 比如,如果cool.properties这个属性文件放置在了/WEB-INF/classes/com/aa/bb/cc目录下, 那么这个baseName的值应该为com.aa.bb.cc.cool。 在这里,com.aa.bb.cc就是这个属性文件名的前缀(basename prefix)。 支持前缀的API会在前缀所声明的目录下寻找相应的文件,比如getBundle()。 如果没有特殊的配置,ResourceBundleThemeSource不支持前缀, 在这种情况下你要把它所需要的属性文件放在/WEB-INF/classes目录下。

13.7.3. 主题解析器

现在我们已经知道如何定义主题了,剩下的事就是决定该用哪个主题。 DispatcherServlet会寻找一个叫“themeResolver”的bean, 这个bean应该实现了ThemeResolver接口。 主题解析器的工作流程和LocaleResolver差不多。 它可以解析每个请求所对应的主题,也可以动态地更换主题。下面是Spring提供的几个主题解析器:

Table 13.6. ThemeResolver的实现

Java类描述
FixedThemeResolver 选用一个固定的主题,这个主题由“defaultThemeName”属性决定。
SessionThemeResolver 主题保存在用户的HTTP session。在每个session中,这个主题只需要被设置一次,但是每个新session的主题都要重新设置。
CookieThemeResolver 用户所选择的主题以cookie的形式存在客户端的机器上面。

Spring 也支持一个叫ThemeChangeInterceptor 的请求拦截器。它可以根据请求中包含的参数来动态地改变主题。

13.8. Spring对分段文件上传(multipart file upload)的支持

13.8.1. 介绍

Spring支持web应用中的分段文件上传。这种支持是由即插即用的MultipartResolver来实现。 这些解析器都定义在org.springframework.web.multipart包里。 Spring提供了现成的MultipartResolver可以支持Commons FileUploadhttp://jakarta.apache.org/commons/fileupload)和 COS FileUpload(http://www.servlets.com/cos)。 本章后面的部分描述了Spring是如何支持文件上传的。

通常情况下,Spring是不处理文件上传的,因为一些开发者想要自己处理它们。 如果想使用Spring的这个功能,需要在web应用的上下文中添加分段文件解析器。 这样,每个请求就会被检查是否包含文件上传。如果没有,这个请求就被正常的处理, 否则,应用上下文中已经定义的MultipartResolver就会被调用。 然后,请求中的文件属性就会像其它属性一样被处理。

13.8.2. 使用MultipartResolver

下面的例子说明了如何使用CommonsMultipartResolver

<bean id="multipartResolver"
    class="org.springframework.web.multipart.commons.CommonsMultipartResolver">

    <!-- one of the properties available; the maximum file size in bytes -->
    <property name="maxUploadSize" value="100000"/>
</bean>

下面这个例子使用CosMultipartResolver

<bean id="multipartResolver" class="org.springframework.web.multipart.cos.CosMultipartResolver">

    <!-- one of the properties available; the maximum file size in bytes -->
    <property name="maxUploadSize" value="100000"/>
</bean>

当然,需要在classpath中为分段文件解析器提供正确的jar文件。 如果是CommonsMultipartResolver, 需要使用commons-fileupload.jar,如果是CosMultipartResolver, 则使用cos.jar

已经看到如何设置Spring处理文件上传请求,接下来我们看看如何使用它。 当Spring的DispatcherServlet发现文件上传请求时,它会激活定义在上下文中的解析器来处理请求。 这个解析器随后是将当前的HttpServletRequest封装成MultipartHttpServletRequest,后者支持分段文件上传。 使用MultipartHttpServletRequest,可以获取请求所包含的上传信息,甚至可以在控制器中获取分段文件的内容。

13.8.3. 在表单中处理分段文件上传

MultipartResolver完成分段文件解析后,这个请求就会和其它请求一样被处理。 为了使用文件上传,你需要创建一个带文件上传域(upload field)的(HTML)表单,让Spring将文件绑定到你的表单上(如下所示):

<html>
    <head>
        <title>Upload a file please</title>
    </head>
    <body>
        <h1>Please upload a file</h1>
        <form method="post" action="upload.form" enctype="multipart/form-data">
            <input type="file" name="file"/>
            <input type="submit"/>
        </form>
    </body>
</html>

可以看到,在上面这个表单里有一个input元素,这个元素的名字(“file”)和服务器端处理这个表单的bean(在下面将会提到)中类型为byte[]的属性名相同。 在这个表单里我们也声明了编码参数(enctype="multipart/form-data")以便让浏览器知道如何对这个文件上传表单进行编码(千万不要忘记这么做!)。

和其它不能自动转为字符串类型或者基本类型(primitive type)的属性一样,为了将上传的二进制数据存成bean的属性, 必须通过ServletRequestDatabinder注册一个属性编辑器。 Spring中内置了几个这样的编辑器,它们可以处理文件,然后将结果存成bean的属性。 比如,StringMultipartEditor可以将文件转换成一个字符串(使用用户声明的字符集)。 ByteArrayMultipartEditor可以以将文件转换为byte数组。 他们的功能和CustomDateEditor相似。

总而言之,为了使用(HTML)表单上传文件,需要声明一个解析器,一个控制器,再将文件上传的URL映射到控制器来处理这个请求。 下面是这几个bean的声明。

<beans>
	<!-- lets use the Commons-based implementation of the MultipartResolver interface -->
    <bean id="multipartResolver"
        class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>

    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <value>
                /upload.form=fileUploadController
            </value>
        </property>
    </bean>

    <bean id="fileUploadController" class="examples.FileUploadController">
        <property name="commandClass" value="examples.FileUploadBean"/>
        <property name="formView" value="fileuploadform"/>
        <property name="successView" value="confirmation"/>
    </bean>

</beans>

下面的代码定义了控制器和用来存放文件的那个bean。

public class FileUploadController extends SimpleFormController {

    protected ModelAndView onSubmit(
        HttpServletRequest request,
        HttpServletResponse response,
        Object command,
        BindException errors) throws ServletException, IOException {

         // cast the bean
        FileUploadBean bean = (FileUploadBean) command;

         let's see if there's content there
        byte[] file = bean.getFile();
        if (file == null) {
             // hmm, that's strange, the user did not upload anything
        }

         // well, let's do nothing with the bean for now and return
        return super.onSubmit(request, response, command, errors);
    }

    protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder)
        throws ServletException {
        // to actually be able to convert Multipart instance to byte[]
        // we have to register a custom editor
        binder.registerCustomEditor(byte[].class, new ByteArrayMultipartFileEditor());
        // now Spring knows how to handle multipart object and convert them
    }

}

public class FileUploadBean {

    private byte[] file;

    public void setFile(byte[] file) {
        this.file = file;
    }

    public byte[] getFile() {
        return file;
    }
}

FileUploadBean用一个byte[]类型的属性来存放文件。 前面已经提到过,通常控制器注册一个自定义的编辑器以便让Spring知道如何将解析器找到的multipart对象转换成bean指定的属性, 但在上面的例子中,我们除了将byte数组记录下来以外,没有对这个文件进行任何操作, 在实际的应用程序中你可以做任何你想做的事情(比如将文件存储在数据库中,通过电子邮件发送给某人等等)。

在下面这个例子里,上传的文件被绑定为(表单支持的)对象(form backing)的String属性:

public class FileUploadController extends SimpleFormController {

    protected ModelAndView onSubmit(
        HttpServletRequest request,
        HttpServletResponse response,
        Object command,
        BindException errors) throws ServletException, IOException {

         // cast the bean
        FileUploadBean bean = (FileUploadBean) command;

         let's see if there's content there
        String file = bean.getFile();
        if (file == null) {
             // hmm, that's strange, the user did not upload anything
        }

         // well, let's do nothing with the bean for now and return
        return super.onSubmit(request, response, command, errors);
    }

    protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder)
        throws ServletException {
        // to actually be able to convert Multipart instance to a String
        // we have to register a custom editor
        binder.registerCustomEditor(String.class, new StringMultipartFileEditor());
        // now Spring knows how to handle multipart object and convert them
    }

}

public class FileUploadBean {

    private String file;

    public void setFile(String file) {
        this.file = file;
    }

    public String getFile() {
        return file;
    }
}

如果仅仅是处理一个文本文件的上传,上面这个例子的做法还是合理的(但如果上传的是一张图片, 那段代码就会出问题)。

最后的解决方法就是将表单支持对象(form backing)的相关属性设成MultipartFile类型。 这样的话,没有类型转换的需要,我们也就不需要声明任何属性编辑器(PropertyEditor)。

public class FileUploadController extends SimpleFormController {

    protected ModelAndView onSubmit(
        HttpServletRequest request,
        HttpServletResponse response,
        Object command,
        BindException errors) throws ServletException, IOException {

         // cast the bean
        FileUploadBean bean = (FileUploadBean) command;

         let's see if there's content there
        MultipartFile file = bean.getFile();
        if (file == null) {
             // hmm, that's strange, the user did not upload anything
        }

         // well, let's do nothing with the bean for now and return
        return super.onSubmit(request, response, command, errors);
    }
}

public class FileUploadBean {

    private MultipartFile file;

    public void setFile(MultipartFile file) {
        this.file = file;
    }

    public MultipartFile getFile() {
        return file;
    }
}

13.9. 使用Spring的表单标签库

从2.0版本开始,如果使用JSP和Spring Web MVC的话,Spring提供了一套支持数据绑定的标签集合,用于处理表单元素。 每个标签所支持的属性集合和与其对应的HTML标签相同,这就让这些标签看起来很熟悉,而且用起来很直观。 由这些标签库生成的HTML页面符合HTML 4.01/XHTML 1.0标准。

与其它表单/输入标签库不同,Spring的表单标签库与Spring Web MVC集成在一起, 使标签可以使用命令对象(command object)和你的控制器处理的参考数据(reference data)。 就像下面这些例子展示的一样,表单标签让JSP更易于开发、阅读和维护。

让我们进入表单标签的领域,并通过一个例子研究每个标签如何使用。 当某些标签需要更进一步的解释时,我们已经把所生成的HTML片段也一起列了进来。

13.9.1. 配置

Spring的表单标签库包含在spring.jar中。 这个库的描述符(descriptor)叫做spring-form.tld

要使用这个库中的标签,在JSP页面的开头加入下面声明:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

在上面的声明中,form就是为想要使用的这个库中的标签所确定的标签命名前缀。

13.9.2. form标签

这个标签会生成一个HTML的“form”标签,同时为内部标签的绑定暴露了一个绑定路径(binding path)。 它把命令对象(command object)放在PageContext中,这样内部的标签就可以访问这个命令对象了。 这个库中的其他标签都是form标签的嵌套标签

假设我们有一个叫做User的领域对象,它是一个JavaBean, 拥有诸如firstNamelastName这样的属性。 我们将把它当作返回form.jsp的表单控制器的表单支持对象(form backing object)。 下面是一个form.jsp可能的样子的例子:

<form:form>
    <table>
        <tr>
            <td>First Name:</td>
            <td><form:input path="firstName" /></td>
        </tr>
        <tr>
            <td>Last Name:</td>
            <td><form:input path="lastName" /></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="Save Changes" />
            </td>
        </tr>
    </table>
</form:form>

上面firstNamelastName的值是从由页面控制器放置在PageContext中的命令对象中得到的。 请继续读下去来看几个关于如何使用form标签的内部标签的例子。

生成的HTML看起来就是一个标准的form:

<form method="POST">
    <table>
      <tr>
          <td>First Name:</td>
          <td><input name="firstName" type="text" value="Harry"/></td>
      </tr>
      <tr>
          <td>Last Name:</td>
          <td><input name="lastName" type="text" value="Potter"/></td>
      </tr>
      <tr>
          <td colspan="2">
            <input type="submit" value="Save Changes" />
          </td>
      </tr>
    </table>
</form>

上面的JSP有一个预设前提,就是表单支持对象(form backing)的变量名是“command”。 如果你将这个表单支持对象用其他名称加以定义(这可算是一种最佳实践),你就可以将这个命名变量绑定到表单上,如下例所示:

<form:form commandName="user">
    <table>
        <tr>
            <td>First Name:</td>
            <td><form:input path="firstName" /></td>
        </tr>
        <tr>
            <td>Last Name:</td>
            <td><form:input path="lastName" /></td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="Save Changes" />
            </td>
        </tr>
    </table>
</form:form>

13.9.3. input标签

这个标签生成一个“text”类型的HTML“input”标签。 关于这个标签的示例,请见Section 13.9.2, “form标签”

13.9.4. checkbox标签

这个标签生成一个“checkbox”类型的HTML“input”标签。

让我们假设我们的User有比如新闻订阅和其他一组业余爱好这样的偏好。 下面就是一个Preferences类的例子:

public class Preferences {

    private boolean receiveNewsletter;

    private String[] interests;

    private String favouriteWord;

    public boolean isReceiveNewsletter() {
        return receiveNewsletter;
    }

    public void setReceiveNewsletter(boolean receiveNewsletter) {
        this.receiveNewsletter = receiveNewsletter;
    }

    public String[] getInterests() {
        return interests;
    }

    public void setInterests(String[] interests) {
        this.interests = interests;
    }

    public String getFavouriteWord() {
        return favouriteWord;
    }

    public void setFavouriteWord(String favouriteWord) {
        this.favouriteWord = favouriteWord;
    }
}

form.jsp如下:

<form:form>
    <table>
        <tr>
            <td>Subscribe to newsletter?:</td>
            <%-- Approach 1: Property is of type java.lang.Boolean --%>
            <td><form:checkbox path="preferences.receiveNewsletter"/></td>
        </tr>

        <tr>
            <td>Interests:</td>
            <td>
                <%-- Approach 2: Property is of an array or of type java.util.Collection --%>
                Quidditch: <form:checkbox path="preferences.interests" value="Quidditch"/>
                Herbology: <form:checkbox path="preferences.interests" value="Herbology"/>
                Defence Against the Dark Arts: <form:checkbox path="preferences.interests"
                    value="Defence Against the Dark Arts"/>
            </td>
        </tr>
        <tr>
            <td>Favourite Word:</td>
            <td>
                <%-- Approach 3: Property is of type java.lang.Object --%>
                Magic: <form:checkbox path="preferences.favouriteWord" value="Magic"/>
            </td>
        </tr>
    </table>
</form:form>

有三种checkbox标签的使用方法满足你对checkbox的需求。

  • 第一种用法:若绑定值是java.lang.Boolean类型,则值为true时,input(checkbox)就标记为选中。其value属性对应于setValue(Object)值的属性的解析值。

  • 第二种用法:若绑定值是arrayjava.util.Collection类型,则当设定的setValue(Object)值出现在绑定的Collection中时,input(checkbox)就标记为选中。

  • 第三种用法:若绑定值为其他类型,则当设定的setValue(Object)与其绑定值相等时,input(checkbox)才标记为选中。

注意,不管使用哪种方法,生成的HTML结构都是相同的。下面是包含一些checkbox的HTML片段:

<tr>
    <td>Interests:</td>
    <td>
        Quidditch: <input name="preferences.interests" type="checkbox" value="Quidditch"/>
        <input type="hidden" value="1" name="_preferences.interests"/>
        Herbology: <input name="preferences.interests" type="checkbox" value="Herbology"/>
        <input type="hidden" value="1" name="_preferences.interests"/>
        Defence Against the Dark Arts: <input name="preferences.interests" type="checkbox"
            value="Defence Against the Dark Arts"/>
        <input type="hidden" value="1" name="_preferences.interests"/>
    </td>
</tr>

也许没有注意到的是在每个checkbox背后还隐藏了其他字段(field)。 当一个HTML页面中的checkbox没有被选中时,它的值不会在表单提交时作为HTTP请求参数的一部分发送到服务器端, 因此我们需要给这个HTML的奇怪动作想出一个变通方案,来让Spring的表单数据绑定可以工作。 checkbox标签遵循了Spring现有的惯例,就是对于每个checkbox都包含了一个下划线("_"),再跟上一个隐藏参数。 这样一来,就相当于告诉Spring“ 这个checkbox在表单中是可见的,并且希望表单数据将要被绑定到的对象能够反映出任意的checkbox的状态”。

13.9.5. checkboxes标签

这个标签生成多个“checkbox”类型的HTML“input”标签。

这一节建立在上一节checkbox标签的例子之上。 有时倾向于并不在JSP页面中列出全部可能的业余爱好,而是想在运行时提供一个可用选项的清单,并把它传递给相应标签。 这就是checkboxes标签的目标。 传入一个ArrayList,或者Map, 并把可用选项包含在“items”属性中。典型的情况是,这个绑定的属性是一个集合,这样它才能持有用户选择的多个值。 下面是使用了这个标签的JSP的一个例子:

<form:form>
    <table>
        <tr>
            <td>Interests:</td>
            <td>
                <%-- Property is of an array or of type java.util.Collection --%>
                <form:checkboxes path="preferences.interests" items="${interestList}"/>
            </td>
        </tr>
    </table>
</form:form>

这个例子假定了“interestList”是一个List,作为模型属性它包含了用于被选择的字符串的值。 而在使用一个Map的情况下,map条目的键被用作值,map条目的值被用作显示的文本标记。 也可以使用一个定制的对象,提供“itemValue”属性存放值,“itemLabel”属性存放文本标记。

13.9.6. radiobutton标签

这个标签生成一个“radio”类型的HTML“input”标签。

一个典型用法是把多个标签实例绑定到同一属性上,但它们有不同的值。

<tr>
    <td>Sex:</td>
    <td>Male: <form:radiobutton path="sex" value="M"/> <br/>
        Female: <form:radiobutton path="sex" value="F"/> </td>
</tr>

13.9.7. radiobuttons标签

这个标签生成多个“radio”类型的HTML“input”标签。

正像上面提到的checkboxes标签,你可能希望传入一个运行时的变量作为可用的选项。 这种用法下就需要使用radiobuttons标签。 传入一个ArrayList,或者Map, 并把可用选项包含在“items”属性中。在使用Map的情况下,map条目的键被用作值, map条目的值被用作显示的文本标记。也可以使用一个定制的对象,提供“itemValue”属性存放值,“itemLabel”属性存放文本标记。

<tr>
    <td>Sex:</td>
    <td><form:radiobuttons path="sex" items="${sexOptions}"/></td>
</tr>

13.9.8. password标签

这个标签生成一个“password”类型的HTML“input”标签,并赋以绑定的值。

<tr>
    <td>Password:</td>
    <td>
        <form:password path="password" />
    </td>
</tr>

请注意默认情况下,口令的值不会被显示出来。 如果一定要显示口令的值,就把“showPassword”属性的值置为true。

<tr>
    <td>Password:</td>
    <td>
        <form:password path="password" value="^76525bvHGq" showPassword="true" />
    </td>
</tr>

13.9.9. select标签

这个标签生成一个HTML“select”元素。它支持被选中的选项的数据绑定, 也支持使用嵌套的optionoptions标签。

我们假设有一个User拥有多项专长。

<tr>
    <td>Skills:</td>
    <td><form:select path="skills" items="${skills}"/></td>
</tr>

如果这个User的专长是草药学,那么“Skills”这一行的HTML源代码就如下所示:

<tr>
    <td>Skills:</td>
    <td><select name="skills" multiple="true">
        <option value="Potions">Potions</option>
        <option value="Herbology" selected="selected">Herbology</option>
        <option value="Quidditch">Quidditch</option></select>
    </td>
</tr>

13.9.10. option标签

这个标签生成一个HTML的“option”。根据绑定的值,它会恰当的设置“selected”属性。

<tr>
    <td>House:</td>
    <td>
        <form:select path="house">
            <form:option value="Gryffindor"/>
            <form:option value="Hufflepuff"/>
            <form:option value="Ravenclaw"/>
            <form:option value="Slytherin"/>
        </form:select>
    </td>
</tr>

如果这个User的宿舍在Gryffindor(Gryffindor是哈利·波特在Hogwarts的宿舍——译者注), 那么“House”这一行的HTML源代码就如下所示:

<tr>
    <td>House:</td>
    <td>
        <select name="house">
            <option value="Gryffindor" selected="selected">Gryffindor</option>
            <option value="Hufflepuff">Hufflepuff</option>
            <option value="Ravenclaw">Ravenclaw</option>
            <option value="Slytherin">Slytherin</option>
        </select>
    </td>
 </tr>

13.9.11. options标签

这个标签生成一个HTML的“option”标签的列表。根据绑定的值,它会恰当的设置“selected”属性。

<tr>
    <td>Country:</td>
    <td>
        <form:select path="country">
            <form:option value="-" label="--Please Select"/>
            <form:options items="${countryList}" itemValue="code" itemLabel="name"/>
        </form:select>
    </td>
</tr>

如果这个User生活在英国,那么“Country”这一行的HTML源代码就如下所示:

<tr>
    <td>Country:</td>
    <td>
        <select name="country">
            <option value="-">--Please Select</option>
            <option value="AT">Austria</option>
            <option value="UK" selected="selected">United Kingdom</option>
            <option value="US">United States</option>
        </select>
    </td>
</tr>

上面的例子展示了结合使用option标签和options标签产生相同的标准HTML, 也让你在JSP中显式的指定一个值只用于显示目的(在它所处的位置),例如例子中的默认字符串:“--Please Select”。

13.9.12. textarea标签

这个标签生成一个HTML的“textarea”。

<tr>
    <td>Notes:</td>
    <td><form:textarea path="notes" rows="3" cols="20" /></td>
    <td><form:errors path="notes" /></td>
</tr>

13.9.13. hidden标签

这个标签使用绑定的值生成类型为“hidden”的HTML“input”标签。 在生成的HTML代码中,input标签的值和表单支持对象相应属性的值保持一致。 如果要提交一个未绑定的值,就只能使用类型为“hidden”的HTML input标签了。

<form:hidden path="house" />

如果我们选择以隐藏形式提交“house”的值,HTML代码将如下所示:

<input name="house" type="hidden" value="Gryffindor"/>

13.9.14. errors标签

这个标签通过一个HTML“span”标签展现字段的错误。它提供了访问由你的控制器或者与你的控制器关联的任何验证器产生的错误的途径。

假设我们想要在表单提交时显示所有与firstNamelastName字段有关的错误。 我们为User类的实例编写了名为UserValidator的验证器。

public class UserValidator implements Validator {

    public boolean supports(Class candidate) {
        return User.class.isAssignableFrom(candidate);
    }

    public void validate(Object obj, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.");
    }
}

form.jsp将如下所示:

<form:form>
    <table>
        <tr>
            <td>First Name:</td>
            <td><form:input path="firstName" /></td>
            <%-- Show errors for firstName field --%>
            <td><form:errors path="firstName" /></td>
        </tr>

        <tr>
            <td>Last Name:</td>
            <td><form:input path="lastName" /></td>
            <%-- Show errors for lastName field --%>
            <td><form:errors path="lastName"  /></td>
        </tr>
        <tr>
            <td colspan="3">
                <input type="submit" value="Save Changes" />
            </td>
        </tr>
    </table>
</form:form>

如果我们提交的表单的firstNamelastName字段均为空值, HTML页面将如下所示:

<form method="POST">
    <table>
        <tr>
            <td>First Name:</td>
            <td><input name="firstName" type="text" value=""/></td>
            <%-- Associated errors to firstName field displayed --%>
            <td><span name="firstName.errors">Field is required.</span></td>
        </tr>

        <tr>
            <td>Last Name:</td>
            <td><input name="lastName" type="text" value=""/></td>
            <%-- Associated errors to lastName field displayed --%>
            <td><span name="lastName.errors">Field is required.</span></td>
        </tr>
        <tr>
            <td colspan="3">
                <input type="submit" value="Save Changes" />
            </td>
        </tr>
    </table>
</form>

如果我们想要显示一个指定页面上的所有错误清单,又该如何呢? 下面的例子展示了errors标签也支持一些基本的通配符功能。

  • path="*":显示所有错误

  • path="lastName*":显示所有与lastName字段有关的错误

下面的例子将会在页面头部显示一个错误清单,后面则在每个字段旁边显示该字段特有的错误:

<form:form>
    <form:errors path="*" cssClass="errorBox" />
    <table>
        <tr>
            <td>First Name:</td>
            <td><form:input path="firstName" /></td>
            <td><form:errors path="firstName" /></td>
        </tr>
        <tr>
            <td>Last Name:</td>
            <td><form:input path="lastName" /></td>
            <td><form:errors path="lastName"  /></td>
        </tr>
        <tr>
            <td colspan="3">
                <input type="submit" value="Save Changes" />
            </td>
        </tr>
    </table>
</form:form>

HTML代码将如下所示:

<form method="POST">
    <span name="*.errors" class="errorBox">Field is required.<br/>Field is required.</span>
    <table>
        <tr>
            <td>First Name:</td>
            <td><input name="firstName" type="text" value=""/></td>
            <td><span name="firstName.errors">Field is required.</span></td>
        </tr>

        <tr>
            <td>Last Name:</td>
            <td><input name="lastName" type="text" value=""/></td>
            <td><span name="lastName.errors">Field is required.</span></td>
        </tr>
        <tr>
            <td colspan="3">
                <input type="submit" value="Save Changes" />
            </td>
        </tr>
</form>

13.10. 处理异常

当与请求匹配的控制器处理请求时,可能会发生意料之外的异常。 Spring提供了HandlerExceptionResolvers来减轻这些异常带来的痛苦。 HandlerExceptionResolvers有点像在Web应用程序描述符web.xml中定义的异常映射(exception mappings), 但是它处理异常的方式更加灵活。它可以提供当异常被抛出时是什么处理程序在执行的信息。 更进一步,一个以编程方式处理异常的途径,让你对于在请求被指向另一个URL(与使用按servlet的异常映射的最终结果一样)之前如何恰当的响应有了更多选择。

实现HandlerExceptionResolver接口很简单, 只需实现resolveException(Exception,Handler)方法, 并返回一个ModelAndView,除此之外, 也可以直接使用SimpleMappingExceptionResolver。 这个解析器允许你取得任何可能被抛出的异常的类名,并把它映射到一个视图名。 这和Servlet API中提供的异常映射特性在功能上是相当的,但是,它还允许对来自不同处理程序的异常实现更细粒度的异常映射。

13.11. 惯例优先原则(convention over configuration)

对于很多项目来说,严格遵从已有惯例和使用合理的缺省选项大概是这些项目需要的……现在Spring Web MVC明确的支持了这种惯例优先原则的主旨。 这意味着,如果建立了一套命名规范,诸如此类,就可以显著地减少系统所需配置项目的数量, 来建立处理器映射、视图解析器、ModelAndView实例,等等。 这为快速原型开发提供了很大方便。同时提供了一定程度的(通常是好事情)代码库的一致性,进而可以从中选择并发展为成型产品。

[Tip]Tip

Spring分发版本包含了一个展现了惯例优先原则支持的Web应用程序,我们将在这一节描述这一原则。 这个应用程序可以在samples/showcases/mvc-convention目录中找到。

惯例优先原则支持体现在MVC的三个核心领域:模型、视图和控制器。

13.11.1. 对控制器的支持:ControllerClassNameHandlerMapping

ControllerClassNameHandlerMapping类是HandlerMapping接口的一个实现。 它使用惯例来确定请求的URL和用于处理它们的Controller实例间的映射关系。

举个例子,考虑下面的(直观的)Controller实现, 请特别注意这个类的名称

public class ViewShoppingCartController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
        // the implementation is not hugely important for this example...
    }
}

下面是与之伴随的Spring Web MVC配置文件的一个片段:

<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>
                
<bean id="viewShoppingCart" class="x.y.z.ViewShoppingCartController">
    <!-- inject dependencies as required... -->
</bean>

ControllerClassNameHandlerMapping在它的应用上下文中找出所有不同的处理器(handler)(或Controller)bean, 并去掉名称中的Controller,来定义它的处理器映射。

让我们看更多的例子,这样其中的中心思想就马上就清楚了。

  • WelcomeController映射到“/welcome*”请求URL

  • HomeController映射到“/home*”请求URL

  • IndexController映射到“/index*”请求URL

  • RegisterController映射到“/register*”请求URL

  • DisplayShoppingCartController映射到“/displayshoppingcart*请求URL

    (注意大小写——全部小写——对于驼峰式大小写(第一个词的首字母小写,随后的每个词首字母大写)的Controller类名。)

当控制器是MultiActionController处理器类时,生成的映射就(有一点点)更为复杂,但幸而没有更难理解。 下面例子中的几个Controller名字假设都是MultiActionController的实现。

  • AdminController映射到“/admin/*”请求URL

  • CatalogController映射到“/catalog/*”请求URL

如果遵循漂亮而且标准的规范把你的Controller实现命名为xxxController, 那么ControllerClassNameHandlerMapping将使你免于忍受必须首先定义它们, 然后还要维护冗——长——的——SimpleUrlHandlerMapping(或者类似的东西)的枯燥。

ControllerClassNameHandlerMappingAbstractHandlerMapping的子类, 从而使你能够像对待大量其他HandlerMapping实现一样的定义HandlerInterceptor实例和其他任何东西。

13.11.2. 对模型的支持:ModelMapModelAndView

ModelMap类首先是一个绚丽的Map实现, 它可以使新增的将要显示在View中(或上)的对象也遵循同一命名规范。 考虑下面的Controller实现,注意对象被加入ModelAndView, 而并没有指定任何名称。

public class DisplayShoppingCartController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
        
        List cartItems = // get a List of CartItem objects
        User user = // get the User doing the shopping
        
        ModelAndView mav = new ModelAndView("displayShoppingCart"); <-- the logical view name

        mav.addObject(cartItems); <-- look ma, no name, just the object
        mav.addObject(user); <-- and again ma!

        return mav;
    }
}

ModelAndView类使用的ModelMap类是一个自定义的Map的实现。 当有一个新对象加入的时候,它就被用于为这个对象自动生成一个键。 决定某个加入的对象的名字的策略是,当它是一个标量对象(scalar object),比如User时, 就使用这个对象所属类的简短类名。下面的几个例子中,几个为标量对象生成的名字被加入ModelMap实例中。

  • 将会为一个新增的x.y.User实例生成“user”作为名称

  • 将会为一个新增的x.y.Registration实例生成“registration”作为名称

  • 将会为一个新增的x.y.Foo实例生成“foo”作为名称

  • 将会为一个新增的java.util.HashMap实例生成“hashMap”作为名字(在这个情形下你可能想要让名字更加明确一些,因为“hashMap不太直观)。

  • 新增null将会导致抛出一个IllegalArgumentException。 如果正在加入的这个(或这些)对象可能潜在的是null的话,就要让名字更明确一些。

在加入一个SetList或者对象数组之后, 生成名称的策略是深入这个集合,取出集合中第一个对象的简短类名,并使用这个名称并在后面加上List。 一些例子将会让集合的名称生成方式更清晰……

  • 将会为一个新增的包含了一个或多个x.y.User元素的x.y.User[]实例生成“userList”作为名称

  • 将会为一个新增的包含了一个或多个x.y.User元素的x.y.Foo[]实例生成“fooList”作为名称

  • 将会为一个新增的包含了一个或多个x.y.User元素的java.util.ArrayList实例生成“userList”作为名称

  • 将会为一个新增的包含了一个或多个x.y.Foo元素的java.util.HashSet实例生成“fooList”作为名称

  • 一个java.util.ArrayList根本不会被加入(也就是说,addObject(..)调用其实什么都没做)。

13.11.3. 对视图的支持:RequestToViewNameTranslator

RequestToViewNameTranslator接口的功能是当没有显式的提供这样一个逻辑视图名称的时候, 确定一个逻辑的View名称。 这个接口只有一个实现,精明的命名为DefaultRequestToViewNameTranslator

为了解释DefaultRequestToViewNameTranslator将请求的URL映射到逻辑的视图名的方式, 最好还是求助于一个例子。

public class RegistrationController implements Controller {
                
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
        // process the request...
        ModelAndView mav = new ModelAndView();
        // add data as necessary to the model...
        return mav;
        // notice that no View or logical view name has been set
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
        "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>

    <!-- this bean with the well known name generates view names for us -->
    <bean id="viewNameTranslator" class="org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator"/>

    <bean class="x.y.RegistrationController">
        <!-- inject dependencies as necessary -->
    </bean>
    
    <!-- maps request URLs to Controller names -->
    <bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

请注意,在这个handleRequest(..)方法的实现中, 没有在返回的ModelAndView上设置任何的View或者逻辑视图名称。 而是把从请求的URL生成一个逻辑视图名称的任务交给了DefaultRequestToViewNameTranslator。 在上面这个RegistrationControllerControllerClassNameHandlerMapping联合使用的例子中, 一个“http://localhost/registration.html”请求URL将会由DefaultRequestToViewNameTranslator生成一个“registration”逻辑视图名称。 这个逻辑视图名称接下来就会被InternalResourceViewResolver bean解析为“/WEB-INF/jsp/registration.jsp”视图。

[Tip]Tip

甚至不需要显式的定义一个DefaultRequestToViewNameTranslator bean。 如果DefaultRequestToViewNameTranslator的缺省设置符合你的要求, 就可以依赖这样一个事实,Spring Web MVC的DispatcherServlet将会在没有显式配置的情况下自动的生成这个类的一个实例。

当然,如果需要修改缺省设置,那么就需要显式的配置自己的DefaultRequestToViewNameTranslator bean。 关于可以设置的各种属性的细节,请参阅DefaultRequestToViewNameTranslator的相当详细的Javadoc。

13.12. 基于注解的控制器配置

现时对于一些类型的配置数据有一个趋势,就是偏爱注解方式而不是XML文件。为了方便实现,Spring现在(从2.5开始)提供了使用注解配置MVC框架下的组件的支持。

Spring 2.5为MVC控制器引入了一种基于注解的编程模型,在其中使用诸如@RequestMapping@RequestParam@ModelAttribute,等等。 这种注解支持在Servlet MVC和Portlet MVC中均可使用。通过这种方式实现的控制器不必由特定的基类继承而来,或者实现特定的接口。 更进一步的,它们通常并不直接依赖于Servlet或Portlet API,虽然如果需要,它们可以方便的访问Servlet或Portlet的功能。

[Tip]Tip

Spring发行版本附带了PetClinic示例,它是一个在简单的表单处理的上下文中, 利用了本节中说明的注解支持的Web应用程序。 可以在“samples/petclinic”目录中找到PetClinic应用程序。

另外一个建立在基于注解的Web MVC上的示例应用程序,请见imagedb。 这个示例集中在无状态的multi-action控制器,包括多段文件上传的处理。 可以在“samples/imagedb”目录找到imagedb应用程序。

下面的章节记录了这些注解以及通常如何使用它们。

13.12.1. 建立dispatcher实现注解支持

只有对应的HandlerMapping(为了实现类型级别的注解)和/或HandlerAdapter(为了实现方法级别的注解)出现在dispatcher中时, @RequestMapping才会被处理。 这在DispatcherServletDispatcherPortlet中都是缺省的行为。

然而,如果是在定义自己的HandlerMappingsHandlerAdapters, 就需要确保一个对应的自定义的DefaultAnnotationHandlerMapping和/或AnnotationMethodHandlerAdapter同样被定义——假设想要使用@RequestMapping

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    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">

    <bean class="org.springframework.web.servlet.mvc.DefaultAnnotationHandlerMapping"/>

    <bean class="org.springframework.web.servlet.mvc.AnnotationMethodHandlerAdapter"/>

    ... (controller bean definitions) ...

</beans>

如果你想要自定义映射策略,显式的定义一个DefaultAnnotationHandlerMapping和/或AnnotationMethodHandlerAdapter也有实际意义。 例如,指定一个自定义的PathMatcher或者WebBindingInitializer(见下面)。

13.12.2. 使用@Controller定义一个控制器

注解@Controller指明一个特定的类承担控制器的职责, 而没有扩展任何控制器基类或者引用Servlet API的必要。当然,如果需要还是可以引用特定Servlet功能。

注解@Controller的基本目标是担任所注解的类的原型的角色,指明它的职责。 Dispatcher将会在这样被注解的类中扫描映射的方法,探测注解@RequestMapping(见下一节)。

所注解的控制器bean可以被显式定义,这个过程是在dispatcher的上下文中使用一个标准的Spring bean定义完成的。 然而,@Controller原型也允许自动探测,就像Spring 2.5对探测组件的类以及为它们自动注册bean定义的普遍支持一样。

要实现对这样的所注解的控制器的自动探测,必须要向配置中加入组件扫描的部分。 通过使用在下面的XML片段中所展示出的spring-context schema,这很容易实现:

<?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">

    <context:component-scan base-package="org.springframework.samples.petclinic.web"/>

    ...

</beans>

13.12.3. 使用@RequestMapping映射请求

注解@RequestMapping被用于映射如“/editPet.do”这样的URL到一个完整的类或者一个特定的处理方法。 典型的,顶层的注解映射一个特定的请求路径(或者路径模式)到一个表单控制器,另外的方法一级的注解可以缩小这个主要映射的范围,包括对于一个特定的HTTP请求方法(“GET/POST”)或者特定的HTTP请求参数。

[Tip]Tip

@RequestMapping在类型一级也可以被用于Controller接口的普通实现。 在这种情况下,请求处理的代码会遵循传统的handleRequest模样,而控制器的映射将会通过一个@RequestMapping注解体现。 这对于预先构建的Controller基类,诸如SimpleFormController,也一样有效。

在下面的讨论中,我们将会关注基于通过注解实现的处理方法的控制器。

下面是一个使用了这种注解的表单控制器的例子,它选自PetClinic:

@Controller
@RequestMapping("/editPet.do")
@SessionAttributes("pet")
public class EditPetForm {

	private final Clinic clinic;

	@Autowired
	public EditPetForm(Clinic clinic) {
		this.clinic = clinic;
	}

	@ModelAttribute("types")
	public Collection<PetType> populatePetTypes() {
		return this.clinic.getPetTypes();
	}

	@RequestMapping(method = RequestMethod.GET)
	public String setupForm(@RequestParam("petId") int petId, ModelMap model) {
		Pet pet = this.clinic.loadPet(petId);
		model.addAttribute("pet", pet);
		return "petForm";
	}

	@RequestMapping(method = RequestMethod.POST)
	public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result, 
			SessionStatus status) {
		new PetValidator().validate(pet, result);
		if (result.hasErrors()) {
			return "petForm";
		}
		else {
			this.clinic.storePet(pet);
			status.setComplete();
			return "redirect:owner.do?ownerId=" + pet.getOwner().getId();
		}
	}

}

对于一个传统的multi-action控制器,由于控制器会响应多个URL,URL就通常被直接映射到方法上。 下面是一个使用了@RequestMapping的multi-action控制器的例子,它选自PetClinic:

@Controller
public class ClinicController {

	private final Clinic clinic;

	@Autowired
	public ClinicController(Clinic clinic) {
		this.clinic = clinic;
	}

	/**
	 * Custom handler for the welcome view.
	 * Note that this handler relies on the RequestToViewNameTranslator to
	 * determine the logical view name based on the request URL: "/welcome.do"
	 * -> "welcome".
	 */
	@RequestMapping("/welcome.do")
	public void welcomeHandler() {
	}

	/**
	 * Custom handler for displaying vets.
	 * Note that this handler returns a plain {@link ModelMap} object instead of
	 * a ModelAndView, thus leveraging convention-based model attribute names.
	 * It relies on the RequestToViewNameTranslator to determine the logical
	 * view name based on the request URL: "/vets.do" -> "vets".
	 *
	 * @return a ModelMap with the model attributes for the view
	 */
	@RequestMapping("/vets.do")
	public ModelMap vetsHandler() {
		return new ModelMap(this.clinic.getVets());
	}

	/**
	 * Custom handler for displaying an owner.
	 * Note that this handler returns a plain {@link ModelMap} object instead of
	 * a ModelAndView, thus leveraging convention-based model attribute names.
	 * It relies on the RequestToViewNameTranslator to determine the logical
	 * view name based on the request URL: "/owner.do" -> "owner".
	 *
	 * @param ownerId the ID of the owner to display
	 * @return a ModelMap with the model attributes for the view
	 */
	@RequestMapping("/owner.do")
	public ModelMap ownerHandler(@RequestParam("ownerId") int ownerId) {
		return new ModelMap(this.clinic.loadOwner(ownerId));
	}

}

使用@RequestMapping注解的处理器方法允许具有非常灵活的外观。 它们可以拥有下面类型的参数,在任意的顺序下(除非是对于验证结果,它需要紧跟在对应的命令对象后面,如果需要):

  • 请求和/或响应对象(Servlet API或者Portlet API)。 可以选择任何特定的请求/响应类型,例如,ServletRequest/HttpServletRequest或者PortletRequest/ActionRequest/RenderRequest。 注意那个Portlet的例子里,一个被显式声明了的action/render参数被用于映射特定的请求类型到一个处理方法(在没有提供其他信息来区分action和render requests的情况下)。

  • 会话对象(Servlet API或者Portlet API):不管是HttpSession还是PortletSession。 一个此种类型的参数将会保证出现一个对应的会话。这样就造成,这样一个参数永远也不可以是<code>null</code>注意会话访问可以并不是线程安全的,特别是在Servlet环境中:如果允许多个请求同时访问一个会话,就考虑把AnnotationMethodHandlerAdapter的“synchronizeOnSession”旗标置为“true”

  • org.springframework.web.context.request.WebRequestorg.springframework.web.context.request.NativeWebRequest。 允许像访问请求/会话属性一样的访问一般的请求参数,而不是锁定在原生的Servlet/Portlet API上。

  • java.util.Locale用于当前请求区域属性(由可用的最接近的区域属性解析器决定,也就是, 在Servlet环境中配置好的LocaleResolver以及在Portlet环境中的portal locale)。

  • java.io.InputStream/java.io.Reader用于访问请求的内容。 这将是Servlet/Portlet API暴露出的天然的InputStream/Reader。

  • java.io.OutputStream/java.io.Writer用于生成响应的内容。 这将是Servlet/Portlet API暴露出的天然的OutputStream/Writer。

  • @RequestParam注解的参数用于访问特定的Servlet/Portlet请求参数。 参数的值将被转换为声明的方法参数类型。

  • java.util.Map/org.springframework.ui.Model/org.springframework.ui.ModelMap用于充实将被暴露到Web视图的隐含模型。

  • 绑定参数到的命令/表单对象:带有自定义的类型转换的bean属性或者域,依赖于@InitBinder方法和/或HandlerAdapter配置——参见AnnotationMethodHandlerAdapter的“webBindingInitializer”属性。 这样的命令对象,包括它们的验证结果,将会暴露为模型属性,默认的会在属性注解中使用非限定的命令类名(例如,对于类型“mypackage.OrderAddress”使用“orderAddress”)。 为声明一个特定的模型属性名称指定一个参数级别的ModelAttribute注解。

  • org.springframework.validation.Errors/org.springframework.validation.BindingResult验证结果用于前面的一个命令/表单对象(前面紧接的参数)。

  • org.springframework.web.bind.support.SessionStatus状态处理用于把表单处理过程标记为已完成(触发会话属性的清理,这些会话属性是在句柄类型级别由@SessionAttributes注解指示出的)。

13.12.4. 使用@RequestParam绑定请求参数到方法参数

@RequestParam注解是用于在控制器中绑定请求参数到方法参数。

下面取自PetClinic实例程序的代码片段说明了这种用法:

@Controller
@RequestMapping("/editPet.do")
@SessionAttributes("pet")
public class EditPetForm {

    // ...

	@RequestMapping(method = RequestMethod.GET)
	public String setupForm(@RequestParam("petId") int petId, ModelMap model) {
		Pet pet = this.clinic.loadPet(petId);
		model.addAttribute("pet", pet);
		return "petForm";
	}

    // ...

使用这个注解的参数默认是必需的,但是可以把@RequestParamrequired属性置为false从而让这个参数可选(例如,@RequestParam(value="id", required="false"))。

13.12.5. 使用@ModelAttribute提供一个从模型到数据的链接

@ModelAttribute在控制器中有两种使用场景。 当作为一个方法参数时,@ModelAttribute用于映射一个模型属性到特定的注解的方法参数(见下面的processSubmit()方法)。 这是控制器获得持有表单数据的对象引用的方法。另外,这个参数也可以被声明为特定类型的表单支持对象,而不是一般的java.lang.Object,这就增加了类型安全性。

@ModelAttribute也用于在方法级别为模型提供引用数据(见下面的populatePetTypes()方法)。 在这种用法中,方法编写可以包含与上面描述的@RequestMapping注解相同的类型。

注意:使用@ModelAttribute注解的方法将会在选定的使用@RequestMapping注解的方法之前执行。 它们有效的使用特定的属性预先填充隐含的模型,这些属性常常来自一个数据库。 这样一个属性也就可以通过在选定的方法中使用@ModelAttribute注解的句柄方法参数来访问了,潜在的可以应用绑定和验证。

下面的代码片段展示了此注解的这两种用法:

@Controller
@RequestMapping("/editPet.do")
@SessionAttributes("pet")
public class EditPetForm {

	// ...

	@ModelAttribute("types")
	public Collection<PetType> populatePetTypes() {
		return this.clinic.getPetTypes();
	}

	@RequestMapping(method = RequestMethod.POST)
	public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result,
			SessionStatus status) {
		new PetValidator().validate(pet, result);
		if (result.hasErrors()) {
			return "petForm";
		}
		else {
			this.clinic.storePet(pet);
			status.setComplete();
			return "redirect:owner.do?ownerId=" + pet.getOwner().getId();
		}
	}

}

13.12.6. 使用@SessionAttributes指定存储在会话中的属性

类型级别的@SessionAttributes注解使用一个特定的句柄声明会话属性。 这通常会列出模型属性的名称,这些属性应被透明的保存在会话或者对话存储中,用于在后续的请求之间作为表单支持beans。

下面的代码片段展示了此注解的这种用法:

@Controller
@RequestMapping("/editPet.do")
@SessionAttributes("pet")
public class EditPetForm {
    // ...
}

13.12.7. 自定义WebDataBinder初始化

为了通过Spring的WebDataBinder使用PropertyEditors等自定义请求参数绑定,可以或者使用@InitBinder——在控制器之内的注解的方法, 或者通过提供一个定制的WebBindingInitializer把配置具体化。

13.12.7.1. 使用@InitBinder自定义数据绑定

使用@InitBinder注解控制器方法,可以在控制器类内部直接配置Web数据绑定。 @InitBinder指定初始化WebDataBinder的方法, 后者被用于填充注解的句柄方法的命令和表单对象参数。

这个init-binder方法支持@RequestMapping支持的全部参数,除了命令/表单对象和对应的验证结果对象。 Init-binder方法必须没有返回值。因此,它们常被声明为void。 典型的参数,包括 WebDataBinder以及WebRequest或者java.util.Locale,允许代码注册上下文特定的编辑器。

下面的例子说明了@InitBinder的用法,为所有的java.util.Date表单属性配置一个CustomDateEditor

@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));
    }

    // ...
}

13.12.7.2. 配置一个定制的WebBindingInitializer

为了外化数据绑定初始化的过程,可以提供一个WebBindingInitializer接口的自定义实现。 通过为一个AnnotationMethodHandlerAdapter提供一个定制的bean配置可以使它启用,这样就覆盖了默认配置。

下面取自PetClinic应用的例子展示了一个使用WebBindingInitializer接口的自定义实现的配置——org.springframework.samples.petclinic.web.ClinicBindingInitializer, 完成多个PetClinic控制器都需要的PropertyEditors的配置。

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="cacheSeconds" value="0" />
    <property name="webBindingInitializer">
        <bean class="org.springframework.samples.petclinic.web.ClinicBindingInitializer" />
    </property>
</bean>

13.13. 更多资源

从下面的链接和指向可以找到关于Spring Web MVC的更多资源。

  • Spring发行版本附带了一个Spring Web MVC的教程,用逐个步骤进行的方法指导读者开发一个完整的基于Spring Web MVC的应用程序。教程位于Spring发行版本的“docs”目录。在线版本可以在Spring Framework网站上找到。

  • Seth Ladd和其它人合著的“Expert Spring Web MVC and WebFlow”(由Apress出版)一书是一本优秀的纸版作品,其中对Spring Web MVC的优势做了详细介绍。