Chapter 13. Web框架

13.1. 介绍

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

你可以使用任何对象作为命令对象(或表单对象):不必实现某个接口或从某个基类继承。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. 与其他web框架的集成

由于种种原因,许多团队倾向于使用其他的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框架,经注册的所有bean和所有Spring服务仍然可以使用。从这个用法上来讲,Spring并没有和Struts或WebWork竞争,它只是提供这些纯粹的web框架所没有的功能,例如:bean的配置、数据访问和事务处理。因此你可以使用Spring的中间层或者数据访问层来增强你的应用,即便你只是需要使用像JDBC或Hibernate事务抽象这样的功能。

13.1.2. Spring Web MVC框架的特点

Spring Web MVC框架提供了大量独特的功能,包括:

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

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

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

  • 可重用的业务代码:你可以使用现有的业务对象作为命令或表单对象,而不需要在类似ActionForm的子类中重复它们的定义。

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

  • 可定制的handler mapping和view resolution:Spring提供从最简单的的URL映射,到复杂的、专用的定制策略。与某些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.3, “其他作用域”有详细描述。

13.2. DispatcherServlet

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

下图展示了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, “ApplicationContext”中所描述的,Spring中的ApplicationContext可以被限制在不同的作用域(scope)中。在web MVC框架中,每个DispatcherServlet有它自己的WebApplicationContext,这个context继承了根 WebApplicationContext的所有bean定义。这些继承的bean也可以在每个serlvet自己的所属的域中被覆盖(override),覆盖后的bean可以被设置成只有这个servlet实例自己才可以使用的属性。

Spring Web MVC中的Context体系

DispatcherServlet的初始化过程中,Spring会在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

名称描述
控制器(Controller)控制器 实现的是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接收到与其对应的请求之时,处理就开始了。下面的列表描述了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而得到通知。
synchronizeSession 指定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>

该controller返回的ModelAndView使用了硬编码的视图名(尽管这样做不好),并通知客户端将响应数据缓存2分钟。除了通过以上方式创建和配置controller之外,还需要配置handler mapping(请参考Section 13.4, “处理器映射(handler mapping)”),这样该controller就可以工作了。

13.3.2. 其它的简单控制器

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

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

13.3.3. MultiActionController

MultiActionController将多个行为(action)合并在一个控制器里,这样可以把相关功能组合在一起。MultiActionController位于org.springframework.web.mvc.multiaction包中,它通过将请求映射到正确的方法名来调用方法。当在一个控制器存在大量公共的行为,但是有多个调用入口时,使用MultiActionController就特别方便。

Table 13.4. MultiActionController提供的功能

功能描述
delegateMultiActionController有两种使用方式。第一种是你继承MultiActionController,并在子类中指定由MethodNameResolver解析的方法(这种情况下不需要这个delegate参数)。第二种是你定义一个代理对象,由它提供MethodNameResolver解析出来的方法(这种情况下,你必须使用这个配置参数定义代理对象)。
methodNameResolverMultiActionController需要一种策略,使其可以通过解析请求信息来获得要调用的方法。这个解析策略由MethodNameResolver这个接口定义的。这个参数允许你实现MethodNameResolver接口,然后在控制器中使用你的策略。

MultiActionController所支持的方法需要符合下列格式:

// anyMeaningfulName can be replaced by any methodname
public [ModelAndView | Map | void] anyMeaningfulName(HttpServletRequest, HttpServletResponse [, Exception | AnyObject]);

注意:在此不允许方法重载,因为MultiActionController无法分辨出重载(overloading)了的方法。此外,你可以定义exception handler来处理方法中抛出的异常。

Exception 参数是可选的,它可以是任何异常,只要它是java.lang.Exceptionjava.lang.RuntimeException的子类。AnyObject参数也是可选的,它可以是任何对象。HTTP Request中的参数会存在这个对象中,以便使用。

下面几个例子示范了MultiActionController正确的方法定义。

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

public ModelAndView doRequest(HttpServletRequest, HttpServletResponse)

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

public ModelAndView doLogin(HttpServletRequest, HttpServletResponse, Login)

下面这个方法可以处理Exception

public ModelAndView processException(HttpServletRequest, HttpServletResponse, IllegalArgumentException)

