在Servlet中使用FreeMarker

作为基础了解,在web应用程序范畴内使用 FreeMarker 和其它并没有什么不同; FreeMarker将它的输出写入传递给 Template.process 方法的 Writer 对象,它不关心 Writer 将输出写入控制台,文件或是 HttpServletResponse 的输出流。 FreeMarker 并不知道什么是servlet和web;它仅仅是使用模板文件来合并Java对象, 之后从它们中间生成输出文本。从这里可知,如何创建一个Web应用程序都随你的习惯来。

但是,你可能想在已经存在的Web应用框架中使用FreeMarker。 许多框架都是基于"Model 2"架构的,JSP页面来控制显示。 如果你使用了这样的框架(比如Apache Struts), 那么可以继续阅读本文。对于其他框架请参考它们的文档。

在"Model 2"中使用FreeMarker

许多框架依照HTTP请求转发给用户自定义的"action"类, 将数据作为属性放在 ServletContextHttpSessionHttpServletRequest 对象中, 之后请求被框架派发到一个JSP页面中(视图层),使用属性传递过来的数据来生成HTML页面, 这样的策略通常就是所指的Model 2模型。

Figure

使用这样的框架,你就可以非常容易地用FTL文件来代替JSP文件。 但是,因为你的Servlet容器(Web应用程序服务器),不像JSP文件, 它可能并不知道如何处理FTL文件,那么就需要对Web应用程序进行一些额外的配置:

  1. 复制 freemarker.jar (从FreeMarker发布包的lib目录中) 到Web应用程序的 WEB-INF/lib 目录下。

  2. 将下面的部分添加到Web应用程序的 WEB-INF/web.xml 文件中 (调整部分内容是否需要):

<servlet>
  <servlet-name>freemarker</servlet-name>
  <servlet-class>freemarker.ext.servlet.FreemarkerServlet</servlet-class>
    
  <!-- FreemarkerServlet settings: -->
  <init-param>
    <param-name>TemplatePath</param-name>
    <param-value>/</param-value>
  </init-param>
  <init-param>
    <param-name>NoCache</param-name>
    <param-value>true</param-value>
  </init-param>
  <init-param>
    <param-name>ContentType</param-name>
    <param-value>text/html; charset=UTF-8</param-value> <!-- Forces UTF-8 output encoding! -->
  </init-param>
    
  <!-- FreeMarker settings: -->
  <init-param>
    <param-name>incompatible_improvements</param-name>
    <param-value>2.3.22</param-value>
    <!-- Recommended to set to a high value. For the details, see the Java API docs of
         freemarker.template.Configuration#Configuration(Version). -->
  </init-param>
  <init-param>
    <param-name>template_exception_handler</param-name>
    <!-- Use "html_debug" instead during development! -->
    <param-value>rethrow</param-value>
  </init-param>
  <init-param>
    <param-name>template_update_delay</param-name>
    <!-- ATTENTION, 0 is for development only! Use higher value otherwise. -->
    <param-value>0</param-value>
  </init-param>
  <init-param>
    <param-name>default_encoding</param-name>
    <!-- The encoding of the template files. -->
    <param-value>UTF-8</param-value>
  </init-param>
  <init-param>
    <param-name>locale</param-name>
    <!-- Influences number and date/time formatting, etc. -->
    <param-value>en_US</param-value>
  </init-param>
  <init-param>
    <param-name>number_format</param-name>
    <param-value>0.##########</param-value>
  </init-param>

  <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
  <servlet-name>freemarker</servlet-name>
  <url-pattern>*.ftl</url-pattern>
</servlet-mapping>

...

<!--
  Prevent the visiting of MVC Views from outside the servlet container.
  RequestDispatcher.forward/include should, and will still work.
  Removing this may open security holes!
-->
<security-constraint>
  <web-resource-collection>
    <web-resource-name>FreeMarker MVC Views</web-resource-name>
    <url-pattern>*.ftl</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <!-- Nobody is allowed to visit these directly. -->
  </auth-constraint>
</security-constraint>

在这之后,你可以像使用JSP(*.jsp) 文件那样使用FTL文件(*.ftl)了。 (当然你可以选择除 ftl 之外的扩展名;这只是惯例)

Note:

它是怎么工作的?让我们先来看看JSP是怎么工作的。 许多servlet容器处理JSP时使用一个映射为 *.jsp 的servlet请求URL格式。这样servlet就会接收所有URL是以 .jsp 结尾的请求,查找请求URL地址中的JSP文件, 内部编译后生成 Servlet,然后调用生成好的serlvet来生成页面。 这里为URL类型是 *.ftl 映射的 FreemarkerServlet 也是相同功能,只是FTL文件不会编译成 Servlet,而是给 Template 对象, 之后 Template 对象的 process 方法就会被调用来生成页面。

