Chapter 14. 集成视图技术

14.1. 简介

Spring的一个优秀之处在于,把view层技术与MVC框架的其他部分分离开来。例如,选择使用Velocity或者XSLT来代替已有的JSP方式只需要在配置上做改动就可以了。本章涵盖了和Spring协同工作的主流view层技术并简要介绍了如何增加新的方式。这里假设你已经熟悉 Section 13.5, “视图与视图解析” 中“mvc-view resolver”的知识,那里讲述了view层与MVC框架协作的基础。

14.2. JSP和JSTL

Spring为JSP和JSTL这些view层技术提供了几个即取即用的解决方案。使用JSP和JSTL的话,采用WebApplicationContext中定义的普通视图解析器就好;当然,还得自己写一些实际做渲染的JSP页面。本章介绍了一些Spring提供的用于简化JSP开发的额外特性。

14.2.1. 视图解析器

与你在Spring中采用的任何其他视图技术一样,使用JSP方式的话你需要一个用来解析你的视图的视图解析器,常用的是在WebApplicationContext中定义的 InternalResourceViewResolverResourceBundleViewResolver

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

# And a sample properties file is uses (views.properties in WEB-INF/classes):
welcome.class=org.springframework.web.servlet.view.JstlView
welcome.url=/WEB-INF/jsp/welcome.jsp

productList.class=org.springframework.web.servlet.view.JstlView
productList.url=/WEB-INF/jsp/productlist.jsp

正如你所看到的,ResourceBundleViewResolver需要一个属性文件来定义view名到1) class 2) URL的映射。使用ResourceBundleViewResolver,你可以只使用一个解析器来混用不同类型的视图技术。

<bean id="viewResolver" 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>

如上例所示,使用JSP时可以配置一个InternalResourceBundleViewResolver。作为一个最佳实践,我们强烈推荐你用 WEB-INF 下的一个目录来存放JSP文件,以避免被客户端直接访问。

14.2.2. 'Plain-old' JSPs versus JSTL 'Plain-old' JSP与JSTL

使用JSTL时,你必须使用一个特别的view类 JstlView,因为JSTL需要一些准备工作,然后像i18N这样的特性才能工作。

14.2.3. 帮助简化开发的额外的标签

前面的章节中提到过,Spring提供了从请求参数到命令对象的数据绑定。为了简化与数据绑定特性配合使用的JSP页面的开发,Spring提供了一些标签让事情变得更简单。这些标签都提供了 html escaping 的特性,能够打开或关闭字符转码的功能。

spring.jar 包含了标签库描述符(TLD),就好像它自己的tag。关于每个tag的更多资料请参阅附录Appendix D, spring.tld

14.3. Tiles

在使用了Spring的web项目中,很可能会用到Tiles--就像任何其它的web层技术。下面粗略讲述了如何使用。

14.3.1. 需要的资源

使用Tiles项目中必须得包含一些额外的资源,以下是你需要的资源列表:

  • Struts 1.1以及更高版本

  • Commons BeanUtils

  • Commons Digester

  • Commons Lang

  • Commons Logging

这些资源全部包含于Spring的发行包中

14.3.2. 如何集成Tiles