下面这个方法不返回任何数值。 (请参考后面的章节 Section 13.11, “惯例优先原则(convention over configuration)”)

public void goHome(HttpServletRequest, HttpServletResponse)

This signature has a Map return type (see the section entitled Section 13.11, “惯例优先原则(convention over configuration)” below).

下面这个方法返回一个Map。 (请参考后面的章节Section 13.11, “惯例优先原则(convention over configuration)”)

public Map doRequest(HttpServletRequest, HttpServletResponse)

MethodNameResolver负责从请求中解析出需要调用的方法名称。下面是Spring中内置的三个MethodNameResolver 实现。

  • ParameterMethodNameResolver - 解析请求参数,并将它作为方法名。(对应http://www.sf.net/index.view?testParam=testIt的请求,会调用 testIt(HttpServletRequest,HttpServletResponse)方法)。使用paramName配置参数,可以设定要检查的参数。

  • InternalPathMethodNameResolver -从路径中获取文件名作为方法名 (http://www.sf.net/testing.view的请求会调用testing(HttpServletRequest,HttpServletResponse)方法。

  • PropertiesMethodNameResolver - 使用用户自定义的属性对象,将请求的URL映射到方法名。当属性中包含/index/welcome.html=doIt时,发到/index/welcome.html 的请求会调用doIt(HttpServletRequest, HttpServletResponse)这个方法。 这个方法名解析器可以和PathMatcher一起工作,比如上边那个URL写成/**/welcom?.html也是可以的。

我们来看一组例子。首先是一个使用ParameterMethodNameResolver和代理(delegate)属性的例子,它接受包含参数名"method"的请求,调用方法retrieveIndex

<bean id="paramResolver" class="org....mvc.multiaction.ParameterMethodNameResolver">
  <property name="paramName" value="method"/>
</bean>

<bean id="paramMultiController" class="org....mvc.multiaction.MultiActionController">
  <property name="methodNameResolver" ref="paramResolver"/>
  <property name="delegate" ref="sampleDelegate"/>
</bean>

<bean id="sampleDelegate" class="samples.SampleDelegate"/>

## together with

public class SampleDelegate {

    public ModelAndView retrieveIndex(HttpServletRequest req, HttpServletResponse resp) {

        return new ModelAndView("index", "date", new Long(System.currentTimeMillis()));
    }
}

当使用上面的代理对象时,我们也可以使用PropertiesMethodNameRseolver来匹配一组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" ref="sampleDelegate"/>
</bean>

13.3.4. 命令控制器

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

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

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

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

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

    你有可能也需要写一个构造器,它至少需要调用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

  • urlPathHelper:指定在分析URL时使用的UrlPathHelper。通常使用其默认值。

  • urlDecode: 这个属性的默认值是false。HttpServletRequest返回未解码的访问URL和URI。HttpServletRequest中请求的URL和URI还保留在HTTP协议所定义编码状态,如果你想在HandlerMapping使用它们发现合适的处理器之前对URL进行解码,你应该把这个属性设成true (注意这需要JDK 1.4的支持)。解码方法会选用HTTP请求中指定的编码格式,或缺省的ISO-8859-1编码方法。 HTTP请求中一般会声明编码的格式,如果没有的话,默认值是ISO-8859-1。Spring会使用相应的解码算法。

  • 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>

The above web.xml configuration snippet enables all requests ending with .html and .form to be handled by the sample dispatcher servlet.

上面的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. 视图解析器

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

Table 13.5. 视图解析器

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 去下载表单提交后的结果页面,而不是重新提交表单。

13.5.3.1. RedirectView

在控制器中强制重定向的方法之一是让控制器生成并返回一个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前缀的视图名和其他视图名一样被注入控制器,控制器根本不知道forward是否发生了。

13.6. 本地化解析器

Spring架构的绝大部分都支持国际化,Spring的web框架也不例外。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">

    <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.6. CookieLocaleResolver的属性

属性缺省值描述
cookieNameclassname + LOCALE cookie的名字。
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.7. 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 FileUpload(http://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相似。

总而言之,为了使用表单上传文件,你需要声明一个解析器,一个控制器,再将文件上传的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 object)的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 object) 的相关属性设成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开始,Spring提供全面的,支持数据绑定的JSP标签来处理表单元素(如果你使用JSP和Spring的Web MVC框架的话)。 每个标签所支持的属性跟其对应的HTML标签相同,这样这些标签看起来就不陌生,而且很容易用。 由这些标签库生成的HTML页面符合HTML 4.01/XHTML 1.0标准。

与其它的标签库不同,Spring的表单标签库和Spring Web MVC框架是集成在一起的,因此它们可以直接使用命令对象(command object) 和其他由控制器处理的数据对象。 就像下面这些例子展示的一样,使用这些标签后,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标签,同时为form内部所包含的标签提供一个绑定路径(binding path)。 它把命令对象(command object)存在PageContext中,这样form内部的标签 就可以使用这个对象了。标签库中的其他标签都声明在form标签的内部

让我们假设有一个叫User的领域对象,它是一个JavaBean,有着诸如 firstNamelastName这样的属性。我们将把它当作 一个表单支持对象(form backing object),它对应的表单控制器用 form.jsp页面来显示表单。下面是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="3">
                <input type="submit" value="Save Changes" />
            </td>
        </tr>
    </table>
</form:form>

上面例子中的firstNamelastName由控制器从 存在PageContext中的命令对象中得到。 下面几个更复杂的例子展示了form内部标签的用法。

这是由form标签所生成的HTML代码,和标准的HTML form没有什么区别:

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

上面这个例子有一个隐藏的前提:表单支持对象(form backing object)的变量名是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="3">
                <input type="submit" value="Save Changes" />
            </td>
        </tr>
    </table>
</form:form>

13.9.3. input标签

这个标签生成text类型的HTML input标签。使用这个标签时,path属性的值将对应 HTML input标签中name属性的值。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>
            <td></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>
            <td></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>
            <td></td>
        </tr>
    </table>
</form:form>

checkbox有三种使用方法,应该可以满足我们全部可能的需求。

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

  • 第二种用法:若绑定值是array(数组)类型或java.util.Collection,则配置的setValue(Object)值出现在绑定的Collection中时,input(checkbox)标为checked(选中)。

  • 第三种用法:若绑定值为其他类型,则当配置的setValue(Object)等于其绑定值时,input(checkbox)标为checked(选中)。

不管使用那一种方法,生成的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>
    <td></td>
</tr>

也许你注意到了每个checkbox元素后面都跟着一个隐藏区域(hidden field)。当一个HTML页面中的checkbox没有被选中时,这个checkbox的值不会在表单提交时作为HTTP请求参数发送到服务器端。这给Spring的表单数据绑定造成了麻烦。解决方法就是在每个checkbox后面加一个隐藏区域,并且每个隐藏区域的名字是在其对应的checkbox名字前加下划线("_")。这是Spring已有的惯例。这样一来,你相当于告诉Spring“这个表单中存在这样一个checkbox,我希望表单支持对象中相对应的属性和这个checkbox的状态保持一致 ”。

13.9.5. radiobutton标签

这个标签生成类型为radio的HTML input 标签。

这个标签的典型用法是一次声明多个标签实例,所有的标签都有相同的path属性,但是他们的value属性不同。

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

13.9.6. password标签

这个标签生成类型为password的HTML input标签。input标签的值和表单支持对象相应属性的值保持一致。

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

13.9.7. select标签

这个标签生成HTML select标签。在生成的HTML代码中,被选中的选项和表单支持对象相应属性的值保持一致。这个标签也支持嵌套的optionoptions标签。

在下面的例子中,我们假设User可以选择自己的专业技能(多项选择):

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

如果某个User的专业是草药学(Herbology),生成的HTML代码就会像下面这样:

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

13.9.8. option标签

这个标签生成HTML option标签。在生成的HTML代码中,被选中的选项和表单支持对象相应属性的值保持一致。

<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,生成的HTML代码就会像下面这样:

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

译者注:这一节中的几个例子都跟《哈里波特》这本小说的内容有关。

13.9.9. options标签

这个标签生成一系列的HTML option标签。在生成的HTML代码中,被选中的选项和表单支持对象相应属性的值保持一致。

<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>
    <td></td>
</tr>

如果某个User住在英国,生成的HTML代码就会像下面这样:

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

上面的这个例子同时使用了option标签和options标签。这两个标签生成的HTML代码是相同的,但是第一个option标签允许你在JSP中明确声明这个标签的值只供显示使用,并不绑定到表单支持对象的属性上。

13.9.10. 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.11. hidden标签

这个标签生成类型为hidden的HTML input标签。在生成的HTML代码中,input标签的值和表单支持对象相应属性的值保持一致。如果你需要声明一个类型为hidden的input标签,但是表单支持对象中没有对应的属性,你只能使用HTML的标签。

<form:hidden path="house" />

上面的例子表示我们需要将house的值以隐含参数的形式提交,生成的HTML代码如下:

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

13.9.12. errors标签

这个标签生成类型为'span'的HTML标签,用来显示表单验证时出现的错误信息。通过这个标签,你可以访问控制器(controller)和与控制器关联的验证器(validator)产生的错误信息。

假设我们需要在表单提交时显示所有跟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>

如果我们提交表单时没有填firstHamelastName这两个栏目,服务器返回的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="*" - displays all errors

    path="*": 显示所有的错误信息

  • path="lastName*" - displays all errors associated with the lastName field

    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提供了HandlerExceptionResolver这个异常解析器接口。 HandlerExceptionResolvers 有点像你在web.xml中所声明的异常映射(exception mapping),但是它处理异常的方式更加灵活。它可以提供当异常产生时控制器的运行状态。同时,在程序中你也有更多应对异常的选项。当你的程序处理完异常后,产生异常的请求会被传递给另一个URL (这个最终结果和你在web.xml中声明的异常处理的效果一样)。

实现HandlerExceptionResolver接口很简单,你只需要实现resolveException(Exception, Handler)这个方法,返回一个ModelAndView对象即可。你也可以直接使用Spring内置的SimpleMappingExceptionResolver。 这个解析器允许你把异常的类名映射到处理完异常后显示的视图名。这和Servlet API中提供的异常处理功能相同。不同的是,它还允许通过对不同的处理器实现更细粒度的异常映射。

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

对于很多项目来说,遵从已有的惯例和使用合理的缺省选项大概是最合情合理的做法。现在Spring Web MVC框架也明确支持这种惯例优先的配置。具体来说,如果你在项目中遵守一定的惯例(比如命名规范),你可以显著地减少系统需要的配置(比如处理器映射,视图解析器配置,ModelAndView的声明,等等)。这对快速系统建模(rapid prototyping)是非常有利的。如果你打算进一步把模型完成为可以工作的系统,这样写出的代码也具有很好的一致性。

[Tip]Tip

Spring的开发包中有一个web应用的范例。这个范例演示了这一节提到的惯例优先原则。你可以在samples/showcases/mvc-convention目录中找到这个范例。

This convention over configuration support address the three core areas of MVC - namely, the models, views, and controllers.

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

13.11.1. 对控制器的支持: ControllerClassNameHandlerMapping

ControllerClassNameHandlerMappingHandlerMapping接口的一个实现。它检查请求的URL,然后通过惯例来决定与之相对应的控制器。

比如,下面有个非常简单的控制器实现,请特别注意这个类的名字

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。把这些bean的名字中Controller后缀去掉,就得到了每个控制器所能处理的URL。

让我们通过举例来进一步解释这个映射的工作原理。

  • WelcomeController映射到'/welcome*'这个URL

  • HomeController映射到'/home*'这个URL

  • IndexController映射到'/index*'这个URL

  • RegisterController映射到'/register*'这个URL

  • DisplayShoppingCartController映射到'/displayshoppingcart*'这个URL

    (注意字母的大小写。URL全部都用小写,但在Java类名中每个单词的第一个字母要大写。)

当控制器是MultiActionController的子类时,自动生成的映射就稍有点复杂,但应该还是比较好理解的。下面例子中这几个控制器都是MultiActionController

  • AdminController映射到 '/admin/*' 这个URL。

  • CatalogController映射到 '/catalog/*' 这个URL。

如果你的控制器类遵守这些命名规范(xxxController),ControllerClassNameHandlerMapping可以自动生成映射,这样你就不必费劲的定义和维护一长串SimpleUrlHandlerMapping(或者类似的映射策略)。

ControllerClassNameHandlerMappingAbstractHandlerMapping的子类,所以你仍旧可以像对待其他HandlerMapping实例一样来定义HandlerInterceptor的实例。

13.11.2. 对模型的支持:ModelMap (ModelAndView)

ModelMap是一个加强版的Map实现。在这个Map里,每个对象的键都遵守一个命名规范,然后这些对象就可以显示在视图中。这个类的使用其实很简单,不需要长篇大论。下面让我们看几个例子,然后我们结合例子进行讲解。

下面是一个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 a'ain ma!

        return mav;
    }
}