比如,代替这个JSP页面 (注意它使用了Struts标签库来保存设计,而不是嵌入可怕的Java代码):

<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>

<html>
<head><title>Acmee Products International</title>
<body>
  <h1>Hello <bean:write name="user"/>!</h1>
  <p>These are our latest offers:
  <ul>
    <logic:iterate name="latestProducts" id="prod">
      <li><bean:write name="prod" property="name"/>
        for <bean:write name="prod" property="price"/> Credits.
    </logic:iterate>
  </ul>
</body>
</html>

你可以使用这个FTL文件(使用 ftl 扩展名而不是 jsp):

<html>
<head><title>Acmee Products International</title>
<body>
  <h1>Hello ${user}!</h1>
  <p>These are our latest offers:
  <ul>
    <#list latestProducts as prod>
      <li>${prod.name} for ${prod.price} Credits.
    </#list>
  </ul>
</body>
</html>
Warning!

在 FreeMarker 中,<html:form action="/query">...</html:form> 仅仅被视为是静态文本,所以它会按照原本输出出来了,就像其他XML或HTML标记一样。 JSP标签也仅仅是FreeMarker的指令,没有什么特殊之处,所以你可以 使用FreeMarker语法 形式来调用它们,而不是JSP语法: <@html.form action="/query">...</@html.form>。 注意在FreeMarker语法中 不能像JSP那样在参数中使用 ${...}, 而且不能给参数值加引号。 所以这样是错误的

<#-- WRONG: -->
<@my.jspTag color="${aVariable}" name="aStringLiteral"
            width="100" height=${a+b} />

但下面这样是正确的:

<#-- Good: -->
<@my.jspTag color=aVariable name="aStringLiteral"
            width=100 height=a+b />

在这两个模板中,当你要引用 userlatestProduct 时,首先它会尝试去查找已经在模板中创建的同名变量 (比如 prod;如果你使用JSP:这是一个page范围内的属性)。 如果那样做不行,它会尝试在 HttpServletRequest 对象中查找那个名字的属性, 如果没有找到就在 HttpSession 中找,如果还没有找到那就在 ServletContext 中找。FTL按这种情况工作是因为 FreemarkerServlet 创建数据模型由上面提到的3个对象中的属性而来。 那也就是说,这种情况下根哈希表root不是 java.util.Map (正如本手册中的一些例子那样),而是 ServletContext+HttpSession+HttpServletRequest ;FreeMarker 在处理数据模型类型的时候非常灵活。所以如果你想将变量 "name" 放到数据模型中,那么你可以调用 servletRequest.setAttribute("name", "Fred");这是模型2的逻辑, 而 FreeMarker 将会适应它。

FreemarkerServlet 也会在数据模型中放置3个哈希表, 这样你就可以直接访问3个对象中的属性了。这些哈希表变量是:RequestSessionApplication (和ServletContext对应)。它还会暴露另外一个名为 RequestParameters 的哈希表,这个哈希表提供访问HTTP请求中的参数。

FreemarkerServlet 也有很多初始参数。 它可以被设置从任意路径来加载模板,从类路径下,或相对于Web应用程序的目录。 你可以设置模板使用的字符集。你还可以设置想使用的对象包装器等等。

通过子类别,FreemarkerServlet 易于定制特殊需要。 那就是说,如果你需要对所有模板添加一个额外的可用变量,使用servlet的子类, 覆盖 preTemplateProcess() 方法,在模板被执行前, 将你需要的额外数据放到模型中。或者在servlet的子类中,在 Configuration 中设置这些全局的变量作为 共享变量

要获取更多信息,可以阅读该类的Java API文档。

包含其它Web应用程序资源中的内容

你可以使用由 FreemarkerServlet (2.3.15版本之后) 提供的客户化标签<@include_page path="..."/> 来包含另一个Web应用资源的内容到输出内容中;这对于整合JSP页面 (在同一Web服务器中生活在FreeMarker模板旁边) 的输出到FreeMarker模板的输出中非常有用。使用:

<@include_page path="path/to/some.jsp"/>

和在JSP中使用该标签是相同的:

<jsp:include page="path/to/some.jsp">
Note:

<@include_page ...> 不能和 <#include ...>搞混, 后者是为了包含FreeMarker模板而不会牵涉到Servlet容器。 使用 <#include ...> 包含的模板和包含它的模板共享模板处理状态, 比如数据模型和模板语言变量,而 <@include_page ...> 开始一个独立的HTTP请求处理。

Note:

一些Web应用框架为此提供它们自己的解决方案, 这种情况下你就可以使用它们来替代。 而一些Web应用框架不使用 FreemarkerServlet, 所以 include_page 是不可用的。