使用Tiles,你必须为它配置一些包含了定义信息的文件(关于Tiles定义和其他概念的信息,可以参考 http://jakarta.apache.org/struts)。在Spring中,你可以使用 TilesConfigurer 来完成这项工作。看看下面这个应用上下文配置的例子:

<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles.TilesConfigurer">
  <property name="factoryClass" value="org.apache.struts.tiles.xmlDefinition.I18nFactorySet"/>
  <property name="definitions">
    <list>
      <value>/WEB-INF/defs/general.xml</value>
      <value>/WEB-INF/defs/widgets.xml</value>
      <value>/WEB-INF/defs/administrator.xml</value>
      <value>/WEB-INF/defs/customer.xml</value>
      <value>/WEB-INF/defs/templates.xml</value>
    </list>
  </property>
</bean>

正如你所看到的,有五个包含定义的文件,都放在 'WEB-INF/defs' 目录下。在WebApplicationContext初始化的阶段,这些文件被加载,同时由 factoryClass 属性定义的工厂类被初始化。然后,定义文件中的tiles可以做为views在Spring的web 项目中使用。为使views正常工作,你必须有一个 ViewResolver,就像使用spring提供的任何其它view层技术一样。它有二种选择:InternalResourceViewResolverResourceBundleViewResolver

14.3.2.1.  InternalResourceViewResolver

InternalResourceViewResolver为它解析的每个view实例化一个 viewClass 类的实例。

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="requestContextAttribute" value="requestContext"/>
  <property name="viewClass" value="org.springframework.web.servlet.view.tiles.TilesView"/>
</bean>

14.3.2.2.  ResourceBundleViewResolver

ResourceBundleViewResolver需要一个属性文件,其中包含了它需要使用的视图名和视图类:

<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
  <property name="basename" value="views"/>
</bean>
    ...
welcomeView.class=org.springframework.web.servlet.view.tiles.TilesView
welcomeView.url=welcome (&lt;b&gt;this is the name of a definition&lt;/b&gt;)

vetsView.class=org.springframework.web.servlet.view.tiles.TilesView
vetsView.url=vetsView (again, this is the name of a definition)

findOwnersForm.class=org.springframework.web.servlet.view.JstlView
findOwnersForm.url=/WEB-INF/jsp/findOwners.jsp
...

正如你所看到的,使用ResourceBundleViewResolver时你可以混用不同的view层技术。

14.4. Velocity和FreeMarker

VelocityFreeMarker 是两种模板语言,都可以做为view层技术在Spring MVC 应用中使用。它们的语言风格和适用对象都很相似,这里把它们放在一起讨论。至于它们语义和语法上的不同,可以参考 FreeMarker 站点。

14.4.1. 需要的资源

使用Velocity或FreeMarker需要包含 velocity-1.x.x.jarfreemarker-2.x.jar。另外Velocity还需要 commons-collections.jar。一般把这些jar包放在 WEB-INF/lib 下,这样可以保证J2EE Server找到它们并加到web应用的classpath下。这里同样假设你的 'WEB-INF/lib' 目录下已有 spring.jar!Spring的发布包中已经提供了最新的稳定版本的Velocity、FreeMarker和commons collections,可以从相应的/lib/ 子目录下得到。如果你想在Velocity中使用Spring的dateToolAttribute或numberToolAttribute,那你还需要 velocity-tools-generic-1.x.jar

14.4.2. Context 配置

通过在'*-servlet.xml'中增加相关的配置bean,可以初始化相应的配置,如下:

<!-- 
  该bean使用一个存放模板文件的根路径来配置Velocity环境。你也可以通过指定一个属性文件来更精细地控制Velocity,但对基于文件的模板载入来说,默认的方式已相当健全
-->
<bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
  <property name="resourceLoaderPath" value="/WEB-INF/velocity/"/>
</bean>

<!-- 

  也可以把ResourceBundle或XML文件配置到视图解析器中。如果你需要根据Locale来解析不同的视图,你就得使用resource bundle解析器。

-->
<bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
  <property name="cache" value="true"/>
  <property name="prefix" value=""/>
  <property name="suffix" value=".vm"/>
  
  <!-- 如果你需要使用Spring 对 Velocity宏命令的支持, 将这个属性设为true  -->
  <property name="exposeSpringMacroHelpers" value="true"/>

</bean>
<!-- freemarker config -->
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
  <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
</bean>

<!-- 

  也可以把ResourceBundle或XML文件配置到视图解析器中。如果你需要根据Locale来解析不同的视图,你就得使用resource bundle解析器。.

-->
<bean id="viewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
  <property name="cache" value="true"/>
  <property name="prefix" value=""/>
  <property name="suffix" value=".ftl"/>
  
  <!-- 如果你需要使用Spring 对 FreeMarker 宏命令的支持, 将这个属性设为true  -->
  <property name="exposeSpringMacroHelpers" value="true"/>

</bean>
[Note]Note

对于非web应用,你需要在application context的配置文件中声明 VelocityConfigurationFactoryBean 或者 FreeMarkerConfigurationFactoryBean

14.4.3. 创建模板

模板文件需要存放在配置 *Configurer bean时所指定的目录下,就像上面的例子所示。这里不准备详细叙述使用这两种语言创建模板的细节,你可以参考相应的站点获取那些信息。如果你用了我们推荐的视图解析器,你会发现从逻辑视图名到相应模板文件的映射方式与使用 InternalResourceViewResolver 处理JSP时的映射方式类似。比如若你的控制器返回了ModelAndView对象,其中包含一个叫做"welcome"的视图名,则视图解析器将试图查找 /WEB-INF/freemarker/welcome.ftl/WEB-INF/velocity/welcome.vm

14.4.4. 高级配置

以上着重介绍的基本配置适合大部分应用需求,然而仍然有一些不常见的或高级需求的情况,Spring提供了另外的配置选项来满足这种需求。

14.4.4.1. velocity.properties

这个文件是可选的,不过一旦指定,其所包含的值即影响Velocity运行时状态。只有当你要做一些高级配置时才需要这个文件,这时你可以在上面定义的 VelocityConfigurer 中指定它的位置。

<bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
  <property name="configLocation value="/WEB-INF/velocity.properties"/>
</bean>

另一种方法,你可以直接在Velocity config bean的定义中指定velocity属性,来取代"configLocation"属性。

<bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
  <property name="velocityProperties">
    <props>
      <prop key="resource.loader">file</prop>
      <prop key="file.resource.loader.class">
        org.apache.velocity.runtime.resource.loader.FileResourceLoader
      </prop>
      <prop key="file.resource.loader.path">${webapp.root}/WEB-INF/velocity</prop>
      <prop key="file.resource.loader.cache">false</prop>
    </props>
  </property>
</bean>
				

关于Spring中Velocity的配置请参考 API文档,或者参考Velocity自身文档中的例子和定义来了解如何配置 'velocity.properties'

14.4.4.2. FreeMarker

FreeMarker的'Settings'和'SharedVariables'配置可以通过直接设置 FreeMarkerConfigurer 的相应属性来传递给Spring管理的FreeMarker Configuration 对象,其中 freemarkerSettings 属性需要一个 java.util.Properties 类型对象,freemarkerVariables 需要一个 java.util.Map 类型对象。

<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
  <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
  <property name="freemarkerVariables">
    <map>
      <entry key="xml_escape" value-ref="fmXmlEscape"/>
    </map>
  </property>
</bean>

<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>

关于settings和variables如何影响 Configuration 对象的细节信息,请参考FreeMarker的文档。

14.4.5. 绑定支持和表单处理

Spring提供了一个在JSP中使用的标签库,其中包含一个 <spring:bind/> 标签,它主要用来在表单中显示支持对象(译者注:即表单数据传输对象)的数据,并在一个 Validator(工作在Web层或业务逻辑层)校验失败时显示失败信息。从1.1版本开始,Spring为Velocity和FreeMarker也提供了同样的功能,而且还另外提供了便于使用的宏,用来生成表单输入元素。

14.4.5.1. 用于绑定的宏

spring.jar 文件为这两种语言维护了一套标准宏,对于正确配置的应用,它们总是可用的,前提是你将VelocityView bean 或者 FreeMarkerView bean 的 exposeSpringMacroHelpers 属性设为'true'。其实还有更方便的方法,如果你恰好在使用 VelocityViewResolverFreeMarkerViewResolver,你也可以设置它们的这个属性,这样你的视图都会继承这个值。注意,对任何HTML表单处理方面的问题来说,这个属性是 不必要 的,除非 你确定需要Spring宏提供的好处。下面是一份view.properties文件的例子,其中展示了对这两种语言都适用的正确配置。

personFormV.class=org.springframework.web.servlet.view.velocity.VelocityView
personFormV.url=personForm.vm
personFormV.exposeSpringMacroHelpers=true
personFormF.class=org.springframework.web.servlet.view.freemarker.FreeMarkerView
personFormF.url=personForm.ftl
personFormF.exposeSpringMacroHelpers=true

下面是一个完整的Spring Web MVC 配置文件。通过这个配置,每个Velocity视图都可以调用标准的 Velocity宏命令。

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

    <bean name="helloController" class="info.wilhelms.springwebapp.SampleController">
        <property name="commandName" value="command"/>
        <property name="commandClass" value="info.wilhelms.springwebapp.Message"/>
        <property name="formView" value="foo"/>
        <property name="successView" value="banjo"/>
        <property name="bindOnNewForm" value="true"/>
        <property name="sessionForm" value="true"/>
    </bean>

    <bean id="velocityConfig"
          class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath" value="/WEB-INF/velocity/"/>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="cache" value="false"/>
        <property name="prefix" value=""/>
        <property name="suffix" value=".vm"/>
        <property name="exposeSpringMacroHelpers" value="true"/>
    </bean>

    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <value>
                **/hello.htm=helloController
            </value>
        </property>
    </bean>

</beans>

Spring 库中定义的一些宏被认为是内部的(私有的),但宏定义中没有这种限制范围的方式,这使得对调用代码和用户模板来说,所有的宏都是可见的。下面的内容集中于供用户模板直接调用的宏。如果你希望看看宏定义的代码,可以分别参考 org.springframework.web.servlet.view.velocity 包中的spring.vm文件和 org.springframework.web.servlet.view.freemarker 包中的spring.ftl文件。

14.4.5.2. 简单绑定

在扮演Spring表单控制器对应视图的html表单(或vm/ftl模板)里,你可以模仿下面的代码来绑定表单数据并显示错误信息(和JSP的形式非常相似)。注意默认情况下命令对象的名字是"command",你可以在配置自己的表单控制器时通过设置'commandName'属性来覆盖默认值。例子代码如下(其中的 personFormVpersonFormF 是前面定义的视图):

<!-- velocity宏自动可用 -->
<html>
...
<form action="" method="POST">
  Name: 
  #springBind( "command.name" )
  <input type="text" 
    name="${status.expression}" 
    value="$!status.value" /><br>
  #foreach($error in $status.errorMessages) <b>$error</b> <br> #end
  <br>
  ... 
  <input type="submit" value="submit"/>
</form>
...
</html>
<!-- FreeMarker宏必须导入到一个名称空间,这里推荐你定义为'spring'空间 -->
<#import "spring.ftl" as spring />
<html>
...
<form action="" method="POST">
  Name: 
  <@spring.bind "command.name" /> 
  <input type="text" 
    name="${spring.status.expression}" 
    value="${spring.status.value?default("")}" /><br>
  <#list spring.status.errorMessages as error> <b>${error}</b> <br> </#list>
  <br>
  ... 
  <input type="submit" value="submit"/>
</form>
...
</html>

#springBind / <@spring.bind> 需要一个'path'属性,格式为命令对象的名字(默认值为'command',除非你在配置FormController的属性时改变它)后跟圆点再加上你希望绑定到的命令对象的属性名。你也可以使用类似"command.address.street"的格式来处理嵌套对象。使用 bind 宏时,HTML转码行为由web.xml中名为 defaultHtmlEscape 的ServletContext参数指定。

上述宏的另一种可选形式是 #springBindEscaped / <@spring.bindEscaped>,它另外接受一个布尔型参数,显式指定了输出值或错误信息这些状态信息时是否使用HTML转码。附加的表单处理宏简化了HTML转码的使用,只要有可能,你就应该使用它们。关于它们的细节将在下节讲述。

14.4.5.3. 表单输入生成宏

为这两种语言附加的一些很方便的宏同时简化了表单绑定和表单生成(包括显示校验错误信息)。不需要使用这些宏来生成表单输入域,它们可以被混杂并匹配到简单HTML,或者直接调用前面讲过的spring绑定宏。

下表展示了可用的宏的VTL定义和FTL定义,以及它们需要的参数。

Table 14.1. 宏定义表

VTL定义FTL定义
message(输出一个根据code参数选择的资源绑定字符串)#springMessage($code)<@spring.message code/>
messageText(输出一个根据code参数选择的资源绑定字符串,找不到的话输出default参数的值)#springMessageText($code $text)<@spring.messageText code, text/>
url(在URL相对路径前面添加应用上下文根路径application context root)#springUrl($relativeUrl)<@spring.url relativeUrl/>
formInput(标准表单输入域)#springFormInput($path $attributes)<@spring.formInput path, attributes, fieldType/>
formHiddenInput *(表单隐藏输入域)#springFormHiddenInput($path $attributes)<@spring.formHiddenInput path, attributes/>
formPasswordInput *(标准表单密码输入域;注意不会为这种类型的输入域装配数据)#springFormPasswordInput($path $attributes)<@spring.formPasswordInput path, attributes/>
formTextarea(大型文本(自由格式)输入域)#springFormTextarea($path $attributes)<@spring.formTextarea path, attributes/>
formSingleSelect(单选列表框)#springFormSingleSelect( $path $options $attributes)<@spring.formSingleSelect path, options, attributes/>
formMultiSelect(多选列表框)#springFormMultiSelect($path $options $attributes)<@spring.formMultiSelect path, options, attributes/>
formRadioButtons(单选框)#springFormRadioButtons($path $options $separator $attributes)<@spring.formRadioButtons path, options separator, attributes/>
formCheckboxes(复选框)#springFormCheckboxes($path $options $separator $attributes)<@spring.formCheckboxes path, options, separator, attributes/>
showErrors(简化针对所绑定输入域的校验错误信息输出)#springShowErrors($separator $classOrStyle)<@spring.showErrors separator, classOrStyle/>

* 在FTL(FreeMarker)中,这二种宏实际上并不是必需的,因为你可以使用普通的 formInput 宏,指定fieldType参数的值为 'hidden' 或 'password'即可 。

上面列出的所有宏的参数都具有一致的含义,如下述:

  • path:待绑定属性的名字(如:command.name)

  • 选项:一个Map,其中保存了所有可从输入域中选择的值。map中的键值(keys)代表将从表单绑定到命令对象然后提交到后台的实值(values)。存储在Map中的与相应键值对应的对象就是那些在表单上显示给用户的标签,它们可能与提交到后台的值不同。通常这样的map由控制器以引用数据的方式提供。你可以根据需求的行为选择一种Map实现。比如对顺序要求严格时,可使用一个 SortedMap,如一个 TreeMap 加上适当的Comparator;对要求按插入顺序返回的情况,可以使用commons-collections提供的 LinkedHashMapLinkedMap

  • 分隔符:当使用多选的时候(radio buttons 或者 checkboxes),用于在列表中分隔彼此的字符序列(如 "<br>")。

  • 属性:一个附加的以任意标签或文本构成的字符串,出现在HTML标签内。该字符串被宏照原样输出。例如:在一个textarea标签内你可能会提供'rows="5" cols="60"'这样的属性,或者你会传递'style="border:1px solid silver"'这样的样式信息。

  • classOrStyle:供showErrors宏用来以这种样式显示错误信息,其中错误信息嵌套于使用该CSS类名的span标签内。如果不提供或内容为空,则错误信息嵌套于<b></b>标签内。

宏的例子在下面描述,其中一些是FTL的,一些是VTL的。两种语言之间的用法差别在旁注中解释。

14.4.5.3.1. 输入域

<!-- 上面提到的Name域的例子,使用VTL中定义的表单宏 -->
...
    Name:
    #springFormInput("command.name" "")<br>
    #springShowErrors("<br>" "")<br>

formInput宏接受一个path参数(command.name)和一个附加的属性参数(在上例中为空)。该宏与所有其他表单生成宏一样,对path参数代表的属性实施一种隐式绑定,这种绑定保持有效状态直到一次新的绑定开始,所以showErrors宏不再需要传递path参数——它简单地操作最近一次绑定的属性(field)。

showErrors宏接受两个参数:分隔符(用于分隔多条错误信息的字符串)和CSS类名或样式属性。注意在FreeMarker中可以为属性参数指定默认值(这点儿Velocity做不到)。上面的两个宏调用在FTL中可以这么表达:

<@spring.formInput "command.name"/>
<@spring.showErrors "<br>"/>

上面展示的用于生成name表单输入域的代码片断产生的输出如下,同时还显示了输入值为空的情况下提交表单后产生的校验错误信息(校验过程由Spring的验证框架提供)。

生成的HTML如下:

Name:
  <input type="text" name="name" value=""     
>
<br>
  <b>required</b>
<br>
<br>

参数(属性)用来向textarea传递样式信息或行列数属性。

14.4.5.3.2. 选择输入域

有四种用于在HTML表单中生成通用选择输入框的宏。

  • formSingleSelect

  • formMultiSelect

  • formRadioButtons

  • formCheckboxes

每个宏都将接受一个由选项值和选项标签的集合构成的Map,其中选项值和其标签可以相同。

下面展示了一个在FTL中使用radio按钮的例子。表单支撑对象(form backing object)提供了一个默认值'London',所以该域不需要校验。当渲染表单时,整个待展现的城市列表由模型对象的'cityMap'属性以引用数据的方式提供。

...
  Town:
  <@spring.formRadioButtons "command.address.town", cityMap, "" /><br><br>

这将产生一行radio按钮——cityMap中一个值对应一个按钮,并以""分隔。没有额外的属性,因为宏的最后一个参数不存在。cityMap中所有的key-value都使用String类型值。map中的key用作输入域的值(将被作为请求参数值提交到后台),value用作显示给用户的标签。上述示例中,表单支撑对象提供了一个默认值以及三个著名城市作为可选值,它产生的HTML代码如下:

Town:
<input type="radio" name="address.town" value="London">
London
<input type="radio" name="address.town" value="Paris" checked="checked">
Paris
<input type="radio" name="address.town" value="New York">
New York

如果你希望在应用中按照内部代码来处理城市,你得以适当的键值创建map,如下:

protected Map referenceData(HttpServletRequest request) throws Exception {
  Map cityMap = new LinkedHashMap();
  cityMap.put("LDN", "London");
  cityMap.put("PRS", "Paris");
  cityMap.put("NYC", "New York");
  
  Map m = new HashMap();
  m.put("cityMap", cityMap);
  return m;
}

现在上述代码将产生出以相关代码为值的radio按钮,同时你的用户仍能看到对他们显示友好的城市名。

Town:
<input type="radio" name="address.town" value="LDN">
London
<input type="radio" name="address.town" value="PRS" checked="checked">
Paris
<input type="radio" name="address.town" value="NYC">
New York

14.4.5.4. 重载HTML转码行为并使你的标签符合XHTML

缺省情况下使用上面这些宏将产生符合HTML 4.01标准的标签,并且Spring的绑定支持使用web.xml中定义的HTML转码行为。为了产生符合XHTML标准的标签以及覆盖默认的HTML转码行为,你可以在你的模板(或者模板可见的模型对象)中指定两个变量。在模板中指定的好处是稍后的模板处理中可以为表单中不同的域指定不同的行为。

要切换到符合XHTML的输出,你可以设置model/context变量xhtmlCompliant的值为true:

## for Velocity..
#set($springXhtmlCompliant = true)

<#-- for FreeMarker -->
<#assign xhtmlCompliant = true in spring>

在进行完这些处理之后,由Spring宏产生的所有标签都符合XHTML标准了。

类似地,可以为每个输入域指定HTML转码行为:

<#-- 该句覆盖默认HTML转码行为 -->

<#assign htmlEscape = true in spring>
<#-- next field will use HTML escaping -->
<@spring.formInput "command.name" />

<#assign htmlEscape = false in spring>
<#-- all future fields will be bound with HTML escaping off -->

14.5. XSLT

XSLT是一种用于XML的转换语言,并作为一种在web应用中使用的view层技术广为人知。 如果你的应用本来就要处理XML,或者模型数据可以很容易转化为XML,那么XSLT是一个很好的选择。 下面的内容展示了在一个Spring MVC 应用中如何生成XML格式的模型数据,并用XSLT进行转换。

14.5.1. 写在段首

这是一个很小的Spring应用的例子,它只是在Controller中创建一个词语列表,并将它们加至模型数据(model map)。 模型数据和我们的XSLT视图名一块儿返回。 请参考 Section 13.3, “控制器” 中关于Spring MVC Controller接口的细节。 XSLT视图把词语列表转化为一段简单XML,等待后续转换。

14.5.1.1. Bean 定义

这是一个简单的Spring应用的标准配置。dispatcher servlet配置文件包含一个指向 ViewResolver的引用、URL映射和一个简单的实现了我们的词语生成逻辑的controller bean:

<bean id="homeController"class="xslt.HomeController"/>

它实现了我们的词语生成“逻辑”。

14.5.1.2. 标准MVC控制器代码

控制器逻辑封装在一个AbstractController的子类,它的handler方法定义如下:

protected ModelAndView handleRequestInternal(
    HttpServletRequest request,
    HttpServletResponse response) throws Exception {
        
    Map map = new HashMap();
    List wordList = new ArrayList();
        
    wordList.add("hello");
    wordList.add("world");
       
    map.put("wordList", wordList);
      
    return new ModelAndView("home", map);
}

到目前为止,我们还没有做什么特定于XSLT的事情。在任何一种Spring MVC应用中,模型数据都以同样的方式被创建。 现在根据应用的配置,词语列表可以作为请求属性加入从而被JSP/JSTL渲染,或者通过加入VelocityContext来被Velocity处理。 为了使用XSLT渲染它们,应该以某种方式把它们转化为XML文档。有些软件包能自动完成对象图到XML文档对象模型的转化。 但在Spring中,你有完全的自由度,能以任何方式完成从模型数据到XML的转化。 这可以防止XML转化部分在你的模型结构中占据太大的比重,使用额外工具来管理转化过程是一种风险。

14.5.1.3. 把模型数据转化为XML

为了从词语列表或任何其他模型数据创建XML文档,我们必须创建一个 org.springframework.web.servlet.view.xslt.AbstractXsltView 的子类, 通常我们也必须实现抽象方法 createXsltSource(..)s。其第一个参数即model Map。 下面是我们这个小应用中HomePage类的完整代码:

package xslt;

// imports omitted for brevity

public class HomePage extends AbstractXsltView {

    protected Source createXsltSource(Map model, String rootName, HttpServletRequest
        request, HttpServletResponse response) throws Exception {

        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        Element root = document.createElement(rootName);

        List words = (List) model.get("wordList");
        for (Iterator it = words.iterator(); it.hasNext();) {
            String nextWord = (String) it.next();
            Element wordNode = document.createElement("word");
            Text textNode = document.createTextNode(nextWord);
            wordNode.appendChild(textNode);
            root.appendChild(wordNode);
        }
        return new DOMSource(root);
    }

}

你可以在上述子类中定义一些传给转化对象的参数,它们由健值对(name/value pairs)构成, 其中参数名必须与XSLT模板中定义的 <xsl:param name="myParam">defaultValue</xsl:param> 一致。 为了指定这些参数,你需要覆写继承自AbstractXsltViewgetParameters() 方法并返回一个包含健值对的Map。 如果你需要从当前请求中获取信息,你可以选择覆写 getParameters(HttpServletRequest request) 方法。(这个方法只有Spring 1.1以后的版本才支持。)

比起JSTL和Velocity,XSLT对本地货币和日期格式的支持相对较弱。 基于这点,Spring提供了一个辅助类,你可以在 createXsltSource(..) 方法中调用它来获得这样的支持。 请参考 org.springframework.web.servlet.view.xslt.FormatHelper 类的Javadoc。

14.5.1.4. 定义视图属性

对于“写在段首”中的只有一个视图的情况来说,views.properties文件(或者等价的xml文件,如果你用一种基于XML的视图解析器的话,就像在上面的Velocity例子中)看起来是这样的:

home.class=xslt.HomePage
home.stylesheetLocation=/WEB-INF/xsl/home.xslt
home.root=words

这里你可以看到,第一个属性'.class'指定了视图类,即我们的HomePage,其中完成从模型数据到XML文档的转化。 第二个属性'stylesheetLocation'指定了XSLT文件的位置,它用于完成从XML到HTML的转化。 最后一个属性'.root'指定了用作XML文档根元素的名字,它被作为 createXsltSource(..) 方法的第二个参数传给HomePage类。

14.5.1.5. 文档转换

最后,我们有一段转换上述文档的XSLT代码。 正如在'views.properties'中看到的,它被命名为 'home.xslt',存放在war文件中的 'WEB-INF/xsl'目录下。

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output method="html" omit-xml-declaration="yes"/>

    <xsl:template match="/">
        <html>
            <head><title>Hello!</title></head>
            <body>
                <h1>My First Words</h1>
                <xsl:apply-templates/>
            </body>
        </html>
    </xsl:template>

    <xsl:template match="word">
        <xsl:value-of select="."/><br/>
    </xsl:template>

</xsl:stylesheet>

14.5.2. 小结

下面是一个简化的WAR目录结构,其中总结了上面提到的文件和它们在WAR中的位置:

ProjectRoot
  |
  +- WebContent
      |
      +- WEB-INF
          |
          +- classes
          |    |
          |    +- xslt
          |    |   |
          |    |   +- HomePageController.class 
          |    |   +- HomePage.class
          |    |
          |    +- views.properties
          |
          +- lib
          |   |
          |   +- spring.jar
          |
          +- xsl
          |   |
          |   +- home.xslt
          |
          +- frontcontroller-servlet.xml

你要确保classpath下存在XML解析器和XSLT引擎。JDK1.4默认已提供了这些,多数J2EE容器也提供了,但还是要警惕,它可能是一些错误的根源。

14.6. 文档视图(PDF/Excel)

14.6.1. 简介

对看模型数据输出的用户来说,返回一个HTML页面并不总是最好的方法。 Spring简化了根据模型数据动态输出PDF文档或Excel电子表格的工作。 这些文档即最终视图,它们将以适当的内容类型用流的形式从服务器输出,并在客户端PC相应地启动PDF或电子表格浏览器(希望如此)。

为了使用Excel视图,你需要把'poi'库加到classpath中;使用PDF的话需要iText.jar。 它们都已经包含在Spring的主发行包里。

14.6.2. 配置和安装

基于文档的视图几乎与XSLT视图的处理方式相同。 下面的内容将在前文基础上介绍,XSLT例子中的controller如何被用来渲染同一个的模型数据,分别产生PDF或Excel输出(输出文档可以用Open Office浏览和编辑)。

14.6.2.1. 文档视图定义

首先,我们修改view.properties(或等价的xml文件),增加两种文档类型的视图定义。 整个文件现在看起来是这个样子:

home.class=xslt.HomePage
home.stylesheetLocation=/WEB-INF/xsl/home.xslt
home.root=words

xl.class=excel.HomePage

pdf.class=pdf.HomePage

如果你想在一个电子表格模板基础上添加模型数据,可以在视图定义中为'url'属性指定一个文件位置。

14.6.2.2. Controller 代码

这里用的controller代码,除了视图名以外,其他的与XSLT例子中的完全一样。 当然,你可能有更聪明的做法,通过URL参数或其他方式选择视图名,这也证明了Spirng在控制器与视图的解耦方面确实非常优秀!

14.6.2.3. Excel视图子类

和在XSLT例子中一样,我们需要从适当的抽象类扩展一个具体类,以实现输出文档的行为。 对Excel来说,这意味着创建一个 org.springframework.web.servlet.view.document.AbstractExcelView(使用POI)或 org.springframework.web.servlet.view.document.AbstractJExcelView(使用JExcelApi)的子类, 并实现buildExcelDocument方法。

下面是一段使用POI生成Excel视图的完整代码,它从模型数据中取得词语列表,把它显示为电子表格中第一栏内连续的行:

package excel;

// imports omitted for brevity

public class HomePage extends AbstractExcelView {

    protected void buildExcelDocument(
        Map model,
        HSSFWorkbook wb,
        HttpServletRequest req,
        HttpServletResponse resp)
        throws Exception {
    
        HSSFSheet sheet;
        HSSFRow sheetRow;
        HSSFCell cell;

        // Go to the first sheet
        // getSheetAt: only if wb is created from an existing document
        //sheet = wb.getSheetAt( 0 );
        sheet = wb.createSheet("Spring");
        sheet.setDefaultColumnWidth((short)12);

        // write a text at A1
        cell = getCell( sheet, 0, 0 );
        setText(cell,"Spring-Excel test");

        List words = (List ) model.get("wordList");
        for (int i=0; i < words.size(); i++) {
            cell = getCell( sheet, 2+i, 0 );
            setText(cell, (String) words.get(i));

        }
    }
}

这是一个使用JExcelApi的版本,生成同样的Excel文件:

package excel;
				
// imports omitted for brevity

public class HomePage extends AbstractExcelView {

    protected void buildExcelDocument(Map model,
        WritableWorkbook wb,
        HttpServletRequest request,
        HttpServletResponse response)
    throws Exception {
			
        WritableSheet sheet = wb.createSheet("Spring");

        sheet.addCell(new Label(0, 0, "Spring-Excel test");
		
        List words  = (List)model.get("wordList");
        for (int i = -; i < words.size(); i++) {
            sheet.addCell(new Label(2+i, 0, (String)words.get(i));
        }
    }
}

注意这些API间的差别。我们发现JExcelApi使用起来更直观,而且在图像处理方面更好。 但也发现使用JExcelApi处理大文件时有些内存问题。

如果你现在修改controller的代码,让它返回名为 xl 的视图(return new ModelAndView("xl", map);), 然后再次运行你的应用,你会发现,当你请求同样的页面时,Excel电子表格被创建出来并自动开始下载。

14.6.2.4. PDF视图子类

生成PDF版本的词语列表就更简单了。 现在,你创建一个 org.springframework.web.servlet.view.document.AbstractPdfView 的子类,并实现buildPdfDocument()方法,如下:

package pdf;

// imports omitted for brevity

public class PDFPage extends AbstractPdfView {

    protected void buildPdfDocument(
        Map model,
        Document doc,
        PdfWriter writer,
        HttpServletRequest req,
        HttpServletResponse resp)
        throws Exception {
        
        List words = (List) model.get("wordList");
        
        for (int i=0; i<words.size(); i++)
            doc.add( new Paragraph((String) words.get(i)));
    
    }
}

同样地,修改controller,让它返回名为 pdf 的视图(return new ModelAndView("pdf", map);), 运行你的应用并请求同样的URL,这次将会打开一个PDF文档,列出模型数据中的每个词语。

14.7. JasperReports

JasperReports (http://jasperreports.sourceforge.net) 是一个功能强大,开源的报表引擎, 支持使用一种易于理解的XML文档创建报表设计,并可以输出4种格式的报表:CSV、Excel、HTML和PDF。

14.7.1. 依赖的资源

应用程序需要包含最新版本的JasperReports(写本文档的时候是 0.6.1)。 JasperReports自身依赖于下面的项目:

  • BeanShell

  • Commons BeanUtils

  • Commons Collections

  • Commons Digester

  • Commons Logging

  • iText

  • POI

JasperReports还需要一个JAXP解析器。

14.7.2. 配置

要在 ApplicationContext 中配置JasperReports,你必须定义一个 ViewResolver 来把视图名映射到适当的视图类,这取决于你希望输出什么格式的报表。

14.7.2.1. 配置ViewResolver

通常,你会使用 ResourceBundleViewResolver 来根据一个属性文件把视图名映射到视图类和相关文件:

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

这里我们已经定义了一个 ResourceBundleViewResolver 的实例, 它将通过基名(base name)views 在资源文件中查找视图映射。 这个文件的详细内容将在下节内容叙述。

14.7.2.2. 配置View

Spring中包含了JasperReports的五种视图实现,其中四种对应到JasperReports支持的四种输出格式,另一种支持在运行时确定输出格式。

Table 14.2. JasperReports View Classes

类名渲染格式
JasperReportsCsvViewCSV
JasperReportsHtmlViewHTML
JasperReportsPdfViewPDF
JasperReportsXlsViewMicrosoft Excel
JasperReportsMultiFormatView运行时确定格式(参考 Section 14.7.2.4, “使用 JasperReportsMultiFormatView ”

把这些类映射到视图名和报表文件只需要简单地在前述资源文件中添加适当的条目。如下:

simpleReport.class=org.springframework.web.servlet.view.jasperreports.JasperReportsPdfView
simpleReport.url=/WEB-INF/reports/DataSourceReport.jasper
              

这里你可以看到名为simpleReport的视图被映射到 JasperReportsPdfView类。 这将产生PDF格式的报表输出。该视图的url属性被设置为底层报表文件的位置。

14.7.2.3. 关于报表文件

JasperReports有两种不同的报表文件:一种是设计文件,以 .jrxml 为扩展名;另一种是编译后的格式,以 .jasper 为扩展名。 通常,你使用JasperReports自带的Ant任务来把你的 .jrxml 文件编译为 .jasper文件,然后部署到应用中。 在Spring里你可以把任一种设计文件映射到报表文件,Spring能帮你自动编译 .jrxml 文件。 但你要注意,.jrxml 被编译后即缓存起来并在整个应用活动期间有效,如果你要做一些改动,就得重启应用。

14.7.2.4. 使用 JasperReportsMultiFormatView

JasperReportsMultiFormatView 允许在运行时指定报表格式, 真正解析报表委托给其它JasperReports的view类 - JasperReportsMultiFormatView 类简单地增加了一层包装,允许在运行时准确地指定实现。

JasperReportsMultiFormatView类引入了两个概念:format key和discriminator key。 JasperReportsMultiFormatView使用mapping key来查找实际实现类,而使用format key来查找mapping key。 从编程角度来说,你在model中添加一个条目,以format key作键并以mapping key作值,例如:

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

  String uri = request.getRequestURI();
  String format = uri.substring(uri.lastIndexOf(".") + 1);

  Map model = getModel();
  model.put("format", format);

  return new ModelAndView("simpleReportMulti", model);
}

在这个例子中,mapping key由 request URI的扩展名来决定,并以默认的format key值format 加入了model。 如果希望使用不同的format key,你可以使用 JasperReportsMultiFormatView 类的 formatKey 属性来配置它。

JasperReportsMultiFormatView 中默认已经配置了下列mapping key:

Table 14.3. JasperReportsMultiFormatView默认Mapping Key映射

Mapping KeyView Class
csvJasperReportsCsvView
htmlJasperReportsHtmlView
pdfJasperReportsPdfView
xlsJasperReportsXlsView

所以上例中一个对/foo/myReport.pdf的请求将被映射至 JasperReportsPdfView。 你可以使用 JasperReportsMultiFormatViewformatMappings 属性覆盖mapping key到视图类的映射配置。

14.7.3. 构造ModelAndView

为了以你选择的格式正确地渲染报表,你必须为Spring提供所有需要的报表数据。 对JasperReports来说,这就是报表数据源(report datasource)和参数(report parameters)。 报表参数就是一些可以加到model的 Map 中的简单键值对。

当添加数据源到model中时,有两种选择。 第一种是以任意值为key,添加一个 JRDataSourceCollection 到 model Map。 Spring将从model中找到它并用作报表数据源。例如,你可能这样构造model:

private Map getModel() {
  Map model = new HashMap();
  Collection beanData = getBeanData();
  model.put("myBeanData", beanData);
  return model;
}

第二种方式是以一个特定键值添加 JRDataSourceCollection 的实例, 并把该它赋给视图类的 reportDataKey 属性。 不管哪种方式,Spring都会把 Collection 实例转化为 JRBeanCollectionDataSource实例。例如:

private Map getModel() {
  Map model = new HashMap();
  Collection beanData = getBeanData();
  Collection someData = getSomeData();
  model.put("myBeanData", beanData);
  model.put("someData", someData);
  return model;
}

这里你可以看到有两个Collection实例被加到model里。 为了确保使用正确的那个,我们得适当地改动一下视图的配置:

simpleReport.class=org.springframework.web.servlet.view.jasperreports.JasperReportsPdfView
simpleReport.url=/WEB-INF/reports/DataSourceReport.jasper
simpleReport.reportDataKey=myBeanData
              

注意当使用第一种方式时,Spring将使用它遇到的第一个 JRDataSourceCollection。 如果你要放置多个这样的实例到model中,你就得使用第二种方式。

14.7.4. 使用子报表

JasperReports提供了对嵌入在主报表文件中的子报表的支持。有多种机制支持在报表文件中包含子报表。 最简单的方法是在设计文件中直接写入子报表的路径和SQL查询。 这种方法的缺点很明显:相关信息被硬编码进报表文件,降低了可复用性,并使报表设计难以修改。 为了克服这些,你可以声明式地配置子报表,并为其包含更多直接来自controller的数据。

14.7.4.1. 配置子报表文件

使用Spring时,为了控制哪个子报表文件被包含,你的报表文件必须被配置为能够从外部来源接受子报表。 要完成这些你得在报表文件中声明一个参数,像这样:

<parameter name="ProductsSubReport" class="net.sf.jasperreports.engine.JasperReport"/>

然后,你用这个参数定义一个子报表:

<subreport>
    <reportElement isPrintRepeatedValues="false" x="5" y="25" width="325"
        height="20" isRemoveLineWhenBlank="true" backcolor="#ffcc99"/>
    <subreportParameter name="City">
        <subreportParameterExpression><![CDATA[$F{city}]]></subreportParameterExpression>
    </subreportParameter>
    <dataSourceExpression><![CDATA[$P{SubReportData}]]></dataSourceExpression>
    <subreportExpression class="net.sf.jasperreports.engine.JasperReport">
                  <![CDATA[$P{ProductsSubReport}]]></subreportExpression>
</subreport>

这样就定义了一个主报表文件,接受一个名为 ProductsSubReport 的参数, 它的值是一个 net.sf.jasperreports.engine.JasperReports 类型实例。 然后配置Jasper视图类时,你通过使用 subReportUrls 属性来告诉Spring载入一个报表文件并作为子报表传递给JasperReports引擎。

<property name="subReportUrls">
    <map>
        <entry key="ProductsSubReport" value="/WEB-INF/reports/subReportChild.jrxml"/>
    </map>
</property>

这里 Map 中的key对应于报表设计文件中子报表参数的名字,它的值代表子报表文件的URL。 Spring将载入该文件,需要的话进行编译,然后以给定的key为参数名传递给JasperReports引擎。

14.7.4.2. 配置子报表数据源

当使用Spring配置子报表时,这一步完全是可选的。如果你喜欢,仍可以使用静态查询作为子报表的数据源。 然而,如果你希望Spring把你返回的 ModelAndView 中的数据转化为 JRDataSource, 你就得告诉Spring ModelAndView 中的那个参数需要被转化。 实际操作时,你需要使用所选视图类的 subReportDataKeys 属性,为其配置一个参数名列表:

<property name="subReportDataKeys"
    value="SubReportData"/>

这里提供的key值必须与ModelAndView和报表设计文件中使用的key值对应。

14.7.5. 配置Exporter的参数

如果你对exporter的配置有特殊要求,比如你可能要求特定页面尺寸的PDF报表, 那你可以在Spring配置文件中声明式地配置这些,通过使用视图类的 exporterParameters 属性, 该属性是 Map 型值,其中的key应该是一个代表exporter参数定义的静态域的全限定名,value是要赋给参数的值。 示例如下:

<bean id="htmlReport" class="org.springframework.web.servlet.view.jasperreports.JasperReportsHtmlView">
  <property name="url" value="/WEB-INF/reports/simpleReport.jrxml"/>
  <property name="exporterParameters">
    <map>
      <entry key="net.sf.jasperreports.engine.export.JRHtmlExporterParameter.HTML_FOOTER">
        <value>Footer by Spring!
          &lt;/td&gt;&lt;td width="50%"&gt;&amp;nbsp; &lt;/td&gt;&lt;/tr&gt;
          &lt;/table&gt;&lt;/body&gt;&lt;/html&gt;
        </value>
      </entry>
    </map>
  </property>
</bean>

这里你可以看到,我们为 JasperReportsHtmlView 配置了一个exporter参数, 参数 net.sf.jasperreports.engine.export.JRHtmlExporterParameter.HTML_FOOTER 定义了结果HTML中的页脚。