ModelAndView在内部使用了ModelMapModelMap是一个自定义的Map实现,它可以为加入其中的每个对象自动生成一个键名。这些键名是有规律的。当存入的对象是存储单值的对象(scalar object),比如User,生成的键名就是对象的类名(不包括包的名字)。下面几个例子详细解释了这个命名规范:

  • x.y.User这个类的实例对应user

  • x.y.Registration 这个类的实例对应registration

  • x.y.Foo这个类的实例对应foo

  • java.util.HashMap的实例对应hashMap(在这种情况下你最好还是自己声明键名,hashMap这个名字不那么直观)

  • 当你视图把null这个值加入Map时,你会得到IllegalArgumentException。 所以如果你的某个对象可能为null,你最好也自己声明键名。

当你加入ModelAndView中的对象是SetList或者数组时,Spring会检查这个集合,取出这个集合中的第一个对象,然后用它的类名,加上List后缀,就是最终生成的名字。下面几个例子进一步解释了这个命名规则:

  • 一个由 x.y.User组成的数组对应userList这个名字。

  • 一个由x.y.Foo组成的数组对应fooList这个名字。

  • 一个由x.y.User组成的java.util.ArrayList对应userList这个名字。

  • 一个由x.y.Foo组成的java.util.HashSet对应fooList这个名字。

  • 一个空的java.util.ArrayList根本不可能被加到这个Map中。(在这种情况下,adObject(..)其实什么都没做)。

