在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"类,
将数据作为属性放在 ServletContext
,
HttpSession
和 HttpServletRequest
对象中,
之后请求被框架派发到一个JSP页面中(视图层),使用属性传递过来的数据来生成HTML页面,
这样的策略通常就是所指的Model 2模型。
使用这样的框架,你就可以非常容易地用FTL文件来代替JSP文件。 但是,因为你的Servlet容器(Web应用程序服务器),不像JSP文件, 它可能并不知道如何处理FTL文件,那么就需要对Web应用程序进行一些额外的配置:
-
复制
freemarker.jar
(从FreeMarker发布包的lib
目录中) 到Web应用程序的WEB-INF/lib
目录下。 -
将下面的部分添加到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
之外的扩展名;这只是惯例)
它是怎么工作的?让我们先来看看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 />
在这两个模板中,当你要引用 user
和
latestProduct
时,首先它会尝试去查找已经在模板中创建的同名变量
(比如 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个对象中的属性了。这些哈希表变量是:Request
,
Session
,Application
(和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请求处理。
一些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中使用的一样。
后面会详细解释。
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.ServletContext
,HttpSession
和 ServletRequest
对象中的属性。从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.IterationTag
,
javax.servlet.jsp.tagext.TryCatchFinally
,
javax.servlet.ServletContextListener
,
javax.servlet.ServletContextAttributeListener
,
javax.servlet.http.HttpSessionAttributeListener
,
javax.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 发布包中找到一个可用的示例和这个标签库。