路径可以是相对的,也可以是绝对的。相对路径被解释成相对于当前HTTP请求 (一个可以触发模板执行的请求)的URL,而绝对路径在当前的servlet上下文 (当前的Web应用)中是绝对的。你不能从当前Web应用的外部包含页面。 注意你可以包含任意页面,而不仅仅是JSP页面; 我们仅仅使用以 .jsp 结尾的页面作为说明。

除了参数 path 之外,你也可以用布尔值 (当不指定时默认是true)指定一个名为 inherit_params 可选的参数来指定被包含的页面对当前的请求是否可见HTTP请求中的参数。

最后,你可以指定一个名为 params 的可选参数, 来指定被包含页面可见的新请求参数。如果也传递继承的参数, 那么指定参数的值将会得到前缀名称相同的继承参数的值。params 的值必须是一个哈希表类型,它其中的每个值可以是字符串, 或者是字符串序列(如果你需要多值参数)。这里给出一个完整的示例:

<@include_page path="path/to/some.jsp" inherit_params=true params={"foo": "99", "bar": ["a", "b"]}/>

这会包含 path/to/some.jsp 页面, 传递它的所有的当前请求的参数,除了"foo"和"bar", 这两个会被分别设置为"99"和多值序列"a","b"。 如果原来请求中已经有这些参数的值了,那么新值会添加到原来存在的值中。 那就是说,如果"foo"有值"111"和"123",那么现在它会有"99","111","123"。

事实上使用 params 给参数传递非字符串值是可能的。这样的一个值首先会被转换为适合的Java对象 (数字,布尔值,日期等),之后调用它们Java对象的 toString() 方法来得到字符串值。最好不要依赖这种机制,作为替代, 明确参数值在模板级别不能转换成字符串类型之后, 在使用到它的地方可以使用内建函数 ?string?c

在FTL中使用自定义JSP标签

FreemarkerServlet 将一个哈希表类型的 JspTaglibs 放到数据模型中,就可以使用它来访问JSP标签库了。 自定义JSP标签库将被视为普通用户自定义指令来访问,自定义EL函数 (从 FreeMarker 2.3.22 版本开始)视为方法。例如,这个JSP文件:

<%@ page contentType="text/html;charset=ISO-8859-2" language="java"%>
<%@ taglib prefix="e" uri="/WEB-INF/example.tld" %>
<%@ taglib prefix="oe" uri="/WEB-INF/other-example.tld" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

<%-- Custom JSP tags and functions: --%>

<e:someTag numParam="123" boolParam="true" strParam="Example" anotherParam="${someVar}">
  ...
</e:someTag>

<oe:otherTag />

${e:someELFunction(1, 2)}


<%-- JSTL: --%>

<c:if test="${foo}">
  Do this
</c:if>

<c:choose>
  <c:when test="${x == 1}">
      Do this
  </c:when>
  <c:otherwise>
      Do that
  </c:otherwise>
</c:choose>

<c:forEach var="person" items="${persons}">
  ${person.name}
</c:forEach>

${fn:trim(bar)}

基本一致的FTL是:

<#assign e=JspTaglibs["/WEB-INF/example.tld"]>
<#assign oe=JspTaglibs["/WEB-INF/other-example.tld"]>

<#-- Custom JSP tags and functions: --#>

<@e.someTag numParam=123 boolParam=true strParam="Example" anotherParam=someVar>
  ...
</@e.someTag>

<@oe.otherTag />

${e.someELFunction(1, 2)}


<#-- JSTL - Instead, use native FTL constructs: -->

<#if foo>
  Do this
</#if>

<#if x == 1>
  Do this
<#else>
  Do that
</#if>

<#list persons as person>
  ${person.name}
</#list>

${bar?trim}
Note:

参数值没有使用引号,而且 "${...}" 和JSP中使用的一样。 后面会详细解释。

Note:

JspTaglibs 不是 FreeMarker 的核心特性; 它只存在于通过 FreemarkerServlet 调用的模板。 这是因为JSP 标签/函数 假定一个servlet环境(FreeMarker不会), 加上一些Servlet概念被模仿成 FreemarkerServlet 创建的特定Freemarker数据模型。很多现代开发框架以纯净的方式使用FreeMarker, 而不是通过 FreemarkerServlet