13.11.3. 对视图的支持: RequestToViewNameTranslator

RequestToViewNameTranslator这个接口的功能是自动寻找请求所对应的视图名(当某个视图名没有明确配置的时候)。这个接口目前只有一个实现,类名为DefaultRequestToViewNameTranslator

为了解释DefaultRequestToViewNameTranslator是如何将请求的URL映射到视图名,最好的方法就是举例说明。下面是一个Controller的实现,和它对应的配置文件。

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.RegistrationControllerController">
        <!-- 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中分析出逻辑视图名(logical view name)的是DefaultRequestToViewNameTranslator。在上面这个例子中,RegistrationControllerControllerControllerClassNameHandlerMapping在一起使用,所有对http://localhost/registration.html这个URL的请求将会对应registration这个逻辑视图名。这个视图名正是由DefaultRequestToViewNameTranslator解析出来的。然后,InternalResourceViewResolver这个bean会将这个逻辑视图名进一步解析成/WEB-INF/jsp/registration.jsp这个视图。

[Tip]Tip

你甚至不需要配置类型为DefaultRequestToViewNameTranslator的bean。如果DefaultRequestToViewNameTranslator的缺省行为已经符合你的要求,你就可以使用这个类。当你没有明确声明时,Spring Web MVC 中DispatcherServlet这个类会自动生成一个DefaultRequestToViewNameTranslator的实例。

当然,如果你有自己特殊的要求,你就需要配置DefaultRequestToViewNameTranslator bean。如果你需要知道这个类有哪些可以设置的参数,请参阅DefaultRequestToViewNameTranslator的Javadoc。

13.12. 其它资源

下面几个链接也是有关于Spring Web MVC框架的。

  • Spring的开发包附带了一个关于Spring Web MVC框架的教程(位于docs目录)。这个教程教给你怎样一步一步地建立一个基于Spring Web MVC的应用。在Spring Framework上也可以找到该教程的在线版本。

  • Seth Ladd 和其它几个人合写的“《Expert Spring Web MVC and WebFlow》”(由Apress出版)是一部关于Spring Web MVC框架的优秀作品,其中对Spring Web MVC的特点和优势做了比较详细的介绍。