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层技术。下面简要讲述了如何使用。

注意: 这部分重点在于Spring中的 org.springframework.web.servlet.view.tiles2 包对Tiles 2(Tiles独立版本,要求Java 5+)的支持。在最初的 org.springframework.web.servlet.view.tiles 包中Spring同样继续支持Tiles 1.x(也可以叫“Struts Tiles”,作为Struts 1.1+的成员;兼容 Java 1.4)。

14.3.1. 需要的资源

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

  • Tiles version 2.0.4以及更高版本

  • Commons BeanUtils

  • Commons Digester

  • Commons Logging

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

14.3.2. 如何集成Tiles

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

<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
  <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层技术一样。它有二种选择: UrlBasedViewResolverResourceBundleViewResolver

14.3.2.1.  UrlBasedViewResolver

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

<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.tiles2.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.tiles2.TilesView
welcomeView.url=welcome 
						(this is the name of a Tiles definition)

					

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

					

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

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

注意,Tiles 2的TilesView 类支持 JSTL(JSP标准标签库)的开箱即用,同时,为支持Tiles 1.x,提供一个单独的TilesJstlView子类。

14.3.2.3.  SimpleSpringPreparerFactorySpringBeanPreparerFactory

作为一个高级的特性,Spring支持两个特定的Tiles 2PreparerFactory实现。如何Tiles定义文件中使用 ViewPreparer引用,详细资料查看Tiles文档。

基于指定的preparer类,指定SimpleSpringPreparerFactory 自动装配 ViewPreparer 实体,不但应用 Spring的容器回调而且应用配置Spring BeanPostProcessors。假如Spring的上下文范围 annotation-config 已经被激活,ViewPreparer 类中的注解将被自动检测到及应用。注意,在 Tiles 定义文件中预计的preparer classes,就像默认PreparerFactory所作的一样。

指定SpringBeanPreparerFactory来操作指定preparer名称,而不是类,从DispatcherServlet的应用上下文 获取相应的Spring bean。在这种情况下,完整的 bean 创建过程将在Spring应用上下文控制中,允许明确使用依赖注入,范围bean等等。 注意,需要定义为每一个preparer名称定义一个Spring bean(用于Tiles定义中)。

<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
  <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>

  
						<!-- resolving preparer names as Spring bean
						definition names -->

					
  <property name="preparerFactoryClass"
       value="org.springframework.web.servlet.view.tiles2.SpringBeanPreparerFactory"/>

</bean>

14.4. Velocity和FreeMarker

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

14.4.1. 需要的资源

使用Velocity或FreeMarker需要包含 velocity-1.x.x.jar freemarker-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"/>
</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"/>
</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文件为这两种语言维护了一套标准宏,对于正确配置的应用,它们总是可用的。

在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. 宏定义表

macro VTL definition FTL definition
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 MVCController 接口的细节。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

类名渲染格式
JasperReportsCsvView CSV
JasperReportsHtmlView HTML
JasperReportsPdfView PDF
JasperReportsXlsView Microsoft 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
csv JasperReportsCsvView
html JasperReportsHtmlView
pdf JasperReportsPdfView
xls JasperReportsXlsView

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

14.7.3.  构造ModelAndView

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

当添加数据源到model中时,有两种选择。第一种是以任意值为key,添加一个 JRDataSourceCollection 到 modelMap 。 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中的页脚。