因为自定义JSP标签是在JSP环境中来书写操作的,它们假设变量 (在JSP中常被指代"beans")被存储在4个范围中:page范围,request范围, session范围和application范围。FTL没有这样的表示法(4种范围),但是 FreemarkerServlet给自定义标签提供仿真的环境, 这样就可以维持JSP范围中的"beans"和FTL变量之间的对应关系。 对于自定义的JSP标签,请求request,会话session和应用application是和真实JSP相同的: javax.servlet.ServletContextHttpSessionServletRequest 对象中的属性。从FTL的角度来看, 这三种范围都在数据模型中,这点前面已经解释了。page范围和FTL全局变量(参见global指令)是对应的。 那也就是,如果你使用 global 指令创建一个变量,通过仿真的JSP环境, 它会作为page范围变量对自定义标签可见。而且,如果一个JSP标签创建了一个新的page范围变量, 那么结果和用 global 指令创建的是相同的。 要注意在数据模型中的变量作为page范围的属性对JSP标签是不可见的,尽管它们在全局是可见的, 因为数据模型和请求,会话,应用范围是对应的,而不是page范围。

在JSP页面中,你可以对所有属性值加引号,这和参数类型是字符串, 布尔值或数字没有关系。但是因为在FTL模板中自定义标签可以被用户自定义FTL指令访问到, 你将不得不在自定义标签中使用FTL语法规则,而不是JSP语法。所以当你指定一个"属性"的值时, 那么在 = 的右边是一个 FTL 表达式。因此, 你不能对布尔值和数字值的参数加引号 (比如:<@tiles.insert page="/layout.ftl" flush=true/>), 否则它们将被解释为字符串值,当FreeMarker试图传递值到期望非字符串值的自定义标记中时, 这就会引起类型不匹配错误。而且还要注意,这很自然,你可以使用任意FTL表达式作为属性的值, 比如变量,计算的结果值等。(比如:<@tiles.insert page=layoutName flush=foo && bar/>)

Servlet容器运行过程中,因为它实现了自身的轻量级JSP运行时环境, 它用到JSP标签库,而 FreeMarker 并不依赖于JSP支持。这是一个很小但值得注意的地方: 在它们的TLD文件中,开启 FreeMarker 的JSP运行时环境来分发事件到JSP标签库中注册时间监听器, 你应该将下面的内容添加到Web应用下的 WEB-INF/web.xml 文件中:

<listener>
  <listener-class>freemarker.ext.jsp.EventForwarding</listener-class>
</listener>

请注意,尽管servlet容器没有本地的JSP支持,你也可以在 FreeMarker 中使用JSP标签库。 只是确保对JSP 1.2版本(或更新)的 javax.servlet.jsp.* 包在Web应用程序中可用就行。如果你的servlet容器只对JSP 1.1支持, 那么你不得不将下面六个类(比如你可以从Tomcat 5.x或Tomcat 4.x的jar包中提取)复制到Web应用的 WEB-INF/classes/...目录下: javax.servlet.jsp.tagext.IterationTagjavax.servlet.jsp.tagext.TryCatchFinallyjavax.servlet.ServletContextListenerjavax.servlet.ServletContextAttributeListenerjavax.servlet.http.HttpSessionAttributeListenerjavax.servlet.http.HttpSessionListener。但是要注意, 因为容器只支持JSP 1.1,通常是使用较早的Servlet 2.3之前的版本, 事件监听器可能就不支持,因此JSP 1.2标签库来注册事件监听器会正常工作。

在撰写本文档时,JSP已经升级到2.1了,许多特性也已经实现了, 除了JSP 2(也就是说JSP自定义标记在JSP语言中实现了)的"标签文件"特性。 标签文件需要被编译成Java类文件,在 FreeMarker 下才会有用。

JspTaglibs[uri] 会去找到URI指定的TLD,就像JSP的 @taglib 指令所做的。 它实现了JSP规范中所描述的TLD发现机制。这里可以阅读更多,但简而言之, 它会在 WEB-INF/web.xml taglib 元素中, 在 WEB-INF/**/*.tld 文件中,还有 WEB-INF/lib/*.{jar,zip}/META-INF/**/*.tld 文件中寻找TLD。 此外,当设置了 FreemarkerServlet 的初始化参数(从 2.3.22版本开始) MetaInfTldSources 和/或 ClasspathTlds, 即便是在WAR结构之外,它也会发现对于类加载器可见的TLD。参考 FreemarkerServlet 的Java API文档来获取更多描述。 它也可以从Java系统属性中来设置,当你想在Eclipse运行配置中来修改而不去修改 web.xml时,就可以随手完成;再强调一点,请参考 FreemarkerServlet API 文档。 FreemarkerServlet 也会识别 org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern servlet 上下文属性,并且将它中间的配置项添加到 MetaInfTldSources

在JSP页面中嵌入FTL

有一个标签库允许你将FTL片段放到JSP页面中。 嵌入的FTL片段可以访问JSP 的4种范围内的属性(Beans)。 你可以在 FreeMarker 发布包中找到一个可用的示例和这个标签库。