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>

注意

对于非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定义,以及它们需要的参数。

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