Freemarker 简介 及手册

来源:互联网 发布:手绘动画mac 编辑:程序博客网 时间:2024/05/22 15:52

FreeMarker 手册

什么是FreeMarker?
FreeMarker是一款模板引擎:即一种基于模板、用来生成输出文本(任何来自于HTML格式的文本用来自动生成源代码)的通用工具。它是为Java程序员提供的一个开发包,或者说是一个类库。它不是面向最终用户的,而是为程序员提供的一款可以嵌入他们所开发产品的应用程序。
FreeMarker实际上是被设计用来生成HTML页面,尤其是通过实现了基于MVC(Model View Controller,模型-视图-控制器)模式的Java Servlet应用程序。使用MVC模式的动态页面的设计构思使得你可以将前端设计师(编写HTML页面的人员)从程序员中分离出来。那么,所有人各司其职,发挥其最擅长的一面。网页设计师可以改写页面的显示效果而不受程序员编译代码的影响,因为应用程序的逻辑(这里是Java程序)和页面设计(这里是FreeMarker模板)已经被分开了。页面模板代码不会受到复杂程序代码的影响。这种分离的思想即便对一个程序员和页面设计师是同一个人的项目来说也都是非常有用的,因为分离使得代码保持简洁而且易于维护。
尽管FreeMarker也拥有一些编程能力,但是它却不像PHP那样,是的一种全面的编程语言。反而,Java程序准备的数据来进行显示(比如SQL数据库查询),FreeMarker仅仅是使用模板生成文本页面来呈现已经准备好的数据而已。
FreeMarker不是Web开发的应用程序框架。它是一个适用于Web应用程序框架中的组件,但是FreeMarker引擎本身并不知道HTTP协议或Java Servlet的存在。它仅仅来生成文本内容。既然是这样,它也非常适用于非Web应用程序的开发环境。只是要注意的是,我们使用FreeMarker作为视图层的组件,是为了给诸如Struts这样的Model 2应用框架提供现成的解决方案。
FreeMarker本身是免费的,它基于BSD规则的许可协议。它也是OSI认证的开源软件。OSI认证是开源倡议的认证标识。

第一部分 模板开发指南
第一章 模板开发入门
1.1 简介
本章内容是对FreeMarker进行简略的介绍,后续章节中将会详细展开。不过没有关系,只要你阅读了本章节的内容后,你就能够编写简单,但却很有用的FreeMarker模板程序了。
1.2 模板 + 数据模型 = 输出
假设你在一个在线商店的应用系统中需要一个HTML页面,和下面这个页面类似:
在这里,比方说用户名(所有的”Big Joe”),应该是登录这个网页的访问者的名字,并且最新产品的数据应该来自于数据库,这样它们才可以随时进行更新。这样的情况下,你不能在HTML页面中直接输入登录用户的用户名,最新产品的URL和名称,你不能使用静态的HTML代码,那样是不能即时改变的。
对于这个问题,FreeMarker的解决方案是使用模板来代替静态的HTML文本。模板文件同样是静态的HTML代码,但是除了这些HTML代码外,代码中还包括了一些FreeMarker指令元素,这些指令就能够做到动态效果。
<html>
<head>
<title>Welcome!</title>
</head>
<body>
<h1>Welcome Big Joe!</h1>
<p>Our latest product:
<a href="products/greenmouse.html">green mouse</a>!
</body>
</html>
<html>
<head>
<title>Welcome!</title>
</head>
<body>
<h1>Welcome ${user}!</h1>
<p>Our latest product:
<a href="${latestProduct.url}">${latestProduct.name}</a>!
</body>
</html>
这个模板存放在Web服务器上,看上去像是静态的HTML页面。但不管何时,只要有人来访问这个页面,FreeMarker将会介入执行,然后动态转换模板,用最新的数据内容替换模板中${…}的部分(例如:用Big Joe或者其他的访问者的用户名来代替${user}),生成普通的HTML文本并发送结果到访问者的Web浏览器中去显示。所以访问者的Web浏览器会接收到类似于第一个HTML示例的内容(也就是说,显示普通的HTML文本而没有FreeMarker的指令),因为浏览器也不会感知到FreeMarker在服务器端被调用了。模板文件本身(存储在Web服务器端的文件)在这个过程中也不会改变什么,所以这个转换过程发生在一次又一次的访问中。这样就保证了显示的信息总是即时的。
现在,也许你已经注意到,该模板并没有包含关于如何找出当前的访问者是谁,或者是如何去查询数据库中查找最新的产品的指令。它似乎已经知道了这些数据是什么。事实也确实是这样的,在FreeMarker背后(确切的说是在MVC模式的背后)的重要思想就是表现逻辑和业务逻辑相分离。在模板中,只是处理显示相关的问题,也就是视觉设计问题和格式问题。所准备要显示的数据(如用户名等)与FreeMarker无关,这通常是使用Java语言或其他目的语言来编写的程序。所以模板开发人员不需要关心这些数值是如何计算出来的。事实上,在模板保持不变的同时,这些数值的计算方式可以发生根本的变化。而且,除了模板外,页面外观发生的变化可以完全不触碰其他任何东西。当模板开发人员和程序员是不同一个人的时候,分离带来的好处更是显而易见的。
FreeMarker(还有模板开发人员)并不关心数据是如何计算出来的,FreeMarker只是知道真实的数据是什么。模板能用的所有数据被包装成data-model数据模型。数据模型的创建是通过已经存在的程序计算得到的。至于模板开发人员,数据模型像是树形结构(比如硬盘上的文件夹和文件),正如本例中的数据模型,就可以如下形式来描述:
(为了避免误解:数据模型并不是文本文件,上面所描述的只是一种数据模型的表现形式。它来自于Java对象,但这会成为Java程序员要面对的问题。)
比较之前你在模板中看到的${user}和${latestProduct.name}。作为一种比喻:数据模型就像计算机文件系统上的内容:根root和latestProduct对应目录(文件夹),user,url和name对应文件。url和name在latestProduct目录中,所以latestProduct.name就像是说latestProduct目录的name一样。但是我所说的,这仅仅是个比喻,这里并没有真实的文件和目录。
概括地讲,模板和数据模型是FreeMarker所需,并用来生成输出内容的(比如之前展示的HTML):模板+数据模型=输出
1.3 数据模型一览
正如你看到的,数据模型基本结构是树状的。这棵树可以复杂而且有很大的深度,比如:
(root)
|
+- user = "Big Joe"
|
+- latestProduct
|
+- url = "products/greenmouse.html"
|
+- name = "green mouse"
上图中变量扮演目录的角色(根root,animal,mouse,elephant,python,whatnot)被称为hash哈希表。哈希表通过可查找的名称(例如:”animal”, ”mouse”, ”price”)来访问存储的其他变量(如子变量)。
如果仅存储单值的变量(size,price,text和because)则它们被称为scalars标量。
如果要在模板中使用子变量,那应该从根root开始指定它的路径,每级之间用点来分隔。要访问price和mouse的话,应该从根开始,先是animals,然后是mouse,最后是price,所以应该这样写:animals.mouse.price。当放置${…}这种特定代码在表达式的前后时,我们就告诉FreeMarker在那个位置上要来输出对应的文本。
sequences序列也是一种非常重要的变量,它们和哈希表变量相似,但是它们不存储所包含变量的名称,而是按顺序存储子变量。这样,就可以使用数字索引来访问这些子变量。在这种数据模型中,animal和whatnot.fruits就是序列:
(root)
|
+- animals
| |
| +- mouse
| | |
| | +- size = "small"
| | |
| | +- price = 50
| |
| +- elephant
| | |
| | +- size = "large"
| | |
| | +- price = 5000
| |
| +- python
| |
| +- size = "medium"
| |
| +- price = 4999
|
+- test = "It is a test"
|
+- whatnot
|
+- because = "don't know"
可以使用数组的方括号方式来访问一个序列的子变量。索引从零开始(从零开始是程序员写代码的传统习惯),那么就意味着序列第一项的索引是0,第二项的索引是1,并以此类推。要得到第一个动物的名称的话,那么就应该这么写代码:animals[0].name。要得到whatnot.fruits(就是”banana”这个字符串)的第二项,那么就应该这么来写:whatnot.fruits[1]。
标量可以分为如下类别:
字符串:这是文本类型,字符的任意序列,比如”m”,“o”,“u”,“s”,“e”这些,而且name-S和size-S也是字符串范畴。
(root)
|
+- animals
| |
| +- (1st)
| | |
| | +- name = "mouse"
| | |
| | +- size = "small"
| | |
| | +- price = 50
| |
| +- (2nd)
| | |
| | +- name = "elephant"
| | |
| | +- size = "large"
| | |
| | +- price = 5000
| |
| +- (3rd)
| |
| +- name = "python"
| |
| +- size = "medium"
| |
| +- price = 4999
|
+- whatnot
|
+- fruits
|
+- (1st) = "orange"
|
+- (2nd) = "banana"
数字:这是数字值类型,比如price-S这些。在FreeMarker中字符串”50”和数字50是两种完全不同的类型。前者只是两个字符的序列(这恰好是我们可以读的一个数字),而后者是一个可以在算数运算中直接被使用的数值。
日期/时间:这是时间日期类型。例如动物被捕捉的日期,或商店开始营业的时间。
布尔值:对应对/错(是/否,开/关等)这样仅仅代表正反的值。比如动物可以有一个受保护(protected,译者注)的子变量,这个变量存储这个动物是否被保护起来。
总结:
数据模型可以被看做是树状结构的。
标量存储单一的值,这种类型的值可以是字符串,数字,日期/时间或者是布尔值。
哈希表是存储变量和与其相关且有唯一标识名称变量的容器。
序列是存储有序变量的容器。存储的变量可以通过数字索引来检索,索引通常从零开始。
1.4 模板一览
1.4.1 简介
最简单的模板是普通HTML文件(或者是其他任何文本文件—FreeMarker本身不属于HTML)。当客户端访问页面时,FreeMarker要发送HTML代码至客户端浏览器端显示。如果想要页面动起来,就要在HTML中放置能被FreeMarker所解析的特殊部分。
${…}:FreeMarker将会输出真实的值来替换花括号内的表达式,这样的表达式被称为interpolations插值,可以参考第上面示例的内容。
FTL tags标签(FreeMarker 模板的语言标签):FTL标签和HTML标签有一点相似,但是它们是FreeMarker的指令而且是不会直接输出出来的东西。这些标签的使用一般以符号#开头。(用户自定义的FTL标签使用@符号来代替#,但这是更高级的主题内容了,后面会详细地讨论)
Comments注释:FreeMarker的注释和HTML的注释相似,但是它用<#--和-->来分隔的。任何介于这两个分隔符(包含分隔符本身)之间内容会被FreeMarker忽略,就不会输出出来了。
其他任何不是FTL标签,插值或注释的内容将被视为静态文本,这些东西就不会被FreeMarker所解析,会被按照原样输出出来。
directives指令:就是所指的FTL标签。这些指令在HTML的标签(如<table>和</table>)和HTML元素(如table元素)中的关系是相同的。(如果现在你还不能区分它们,那么把“FTL标签”和“指令”看做是同义词即可。)
1.4.2 指令示例
尽管FreeMarker有很多指令,作为入门,在快速了解过程中我们仅仅来看三个最为常用的指令。
1.4.2.1 if指令
使用if指令可以有条件地跳过模板的一部分,这和程序语言中if是相似的。假设在第一个示例中,你只想向你的老板Big Joe(而不是其他人)问好,就可以这样做:
在这里,我们告诉FreeMarker,我们尊敬的领导才是if条件中那唯一的user变量值,当它和”Big Joe”相同时才显示出来。那么,当condition的判断结果为false(布尔值)时,在<#if condition>和</#if>标签之间的内容将会被略过。
我们来详细说说condition的使用:==是来判断在它两侧的值相等的操作符,比较的结果是布尔值,true或者false。在==的左侧,是引用的变量,我们很熟悉这样的语法,它会被变量的值来替代。右侧是指定的字符串,在模板中的字符串必须放在引号内。
当price是0的时候,下面的代码将会打印:”Pythons are free today!”
和前面的示例相似,字符串被直接指定,但是这里则是数字(0)被直接指定。注意到数字是不用放在引号内的。如果将0放在引号内(”0”),FreeMarker就会将其误判为字符串了。
当price不是0的时候,下面的代码将会打印:”Pythons are not free today!”
你也许会猜测了,!=就是不等于。
你也可以这样来写代码(使用数据模型来描述哈希表变量):
<html>
<head>
<title>Welcome!</title>
</head>
<body>
<h1>
Welcome ${user}<#if user == "Big Joe">, our beloved leader</#if>!
</h1>
<p>Our latest product:
<a href="${latestProduct.url}">${latestProduct.name}</a>!
</body>
</html>
<#if animals.python.price == 0>
Pythons are free today!
</#if>
<#if animals.python.price != 0>
Pythons are free today!
</#if>
<#if animals.python.price < animals.elephant.price>
Pythons are cheaper than elephants today.
</#if>
使用<#else>标签可以指定当条件为假时程序执行的内容。例如:
如果蟒蛇的价格比大象的价格低,将会打印”Python are cheaper than elephants today.”,否则就打印”Pythons are not cheaper than elephants today.”
如果变量本身就是布尔值(true或者false),那么可以直接让其作为if的条件condition:
1.4.2.2 list指令
当需要用列表来遍历集合的内容时,list指令是非常好用的。例如,如果在模板中用前面示例描述序列的数据模型。
那么输出结果将会是这样的:
list指令的一般格式为:
<#list sequence as loopVariable>repeatThis</#list>
repeatThis部分将会在给定的sequence遍历时在每项中重复,从第一项开始,一个接着一个。在所有的重复中,loopVariable将持有当前项的值。这个循环变量仅存在于<#list …>和</#list>标签之间。
再看一个示例,遍历示例数据模型fruits。
<#if animals.python.price < animals.elephant.price>
Pythons are cheaper than elephants today.
<#else>
Pythons are not cheaper than elephants today.
</#if>
<#if animals.python.protected>
Warning! Pythons are protected animals!
</#if>
<p>We have these animals:
<table border=1>
<tr><th>Name<th>Price
<#list animals as being>
<tr><td>${being.name}<td>${being.price} Euros
</#list>
</table>
<p>We have these animals:
<table border=1>
<tr><th>Name<th>Price
<tr><td>mouse<td>50 Euros
<tr><td>elephant<td>5000 Euros
<tr><td>python<td>4999 Euros
</table>
whatnot.fruits表达式应该很熟悉了,我们引用了数据模型章节中示例的变量。
1.4.2.3 include指令
使用include指令,我们可以在当前的模板中插入其他文件的内容。
假设要在一些页面中显示版权声明的信息。那么可以创建一个文件来单独包含版权声明,之后在需要它的地方插入即可。比方说,我们可以将版权信息单独存放在页面文件copyright_footer.html中。
当需要用到这个文件时,可以使用include指令来实现插入。
输出的内容为:
<p>And BTW we have these fruits:
<ul>
<#list whatnot.fruits as fruit>
<li>${fruit}
</#list>
<ul>
<hr>
<i>
Copyright (c) 2000 <a href="http://www.acmee.com">Acmee Inc</a>,
<br>
All Rights Reserved.
</i>
<html>
<head>
<title>Test page</title>
</head>
<body>
<h1>Test page</h1>
<p>Blah blah...
<#include "/copyright_footer.html">
</body>
</html>
如果改变了copyright_footer.html中的内容,那么访问者就会在所有页面中
看到新的版权声明信息了。
1.4.2.4 联合使用指令
在页面也可以多次使用指令,而且指令间可以相互嵌套,正如在HTML元素中嵌套使用标签一样。下面的代码会遍历动物集合,用大号字体来打印大型动物的名字。
注意到FreeMarker并不解析FTL标签外的文本,插值和注释,当条件不满足时它也会忽略所有嵌套的font标签。
1.4.2.5 处理不存在的变量
在实际应用中数据模型经常会有可选的变量(也就是说有时可能不存在实际值)。除了一些典型的人为原因导致失误,FreeMarker不能容忍引用不存在的变量,除非明确地告诉它当变量不存在时如何处理。这里介绍两种典型的处理方法。
这部分对程序员而言:一个不存在的变量和一个是null的变量,对于FreeMarker来
<html>
<head>
<title>Test page</title>
</head>
<body>
<h1>Test page</h1>
<p>Blah blah...
<hr>
<i>
Copyright (c) 2000 <a href="http://www.acmee.com">Acmee Inc</a>,
<br>
All Rights Reserved.
</i>
</body>
</html>
<p>We have these animals:
<table border=1>
<tr><th>Name<th>Price
<#list animals as being>
<tr>
<td>
<#if being.size == "large"><font size="+1"></#if>
${being.name}
<#if being.size == "large"></font></#if>
<td>${being.price} Euros
</#list>
</table>
说是一样的,所以这里所指的丢失包含这两种情况。
不论在哪里引用变量,都可以指定一个默认值来避免变量丢失这种情况,通过在变量名后面跟着一个!和默认值。就像下面的例子,当user从数据模型中丢失时,模板将会将user的值表示为字符串”Anonymous”。(若user并没有丢失,那么模板就会表现出”Anonymous”不存在一样):
当然也可以在变量名后面通过放置??来询问FreeMarker一个变量是否存在。将它和if指令合并,那么如果user变量不存在的话将会忽略整个问候代码段:
关于多级访问的变量,比如animals.python.price,书写代码:animals.python.price!0,仅当animals.python存在而仅仅最后一个子变量price可能不存在(这种情况下我们假设价格是0)。如果animals或者python不存在,那么模板处理过程将会以“未定义的变量”错误而停止。为了防止这种情况的发生,可以这样来书写代码(animals.python.price)!0。这种情况下当animals或python不存在时表达式的结果仍然是0。对于??也是同样用来的处理这种逻辑的:animals.python.price??对比(animals.python.price)??来看。
<h1>Welcome ${user!"Anonymous"}!</h1>
<#if user??><h1>Welcome ${user}!</h1></#if>
第二章 数值和类型
2.1 基本内容
2.1.1 简介
注意:
这里假设你已经阅读完入门章节的内容了。
理解数值和类型的概念是理解数据模型的关键和基础。然而,数值和类型的概念并不局限于数据模型,下面你就会看到了。
2.1.2 什么是数值?
这部分对于程序员来说可以直接跳过这,它和程序语言中的数值类型是相似的。
你所知道的来自于每天所使用的数字,比如16,0.5等这些用语就是数值的示例,也就是数字。在计算机语言中,这些用语有着更广泛的含义,比如数值并不一定是数字值,看下面这个数据模型:
(root)
|
+- user = "Big Joe"
|
+- today = Jul 6, 2007
|
+- todayHoliday = false
|
+- lotteryNumbers
| |
| +- (1st) = 20
| |
| +- (2st) = 14
| |
| +- (3rd) = 42
| |
| +- (4th) = 8
| |
| +- (5th) = 15
|
+- cargo
|
+- name = "coal"
|
+- weight = 40
我们说变量user的数值是”Big Joe”(字符串),today的数值是Jul 6,2007(日期),todayHoilday的数值是false(布尔值,是/否,这样的值)。lotteryNumbers的数值是包含20,14,42,8,15的序列。在这种意义上,lotteryNumbers是多值的,它包含多个数值(如其中的第二项是14),但是lotteryNumbers本身还是单值。它像一个装有很多东西的盒子,整个盒子被看做是独立的。最后有一个数值cargo,它是一个哈希表(也可以看做是盒子)。所以数值就是存储在变量中的(在user,cargo或cargo.name中)东西。但是不需要存储的数值也可以称之为数值,比如这里的数字100:
当模板被执行时,计算的临时结果也称为数值,比如20+120(它会打印120):
这最后一种的解释:两个数40(货物的重量)和2相除的结果是20,这是一个新计算出的数值。把它和100相加,那么120就出来了,接着就打印出来了(${…}),接着模板继续向下执行直到所有结果都计算出来。
现在你应该能体会到数值这个词的含义了,不仅仅是数字的值。
2.1.3 什么是类型?
数值中非常重要的一个概念就是类型。比方说,变量user的类型是字符串,lotteryNumbers的类型是序列。数值的类型非常重要,因为它决定了这些数值可以在哪里使用的最大限度。比如${user/2}就是错误的,但是${cargo.weight/2}就能计算出结果20,除法仅对数字值有效,而不能作用于字符串。仅当cargo是一个哈希表时cargo.name可以使用。也可以用<#list …>仅仅来遍历序列。<#if …>指令的条件condition只能是布尔值等。
注意:
这里说一点点术语:称 “布尔”或“布尔值”或“布尔类型”都是相同的含义。
数值同时也可以含有多种类型,尽管这样很少使用。看下面这个数据模型mouse,就又是字符串又是哈希表。
如果用上面的数据模型合并到模板中,就该这么来写:
它的输出内容为:
<#if cargo.weight < 100>Light cargo</#if>
${cargo.weight / 2 + 100}
(root)
|
+- mouse = "Yerri"
|
+- age = 12
|
+- color = "brown"
${mouse} <#-- 用 mouse 作为字符串 -->
${mouse.age} <#-- 用 mouse 作为哈希表 -->
${mouse.color} <#-- 用 mouse 作为哈希表 -->
2.1.4 数据模型是哈希表
注意观察每个你已经知道的数据模型:被”(root)”标识的内容就是哈希表类型的数值。当书写如user这样的代码,那就意味着想要把”user”变量存储在哈希表的根上。而如果代码是:root.user,也没有名为”root”的变量,那么这就没有任何作用。
某些人也许会被这种数据模型的例子所困惑,也就是说,根哈希表包含更多的哈希表或序列(如lotteryNumbers和cargo)。其他就没有更特殊的了。哈希表包含其他变量,那些变量包含数值,数值可以是字符串,数字等,当然也可以是哈希表或序列。最初我们解释过,就像字符串和数字,序列或哈希表也是数值。
2.2 类型
2.2.1 简介
支持的类型有:
 标量:
 字符串
 数字
 布尔值
 日期
 容器:
 哈希表
 序列
 集
 子程序:
 方法和函数
 用户自定义指令
 其它/很少使用:
 节点
2.2.2 标量
标量是最基本,最简单的数值类型,它们可以是:
 字符串:简单的文本,例如:产品的名称。
如果想在模板中直接给出字符串的值,而不是使用数据模型中的变量,那么将文本写在引号内即可,比如”green mouse”或者’green mouse’。(关于语法
Yerri
12
brown
的更多细节请看后续章节)
 数字:例如:产品的价格。整数和非整数是不区分的,只有单一的数字类型。比如使用了计算器,计算3/2的结果是1.5而不是1。
如果要在模板中直接给出数字的值,可以这么来写:150,-90.05,或者0.001。(关于语法的更多细节请看后续章节)
 布尔值:布尔值代表了逻辑上的对或错(是或否)。例如:用户到底是否登录了。典型的应用是使用布尔值作为if指令的条件,比如<#if loggedIn>…</#if>或者<#if price==0>…</#if>,后面这个price==0部分的结果就是布尔值。
在模板中可以使用保留字true和false来指定布尔值。
 日期:日期变量可以存储和日期/时间相关的数据。一共有三种变化。
 精确到天的日期(通常指的是“日期”),比如April 4, 2003
 每天的时间(不包括日期部分),比如10:19:18 PM。时间的存储精确到毫秒。
 日期-时间(也称作“时间戳”),比如April 4, 2003 10:19:18 PM。时间部分的存储精确到毫秒。
不幸的是,受到Java平台的限制,FreeMarker是不能决定日期的部哪分来使用(也就是说,是日期-时间格式,每天的时间格式等)。这个问题的解决方法是高级主题了,后面的章节将会讨论到。
在模板中直接定义日期数值是可以的,但这也是高级主题,后面的章节将会讨论到。
要记住,FreeMarker区别字符串,数字和布尔值,所以字符串”150”和数字150是完全不同的两种数值。数字持有的是数字的值,布尔值表达的是逻辑上的对或错。字符串可以是任意字符的序列。
2.2.3 容器
这些值存在的目的是为了包含其他变量,它们仅仅作为容器。被包含的变量通常是子变量。容器的类型有:
 哈希表:每个子变量都可以通过一个唯一的名称来查找,这个名称是不受限制的字符串。哈希表并不确定其中子变量的顺序,也就是说没有第一个变量,第二个变量这样的说法,变量仅仅是通过名称来访问的。(就像Java语言中的HashMap一样,是实现了Hash算法的Map,不记录内部元素的顺序,仅仅通过名称来访问。译者注)
 序列:每个子变量通过一个整数来标识。第一个子变量的标识符是0,第二个是1,第三个是2,这样来类推,而且子变量是有顺序的。这些数字通常被称为是子变量的索引。序列通常比较密集,也就是所有的索引,包括最后一个子变量的,它们和子变量都是相关联的,但不是绝对必要的。子变量的数值类型也并不需要完全一致。
 集: 从模板设计者角度来看,集是有限制的序列。不能获取集的大小,也不能通过索引取出集中的子变量,但是它们仍然可以通过list指令来遍历。
要注意一个数值也可有多种类型,对于一个数值可能存在哈希表和序列这两种类型,这时,该变量就支持索引和名称两种访问方式。不过容器基本是当作哈希表或者序列来使用的,而不是两者同时使用。
尽管存储在哈希表,序列(集)中的变量可以是任意类型的,这些变量也可以是哈希表,序列(集)。这样就可以构建任意深度的数据结构。
数据模型本身(最好说成是它的根)也是哈希表。
2.2.4 子程序
2.2.4.1 方法和函数
一个值是方法或函数的时候那么它就可以计算其他值,结果取决于传递给它的参数。
这部分是对程序员来说的:方法/函数是第一类值,就像函数化的编程语言。也就是说函数/方法也可以是其他函数或方法的参数或者返回值,并可以把它们定义成变量。
假设程序员在数据模型中放置了一个方法变量avg,那么它就可以被用来计算数字的平均值。给定3和5作为参数,访问avg时就能得到结果4。
方法的使用后续章节会有解释,下面这个示例会帮助我们理解方法的使用:
可以得到如下的输出:
那么方法和函数有什么区别呢?这是模板作者所关心的,它们没有关系,但也不是一点关系都没有。方法是来自于数据模型(它们反射了Java对象的方法),而函数是定义在模板内的(使用了函数指令-这也是高级主题),但二者可以用同一种方式来使用。
2.2.4.2 用户自定义指令
用户自定义指令(换句话说,就是FreeMarker的标签)这种类型的值也是一种子程序,一种可以复用的模板代码段。但这也是高级主题,我们在后续章节中会详细解释。
这部分是对程序员来说的:用户自定义指令(比如宏),也是第一类值,就像函数/方法一样。
这里仅仅对用户自定义指令有一个认识即可(如果现在还不能理解可以先忽略它)。假设现在有一个变量,box,它的值是用户自定义的指令,用来打印一些特定的HTML信息,这个指令定义了一个标题和其中的信息。
The average of 3 and 5 is: ${avg(3, 5)}
The average of 6 and 10 and 20 is: ${avg(6, 10, 20)}
The average of the price of a python and an elephant is:
${avg(animals.python.price, animals.elephant.price)}
The average of 3 and 5 is: 4
The average of 6 and 10 and 20 is: 12
The average of the price of a python and an elephant is:
4999.5
<@box title="Attention!">
Too much copy-pasting may leads to
maintenance headaches.
</@box>
2.2.4.3 函数/方法和用户自定义指令的比较
这部分内容也是对高级用户来说的(如果你还不能理解可以先忽略它)。如果要使用函数/方法或自定义指令去实现一些东西的时候,二者之间的选择是两难的。按经验来说,
如果能够实现,请先用自定义指令而不要用函数/方法。指令的特征如下:
 输出(返回值)的是标记(HTML,XML等)。主要原因是函数的返回结果可以自动进行XML转义(这是因为${…}的特性),而用户自定义指令的输出则不是(这是因为<@...>的特性所致,它的输出假定为是标记,因此就不再转义)。
 副作用也是很重要的一点,它没有返回值。例如一个指令的目的是往服务器日志中添加一个条目。(事实上你不能得到自定义指令的返回值,但有些反馈的类型是有可能设置非本地变量的)
 会进行流程的控制(就像list或if指令那样),但是不能在函数/方法上这么做。
在模板中,FreeMarker不知道的Java对象的方法通常是可以作为方法来使用的,而不用考虑Java对象方法本身的特性,因为在这里没有其他的选择。
2.2.5 其它
2.2.5.1 节点
节点变量代表了树状结构中的一个节点,而且通常是配合XML格式来处理的,这是专业而且更高级的主题。
这里我们仅对高级用户进行一个概要说明:节点和存储在其他节点中的序列很相似,通常也被当作为子节点。节点存储它所在的容器节点的引用,也就是父节点。节点的主要作用是拓扑信息。其它数据必须通过使用多类型的值来存储。就像一个值可以同时是一个节点和一个数字,这样它存储的数字可以作为如支付额来使用。除了拓扑信息,节点也可以存储一些元信息(即metadata,译者注):如节点名称,它的类型(字符串),命名空间(作为字符串)。若一个节点象征XHTML文档中的h1元素,那么它的名字可以是”h1”,类型可以是”element”,命名空间可以是”http://www.w3.org/1999/xhtml”。但对于数据模型设计者来说,这些元信息,还有如何来使用它们又有什么意义呢。检索拓扑信息和元信息的方法将会在后续章节中来说明(这里你可以先不用理解它们)。
第三章 模板
注意:
这里假设你已经阅读完入门章节,数值和类型章节两部分了。
3.1 总体结构
实际上你用程序语言编写的程序就是模板,模板也被称为FTL(代表FreeMarker模板语言)。这是为编写模板设计的非常简单的编程语言。
模板(FTL编程)是由如下部分混合而成的:
Text文本:文本会照着原样来输出。
Interpolation插值:这部分的输出会被计算的值来替换。插值由${和}所分隔(或者#{和},这种风格已经不建议再使用了)。
FTL tags标签:FTL标签和HTML标签很相似,但是它们却是给FreeMarker的指示,而且不会打印在输出内容中。
Comments注释:FTL的注释和HTML的注释也很相似,但它们是由<#--和-->来分隔的。注释会被FreeMarker所忽略,更不会在输出内容中显示。 我们来看一个具体的模板,其中的内容已经用颜色来标记了:文本,插值,FTL标签,注释,为了看到可见的换行符,这里使用了[BR]。
FTL是区分大小写的。list是指令的名称而List就不是,类似地${name}和${Name}或者${NAME}它们也是不同的。 应该意识到非常重要的一点:插值仅仅可以在文本中间使用(也可以在字符串表达式中,后续将会介绍)。 FTL标签不可以在其他FTL标签和插值中使用。下面这样写就是错的:
<#if <#include 'foo'>='bar'>...</#if> 注释可以放在FTL标签和插值中间。比如:
<html>[BR] <head>[BR] <title>Welcome!</title>[BR] </head>[BR] <body>[BR] <#-- Greet the user with his/her name -->[BR] <h1>Welcome ${user}!</h1>[BR] <p>We have these animals:[BR] <ul>[BR] <#list animals as being>[BR] <li>${being.name} for ${being.price} Euros[BR] </#list>[BR] </ul>[BR] </body>[BR] </html>
注意:
如果目前您已经自己尝试了上面所有的示例的话,那么你也许会注意一些空格、制表符和换行符从模板输出中都不见了,尽管我们之前已经说了文本是按照原样输出的。现在不用为此而计较,这是由于FreeMarker的“空格剥离”特性在起作用,它当然会自动去除一些多余的空格,制表符和换行符了。这个特性后续也会解释到。
3.2 指令
使用FTL标签来调用directives指令,比如调用list指令。在语法上我们使用了两个标签:<#list animals as being>和</#list>。
标签分为两种:
 开始标签:<#directivename parametes>
 结束标签:</#directivename>
除了标签以#开头外,其他都和HTML,XML的语法很相似。如果标签没有嵌套内容(在开始标签和结束标签之内的内容),那么可以只使用开始标签。例如<#if something>...</#if>,但是FreeMarker知道<#include something>中include指令没有可嵌套的内容。
parameters的格式由directivename来决定。
事实上,指令有两种类型:预定义指令和用户自定义指令。对于用户自定义的指令使用@来代替#,比如<@mydirective parameters>...</@mydirective>。更深的区别在于如果指令没有嵌套内容,那么必须这么使用<@mydirective parameters />,这和XML语法很相似(例如<img ... />).但是用户自定义指令是后面要讨论的高级主题。
像HTML标签一样,FTL标签必须正确的嵌套使用。下面这段示例代码就是错的,因为if指令在list指令嵌套内容的内外都有:
注意一下FreeMarker仅仅关心FTL标签的嵌套而不关心HTML标签的嵌套,它只会把HTML看做是相同的文本,不会来解释HTML。
如果你尝试使用一个不存在的指令(比如你输错了指令的名称),FreeMarker就会拒绝<h1>Welcome ${user <#-- The name of user -->}!</h1>[BR] <p>We have these animals:[BR] <ul>[BR] <#list <#-- some comment... --> animals as <#-- again... --> being>[BR]
...
<ul>
<#list animals as being>
<li>${being.name} for ${being.price} Euros
<#if user == "Big Joe">
(except for you)
</#list> <#-- WRONG! The "if" has to be closed first. -->
</#if>
</ul>
执行模板,同时抛出错误信息。
FreeMarker会忽略FTL标签中的多余空白标记,所以你也可以这么来写代码:
当然,也不能在<,</和指令名中间插入空白标记。
指令列表和详细介绍可以参考指令参考部分(但是我建议先看表达式章节)。
注意:
通过配置,FreeMarker可以在FTL标签和FTL注释中,使用[和]来代替<和>,就像[#if user == "Big Joe"]...[/#if]。要获取更多信息,请参考:第四章的其它/替换(方括号)语法部分。
注意:
通过配置,FreeMarker可以不需要#来理解预定义指令(比如<if user == "Big Joe">...</if>)。而我们不建议这样来使用。要获取更多信息,请参考:参考文档部分,废弃的FTL结构/老式FTL语法。
3.3 表达式
3.3.1 简介
当需要给插值或者指令参数提供值时,可以使用变量或其他复杂的表达式。例如,我们设x为8,y为5,那么(x+y)/2的值就会被处理成数字类型的值6.5
在我们展开细节之前,先来看一些具体的例子:
 当给插值提供值时:插值的使用方式为${expression},把它放到你想输出文本的位置上然后给值就可以打印了。即${(5+8)/2}会打印”6.5”出来(如果输出的语言不是英语,也可能打印出”6,5”)。
 当给指令参数提供值时:在入门章节我们已经看到if指令的使用了。这个指令的语法是:<#if expression>...</#if>。这里的表达式计算结果必须是布尔类型的。比如<#if 2 < 3>中的2 < 3(2小于3)是结果为true的布尔表达式。
3.3.2 快速浏览(备忘单)
这里是给已经了解FreeMarker的人或有经验的程序员的一个提醒:
 直接指定值
 字符串:"Foo" 或者 'Foo' 或者 "It's \"quoted\"" 或者 r"C:\raw\string"
 数字:123.45 <#list[BR] animals as[BR] being[BR] >[BR] ${being.name} for ${being.price} Euros[BR] </#list >
 布尔值:true, false
 序列:["foo", "bar", 123.45], 1..100
 哈希表:{"name":"green mouse", "price":150}
 检索变量
 顶层变量:user
 从哈希表中检索数据:user.name, user[“name”]
 从序列中检索:products[5]
 特殊变量:.main
 字符串操作
 插值(或连接):"Hello ${user}!"(或"Free" + "Marker")
 获取一个字符:name[0]
 序列操作
 连接:users + ["guest"]
 序列切分:products[10..19] 或 products[5..]
 哈希表操作
 连接:passwords + {"joe":"secret42"}
 算数运算: (x * 1.5 + 10) / 2 - y % 100
 比较运算:x == y, x != y, x < y, x > y, x >= y, x <= y, x &lt; y, 等等
 逻辑操作:!registered && (firstVisit || fromEurope)
 内建函数:name?upper_case
 方法调用:repeat("What", 3)
 处理不存在的值
 默认值:name!"unknown" 或者(user.name)!"unknown" 或者name! 或者 (user.name)!
 检测不存在的值:name?? 或者(user.name)??
参考:运算符的优先级
3.3.3 直接确定值
通常我们喜欢是使用直接确定的值而不是计算的结果。
3.3.3.1 字符串
在文本中确定字符串值的方法是看引号和单引号,比如"some text"或'some text',这两种形式是相等的。如果文本本身包含用于字符引用的引号(双引号”或单引号’)或反斜杠时,应该在它们的前面再加一个反斜杠,这就是转义。转义允许你直接在文本中输入任何字符,也包括反斜杠。例如:
${"It's \"quoted\" and
this is a backslash: \\"}
${'It\'s "quoted" and
this is a backslash: \\'}
输出为:
注意:
这里当然可以直接在模板中输入文本而不需要${…}。但是我们在这里用它只是为了示例来说明表达式的使用。
下面的表格是FreeMarker支持的所有转义字符。在字符串使用反斜杠的其他所有情况都是错误的,运行这样的模板都会失败。
转义序列
含义
\
引号(u0022)
\’
单引号(又称为撇号)(u0027)
\\
反斜杠(u005C)
\n
换行符(u000A)
\r
回车(u000D)
\t
水平制表符(又称为标签)(u0009)
\b
退格(u0008)
\f
换页(u000C)
\l
小于号:<
\g
大于号:>
\a
和号:&
\xCode
字符的16进制Unicode码(UCS码)
在\x之后的Code是1-4位的16进制码。下面这个示例中都是在字符串中放置版权符号"\xA9 1999-2001", "\x0A9 1999-2001", "\x00A9 1999-2001":如果紧跟16进制码后一位的字符也能解释成16进制码时,就必须把4位补全,否则FreeMarker的解析就会出现问题。
注意字符序列${(#{)有特殊的含义,它们被用做插入表达式的数值(典型的应用是:"Hello ${user}!")。这将在后续章节中解释。如果想要打印${,就要使用下面所说的原生字符串。
一种特殊的字符串就是原生字符串。在原生字符串中,反斜杠和${没有特殊的含义,它们被视为普通的字符。为了表明字符串是原生字符串,在开始的引号或单引号之前放置字母r,例如:
将会打印:
It's "quoted" and
this is a backslash: \
It's "quoted" and
this is a backslash: \
${r"${foo}"}
${r"C:\foo\bar"}
${foo}
C:\foo\bar
3.3.3.2 数字
输入不带引号的数字就可以直接指定一个数字,必须使用点作为小数的分隔符而不能是其他的分组分隔符。可以使用-或+来表明符号(+是多余的)。科学记数法暂不支持使用(1E3就是错误的),而且也不能在小数点之前不写0(.5也是错误的)。
下面的数字都是合法的:0.08, -5.013, 8, 008, 11, +11。
数值文字08, +8, 8.00和8是完全相等的,它们都是数字8。因此${08}, ${+8}, ${8.00}和${8}打印的都是相同的。
3.3.3.3 布尔值
直接写true或false就表征一个布尔值了,不需使用引号。
3.3.3.4 序列
指定一个文字的序列,使用逗号来分隔其中的每个子变量,然后把整个列表放到方括号中。例如:
将会打印出:
列表中的项目是表达式,那么也可以这样做:[2 + 2, [1, 2, 3, 4], "whatnot"],其中第一个子变量是数字4,第二个子变量是一个序列,第三个子变量是字符串”whatnot”。
也可以用start..end定义存储数字范围的序列,这里的start和end是处理数字值表达式,比如2..5和[2, 3, 4, 5]是相同的,但是使用前者会更有效率(内存占用少而且速度快)。可以看出前者也没有使用方括号,这样也可以用来定义递减的数字范围,比如5..2。(此外,还可以省略end,只需5..即可,但这样序列默认包含5,6,7,8等递增量直到无穷大)
3.3.3.5 哈希表
在模板中指定一个哈希表,就可以遍历用逗号分隔开的“键/值”对,把列表放到花括号内。键和值成对出现并以冒号分隔。看这个例子:{"name":"green mouse", "price":150}。注意到名字和值都是表达式,但是用来检索的名字就必须是字符串类
<#list ["winter", "spring", "summer", "autumn"] as x>
${x}
</#list>
winter
spring
summer
autumn
型的。
3.3.4 检索变量
3.3.4.1 顶层变量
为了访问顶层的变量,可以简单地使用变量名。例如,用表达式user就可以在根上获取以“user”为名存储的变量值。然后就可以打印出存储在里面的内容。
如果没有顶层变量,那么FreeMarker在处理表达式时就会发生错误,进而终止模板的执行(除非程序员事先配置了FreeMarker)。
在这个表达式中变量名可以包含字母(也可以是非拉丁文),数字(也可以是非拉丁数字),下划线(_),美元符号($),at符号(@)和哈希表(#),此外要注意变量名命名时是不能以数字开头的。
3.3.4.2 从哈希表中检索数据
如果有一个表达式的结果是哈希表,那么我们可以使用点和子变量的名字得到它的值,假设我们有如下的数据模型:
现在,就可以通过book.title来读取title表达式,book将返回一个哈希表(就像上一章中解释的那样)。按这种逻辑进一步来说,我们可以使用表达式book.author.name来读取到auther的name。
如果我们想指定同一个表达式的子变量,那么还有另外一种语法格式:book["title"]。在方括号中可以给出任意长度字符串的表达式。在上面这个数据模型示例中你还可以这么来获取title:book[test] ,下面这些示例它们含义都是相等的:book.author.name, book["author"].name, book.author.["name"], book["author"]["name"]。
${user}
(root)
|
+- book
| |
| +- title = "Breeding green mouses"
| |
| +- author
| |
| +- name = "Julia Smith"
| |
| +- info = "Biologist, 1923-1985, Canada"
|
+- test = "title"
当使用点式语法时,顶层变量名的命名也有相同的限制(命名时只能使用字母,数字,下划线,$,@等),而使用方括号语法形式时命名有没有这样的限制,它可以是任意的表达式。(为了FreeMarker支持XML,如果变量名是*(星号)或者**,那么就应该使用方括号语法格式。)
对于顶层变量来说,如果尝试访问一个不存在的变量也会引起错误导致模板解析执行的中断(除非程序员事先配置过FreeMarker)。
3.3.4.3 从序列中检索数据
这和从哈希表中检索是相同的,但是你只能使用方括号语法形式来进行,而且方括号内的表达式最终必须是一个数字而不是字符串。在第一章的数据模型示例中,为了获取第一个动物的名字(记住第一项数字索引是0而不是1)可以这么来写:animals[0].name。
3.3.4.4 特殊变量
特殊变量是由FreeMarker引擎本身定义的,为了使用它们,可以按照如下语法形式来进行:.variable_name。
通常情况下是不需使用特殊变量,而对专业用户来说可能用到。所有特殊变量的说明可以参见参考手册。
3.3.5 字符串操作
3.3.5.1 插值(或连接)
如果要在字符串中插入表达式的值,可以在字符串的文字中使用${…}(#{…})。${...}的作用和在文本区的是相同的。假设用户是”Big Joe”,看下面的代码:
将会打印如下内容:
另外,也可以使用+号来达到类似的效果,这是比较老的方法,也叫做字符串连接。示例如下:
这样打印的效果和多次使用${...}是一样的。
警告:
使用者在使用插值时经常犯的一个错误是:在不能使用插值的地方使用了它。插值只能
${"Hello ${user}!"}
${"${user}${user}${user}${user}"}
Hello Big Joe!
Big JoeBig JoeBig JoeBig Joe
${"Hello " + user + "!"}
${user + user + user + user}
在文本区段(<h1>Hello ${name}!</h1>)和字符串文字(<#include "/footer/${company}.html">)中使用。一个典型的错误使用是<#if ${isBig}>Wow!</#if>,这是语法上的错误。只能这么来写:<#if isBig>Wow!</#if>,<#if "${isBig}">Wow!</#if>来写也是错误的。因为if指令的参数需要的是布尔值,而这里是字符串,那么就会引起运行时的错误。
3.3.5.2 获取一个字符
在给定索引值时可以获取字符串中的一个字符,这和3.3.4.3节中从序列检索数据是相似的,比如user[0]。这个操作执行的结果是一个长度为1的字符串,FTL并没有独立的字符类型。和序列中的子变量一样,这个索引也必须是数字,范围是从0到字符串的长度,否则模板的执行将会发生错误并终止。
由于序列的子变量语法和字符的getter语法冲突,那么只能在变量不是序列时使用字符的getter语法(因为FTL支持多类型值,所以它是可能的),这种情况下使用序列方式就比较多。(为了变通,可以使用内建函数string,比如user?string[0]。不必担心你不理解这个含义,内建函数将会在后续章节中讨论。)
看一个例子(假设user是”Big Joe”)
将会打印出(注意第一个字符的索引是0):
注意:
可以按照切分序列的方式来获取一定范围内的字符,比如${user[1..4]}和${user[4..]}。然而现在这种使用方法已经被废弃了,作为它的替代,可以使用内建函数substring,内建函数将会在后续章节中讨论。
3.3.6 序列操作
3.3.6.1 连接
序列的连接可以使用+号来进行,例如:
将会打印出:
${user[0]}
${user[4]}
B
J
<#list ["Joe", "Fred"] + ["Julia", "Kate"] as user>
- ${user}
</#list>
- Joe
- Fred
- Julia
- Kate
要注意不要在很多重复连接时使用序列连接操作,比如在循环中往序列上追加项目,而这样的使用是可以的:<#list users + admins as person>。尽管序列连接的很快,而且速度是和被连接序列的大小相独立的,但是最终的结果序列的读取却比原先的两个序列慢那么一点。通过这种方式进行的许多重复连接最终产生的序列读取的速度会慢。
3.3.6.2 序列切分
使用[firstindex..lastindex]可以获取序列中的一部分,这里的firstindex和lastindex表达式的结果是数字。如果seq存储序列"a", "b", "c", "d", "e", "f",那么表达式seq[1..4]将会是含有"b", "c", "d", "e"的序列(索引为1的项是"b",索引为4的项是"e")。
lastindex可以被省略,那么这样将会读取到序列的末尾。如果seq存储序列"a", "b", "c", "d", "e", "f",那么seq[3..]将是含有"d", "e", "f"的序列。
注意:
从FreeMarker 2.3.3版本以后lastindex才能省略。
如果试图访问一个序列首变量之前的项或末变量之后的项将会引起错误,模板的执行也会中断。
3.3.7 哈希表操作
3.3.7.1 连接
像连接字符串那样,也可以使用+号的方式来连接哈希表。如果两个哈希表含有键相同的项,那么在+号右侧的哈希表中的项目优先。例如:
将会打印出:
注意很多项目连接时不要使用哈希表连接,比如在循环时往哈希表中添加新项。这和序列连接的情况是一致的。
3.3.8 算数运算
算数运算包含基本的四则运算和求模运算,运算符有:
 加法:+
<#assign ages = {"Joe":23, "Fred":25} + {"Joe":30, "Julia":18}>
- Joe is ${ages.Joe}
- Fred is ${ages.Fred}
- Julia is ${ages.Julia}
- Joe is 30
- Fred is 25
- Julia is 18
 减法:-
 乘法:*
 除法:/
 求模(求余):%
示例如下:
假设x是5,就会打印出:
要保证两个操作数都是结果为数字的表达式。下面的这个例子在运行时,FreeMarker就会发生错误,因为是字符串”5” 而不是数字5。
但这种情况也有一个例外,就是+号,它是用来连接字符串的,如果+号的一端是字符串,另外一端是数字,那么数字就会自动转换为字符串类型(使用适当的格式)。示例如下:
将会输出:
通常来说,FreeMarker不会自动将字符串转换为数字,反之会自动进行。
有时我们只想获取计算结果的整数部分,这可以使用内建函数int来解决。(关于内建函数后续章节会来解释)
仍然假设x的值是5,那么将会输出:
75
2.5
2
${3 * "5"} <#-- WRONG! -->
${3 + "5"}
35
${(x/2)?int}
${1.1?int}
${1.999?int}
${-1.1?int}
${-1.999?int}
2
1
1
-1
-1
${100 – x*x}
${x/2}
${12%10}
3.3.9 比较运算
有时我们需要知道两个值是否相等,或者哪个数的值更大一点。
为了演示具体的例子,我们在这里使用if指令。if指令的用法是:<#if expression>...</#if>,其中的表达式的值必须是布尔类型,否则将会出错,模板执行中断。如果表达式的结果是true,那么在开始和结束标记内的内容将会被执行,否则就会被跳过。
测试两个值相等使用=(或者采用Java和C语言中的==,二者是完全等同的。)
测试两个值不等使用!=。例子中假设user是”Big Joe”。
<#if ...>中的表达式user = "Big Joe"结果是布尔值true,上面的代码将会输出”It is Big Joe”。
=或!=两边的表达式的结果都必须是标量,而且两个标量都必须是相同类型(也就是说字符串只能和字符串来比较,数字只能和数字来比较等)。否则将会出错,模板执行中断。例如<#if 1 = "1">就会导致错误。注意FreeMarker进行的是精确的比较,所以字符串在比较时要注意大小写和空格:"x","x "和"X"是不同的值。
对数字和日期类型的比较,也可以使用<,<=,>=和>。不能把它们当作字符串来比较。比如:
使用>=和>的时候有一点小问题。FreeMarker解释>的时候可以把它当作FTL标签的结束符。为了避免这种问题,不得不将表达式放到括号内:<#if (x > y)>,或者可以在比较关系处使用&gt;和&lt;:<#if x &gt; y>。(通常在FLT标签中不支持实体引用(比如&...;这些),否则就会抛出算数比较异常)。另外,可以使用lt代替<,lte代替<=,gt代替>,gte代替>=, 由于历史遗留的原因,FTL也支持\lt, \lte, \gt 和 \gte,使用他们和使用不带反斜杠的效果一样。
3.3.10 逻辑操作
常用的逻辑操作符:
 逻辑或:||
 逻辑与:&&
 逻辑非:!
逻辑操作符仅仅在布尔值之间有效,若用在其他类型将会产生错误导致模板执行中止。例如:
<#if user = "Big Joe">
It is Big Joe
</#if>
<#if user != "Big Joe">
It is not Big Joe
</#if>
<#if x <= 12>
x is less or equivalent with 12
</#if>
3.3.11 内建函数
正如其名,内建函数提供始终可用的内置功能。内建函数以?形式提供变量的不同形式或者其他信息。使用内建函数的语法和访问哈希表子变量的语法很像,除了使用?号来代替点,其他的都一样。例如得到字符串的大写形式:user?upper_case。
在参考文档中可以查到所有内建函数的资料。现在,我们只需了解一些重要的内建函数就行了。
 字符串使用的内建函数:
 html: 字符串中所有的特殊HTML字符都需要用实体引用来代替(比如<代替&lt;)。
 cap_first:字符串的第一个字母变为大写形式
 lower_case:字符串的小写形式
 upper_case:字符串的大写形式
 trim:去掉字符串首尾的空格
 序列使用的内建函数:
 size:序列中元素的个数
 数字使用的内建函数:
 int:数字的整数部分(比如-1.9?int就是-1)
示例:
假设字符串test存储”Tom & Jerry”,那么输出为:
注意test?upper_case?html,内嵌函数双重使用,test?upper_case的结果是字符串了,但也还可以继续在其后使用html内建函数。
另外一个例子:
假设seasons存储了序列"winter", "spring", "summer", "autumn",那么上面的输出将会是:
<#if x < 12 && color = "green">
We have less than 12 things, and they are green.
</#if>
<#if !hot> <#-- here hot must be a boolean -->
It's not hot.
</#if>
${test?html}
${test?upper_case?html}
Tom &amp; Jerry
TOM &amp; JERRY
${seasons?size}
${seasons[1]?cap_first} <#-- left side can by any expression -->
${"horse"?cap_first}
3.3.12 方法调用
可以使用方法调用操作来使用一个已经定义过的方法。方法调用的语法形式是使用逗号来分割在括号内的表达式而形成的参数列表,这些值就是参数。方法调用操作将这些值传递给方法,然后返回一个结果,这个结果就是整个方法调用表达式的值。
假设程序员定义了一个可供调用的方法repeat。第一个参数字符串类型,第二个参数是数字类型。方法的返回值是字符串类型,而方法要完成是将第一个参数重复显示,显示的次数是第二个参数的值。
将会打印出:
3.3.13 处理不存在的值
要注意这个操作是FreeMarker 2.3.7版本以后才有的(用来代替内建函数default,exists和if_exists)。
正如我们前面解释的那样,当访问一个不存在的变量时FreeMarker将会报错而导致模板执行中断。通常我们可以使用两个操作符来压制这个错误,控制错误的发生。被控制的变量可以是顶层变量,哈希表或序列的子变量。此外这些操作符还能处理方法调用的返回值不存在的情况(这点对Java程序员来说,返回值是null而不是返回值为void类型的方法),通常来说,我们应该使用这些操作符来控制可能不存在的变量。
对于知道Java中null的人来说,FreeMarker 2.3.x版本把它们视为不存在的变量。简单地说,模板语言中没有null这个概念。比如有一个bean,bean中有一个maidenName属性,对于模板而言(假设你没有配置FreeMarker来使用一些极端的对象包装),这个属性的值是null,和不存在这个属性的情况是一致的。调用方法的返回值如果是null的话FreeMarker也会把它当作不存在的变量来处理(假定你只使用了普通的对象包装)。了解更多可以参考FAQ中的相关内容。
注意:
如果你想知道为什么FreeMarker对不存在的变量如此挑剔,请阅读FAQ部分。
3.3.13.1 默认值
使用形式概览:unsafe_expr!default_expr或unsafe_expr!或(unsafe_expr)!default_expr或(unsafe_expr)!
这个操作符允许你为可能不存在的变量指定一个默认值。
4
Spring
Horse
${repeat("What", 3)}
WhatWhatWhat
例如,假设下面展示的代码中没有名为mouse的变量:
将会输出
默认值可以是任何类型的表达式,也可以不必是字符串。你也可以这么写:hits!0或colors!["red", "green", "blue"]。默认值表达式的复杂程度没有严格限制,你还可以这么来写:cargo.weight!(item.weight * itemCount + 10) 。
警告:
如果在!后面有复合表达式,如1 + x,通常使用括号,像${x!(1 + y)}或${(x!1) + y)},这样就根据你的意图来确定优先级。由于FreeMarker 2.3.x版本的源码中的小失误所以必须这么来做。!(作为默认值操作)的优先级非常低。这就意味着${x!1 + y}会被FreeMarker误解为${x!(1 + y)},而真实的意义是${(x!1) + y}。这个源码的错误在FreeMarker 2.4中会得到修正。在编程中注意这个错误,要么就使用FreeMarker 2.4!
如果默认值被省略了,那么结果将会是空串,空序列或空哈希表。(这是FreeMarker允许多类型值的体现)如果想让默认值为0或false,则注意不能省略它。例如:
输出为:
警告:
因为语法的含糊<@something a=x! b=y />将会解释为<@something a=x!(b=y) />,那就是说b=y将会视为是比较运算,然后结果作为x的默认值。而不是想要的参数b是x的默认值,为了避免这种情况,如下编写代码:<@something a=(x!) b=y />。
在不是顶层变量时,默认值操作符可以有两种使用方式:
如果是这样的写法,那么在product中,当color不存在时(返回”red”)将会被处理,但是如果连produce都不存在时将不会处理。也就是说这样写时变量product必须存在,否则模板就会报错。
这时,如果当不存在时也会被处理,那就是说如果product不存在或者product存在而color不存在,都能显示默认值”red”而不会报错。本例和上例写法的重要区别在于用括号时,就允许其中表达式的任意部分可以未定义。
${mouse!"No mouse."}
<#assign mouse="Jerry">
${mouse!"No mouse."}
No mouse.
Jerry
(${mouse!})
<#assign mouse = "Jerry">
(${mouse!})
()
(Jerry)
product.color!"red"
(product.color)!"red"
当然,默认值操作也可以作用于序列,比如:
输出为:
如果序列索引是负数(比如seq[-1]!'-')也会发生错误,这样默认值和其他操作也就不起作用了。
3.3.13.2 检测不存在的值
使用形式概览:unsafe_expr??或(unsafe_expr)??
这个操作符告诉我们一个值是否存在。基于这种情况,结果是true或false。
示例如下,假设并没有名为mouse的变量:
输出为:
访问非顶层变量的使用规则和默认值操作符也是一样的,即product.color??和(product.color)?? 。
<#assign seq = ['a', 'b']>
${seq[0]!'-'}
${seq[1]!'-'}
${seq[2]!'-'}
${seq[3]!'-'}
a
b
-
-
<#if mouse??>
Mouse found
<#else>
No mouse found
</#if>
Creating mouse...
<#assign mouse = "Jerry">
<#if mouse??>
Mouse found
<#else>
No mouse found
</#if>
No mouse found
Creating mouse...
Mouse found
3.3.14 括号
括号可以用来给表达式分组。示例如下:
别忘了方法调用时使用的括号和给表达式分组的括号含义是完全不同的。
3.3.15 表达式中的空格
FTL忽略表达式中的多余空格,下面两种表示是相同的:

还有
3.3.16 操作符的优先级
下面的表格显示了已定义操作符的优先级。表格中的运算符按照优先程度降序排列:上面的操作符优先级高于它下面的。高优先级的运算符执行要先于优先级比它低的。表格同一行上的两个操作符优先级相同。当有相同优先级的二元运算符(运算符有两个参数,比如+和-)挨着出现时,它们按照从左到右的原则运算。
运算符组
运算符
最高优先级运算符
[subvarName][subStringRange].?(methodParams) expr! expr??
一元前缀运算符
+expr -expr !expr
<#-- 输出是: -->
${3 * 2 + 2} <#-- 8 -->
${3 * (2 + 2)} <#-- 12 -->
${3 * ((2 + 2) * (1 / 2))} <#-- 6 -->
${"green " + "mouse"?upper_case} <#-- green MOUSE -->
${("green " + "mouse")?upper_case} <#-- GREEN MOUSE -->
<#if !( color = "red" || color = "green")>
The color is nor red nor green
</#if>
${x + ":" + book.title?upper_case}
${x+":"+book.title?upper_case}
${
x
+ ":" + book . title
? upper_case
}
乘除法,求模
* / %
加减法
+ -
关系运算符
< > <= >= (相当于: gt, lt, 等)
相等,不等
== (也可以是: =) !=
逻辑与
&&
逻辑或
||
数字范围
..
如果你熟悉C语言,Java语言或JavaScript语言,注意FreeMarker中的优先级规则和它们是相同的,除了那些只有FTL本身含有的操作符。
因为编程的失误,默认值操作符(exp!exp)不在表格中,按照向后兼容的原则,在FreeMarker 2.4版本中将会修正它。而且它将是最高优先级的运算符,但是在FreeMarker 2.3.x版本中它右边的优先级由于失误就非常低。所以在默认值操作符的右边中使用复杂表达式时可以使用括号,可以是x!(y + 1)或(x!y) + 1,而不能是x!y + 1。
3.4 插值
插值的使用语法是:${expression},expression可以是所有种类的表达式(比如${100 + x})。
插值是用来给插入具体值然后转换为文本(字符串)。插值仅仅可以在两种位置使用:文本区(如<h1>Hello ${name}!</h1>)和字符串表达式(如<#include "/footer/${company}.html">)中。
警告:
一个常犯的错误是在不能使用插值的地方使用了它。典型的错误就是<#if ${isBig}>Wow!</#if>,这是语法上的错误。只要写为<#if isBig>Wow!</#if>就对了,而且<#if "${isBig}">Wow!</#if>也是错误的,因为这样参数就是字符串类型了,但是if指令的参数要求是布尔值,所以运行时就会发生错误。
插值表达式的结果必须是字符串,数字或日期类型的,因为只有数字和日期类型可以自动转换为字符串类型,其他类型的值(如布尔,序列)只能手动转换为字符串类型,否则就会发生错误导致模板执行中止。
字符串插入指南:不要忘了转义! 如果插值在文本区(也就是说,不再字符串表达式中),如果escapse指令起作用了,即将被插入的字符串会被自动转义。如果你要生成HTML,那么强烈建议你利用它来阻止跨站脚本攻击和非格式良好的HTML页面。这里有一个示例:
<#escape x as x?html>
...
<p>Title: ${book.title}</p>
<p>Description: <#noescape>${book.description}</#noescape></p>
<h2>Comments:</h2>
<#list comments as comment>
<div class="comment">
${comment}
</div>
</#list>
...
</#escape>
这个示例展示了当生成HTML时,你最好将完整的模板放入到escape指令中。那么,如果book.title包含&,它就会在输出中被替换成&amp;,而页面还会保持格式良好的HTML。如果用户注释包含如<iframe>(或其它的元素)的标记,那么就会被转义成如&lt;iframe&gt;的样子,使他们没有任何有害点。但是有时在数据模型中真的需要HTML,比如我们假设上面的book.description在数据库中的存储是HTML格式的,那么此时你不得不使用noescape来抵消escape的转义,模板就会像这样了:
这和之前示例的效果是一样的,但是这里你可能会忘记一些?html内建函数,就会有安全上的问题了。在之前的示例中,你可能忘记一些noescape,也会造成不良的输出,但是起码是没有安全隐患的。
数字插入指南
如果表达式是数字类型,那么根据数字的默认格式,数值将会转换成字符串。这也许会包含最大小数,数字分组和相似处理的问题。通常程序员应该设置默认的数字格式,而模板设计者不需要处理它(但是可以使用number_format设置来进行,详情参考setting指令部分的文档)。你可以使用内建函数string为一个插值来重写默认数值格式。
小数的分隔符通常(其他类似的符号也是这样,如分组符号)是根据所在地的标准(语言,国家)来确定的,这也需要程序员来设置。例如这个模板:
如果当前地区设置为英国时,将会打印:
而如果当前的地区设置为匈牙利时,将会打印:
而当前地区为匈牙利时,将会打印:
这是因为匈牙利人使用逗号作为小数点。
可以使用内建函数string为单独的插值来修改设置。
警告:
可以看出,插值的打印都是给用户看的,而不是给计算机的。有时候这样并不好,比如你要打印数据库记录的主键,用来作为URL中的一部分或HTML表单的隐藏域来作为提交内容,或者要打印CSS/JavaScript的数字。这些值都是给计算机程序去识别的而不是用户,很多程序对数字格式的要求非常严格,它们只能理解一部分简单的美式数字格式。那样的话,可以使用内建函数c(代表计算机)来解决这个问题,比如:
${1.5}
1.5
1,5
<a href="/shop/productdetails?id=${product.id?c}">Details...
</a>
...
<p>Title: ${book.title?html}</p>
<p>Description: ${book.description}</p>
<h2>Comments:</h2>
<#list comments as comment>
<div class="comment">
${comment?html}
</div>
</#list>
...
日期/时间插入指南
如果表达式的值是时间日期类型,那么日期中的数字将会按照默认格式来转换成文本。通常程序员应该设置默认格式,而页面设计者无需处理这一点。(如果需要的话,可以参考date_format,time_format和datetime_format的setting指令的设置来完成响应操作)
这里也可以使用内建函数string为单独的插值重写默认格式。
警告:
为了将日期显示成文本,FreeMarker必须知道日期中的哪一部分在使用,也就是说,如果仅仅日期部分(年,月,日)使用或仅仅时间部分(时,分,秒,毫秒)使用或两部分都用。不幸的是,由于Java平台技术的限制,自动探测一些变量是不现实的。这时可以找程序员对数据模型中可能出问题的变量进行处理。如果找出时间日期变量的哪部分在使用是不太可能的话,就必须帮助FreeMarker使用内建函数date,time和datetime来识别,否则就会出现错误停止执行。
布尔值插入指南
若要使用插值方式来打印布尔值会引起错误,中止模板的执行。例如:${a == 2}就会引起错误,它不会打印”true”或其他内容。
然而,我们可以使用内建函数string来将布尔值转换为字符串形式。比如打印变量”married”(假设它是布尔值),那么可以这么来写: ${married?string("yes", "no")}。
精确的转换规则
对于有兴趣研究的人,表达式的值转换为字符(仍有变量不存在的可能)串精确的规则就是下面这些,以这个顺序进行:
1. 如果这个值是数字,那么它会按照指定的number_format设置规则来转换为字符串。所以这些转换通常是对用户进行的,而不是对计算机。
2. 如果这个值是日期,时间或时间日期类型的一种,那么它们会按照指定的time_format,date_format和datetime_format设置规则来转换为字符串,这要看日期信息中是只包含时间,日期还是全包括了。如果它不能被探测出来是哪种日期类型(日期或时间或日期时间)时,就会发生错误了。
3. 如果值本来就是字符串类型的,不需要转换。
4. 如果FreeMarker引擎在传统兼容模式下:
1. 如果值是布尔类型,那么就转换成”true”,false值将会转换为空串。
2. 如果表达式未被定义(null或者变量未定义),那么就转换为空串。
3. 否则就会发生错误中止模板执行。
5. 否则就会发生错误中止模板执行。
第四章 其它
4.1 自定义指令
4.1.1 简介
自定义指令可以使用macro指令来定义,这是模板设计者所关心的内容。Java程序员若不想在模板中实现定义指令,而是在Java语言中实现指令的定义,这时可以使用freemarker.template.TemplateDirectiveModel类来扩展(后续章节将会说明)。
4.1.2 基本内容
宏是有一个变量名的模板片段。你可以在模板中使用宏作为自定义指令,这样就能进行重复性的工作。例如,创建一个宏变量来打印大号的”Hello Joe!”。
macro指令自身不打印任何内容,它只是用来创建宏变量,所以就会有一个名为greet的变量。在<#macro greet>和</#macro>之间的内容(称为宏定义体)当使用它作为指令时将会被执行。你可以在FTL标记中通过@代替#来使用自定义指令。使用变量名作为指令名。而且,自定义指令的结束标记也是需要的。那么,就可以这样来使用greet宏了:
因为<anything></anything>和<anything/>是相同的,你也可以使用单标记形式(如果你了解XML,那么就很容易理解了,它们是相似的):
将会打印:
宏能做的事情还有很多,因为在<#macro ...>和</#macro>之间的东西是模板片段,也就是说它可以包含插值(${...})和FTL标签(如<#if ...>...</#if>)。
注意:
程序员通常将使用<@...>,这称为宏调用。
<#macro greet>
<font size="+2">Hello Joe!</font>
</#macro>
<@greet></@greet>
<@greet/>
<font size="+2">Hello Joe!</font>
4.1.3 参数
我们来改进greet宏使之可以使用任意的名字,而不仅仅是”Joe”。为了实现这个目的,就要使用到参数。在macro指令中,宏名称的后面位置是用来定义变量的。这里我们仅在greet宏中定义一个变量,person:
那么就可以这样来使用这个宏:
这和HTML的语法是很相似的,它会打印出:
那么我们就看到了,宏参数的真实值是可以作为变量(person)放在宏定义体中的。使用预定义指令时,参数的值(=号后边的值)可以是FTL表达式。这样,不像HTML,"Fred"和"Batman"的引号就可以不用要了。<@greet person=Fred/>也意味着使用变量的值Fred作为person参数,而不是字符串"Fred"。当然参数值并不一定是字符串类型,也可以是数字,布尔值,哈希表,序列等…也可以在=号左边使用复杂表达式(比如someParam=(price + 50)*1.25)。
自定义指令可以有多个参数。如下所示,再添加一个新的参数color:
那么,这个宏就可以这样来使用:
参数的顺序不重要,下面的这个和上面的含义也是相同的。
当调用这个宏的时候,你仅仅可以使用在macro指令中定义的参数(这个例子中是:person和color)。那么当你尝试<@greet person="Fred" color="black" background="green"/>的时候就会发生错误,因为并没有在<#macro ...>中定义参数background。
同时也必须给出在宏中定义所有参数的值。如果你尝试<@greet person="Fred"/>时也会发生错误,因为忘记指定color的值了。很多情况下需要给一个参数指定一个相同的值,所以我们仅仅想在这个值发生变化后重新赋给变量。那么要达到这个目的,在macro指令中必须这么来指定变量:param_name=usual_value。例如,当没有特定值的时候,我们想要给color赋值为"black",那么greet指令就要这么来写:
<#macro greet person>
<font size="+2">Hello ${person}!</font>
</#macro>
<@greet person="Fred"/> and <@greet person="Batman"/>
<font size="+2">Hello Fred!</font>
and <font size="+2">Hello Batman!</font>
<#macro greet person color>
<font size="+2" color="${color}">Hello ${person}!</font>
</#macro>
<@greet person="Fred" color="black"/>
<@greet color="black" person="Fred"/>
现在,我们这么使用宏就可以了:<@greet person="Fred"/>,因为它和<@greet person="Fred" color="black"/>是相同的,这样参数color的值就是已知的了。如果想给color设置为”red”,那么就写成:<@greet person="Fred" color="red"/>,这时macro指令就会使用这个值来覆盖之前设置的通用值,参数color的值就会是”red”了。
根据FTL表达式规则,明白下面这一点是至关重要的,someParam=foo和someParam="${foo}"是不同的。第一种情况,是把变量foo的值作为参数的值来使用。第二种情况则是使用插值形式的字符串,那么参数值就是字符串了,这个时候,foo的值呈现为文本,而不管foo是什么类型(数字,日期等)的。看下面这个例子:someParam=3/4和someParam="${3/4}"是不同的,如果指令需要someParam是一个数字值,那么就不要用第二种方式。切记不要改变这些。
宏参数的另外一个重要的方面是它们是局部变量。更多局部变量的信息可以阅读4.2节内容:在模板中定义变量。
4.1.4 嵌套内容
自定义指令可以嵌套内容,和预定义指令相似:<#if ...>nested content</#if>。比如,下面这个例子中是创建了一个可以为嵌套的内容画出边框:
<#nested>指令执行位于开始和结束标记指令之间的模板代码段。如果这样写:
那么就会输出:
nested指令也可以多次被调用,例如:
<#macro greet person color="black">
<font size="+2" color="${color}">Hello ${person}!</font>
</#macro>
<#macro border>
<table border=4 cellspacing=0 cellpadding=4><tr><td>
<#nested>
</td></tr></table>
</#macro>
<@border>The bordered text</@border>
<table border=4 cellspacing=0 cellpadding=4><tr><td>
The bordered text
</td></tr></table>
<#macro do_thrice>
<#nested>
<#nested>
<#nested>
</#macro>
<@do_thrice>
Anything.
</@do_thrice>
就会输出:
如果不使用nested指令,那么嵌套的内容就会被执行,如果不小心将greet指令写成了这样:
FreeMarker不会把它视为错误,只是打印:
嵌套的内容被忽略了,因为greet宏没有使用nested指令。
嵌套的内容可以是任意有效的FTL,包含其他的用户自定义指令,这样也是对的:
将会输出:
在嵌套的内容中,宏的局部变量是不可见的。为了说明这点,我们来看:
将会打印:
Anything.
Anything.
Anything.
<@greet person="Joe">
Anything.
</@greet>
<font size="+2">Hello Joe!</font>
<@border>
<ul>
<@do_thrice>
<li><@greet person="Joe"/>
</@do_thrice>
</ul>
</@border>
<table border=4 cellspacing=0 cellpadding=4><tr><td>
<ul>
<li><font size="+2">Hello Joe!</font>
<li><font size="+2">Hello Joe!</font>
<li><font size="+2">Hello Joe!</font>
</ul>
</tr></td></table>
<#macro repeat count>
<#local y = "test">
<#list 1..count as x>
${y} ${count}/${x}: <#nested>
</#list>
</#macro>
<@repeat count=3>${y!"?"} ${x!"?"} ${count!"?"}</@repeat>
因为y,x和count是宏的局部(私有)变量,从宏外部定义是不可见的。此外不同的局部变量的设置是为每个宏自己调用的,所以不会导致混乱:
将会打印出:
4.1.5 宏和循环变量
像list这样的预定义指令可以使用循环变量,你可以通过阅读4.2节:在模板中定义变量来理解循环变量。
自定义指令也可以有循环变量。比如我们来扩展先前例子中的do_thrice指令,就可以拿到当前的循环变量的值。而对于预定义指令(如list),当使用指令(就像<#list foos as foo>...</#list>中的foo)时,循环变量的名字是已经给定的,变量值的设置是由指令本身完成的。
将会输出:
语法规则是为特定的循环(也就是嵌套内容的重复)传递循环变量的真实值来作为nested指令(当然参数可以是任意的表达式)的参数。循环变量的名称是在自定义指令的开始标记(<@...>)的参数后面通过分号确定的。
一个宏可以使用多个循环变量(变量的顺序是很重要的):
test 3/1: ? ? ?
test 3/2: ? ? ?
test 3/3: ? ? ?
<#macro test foo>${foo} (<#nested>) ${foo}</#macro>
<@test foo="A"><@test foo="B"><@test foo="C"/></@test></@test>
A (B (C () C) B) A
<#macro do_thrice>
<#nested 1>
<#nested 2>
<#nested 3>
</#macro>
<@do_thrice ; x> <#-- 用户自定义指令 使用";"代替"as" -->
${x} Anything.
</@do_thrice>
1 Anything.
2 Anything.
3 Anything.
<#macro repeat count>
<#list 1..count as x>
<#nested x, x/2, x==count>
</#list>
</#macro>
<@repeat count=4 ; c, halfc, last>
${c}. ${halfc}<#if last> Last!</#if>
</@repeat>
那么,将会输出:
在自定义指令的开始标签(分号之后)为循环变量指定不同的数字是没有问题的,而不能在nested指令上使用。如果在分号之后指定的循环变量少,那么就看不到nested指令提供的最后的值,因为没有循环变量来存储这些值,下面的这些都是可以的:
如果在分号后面指定了比nested指令还多的变量,那么最后的循环变量将不会被创建(在嵌套内容中不会被定义)。
4.1.6 自定义指令和宏进阶
现在你也许已经阅读过FreeMarker参考手册的相关部分了:
 调用自定义指令
 宏指令
你也可以在FTL中定义方法,参见function指令。
也许你对命名空间感兴趣。命名空间可以帮助你组织和重用你经常使用的宏。
4.2 在模板中定义变量
正如我们已经描述过的,模板可以使用在数据模型中定义的变量。在数据模型之外,模板本身也可以定义变量来使用。这些临时变量可以适应FTL指令来创建和替换。要注意每一次模板执行时都维护它自己的这些变量的私有设置,这些变量是在页面用以呈现信息的。变量的初始值是空,当模板执行结束这些变量便被销毁了。
你可以访问一个在模板里定义的变量,就像是访问数据模型根上的变量一样。这个变量比定义在数据模型中的同名参数有更高的优先级,那就是说,如果你恰巧定义了一个名为”foo”的变量,而在数据模型中也有一个名为”foo”的变量,那么模板中的变量就会将数据模型根上的变量隐藏(而不是覆盖!)。例如${foo}将会打印在模板中定义的变量。
在模板中可以定义三种类型的变量:
 简单变量:它能从模板中的任何位置来访问,或者从使用include指令引入的模板访问。可以使用assign或macro指令来创建或替换这些变量。
1. 0.5
2. 1
3. 1.5
4. 2 Last!
<@repeat count=4 ; c, halfc, last>
${c}. ${halfc}<#if last> Last!</#if>
</@repeat>
<@repeat count=4 ; c, halfc>
${c}. ${halfc}
</@repeat>
<@repeat count=4>
Just repeat it...
</@repeat>
 局部变量:它们只能被设置在宏定义体内,而且只在宏内可见。一个局部变量的生存周期只是宏的调用过程。可以使用local指令在宏定义体内创建或替换局部变量。
 循环变量:循环变量是由指令(如list)自动创建的,而且它们只在指令的开始和结束标记内有效。宏的参数是局部变量而不是循环变量。
示例:使用assign创建和替换变量
输出为:
局部变量也会隐藏(不是覆盖)同名的简单变量。循环变量也会隐藏(不是覆盖)同名的局部变量和简单变量。例如:
输出为:
<#assign x = 1> <#-- 创建变量 x -->
${x}
<#assign x = x + 3> <#-- 替换变量 x -->
${x}
1
4
<#assign x = "plain">
1. ${x} <#-- 这里是普通变量 -->
<@test/>
6. ${x} <#-- 普通变量的值没有被改变 -->
<#list ["loop"] as x>
7. ${x} <#-- 现在循环变量隐藏了普通变量 -->
<#assign x = "plain2"> <#-- 替换普通变量, 隐藏在这里不起作用-->
8. ${x} <#-- 它仍然隐藏普通变量 -->
</#list>
9. ${x} <#-- 普通变量的新值 -->
<#macro test>
2. ${x} <#-- 这里我们仍然看到的是普通变量 -->
<#local x = "local">
3. ${x} <#-- 现在局部变量隐藏了它 -->
<#list ["loop"] as x>
4. ${x} <#-- 现在循环变量隐藏了局部变量 -->
</#list>
5. ${x} <#-- 现在又可以看到局部变量了 -->
</#macro>
内部循环变量可以隐藏外部循环变量:
输出为:
注意到循环变量的设置是通过指令调用时创建的(本例中的<list ...>标签)。没有其他的方式去改变循环变量的值(也就是说你不能使用定义指令来改变它的值。)。从上面的示例来看,尽管也可以使用一个循环变量来隐藏另外一个。
有时会发生一个变量隐藏数据模型中的同名变量,但是如果想访问数据模型中的变量,就可以使用特殊变量globals。例如,假设我们在数据模型中有一个名为user,值为”Big Joe”的变量。
想了解更多的变量使用语法,请阅读3.3节:表达式
1. plain
2. plain
3. local
4. loop
5. local
6. plain
7. loop
8. loop
9. plain2
<#list ["loop 1"] as x>
${x}
<#list ["loop 2"] as x>
${x}
<#list ["loop 3"] as x>
${x}
</#list>
${x}
</#list>
${x}
</#list>
loop 1
loop 2
loop 3
loop 2
loop 1
<#assign user = "Joe Hider">
${user} <#-- 打印: Joe Hider -->
${.globals.user} <#-- 打印: Big Joe -->
4.3 命名空间
4.3.1 简介
当运行FTL模板时,就会有使用assign和macro指令创建的变量的集合(可能是空的),可以从前一章节来看如何使用它们。像这样的变量集合被称为namespace命名空间。在简单的情况下可以只使用一个命名空间,称之为main namespace主命名空间。因为通常只使用本页上的命名空间,所以就没有意识到这点。
如果想创建可以重复使用的宏,函数和其他变量的集合,通常用术语来说就是引用library库。使用多个命名空间是必然的。只要考虑你在一些项目中,或者想和他人共享使用的时候,你是否有一个很大的宏的集合。但要确保库中没有宏(或其他变量)名和数据模型中变量同名,而且也不能和模板中引用其他库中的变量同名。通常来说,变量因为名称冲突也会相互冲突。所以要为每个库中的变量使用不同的命名空间。
4.3.2 创建一个库
我们来建立一个简单的库。假设你需要通用的变量copyright和mail(在你疑问之前,宏当作是变量):
把上面的这些定义存储在文件lib/my_test.ftl中(目录是你存放模板的位置)。假设想在aWebPage.ftl中使用这个模板。如果在aWebPage.ftl使用<#include "/lib/my_test.ftl">,那么就会在主命名空间中创建两个变量,这样就不是很好,因为想让它们只在同一个命名空间”My Test Library”中。所以就不得不使用import指令来代替include了。乍一看,这个指令和include很相似,但是它会为lib/my_test.ftl创建一个空的命名空间,然后在那里执行。lib/my_test.ftl会发现它自己在一个新的环境中,那里只有数据模型的变量可以来呈现(因为它们在那里面都是可见的),然后会在这个环境中创建两个变量。现在来看这很不错,但是如果想访问aWebPage.ftl中的两个变量,而它们使用的是主命名空间,就不能看到其他命名空间中的变量。解决方法是import指令不仅仅创建命名空间,而且要通过import的调用者(本例中的主命名空间)创建一个新的哈希表变量,这就成为进入新的命名空间的大门。那么aWebPage.ftl就像下面这样:
要注意它是怎么访问为lib/my_test.ftl创建的命名空间中的变量的,通过新创建的哈希表,my。那么将会打印出:
<#macro copyright date>
<p>Copyright (C) ${date} Julia Smith. All rights reserved.</p>
</#macro>
<#assign mail = "jsmith@acme.com">
<#import "/lib/my_test.ftl" as my>
<#-- 被称为"my"的哈希表就会是那个"大门" -->
<@my.copyright date="1999-2002"/>
${my.mail}
如果在主命名空间中有一个变量,名为mail或copyright,那么就不会引起混乱了,因为两个模板使用了不同的命名空间。例如,在lib/my_test.ftl中修改copyright成如下这样:
然后修改aWebPage.ftl中的内容:
那么将会输出:
当调用了copyright宏之后,输出和上面的是相似的,因为FreeMarker已经暂时转向由import指令为/lib/my_test.ftl生成的命名空间了。因此,copyright宏看到在主命名空间中变量mail存在,而且其他的mail不存在。
4.3.3在引入的命名空间上编写变量
偶尔想要在一个被包含的命名空间上创建或替换一个变量。那么可以使用assign指令在完成,如果用到了它的namespace变量,例如下面这样:
将会输出:
<p>Copyright (C) 1999-2002 Julia Smith. All rights reserved.</p>
jsmith@acme.com
<#macro copyright date>
<p>Copyright (C) ${date} Julia Smith. All rights reserved.
<br>Email: ${mail}</p>
</#macro>
<#import "/lib/my_test.ftl" as my>
<#assign mail="fred@acme.com">
<@my.copyright date="1999-2002"/>
${my.mail}
${mail}
<p>Copyright (C) 1999-2002 Julia Smith. All rights reserved.
<br>Email: jsmith@acme.com</p>
jsmith@acme.com
fred@acme.com
<#import "/lib/my_test.ftl" as my>
${my.mail}
<#assign mail="jsmith@other.com" in my>
${my.mail}
jsmith@acme.com
jsmith@other.com
4.3.4 命名空间和数据模型
数据模型中的变量在任何位置都是可见的。如果在数据模型中有一个名为user的变量,那么lib/my_test.ftl也能访问它,aWebPage.ftl当然也能。
如果user是”Fred”的话,下面这个例子:
将会输出:
不要忘了在模板的命名空间(可以使用assign或macro指令来创建的变量)中的变量有着比数据模型中的变量更高的优先级。因此,数据模型的内容不会干涉到由库创建的变量。
注意:
在通常一些应用中,你也许想在模板中创建所有命名空间都可见的变量,就像数据模型中的变量一样。但是你不能在模板中改变数据模型,却可以通过global指令来达到相似的效果,可以阅读参考手册来获得更多信息。
4.3.5 命名空间的生命周期
命名空间由使用的import指令中所写的路径来识别。如果想多次import这个路径,那么只会为第一次的import引用创建命名空间执行模板。后面相同路径的import只是创建一个哈希表当作访问相同命名空间的“门”。例如,在aWebPage.ftl中:
将会输出:
这里可以看到通过my,foo和bar访问相同的命名空间。
<#macro copyright date>
<p>Copyright (C) ${date} ${user}. All rights reserved.</p>
</#macro>
<#assign mail = "${user}@acme.com">
<#import "/lib/my_test.ftl" as my>
<@my.copyright date="1999-2002"/>
${my.mail}
<p>Copyright (C) 1999-2002 Fred. All rights reserved.</p>
Fred@acme.com
<#import "/lib/my_test.ftl" as my>
<#import "/lib/my_test.ftl" as foo>
<#import "/lib/my_test.ftl" as bar>
${my.mail}, ${foo.mail}, ${bar.mail}
<#assign mail="jsmith@other.com" in my>
${my.mail}, ${foo.mail}, ${bar.mail}
jsmith@acme.com, jsmith@acme.com, jsmith@acme.com
jsmith@other.com, jsmith@other.com, jsmith@other.com
还要注意命名空间是不分层次的,它们相互之间是独立存在的。那么,如果在命名空间N1中import命名空间N2,那N2也不在N1中,N1只是可以通过哈希表来访问N2。这和在主命名空间中importN2,然后直接访问命名空间N2是一样的过程。
每一次模板的执行过程,它都有一个私有的命名空间的集合。每一次模板执行工作都是一个分离且有序的过程,它们仅仅存在一段很短的时间,同时页面用以呈现内容,然后就和所有填充过的命名空间一起消失了。因此,无论何时我们说第一次调用import,一个单一模板执行工作的内容都是这样。
4.3.6为他人编写库
如果你已经为其他人员编写一个有用的,高质量的库,你也许想把它放在网络上(就像http://freemarker.org/libraries.html)。为了防止和其他作者使用库的命名相冲突,而且引入其他库时要书写简单,这有一个事实上的标准,那就是指定库路径的格式。这个标准是:库的路径必须对模板和其他库可用(可引用),就像这样:
/lib/yourcompany.com/your_library.ftl
如果你为Example公司工作,它们拥有www.example.com网的主页,你的工作是开发一个部件库,那么要引入你所写的FTL的路径应该是:
/lib/example.com/widget.ftl
注意到www已经被省略了。第三次路径分割后的部分可以包含子目录,可以像下面这样写:
/lib/example.com/commons/string.ftl
一个重要的规则就是路径不应该包含大写字母,为了分隔词语,使用下划线_,就像wml_form(而不是wmlForm)。
如果你的工作不是为公司或组织开发库,也要注意,你应该使用项目主页的URL,比如/lib/example.sourceforge.net/example.ftl或/lib/geocities.com/jsmith/example.ftl。
4.4 空白处理
4.4.1 简介
在运行中,模板中的空白在某种程度上来说是纠缠所有模板引擎的一个问题。 我们来看这个模板。我已经用颜色标记了模板中的组件:文本,插值, FTL 标签。使用[BR]-s来想象换行。
如果FreeMarker能按照规则输出所有的问题,那将会输出:
这里有太多的不想要的空格和换行了。幸运的是,HTML和XML都不是对空白敏感的,但是这么多多余的空白是很令人头疼的,而且增加处理后的HTML文件大小也是没必要的。当然,对于空白敏感的方式的输出这依旧是个大问题。
FreeMarker提供下面的工具来处理这个问题:
 忽略某些模板文件的空白的工具(解析阶段空白就被移除了):
 剥离空白:这个特性会自动忽略在FTL标签周围多余的空白。这个特性可以通过模板来随时使用和禁用。
 微调指令:t,rt和lt,使用这些指令可以明确地告诉FreeMarker去忽略某些空白。可以阅读参考手册来获取更多信息。
 FTL参数strip_text:这将从模板中删除所有顶级文本。对模板来说这很有用,它只包含某些定义的宏(还有以他一些没有输出的指令),因为它可以移除宏定义和其他顶级指令中的换行符,这样可以提高模板的可读性。
 从输出中移除空白的工具(移除临近的空白): <p>List of users:[BR] <#assign users = [{"name":"Joe", "hidden":false},[BR] {"name":"James Bond", "hidden":true},[BR] {"name":"Julia", "hidden":false}]>[BR] <ul>[BR] <#list users as user>[BR] <#if !user.hidden>[BR] <li>${user.name}[BR] </#if>[BR] </#list>[BR] </ul>[BR] <p>That's all. <p>List of users:[BR] [BR] <ul>[BR] [BR] [BR] <li>Joe[BR] [BR] [BR] [BR] [BR] [BR] <li>Julia[BR] [BR] [BR] </ul>[BR] <p>That's all.
 compress指令
4.4.2 剥离空白
如果对于模板来说使这个特性成为可能的话,那么它就会自动忽略(也就是不在输出中打印出来)两种典型的多余空白:
 缩进空白和在行末尾的尾部空白(包括换行符)将会被忽略,只会留下FTL标签(比如<@myMacro/>,<#if ...>)和FTL注释(如<#-- blah -->),除了被忽略的空白本身。例如,如果一行只包含一个<#if ...>,那么在标签前面的缩进和标签后面的换行符将会被忽略。然而,如果这行上包含<#if ...>x,那么空白就不会被忽略,因为这个x不是FTL标签。注意,根据这些规则,一行上包含<#if ...><#list ...>,空白就会被忽略,而一行上有<#if ...> <#list ...> 这样的就不会,因为在两个FTL标签之间的空白是嵌入的空白,而不是缩进的或尾部空白。
 加在下面这些指令之间的空白会被忽略:macro,function,assign,global,local,ftl,import,但也是仅仅指令之间只有一个空白或FTL注释。实际应用中,它意味着你可以在宏定义和参数定义之间放置空行,因为行间距是为了更好的可读性,不包括打印不必要的空行(换行符)。
使用了剥离空白后,上面那个例子中的输出就会是:
这是因为在剥离之后,模板就变成这样了,被忽略的文本没有被标色:
剥离空白功能可以通过ftl指令在模板中开启或关闭。如果你没有通过ftl指令来指定,那么剥离空白功能是开启还是关闭就依据程序员是如何设置FreeMarker的了。默认的情况下剥离空白是开启的,程序员可以留着不管(建议这样做)。注意开启剥离空白时不<p>List of users:[BR] <ul>[BR] <li>Joe[BR] <li>Julia[BR] </ul>[BR] <p>That's all. <p>List of users:[BR] <#assign users = [{"name":"Joe", "hidden":false},[BR] {"name":"James Bond", "hidden":true},[BR] {"name":"Julia", "hidden":false}]>[BR] <ul>[BR] <#list users as user>[BR] <#if !user.hidden>[BR] <li>${user.name}[BR] </#if>[BR] </#list>[BR] </ul>[BR] <p>That's all.
会降低模板执行的效率,剥离空白的操作在模板加载时就已经完成了。
剥离空白可以为单独的一行关闭,就是使用nt指令(对没有去掉空白的行来说)。
4.4.3使用compress指令
另外一种方法就是使用compress指令,和剥离空白相反,这个工作是直接基于生成的输出内容,而不是对于模板进行。也就是说,它会动态地检查输出内容,而不会检查生成输出FTL的程序。它会很强势地移除缩进,空行和重复的空格/制表符(可以阅读参考手册部分来获取更多信息)。所以对于下面这段代码:
将会输出:
注意compress是完全独立于剥离空白特性的。所以它剥离模板中的空白是可能的。那么之后输出的内容就是被压缩过的。
此外,在默认情况下,名为compress的用户自定义指令是可以在数据模型中存在的(由于向下兼容特性)。这和指令是相同的,除了可以选择设置single_line属性,这将会移除所有的介于其中的换行符。在最后那个例子中,如果使用<@compress single_line=true>...</@compress>来代替<#compress>...</#compress>,那么就会得到如下输出:
4.5替换(方括号)语法
注意:
这个特性从FreeMarker 2.3.4版本后才可用。
FreeMarker支持一个替换的语法。就是在FreeMarker的指令和注释中用[和]来代替<
<#compress>
<#assign users = [{"name":"Joe", "hidden":false},
{"name":"James Bond", "hidden":true},
{"name":"Julia", "hidden":false}]>
List of users:
<#list users as user>
<#if !user.hidden>
- ${user.name}
</#if>
</#list>
That's all.
</#compress>
List of users:
- Joe
- Julia
That's all.
List of users: - Joe - Julia That's all.
和>,例如下面这个例子:
 调用预定义指令:[#list animals as being]...[/#list]
 调用自定义指令:[@myMacro /]
 注释:[#-- the comment --]
为了使用这种语法从而代替默认语法,从模板开始,使用ftl指令都要使用这用语法。如果你不知道什么是ftl指令,那么就用[#ftl]来开始模板,要记住这个要放在文件的最前面(除了它前面的空格)。例如,下面的示例入门章节的最后一个例子使用这种替换语法的样子(假设这是一个完整的模板,而不是一个片段)。
这种替换语法(方括号)和默认语法(尖括号)在一个模板中是相互排斥的。那就是说,整个模板要么全部使用替换语法,要么全部使用默认语法。如果模板使用了替换语法,那么如<#if ...>这样的部分就会被算作是静态文本,而不是FTL标签了。类似地,如果模板使用默认语法,那么如[#if ...]这样的也会被算作是静态文本,而不是FTL标签。
如果你以[#ftl ...](...代表可选的参数列表,当然仅用[#ftl]s也行)来开始文件,那文件就会使用替换(方括号)语法。如果使用<#ftl ...>来开始,那么文件就会使用正常(尖括号)语法。如果文件中没有ftl指令,那么程序员可以通过配置FreeMarker(程序员参看API文档的Configuration.setTagSyntax(int)来使用)来决定使用哪种语法。但是程序员可能使用默认配置。FreeMarker 2.3.x版本默认配置使用常规语法。而2.4版本中的默认配置将会自动检测,也就是说第一个FreeMarker标签决定了语法形式(它可以是任意的,而不仅仅是ftl)。
[#ftl]
<p>We have these animals:
<table border=1>
<tr><th>Name<th>Price
[#list animals as being]
<tr>
<td>
[#if being.size = "large"]<b>[/#if]
${being.name}
[#if being.size = "large"]</b>[/#if]
<td>${being.price} Euros
[/#list]
</table>
第二部分 程序开发指南
第一章 程序开发入门
注意:
如果你还是使用FreeMarker的新手,你应该先阅读模板开发指南部分的入门章节。
1.1 创建配置实例
首先,你应该创建一个freemarker.template.Configuration的实例,然后调整它的设置。Configuration实例是存储FreeMarker应用级设置的核心部分。同时,它也处理创建和缓存预解析模板的工作。
也许你只在应用(可能是servlet)生命周期的开始执行它一次:
从现在开始,应该使用单实例配置。要注意不管一个系统有多少独立的组件来使用FreeMarker,它们都会使用他们自己私有的Configuration实例。
1.2 创建数据模型
在简单的示例中你可以使用java.lang和java.util包下的类,还有用户自定义的Java Bean来构建数据对象。
 使用java.lang.String来构建字符串。
 使用java.lang.Number来派生数字类型。
 使用java.lang.Boolean来构建布尔值。
 使用java.util.List或Java数组来构建序列。
 使用java.util.Map来构建哈希表。
 使用你自己定义的bean类来构建哈希表,bean中的项和bean的属性对应。例如product中的price属性可以用product.price来获取。(bean的action也可以通过这种方式拿到,要了解更多可以参看:bean的包装部分内容)。
让我们为模板开发指南部分演示的第一个例子来构建数据模型,为了方便说明,这里再展示一次示例:
Configuration cfg = new Configuration();
// 指定模板文件从何处加载的数据源,这里设置成一个文件目录。
cfg.setDirectoryForTemplateLoading(
new File("/where/you/store/templates"));
// 指定模板如何检索数据模型,这是一个高级的主题了…
// 但先可以这么来用:
cfg.setObjectWrapper(new DefaultObjectWrapper());
下面是构建这个数据模型的Java代码片段:
对于latestProduct你也可以使用有url和name属性的Java Bean(也就是说,对象要有公共的String getURL()和String getName()方法);它和模板的观点相同。
1.3 获得模板
模板代表了freemarker.template.Template的实例。典型的做法是从Configuration实例中获取一个Template实例。无论什么时候你需要一个模板实例,都可以使用它的getTemplate方法来获取。在之前设置的目录中,用test.ftl存储示例模板,那么就可以这样来做:
当调用这个方法的时候,将会创建一个test.ftl的Template实例,通过文件读取,然后解析(编译)它。Template实例以解析后的形式存储模板,而不是以源文件的文本形式。
Configuration缓存Template实例,当再次获得test.ftl时,它可能不会创建新的Template实例(因此不会读取和解析文件),而是返回第一次创建的实例。
(root)
|
+- user = "Big Joe"
|
+- latestProduct
|
+- url = "products/greenmouse.html"
|
+- name = "green mouse"
// 创建根哈希表
Map root = new HashMap();
// 在根中放入字符串"user"
root.put("user", "Big Joe");
// 为"latestProduct"创建哈希表
Map latest = new HashMap();
// 将它添加到根哈希表中
root.put("latestProduct", latest);
// 在latest中放置"url"和"name"
latest.put("url", "products/greenmouse.html");
latest.put("name", "green mouse");
Template temp = cfg.getTemplate("test.ftl");
1.4 合并模板和数据模型
我们都已经知道的,数据模型+模板=输出,我们已经有了一个数据模型(root)和一个模板(temp)了,所以为了得到输出就需要合并它们。这是由模板的process方法完成的。它用数据模型的根和Writer对象作为参数,然后向Writer对象写入产生的内容。为简单起见,这里我们只做标准的输出:
这会向你的终端输出你在模板开发指南部分的第一个示例中看到的内容。
一旦获得了Template实例,就能将它和不同的数据模型进行不限次数(Template实例是无状态的)的合并。此外,当Template实例创建之后test.ftl文件才能访问,而不是调用处理方法时。
1.5将代码放在一起
这是一个由之前的代码片段组合在一起的源程序文件。千万不要忘了将freemarker.jar放到CLASSPATH中。
Writer out = new OutputStreamWriter(System.out);
temp.process(root, out);
out.flush();
import freemarker.template.*;
import java.util.*;
import java.io.*;
public class Test {
public static void main(String[] args) throws Exception {
/* 在整个应用的生命周期中,这个工作你应该只做一次。 */
/* 创建和调整配置。 */
Configuration cfg = new Configuration();
cfg.setDirectoryForTemplateLoading(
new File("/where/you/store/templates"));
cfg.setObjectWrapper(new DefaultObjectWrapper());
/* 在整个应用的生命周期中,这个工作你可以执行多次 */
/* 获取或创建模板*/
Template temp = cfg.getTemplate("test.ftl");
/* 创建数据模型 */
Map root = new HashMap();
root.put("user", "Big Joe");
Map latest = new HashMap();
root.put("latestProduct", latest);
latest.put("url", "products/greenmouse.html");
latest.put("name", "green mouse");
/* 将模板和数据模型合并 */
Writer out = new OutputStreamWriter(System.out);
注意:
为了简单起见,这里压制了异常(在方法签名中声明了异常,译者注),而在正式运行的产品中不要这样做。
temp.process(root, out);
out.flush();
}
}
第二章 数据模型
这只是一个介绍性的说明,可以查看FreeMarker Java API文档获取更多信息。
2.1 基本内容
在程序开发的入门章节中,我们已经知道如何使用基本的Java类(Map,String等)构建一个数据模型了。在内部,模板中可用的变量都是实现了freemarker.template.TemplateModel接口的Java对象。但在你自己的数据模型中,可以使用基本的Java集合类作为变量,因为这些变量会在内部被替换为适当的TemplateModel类型。这种功能特性被称作是object wrapping对象包装。对象包装功能可以透明地把任何类型的对象转换为实现了TemplateModel接口类型的实例。这就使得下面的转换成为可能,如在模板中把java.sql.ResultSet转换为序列变量,把javax.servlet.ServletRequest对象转换成包含请求属性的哈希表变量,甚至可以遍历XML文档作为FTL变量。包装(转换)这些对象,需要使用合适的,也就是所谓的对象包装器实现(可能是自定义的实现);这将在后面讨论。现在的要点是想从模板访问任何对象,它们早晚都要转换为实现了TemplateModel接口的对象。那么首先你应该熟悉来写TemplateModel接口的实现类。
有一个freemarker.template.TemplateModel粗略的子接口对应每种基本变量类型(TemplateHashModel对应哈希表,TemplateSequenceModel对应序列,TemplateNumberModel对应数字等等)。例如,想为模板使用java.sql.ResultSet变量作为一个序列,那么就需要编写一个TemplateSequenceModel的实现类,这个类要能够读取java.sql.ResultSet中的内容。我们常这么说,你使用TemplateModel的实现类包装了java.sql.ResultSet,基本上只是封装java.sql.ResultSet,来提供使用普通的TemplateSequenceModel接口访问它。要注意一个类可以实现多个TemplateModel接口,这就是为什么FTL变量可以有多种类型(参考:模板开发指南/数值和类型/基本内容部分)。
注意这些接口的一个细小的实现是和freemarker.template包一起提供的。例如,将一个String转换成FTL的字符串变量,可以使用SimpleScalar,将java.util.Map转换成FTL的哈希表变量,可以使用SimpleHash等等。
如果想尝试自己的TemplateModel实现,一个简单的方式是创建它的实例,然后将这个实例放入数据模型中(也就是把它放在哈希表的根上)。对象包装器将会给模板提供它的原状,因为它已经实现了TemplateModel接口,所以没有转换(包装)的需要。(这个技巧当你不想用对象包装器来包装(转换)某些对象时仍然有用。)
2.2 标量
有4种类型的标量:
 布尔值
 数字
 字符串
 日期
每一种标量类型都是TemplateTypeModel接口的实现,这里的Type就是类型的名称。这些接口只定义了一个方法type getAsType();它返回变量的Java类型(boolean,Number,String和Date各自代表的值)的值。
注意:
由于历史遗留的原因,字符串标量的接口是TemplateScalarModel,而不是TemplateStringModel。
这些接口的一个细小的实现和SimpleType类名在freemarker.template包中是可用的。但是却没有SimpleBooleanModel类型;为了代表布尔值,可以使用TemplateBooleanModel.TRUE和TemplateBooleanModel.FALSE来单独使用。
注意:
由于历史遗留的原因,字符串标量的实现类是SimpleScalar,而不是SimpleString。
在FTL中标量是一成不变的。当在模板中设置变量的值时,使用其他的实例来替换TemplateTypeModel实例时,是不用改变原来实例中存储的值的。
2.2.1 数据类型的难点
数据类型还有一些复杂,因为Java API通常不区别java.util.Date-s,只存储日期部分(April 4, 2003),时间部分(10:19:18 PM),或两者都存(April 4, 2003 10:19:18 PM)。为了用本文正确显示一个日期变量,FreeMarker必须知道java.util.Date的哪个部分存储了有意义上的信息,哪部分没有被使用。不幸的是,Java API在这里明确的说,由数据库控制(SQL),因为数据库通常有分离的日期,时间和时间戳(又叫做日期-时间)类型,java.sql有3个对应的java.util.Date子类和它们相匹配。
TemplateDateModel接口有两个方法:分别是java.util.Date getAsDate()和int getDateType()。这个接口典型的实现是存储一个java.util.Date对象,加上一个整数来辨别“数据库存储的类型”。这个整数的值也必须是TemplateDateModel接口中的常量之一:DATE,TIME,DATETIME和UNKNOWN。
什么是UNKNOWN呢?我们之前说过,java.lang和java.util下的类通常被自动转换成TemplateModel的实现类,就是所谓的对象包装器。当对象转换器面对一个java.util.Date对象时,而不是java.sql日期类的实例,它就不能确定“数据库存储的类型”是什么,所以就使用UNKNOWN。往后执行,如果模板需要使用这个变量,操作也需要使用“数据存储的类型”,那就会停止执行并抛出错误。为了避免这种情况的发生,对于那些可能有问题的变量,模板开发人员需要帮助FreeMarker决定“数据库存储的类型”,使用内建函数date,time或datetime就可以解决了。注意一下,如果对要格式化参数使用内建函数string,比如foo?string("MM/dd/yyyy"),那么FreeMarker就不必知道“数据库存储的类型”了。
2.3 容器
容器包括哈希表,序列和集合三种类型。
2.3.1 哈希表
FreeMarker中的哈希表是实现了TemplateHashModel接口的Java对象。TemplateHashModel接口有两个方法:TemplateModel get(String key),这个方法根据给定的名称返回子变量,boolean isEmpty()这个方法表明哈希表是否含有子变量。get方法当在给定的名称没有找到子变量时返回null。
TemplateHashModelEx接口扩展了TemplateHashModel接口。它增加了更多的方法,使得可以使用内建函数values和keys来枚举哈希表中的子变量。
经常使用的实现类是SimpleHash,该类实现了TemplateHashModelEx接口。从内部来说,它使用一个java.util.Hash类型的对象存储子变量。SimpleHash类的方法可以添加和移除子变量。这些方法应该用来在变量被创建之后直接初始化。
在FTL中,容器是一成不变的。那就是说你不能添加,替换和移除容器中的子变量。
2.3.2 序列
序列是实现了TemplateSequenceModel接口的Java对象。它包含两个方法:TemplateModel get(int index)和int size()。
经常使用的实现类是SimpleSequence,该类内部使用一个java.util.List类型的对象存储它的子变量。SimpleSequence有添加子元素的方法。在序列创建之后应该使用这些方法来填充序列。
2.3.3 集合
集合是实现了TemplateCollectionModel接口的Java对象。这个接口只定义了一个方法:TemplateModelIterator iterator()。TemplateModelIterator接口和java.util.Iterator相似,但是它返回TemplateModels而不是Object-s,而且它能抛出TemplateModelException异常。
通常使用的实现类是SimpleCollection。
2.4 方法
方法变量在存于实现了TemplateMethodModel接口的模板中。这个接口仅包含一个方法:TemplateModel exec(java.util.List arguments)。当使用方法调用表达式调用方法时,exec方法将会被调用。形参将会包含FTL方法调用形参的值。exec方法的返回值给出了FTL方法调用表达式的返回值。
TemplateMethodModelEx接口扩展了TemplateMethodModel接口。它没有任何新增的方法。事实上这个对象实现这个标记接口暗示给FTL引擎,形式参数应该直接以TemplateModel-s形式放进java.util.List。否则将会以String-s形式放入List。
一个很明显的原因是这些接口没有默认的实现。
例如这个方法,返回第一个字符串在第二个字符串第一次出现时的索引位置,如果第二个字符串中不包含第一个字符串,则返回“-1”:
如果将一个实例放入根数据模型中,像这样:
那么就可以在模板中调用:
那么输出为:
如果需要访问FTL运行时环境(读/写变量,获取本地信息等),则可以使用Environment.getCurrentEnvironment()来获取。
2.5 指令
Java程序员可以使用TemplateDirectiveModel接口在Java代码中实现自定义指令。详情可以参加API文档。
注意:
TemplateDirectiveModel在FreeMarker 2.3.11版本时才加入。用来代替快被废弃的TemplateTransformModel。
2.5.1 第一个示例
我们要实现一个指令,这个指令可以将在它开始标签和结束标签之内的字符都转换为大写形式。就像这个模板:
public class IndexOfMethod implements TemplateMethodModel {
public TemplateModel exec(List args) throws TemplateModelException {
if (args.size() != 2) {
throw new TemplateModelException("Wrong arguments");
}
return new SimpleNumber(
((String) args.get(1)).indexOf((String) args.get(0)));
}
}
root.put("indexOf", new IndexOfMethod());
<#assign x = "something">
${indexOf("met", x)}
${indexOf("foo", x)}
2
-1
将会输出:
下面是指令类的源代码:
foo
<@upper>
bar
<#-- 这里允许使用所有的FTL -->
<#list ["red", "green", "blue"] as color>
${color}
</#list>
baaz
</@upper>
wombat
foo
BAR
RED
GREEN
BLUE
BAAZ
wombat
package com.example;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
/**
* FreeMarker的用户自定义指令在逐步改变
* 它嵌套内容的输出转换为大写形式
* <p><b>指令内容</b></p>
* <p>指令参数:无
* <p>循环变量:无
* <p>指令嵌套内容:是
*/
public class UpperDirective implements TemplateDirectiveModel {
public void execute(Environment env, Map params,
TemplateModel[] loopVars, TemplateDirectiveBody body)
throws TemplateException, IOException {
// 检查参数是否传入
if (!params.isEmpty()) {
throw new TemplateModelException(
"This directive doesn't allow parameters.");
}
if (loopVars.length != 0) {
throw new TemplateModelException("This directive
doesn't allow loop variables.");
}
// 是否有非空的嵌入内容
if (body != null) {
// 执行嵌入体部分,和FTL中的<#nested>一样,除了
// 我们使用我们自己的writer来代替当前的output writer.
body.render(new UpperCaseFilterWriter(env.getOut()));
} else {
throw new RuntimeException("missing body");
}
}
/**
* {@link Writer}改变字符流到大写形式,
* 而且把它发送到另外一个{@link Writer}中。
*/
private static class UpperCaseFilterWriter extends Writer {
private final Writer out;
UpperCaseFilterWriter (Writer out) {
this.out = out;
}
public void write(char[] cbuf, int off, int len)
throws IOException {
char[] transformedCbuf = new char[len];
for (int i = 0; i < len; i++) {
transformedCbuf[i] = Character.
toUpperCase(cbuf[i + off]);
}
out.write(transformedCbuf);
}
public void flush() throws IOException {
out.flush();
}
现在我们需要创建这个类的实例,然后让这个指令在模板中可以通过名称“upper”来访问(或者是其它我们想用的名字)。一个可行的方案是把这个指令放到数据模型中:
但更好的做法是将常用的指令作为共享变量放到Configuration中。
当然也可以使用内建函数new将指令放到一个FTL库(宏的集,就像在模板中,使用include或import)中。
2.5.2 第二个示例
我们来创建一个指令,这个指令可以一次又一次地执行其中的嵌套内容,这个次数由指定的数字来确定(就像list指令),可以使用<hr>-s将输出的重复内容分开。这个指令我们命名为“repeat”。示例模板如下:
输出为:
public void close() throws IOException {
out.close();
}
}
}
root.put("upper", new com.example.UpperDirective());
<#-- 也许在FTL中你已经有了实现了的指令 -->
<#macro something>
...
</#macro>
<#-- 现在你不能使用<#macro upper>,但是你可以使用: -->
<#assign upper = "com.example.UpperDirective"?new()>
<#assign x = 1>
<@repeat count=4>
Test ${x}
<#assign x = x + 1>
</@repeat>
<@repeat count=3 hr=true>
Test
</@repeat>
<@repeat count=3; cnt>
${cnt}. Test
</@repeat>
指令的实现类为:
Test 1
Test 2
Test 3
Test 4
Test
<hr> Test
<hr> Test
1. Test
2. Test
3. Test
package com.example;
import java.io.IOException;
import java.io.Writer;
import java.util.Iterator;
import java.util.Map;
import freemarker.core.Environment;
import freemarker.template.SimpleNumber;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
/**
* FreeMarker用户自定义指令来重复模板中的一部分,可选的是使用
* <tt>&lt;hr&gt;</tt>来分隔输出内容中的重复部分。
* <p><b>指令内容</b></p>
* <p>参数:
* <ul>
* <li><code>count</code>: 重复的次数。必须!
* 必须是一个非负的数字,如果它不是一个整数,那么小数部分就会被。
* <em>舍去</em>.
* <li><code>hr</code>: 用来辨别HTML的 "hr"元素是否在重复内容之
* 间被打印出来。布尔值。 可选, 默认是<code>false</code>。
* </ul>
* <p>循环变量: 1, 可选的。它给定了当前重复内容的数量,从1开始。
* <p>嵌套内容: 是
*/
public class RepeatDirective implements TemplateDirectiveModel { private static final String PARAM_NAME_COUNT = "count";
private static final String PARAM_NAME_HR = "hr";
public void execute(Environment env,Map params,
TemplateModel[] loopVars,TemplateDirectiveBody body)
throws TemplateException, IOException {
// 处理参数:
int countParam = 0;
boolean countParamSet = false;
boolean hrParam = false;
Iterator paramIter = params.entrySet().iterator();
while (paramIter.hasNext()) {
Map.Entry ent = (Map.Entry) paramIter.next();
String paramName = (String) ent.getKey();
TemplateModel paramValue = (TemplateModel)
ent.getValue();
if (paramName.equals(PARAM_NAME_COUNT)) {
if (!(paramValue instanceof TemplateNumberModel)) {
throw new TemplateModelException("The \"" +
PARAM_NAME_HR + "\" parameter " +
"must be a number.");
}
countParam = ((TemplateNumberModel) paramValue)
.getAsNumber().intValue();
countParamSet = true;
if (countParam < 0) {
throw new TemplateModelException("The \"" +
PARAM_NAME_HR + "\" parameter " +
"can't be negative.");
}
} else if (paramName.equals(PARAM_NAME_HR)) {
if (!(paramValue instanceof TemplateBooleanModel)){
throw new TemplateModelException("The \"" +
PARAM_NAME_HR + "\" parameter " +
"must be a boolean.");
}
hrParam = ((TemplateBooleanModel) paramValue)
.getAsBoolean();
} else {
throw new TemplateModelException(
"Unsupported parameter: " + paramName);
}
}
2.5.3 提示
TemplateDirectiveModel对象通常是有状态的,这一点非常重要。一个经常犯的错误是存储指令的状态然后在对象的属性中调用执行。想一下相同指令的嵌入调用,或者指令对象被用作共享变量,并通过多线程同时访问。
不幸的是,不支持传递参数的位置(而不是参数名称)。从FreeMarker的2.4版本开始,它将被修正。
2.6 节点变量
节点变量体现了树形结构中的节点。节点变量的引入是为了帮助用户在数据模型中处理XML文档,但是它们也可以用于构建树状模型。如需要有关从模板语言角度考虑的节点信息,
if (!countParamSet) {
throw new TemplateModelException("The required \""
+ PARAM_NAME_COUNT + "\" paramter" +
"is missing.");
}
if (loopVars.length > 1) {
throw new TemplateModelException(
"At most one loop variable is allowed.");
}
// 是啊, 它很长而且很无聊...
// 执行真正指令的执行部分:
Writer out = env.getOut();
if (body != null) {
for (int i = 0; i < countParam; i++) {
// 如果"hr"参数为真,那么就在所有重复部分之间打印<hr>:
if (hrParam && i != 0) {
out.write("<hr>");
}
// 如果有循环变量,那么就设置它:
if (loopVars.length > 0) {
loopVars[0] = new SimpleNumber(i + 1);
}
// 执行嵌入体部分(和FTL中的<#nested>一样)。
// 这种情况下,我们不提供一个特殊的writer作为参数:
body.render(env.getOut());
}
}
}
}
那么可以阅读之前模板开发指南部分2.2.5.1节的内容。
节点变量有下列属性,它们都由TemplateNodeModel接口的方法提供。
 基本属性:
 TemplateSequenceModel getChildNodes():一个节点的子节点序列(除非这个节点是叶子节点,这时方法返回一个空序列或者是null)。子节点本身应该也是节点变量。
 TemplateNodeModel getParentNode():一个节点只有一个父节点(除非这个节点是节点树的根节点,这时方法返回null)。
 可选属性。如果一个属性在具体的使用中没有意义,那对应的方法应该返回null:
 String getNodeName():节点名称也是宏的名称,当使用recurse和visit指令时,它用来控制节点。因此,如果想通过节点使用这些指令,那么节点的名称是必须的。
 String getNodeType():在XML技术中:"element","text","comment"等类型。如果这些信息可用,就是通过recurse和visit指令来查找节点的默认处理宏。而且,它对其他有具体用途的应用程序也是有用的。
 String getNamespaceURI():这个节点所属的命名空间(和用于库的FTL命名空间无关)。例如,在XML中,这就是元素和属性所属XML命名空间的URI。这个信息如果可用,就是通过recurse和visit指令来查找存储控制宏的FTL命名空间。
在FTL这里,节点属性的直接使用可以通过内建函数node完成,还有visit和recurse宏。
在很多使用情况下,实现了TemplateNodeModel接口和其它接口的变量,因为节点变量属性仅仅提供基本的节点间导航的方法。需要具体的例子,请参考FreeMarker如何处理XML部分。
2.7 对象包装
当往容器中添加一些对象时,正如在FreeMarker API文档中看到的那样,它可以收到任意java对象类型的参数,而不一定是TemplateModel。这是因为模板实现时会默默地用合适的TemplateModel对象来替换原有对象。比如向容器中加入一个String,也许它将被替换为一个SimpleScalar实例来存储相同的文本。
至于替换什么时候发生,这就是容器业务处理的问题(类的业务实现了容器接口)所在,但是它在获取子变量时必须会发生,因为getter方法(依据接口而定)会返回TemplateModel,而不是Object。SimpleHash,SimpleSequence和SimpleCollection使用最懒的策略,当第一次获取子变量时,它们用一个适合的TemplateModel来替换一个非TemplateModel子变量。
至于什么类型的Java对象可以被替换,又使用什么样的TemplateModel来实现,它可以被实现的容器自身来控制,也可以委派给ObjectWrapper的一个实例。ObjectWrapper是一个接口,其中只定义了一个方法:TemplateModel wrap(java.lang.Object obj)。可以传递一个Object类型的参数,它会返回对应的TemplateModel对象,如果不行则抛出TemplateModelException异常。替换原则是在ObjectWrapper的实现类中编码实现的。
最重要的ObjectWrapper实现类是FreeMarker核心包提供的:
 ObjectWrapper.DEFAULT_WRAPPER:它使用SimpleScalar来替换String,SimpleNumber来替换Number,SimpleSequence来替换List和数组,SimpleHash来替换Map,TemplateBooleanModel.TRUE或TemplateBooleanModel.FALSE来替换Boolean,freemarker.ext.dom.NodeModel来替换W3C组织定义的DOM模型节点类型。对于Jython类型的对象,包装器会调用freemarker.ext.jython.JythonWrapper。而对于其他对象,则会调用BEAN_WRAPPER。
 ObjectWrapper.BEANS_WRAPPER:它可以通过Java 的反射机制来获取到Java Bean的属性和其他任意对象类型的成员变量。在最新的FreeMarker 2.3版本中,它是freemarker.ext.beans.BeansWrapper的实例,后面有单独的一章将会来介绍它。
做一个具体的例子,让我们来看看SimpleXxx类型都是怎么工作的。SimpleHash,SimpleSequence和SimpleCollection使用DEFAULT_WRAPPER来包装子变量(除非在构造方法中传递另外一个包装器)。这个例子在实战中来展示DEFAULT_WRAPPER。
假设root是数据模型的root,那么得到的数据模型将是:
Map map = new HashMap();
map.put("anotherString", "blah");
map.put("anotherNumber", new Double(3.14));
List list = new ArrayList();
list.add("red");
list.add("green");
list.add("blue");
SimpleHash root = new SimpleHash(); // 将会使用默认的包装器
root.put("theString", "wombat");
root.put("theNumber", new Integer(8));
root.put("theMap", map);
root.put("theList", list);
(root)
|
+- theString = "wombat"
|
+- theNumber = 8
|
+- theMap
| |
| +- anotherString = "blah"
| |
| +- anotherNumber = 3.14
|
注意在theMap和theList中的Object-s也可以作为子变量来访问。这是因为,当要访问theMap.anotherString时,SimpleHash(这里作为根哈希表)会静默地使用SimpleHash实例来替换Map(theMap),这个实例使用了和根哈希表相同的包装器。所以当访问其中的子变量anotherString时,就会使用SimpleScalar来替换它。
如果在数据模型中放了任意的对象,那么DEFAULT_WRAPPER就会调用BEANS_WRAPPER来包装这个对象:
假设TestObject是这样的:
数据模型就会是这样:
SimpleHash root = new SimpleHash();
// 可以拿到Java对象"simple":
root.put("theString", "wombat");
// 可以拿到Java对象":
root.put("theObject", new TestObject("green mouse", 1200));
public class TestObject {
private String name;
private int price;
public TestObject(String name, int price) {
this.name = name;
this.price = price;
}
// JavaBean的属性
// 注意公有字段不能直接可见;
// 你必须为它们编写getter方法。
public String getName() {return name;}
public int getPrice() {return price;}
// 一个方法
public double sin(double x) {
return Math.sin(x);
}
}
+- theList
|
+- (1st) = "red"
|
+- (2nd) = "green"
|
+- (3rd) = "blue"
我们可以这样把它和模板合并:
将会输出:
之前我们已经看到了,我们使用java.util.HashMap作为根哈希表,而不是SimpleHash或其他特定的FreeMarker类。因为Template.process(...)自动包装了给定的数据模型参数的对象,所以它才会起作用。它使用受Configuration级设置的对象包装器,object_wrapper(除非明确指定一个ObjectWrapper作为它的参数)。因此,编写简单的FreeMarker应用程序就不需要知道TemplateModel-s了。注意根的类型不需要一定是java.util.Map。它也可以是实现了TemplateHashModel接口的被包装的对象。
object_wrapper设置的默认值是ObjectWrapper.DEFAULT_WRAPPER。如果想改变它,比如换成ObjectWrapper.BEANS_WRAPPER,那么可以这样来配置FreeMarker引擎(在其它线程开始使用它之前):
要注意我们可以在这里设置任何对象实现接口ObjectWrapper,当然也可以用来设置你自己定义的实现类。
对于包装了基本Java容器类型(比如java.util.Map-s和java.util.List-s)的TemplateModel实现类,常规是它们使用像它们父容器那样的相同对象包装器来包装它们的子变量。从技术上讲,它们是被父容器(它对所创建的子类有全部的控制器)实例化的,因为父容器创建了它们,所以它们使用和父容器一样的对象包装器。如果BEANS_WRAPPER用来包装根哈希表,那么它也会被用来包装子变量(子变量的子变量也是如此,以此类推)。这个之前看到的theMap.anotherString是同样的现象。
(root)
|
+- theString = "wombat"
|
+- theObject
|
+- name = "green mouse"
|
+- price = 1200
|
+- number sin(number)
${theObject.name}
${theObject.price}
${theObject.sin(123)}
green mouse
1200
-0,45990349068959124
cfg.setObjectWrapper(ObjectWrapper.BEANS_WRAPPER);
第三章 配置
这里仅仅是一个概览,若要了解更多请阅读FreeMarker的API文档。
3.1 基本内容
配置就是在对象中存储常用(应用级别)的设置和定义某些想在所有模板中可用的变量。它们也会处理Template实例的创建和缓存操作。配置对象是freemarker.template.Configuration的实例,可以通过构造方法来创建它。一个应用程序通常只使用一个共享的Configuration实例。
配置对象通过Template的方法来使用,特别是通过process方法。每个实例都有一个确切的而且和它相关的Configuration实例,这个实例由Template的构造方法分配给Template实例。可以指定一个Configuration实例作为它的参数。通常情况下,使用Configuration.getTemplate(而不是直接调用Template的构造方法)来获得Template实例,这种情况下,相关的Configuration实例就会是getTemplate方法被调用时的那个实例。
3.2 共享变量
Shared variables共享变量是为所有模板所定义的变量。可以使用setSharedVariable方法向配置实例中添加共享变量:
在所有使用这个配置的模板中,名为wrap的用户自定义指令和一个名为company的字符串将会在数据模型的根上可见,那就不用在根哈希表上一次又一次的添加它们。在传递给Template.process根对象里的变量将会隐藏同名的共享变量。
警告!
如果配置对象在多线程环境中使用,不要使用TemplateModel实现类来作为共享变量,因为它是线程不安全的。这也是基于Servlet的Web站点的典型情况。
出于向后兼容的特性,共享变量的集合初始化时(就是对于新的Configuration实例来说)不能为空。它包含下列用户自定义指令(用户自定义指令使用时需要用@来代替#):
名称

capture_output
freemarker.template.utility.CaptureOutput
compress
freemarker.template.utility.StandardCompress
html_escape
freemarker.template.utility.HtmlEscape
Configuration cfg = new Configuration();
...
cfg.setSharedVariable("wrap", new WrapDirective());
// 使用 ObjectWrapper.DEFAULT_WRAPPER cfg.setSharedVariable("company", "Foo Inc.");
normalize_newlines
freemarker.template.utility.NormalizeNewlines
xml_escape
freemarker.template.utility.XmlEscape
3.3 配置信息
Settings配置信息是影响FreeMarker行为的已经被命名的值。配置信息的项有:locale,number_format。
配置信息信息存储在Configuration实例中,可以在Template实例中被覆盖。例如在配置对象中给locale设置为"en_US",那么locale在所有模板中都使用"en_US"的配置,除非在模板中locale被明确地设置成其它不同的(参见本地化设置)值。因此,在Configuration中的值充当默认值,这些值在每个模板中也可以被覆盖。在Configuration和Template实例中的值也可以在单独调用Template.process方法后被覆盖。对于每个调用了freemarker.core.Environment对象的值在内部创建时就持有模板执行的运行时环境,也包括了那个级别被覆盖了的设置信息。在模板执行时,那里存储的值也可以被改变,所以模板本身也可以设置配置信息,比如在输出中途来变换locale设置。
配置信息可以被想象成3层(Configuration,Template,Environment),最高层包含特定的值,它为设置信息提供最有效的值。比如(设置信息A到F仅仅是为这个示例而构想的):
Setting A
Setting B
Setting C
Setting D
Setting E
Setting F
Layer 3:
Environment
1
-
-
1
-
-
Layer 2: Template
2
2
-
-
2
-
Layer 1:
Configuration
3
3
3
3
-
-
配置信息的有效值为:A=1,B=2,C=3,D=1,E=2。而F的设置则是null,或者在你获取它的时候将抛出异常。
让我们看看如何准确设置配置信息:
Configuration层:原则上设置配置信息时使用Configuration对象的setter方法,例如:
在真正使用Configuration对象(通常在初始化应用程序时)之前来配置它,后面必须将其视为只读的对象。
在实践中,比如很多Web应用框架中,就应该使用这种框架特定的配置方式来进行配置,比如使用成对的字符串来配置(像在.properties属性配置文件中那样)。在这种情况下,框架的作者大多数使用Configuration对象的setSetting(String name, String value)方法。这可以参考API文档的setSetting部分来获取可用的设置名和参数的格式的信息。而在Spring框架中,我们可以这样进行:
Configuration myCfg = new Configuration();
myCfg.setLocale(java.util.Locale.ITALY);
myCfg.setNumberFormat("0.####");
这种形式的配置(String键-值对)和直接使用Configuration的API相比,很不幸地被限制了。
Template层:这里不需要设置配置信息,除非想替代freemarker.cache.TemplateCache来管理Template对象,这样的话,应该在Template对象第一次被使用前就设置配置信息,然后就将Template对象视为是只读的。
Environment层:这里有两种配置方法:
 使用Java API:使用Environment对象的setter方法。当然想要在模板执行之前来做,然后当调用myTemplate.process(...)时会遇到问题,因为在内部创建Environment对象后立即就执行模板了,导致没有机会来进行设置。这个问题的解决可以用下面两个步骤进行:
 在模板中直接使用指令,例如:
在这层,当什么时候改变配置信息,是没有限制的。
要知道FreeMarker支持什么样的配置信息,先看看FreeMarker Java API文档中的下面这部分内容:
 在三层中freemarker.core.Configurable的setter方法来配置。
 只在Configuration层可用的freemarker.template.Configuration的setter方法来配置。
 在三层中可用String键-值对书写的freemarker.core.Configurable.setSetting(String, String)配置。
 只在Configuration层中可用用String键-值对书写的freemarker.template.Configuration.setSetting(String, String)配置。
<bean id="freemarkerConfig"
class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="freemarkerSettings">
<props>
<prop key="locale">it_IT</prop>
<prop key="number_format">0.####</prop>
</props>
</property>
</bean>
Environment env = myTemplate.createProcessingEnvironment(root, out);
env.setLocale(java.util.Locale.ITALY);
env.setNumberFormat("0.####");
env.process(); // 处理模板
<#setting locale="it_IT">
<#setting number_format="0.####">
3.4 模板加载
3.4.1 模板加载器
模板加载器是加载基于抽象模板路径下,比如"index.ftl"或"products/catalog.ftl"的原生文本数据对象。这由具体的模板加载器对象来确定它们取得请求数据时使用了什么样的数据来源(文件夹中的文件,数据等等)。当调用cfg.getTemplate(这里的cfg就是Configuration实例)时,FreeMarker询问模板加载器是否已经为cfg建立返回给定模板路径的文本,之后FreeMarker解析文本生成模板。
3.4.1.1 内建模板加载器
在Configuration中可以使用下面的方法来方便建立三种模板加载。(每种方法都会在其内部新建一个模板加载器对象,然后创建Configuration实例来使用它。)


上述的第一种方法在磁盘的文件系统上设置了一个明确的目录,它确定了从哪里加载模板。不要说可能,File参数肯定是一个存在的目录。否则,将会抛出异常。
第二种调用方法使用了一个Class类型的参数和一个前缀。这是让你来指定什么时候通过相同的机制来加载模板,不过是用Java的ClassLoader来加载类。这就意味着传入的Class参数会被用来调用Class.getResource()方法来找到模板。参数prefix是给模板的名称来加前缀的。在实际运行的环境中,类加载机制是首选用来加载模板的方法,因为通常情况下,从类路径下加载文件的这种机制,要比从文件系统的特定目录位置加载安全而且简单。在最终的应用程序中,所有代码都使用.jar文件打包也是不错的,这样用户就可以直接执行包含所有资源的.jar文件了。
第三种调用方式需要Web应用的上下文和一个基路径作为参数,这个基路径是Web应用根路径(WEB-INF目录的上级目录)的相对路径。那么加载器将会从Web应用目录开始加载模板。尽管加载方法对没有打包的.war文件起作用,因为它使用了ServletContext.getResource()方法来访问模板,注意这里我们指的是“目录”。如果忽略了第二个参数(或使用了””),那么就可以混合存储静态文件(.html,.jpg等)和.ftl文件,只是.ftl文件可以被送到客户端执行。当然必须在WEB-INF/web.xml中配置一个Servlet来处理URI格式为*.ftl的用户请求,否则客户端无法获取到模板,因此你将会看到Web服务器给出的秘密提示内容。在站点中不能使用空路径,这将成为一个问题,你应该在WEB-INF目录下的某个位置存储模板文件,这样模板源文件就不会偶然
void setDirectoryForTemplateLoading(File dir);
void setClassForTemplateLoading(Class cl, String prefix);
void setServletContextForTemplateLoading(Object servletContext, String path);
地被执行到,这种机制对servlet应用程序来加载模板来说,是非常好用的方式,而且模板可以自动更新而不需重启Web应用程序,但是对于类加载机制,这样就行不通了。
3.4.1.2 从多个位置加载模板
如果需要从多个位置加载模板,那就不得不为每个位置都实例化模板加载器对象,将它们包装到一个被成为MultiTemplateLoader的特殊模板加载器,最终将这个加载器传递给Configuration对象的setTemplateLoader(TemplateLoader loader)方法。下面给出一个使用类加载器从两个不同位置加载模板的示例:
现在,FreeMarker将会尝试从/tmp/templates目录加载模板,如果在这个目录下没有发现请求的模板,它就会继续尝试从/usr/data/templates目录下加载,如果还是没有发现请求的模板,那么它就会使用类加载器来加载模板。
3.4.1.3 从其他资源加载模板
如果内建的类加载器都不适合使用,那么就需要来编写自己的类加载器了,这个类需要实现freemarker.cache.TemplateLoader接口,然后将它传递给Configuration对象的setTemplateLoader(TemplateLoader loader)方法。可以阅读FreeMarker API文档获取更多信息。
如果你的模板需要通过URL访问其他模板,那么就不需要实现TemplateLoader接口了,可以选择子接口freemarker.cache.URLTemplateLoader来替代,只需实现URL getURL(String templateName)方法即可。
3.4.1.4 模板路径
解析模板的路径是由模板解析器来决定的。但是要和其它对路径的格式要求很严格的组件一起工作。通常来说,强烈建议模板加载器使用URL风格的路径。在URL路径(或在UN*X路径)中有其它含义时,那么路径中不要使用/,./,../和:// 。字符*和?是被保留的。而且,模板加载器也不想模板以/开始;FreeMarker从来不会使用这样的路径来调用模板加
import freemarker.cache.*; // 模板加载器在这个包下
...
FileTemplateLoader ftl1 = new FileTemplateLoader(new File("/tmp/templates"));
FileTemplateLoader ftl2 = new FileTemplateLoader(new File("/usr/data/templates"));
ClassTemplateLoader ctl = new ClassTemplateLoader(getClass(), "");
TemplateLoader[] loaders = new TemplateLoader[] { ftl1, ftl2, ctl };
MultiTemplateLoader mtl = new MultiTemplateLoader(loaders);
cfg.setTemplateLoader(mtl);
载器。FreeMarker再将路径传递给模板加载器之前通常会将路径进行正常化操作,所以相对于假想的模板根目录,路径中不会含有/../这些东西。
注意FreeMarker模板加载时经常使用斜线(而不是反斜线),不管运行的主机操作系统是什么。
3.4.2 模板缓存
FreeMarker是会缓存模板的(假设使用Configuration对象的方法来创建Template对象)。这就是说当调用getTemplate方法时,FreeMarker不但返回了Template对象的结果,而且还会将它存储在缓存中,当下一次再以相同(或相等)路径调用getTemplate方法时,那么它只返回缓存的Template实例,而不会再次加载和解析模板文件了。
如果更改了模板文件,当下次调用模板时,FreeMarker将会自动重新载入和解析模板。然而,要检查模板文件是否改变内容了是需要时间的,有一个Configuration级别的设置被称作为“更新延迟”可以用来配置这个时间。这个时间就是从上次对某个模板检查更新后,FreeMarker再次检查模板所要间隔的时间。其默认值是5秒。如果想要看到模板立即更新的效果,那么就要把它设置为0。要注意某些模板加载器也许在模板更新时可能会有问题。例如,典型的例子就是在基于类加载器的模板加载器就不会注意到模板文件内容的改变。
当调用了getTemplate方法时,与此同时FreeMarker意识到这个模板文件已经被移除了,所以这个模板也会从缓存中移除。如果Java虚拟机认为会有内存溢出时,默认情况它会将任意的模板从缓存中移除。此外,你还可以使用Configuration对象的clearTemplateCache方法手动清空缓存。
何时将一个被缓存了的模板清除的实际应用策略是由配置的属性cache_storage来确定的,通过这个属性可以配置任何CacheStorage的实现。对于大多数用户来说,使用freemarker.cache.MruCacheStorage就足够了。这个缓存存储实现了二级最近使用的缓存。在第一级缓存中,组件都被强烈引用到特定的最大数目(引用次数最多的组件不会被Java虚拟机抛弃,而引用次数很少的组件则相反)。当超过最大数量时,最近最少使用的组件将被送至二级缓存中,在那里它们被很少引用,直到达到另一个最大的数目。引用强度的大小可以由构造方法来指定。例如,设置强烈部分为20,轻微部分为250:
或者,使用MruCacheStorage缓存,它是默认的缓存存储实现。
当创建了一个新的Configuration对象时,它使用一个maxStrongSize值为0的MruCacheStorage缓存来初始化,maxSoftSize的值是Integer.MAX_VALUE(也就是说在实际中,是无限大的)。但是使用非0的maxStrongSize对于高负载的服务器来说也许是一个更好的策略,对于少量引用的组件来说,如果资源消耗已经很高的话,Java虚拟机往往会引发更高的资源消耗,因为它不断从缓存中抛出经常使用的模板,这些模板还不得不再次加载和解析。
cfg.setCacheStorage(new freemarker.cache.MruCacheStorage(20, 250))
cfg.setSetting(Configuration.CACHE_STORAGE_KEY, "strong:20, soft:250");
3.5 错误控制
3.5.1 可能的异常
关于FreeMarker发生的异常,可以分为如下几类:
当配置FreeMarker时发生异常:典型地情况,就是在应用程序初始化时,仅仅配置了一次FreeMarker。在这个过程中,异常就会发生,从FreeMarker的API中,我们可以很清楚的看到这一点。
当加载和解析模板时发生异常:调用了Configuration.getTemplate(...)方法,FreeMarker就要把模板文件加载到内存中然后来解析它(除非模板已经在Configuration对象中被缓存了)。在这期间,有两种异常可能发生:
 因模板文件没有找到而发生的IOException异常,或在读取文件时发生其他的I/O问题。比如没有读取文件的权限,或者是磁盘错误。这些错误的发出者是TemplateLoader对象,可以将它设置到Configuration对象中。(为了正确起见:这里所说的”文件”,是简化形式。例如,模板也可以存储在关系型数据库的表中。这是TemplateLoader所要做的事。)
 根据FTL语言的规则,模板文件发生语法错误时会导致freemarker.core.ParseException异常。当获得Template对象(Configuration.getTemplate(...))时,这种错误就会发生,而不是当执行(Template.process(...))模板的时候。这种异常是IOException异常的一个子类。
当执行(处理)模板时发生的异常,也就是当调用了Template.process(...)方法时会发生的两种异常:
 当试图写入输出对象时发生错误而导致的IOException异常。
 当执行模板时发生的其它问题而导致的freemarker.template.TemplatException异常。比如,一个频繁发生的错误,就是当模板引用一个不存在的变量。默认情况下,当TemplatException异常发生时,FreeMarker会用普通文本格式在输出中打印出FTL的错误信息和堆栈跟踪信息。然后通过再次抛出TemplatException异常而中止模板的执行,然后就可以捕捉到Template.process(...)方法抛出的异常了。而这种行为是可以来定制的。FreeMarker也会经常写TemplatException异常的日志。
3.5.2 根据TemplateException-s来制定处理方式
TemplateException异常在模板处理期间的抛出是由freemarker.template.TemplateExceptionHandler对象控制的,这个对象可以使用setTemplateExceptionHandler(...)方法配置到Configuration对象中。TemplateExceptionHandler对象只包含一个方法:
void handleTemplateException(TemplateException te, Environment env, Writer out) throws TemplateException;
无论TemplateException异常什么时候发生,这个方法都会被调用。异常处理是传递的te参数控制的,模板处理的运行时(Runtime,译者注)环境可以访问env变量,处理器可以使用out变量来打印输出信息。如果方法抛出异常(通常是重复抛出te),那么模板的执行就会中止,而且Template.process(...)方法也会抛出同样的异常。如果handleTemplateException对象不抛出异常,那么模板将会继续执行,就好像什么也没有发生过一样,但是引发异常的语句将会被跳过(后面会详细说)。当然,控制器仍然可以在输出中打印错误提示信息。
任何一种情况下,当TemplateExceptionHandler被调用前,FreeMarker将会记录异常日志。
让我们用实例来看一下,当错误控制器不抛出异常时,FreeMarker是如何跳过出错语句的。假设我们已经使用了如下模板异常控制器:
如果错误发生在非FTL标记(没有被包含在<#...>或<@...>之间)的插值中,那么整个插值将会被跳过。那么下面这个模板(假设badVar在数据模型中不存在):
如果我们使用了MyTemplateExceptionHandler,就会打印:
而下面这个模板也会打印相同信息(除了报错的列数位置会不同):
因为像这样来写时,只要插值内发生任何错误,整个插值都会被跳过。
如果错误发生在指令调用中参数的计算时,或者是指令参数列表发生问题时,或在<@exp ...>中计算exp时发生错误,或者exp是用户自定义的指令,那么整个指令调用都会被跳过。例如:
class MyTemplateExceptionHandler implements TemplateExceptionHandler {
public void handleTemplateException(TemplateException te, Environment env, java.io.Writer out)
throws TemplateException {
try {
out.write("[ERROR: " + te.getMessage() + "]");
} catch (IOException e) {
throw new TemplateException("Failed to print error message. Cause: " + e, env);
}
}
}
...
cfg.setTemplateExceptionHandler(new MyTemplateExceptionHandler());
a${badVar}b
a[ERROR: Expression badVar is undefined on line 1, column 4 in test.ftl.]b
a${"moo" + badVar}b
a<#if badVar>Foo</#if>b
就会打印:
要注意错误发生在if指令的开始标签(<#if badVar>)中,但是整个指令的调用都被跳过了。从逻辑上说,嵌套的内容(Foo)也被跳过了,因为嵌套的内容是受被包含的指令(if)控制(打印)的。
下面这个的输出也是相同的(除了报错的列数会不同):
因为,正如这样来写,如果在参数处理时发生任何一个错误,整个指令的调用都将会被跳过。
如果错误发生在已经开始执行的指令之后,那么指令调用将不会被跳过。也就是说,如果在嵌套的内容中发生任何错误:
或者在一个宏定义体内:
那么输出将会是:
FreeMarker本身带有这些预先编写的错误控制器:
 TemplateExceptionHandler.DEBUG_HANDLER:打印堆栈跟踪信息(包括FTL错误信息和FTL堆栈跟踪信息)和重新抛出的异常。这是默认的异常控制器(也就是说,在所有新的Configuration对象中,它是初始配置的)。
a[ERROR: Expression badVar is undefined on line 1, column 7 in test.ftl.]b
a<#if "foo${badVar}" == "foobar">Foo</#if>b
a
<#if true>
Foo
${badVar}
Bar
</#if>
c
a
<@test />
b
<#macro test>
Foo
${badVar}
Bar
</#macro>
a
Foo
[ERROR: Expression badVar is undefined on line 4, column 5 in test.ftl.]
Bar
c
 TemplateExceptionHandler.HTML_DEBUG_HANDLER:和DEBUG_HANDLER相同,但是它可以格式化堆栈跟踪信息,那么就可以在Web浏览器中来阅读错误信息。当你在制作HTML页面时,建议使用它而不是DEBUG_HANDLER。
 TemplateExceptionHandler.IGNORE_HANDLER:简单地压制所有异常(但是要记住,FreeMarker仍然会写日志)。它对处理异常没有任何作用,也不会重新抛出异常。
 TemplateExceptionHandler.RETHROW_HANDLER:简单重新抛出所有异常而不会做其它的事情。这个控制器对Web应用程序(假设你在发生异常之后不想继续执行模板)来说非常好,因为它在生成的页面发生错误的情况下,给了你很多对Web应用程序的控制权。要获得更多在Web应用程序中处理错误的信息,可以参见FAQ。
3.5.3 在模板中明确地处理错误
尽管它和FreeMarker的配置(本章的主题)无关,但是为了说明的完整性,在这里提及一下,你可以在模板中直接控制错误。通常这不是一个好习惯(尽量保持模板简单,技术含量不要太高),但有时仍然需要:
 控制不存在/为空的变量:请阅读模板开发指南/模板/表达式/处理不存在的值部分。
 在发生障碍的“porlets”中留存下来,还有这样可阅读的部分:参考手册/指令参考/尝试恢复部分。
第四章 其它
这只是一个介绍性的导引,可以查看FreeMarker的API文档获取详细信息。
4.1 变量
本章介绍当模板在访问变量时发生了什么事情,还有变量是如何存储的。
当调用Template.process方法时,它会在方法内部创建一个Environment对象,在process返回之前一直使用。Environment对象存储模板执行时的运行状态信息。除了这些,它还存储由模板中指令,如assign,macro,local或global创建的变量。它从来不会改变你传递给process的数据模型对象,也不会创建或替换存储在配置中的共享变量。
当你想要读取一个变量时,FreeMarker将会以这种顺序来查找,直到发现了完全匹配的的变量名称才会停下来:
1.在Environment对象中:
1.如果在循环中,在循环变量的集合中。循环变量是由(如list指令)来创建的。
2.如果在宏中,在宏的局部变量集合中。局部变量可以由local指令创建。而且,宏的参数也是局部变量。
3.在当前的命名空间中。可以使用assign指令将变量放到一个命名空间中。
4.在由global指令创建的变量集合中。FTL将它们视为数据模型的普通成员变量一样来控制它们。也就是说,它们在所有的命名空间中都可见,你也可以像访问一个数据模型中的数据一样来访问它们。
2.在传递给process方法的数据模型对象中。
3.在Configuration对象存储的共享变量集合中。
在实际操作中,来自模板设计者的观点是这6种情况应该只有4种,因为从那种观点来看,后面3种(由global创建的变量,真实的数据模型对象,共享变量)共同构成了全局变量的集合。
要注意在FTL中从一个确定的层面获取确定的变量是可以的。
4.2 字符集问题
像其它大多数的Java应用程序一样,FreeMarker使用“UNICODE 文本”(UTF-16)来工作。不过,也有必须处理字符集的情况,因为它不得不和外界交换数据,这就会使用到很多字符集。
4.2.1 输入的字符集
当FreeMarker要加载模板文件(或没有解析的文本文件)时,那就必须要知道文件使用的字符集,因为文件的存储是原生的字节数组形式。可以使用配置项encoding来确定字符集。这个配置项只在FreeMarker使用Configuration对象的getTemplate方法加载模板(解析过的或没有解析过的)时起作用。要注意include指令在内部也使用
了这个方法,所以encoding的值对一个已经加载的模板(如果这个模板包含include指令的调用)来说很重要。
encoding配置的getter和setter方法在第一个(配置)层面很特殊。getter方法猜想返回值是基于Locale(本地化,译者注)传递的参数;它在地图区域编码表(称为编码地图)中查询编码,如果没有找到该区域,就返回默认编码。可以使用配置对象的setEncoding(Locale locale, String encoding)方法来填充编码表;编码表初始化时是空的。默认的初始编码是系统属性file.encoding的值,但是可以通过setDefaultEncoding方法来设置一个不同的默认值。
你也可以在模板层或运行环境层(当指定编码值作为getTemplate方法的参数时,应该在模板层覆盖encoding设置)直接给定值来覆盖encoding的设置。如果不覆盖它,那么locale设置的有效值将会是configuration.getEncoding(Locale)方法的返回值。
而且,代替这种基于字符集猜测的机制,你也可以在模板文件中使用ftl指令来指定特定的字符集。
也许你想知道为模板选择什么样的字符集更合适一些。这主要是依赖于你用来创建和修改模板的工具(如文本编辑器)。原则上,使用UTF-8字符集是最好的,但是在2004年时只有很少的一部分工具支持UTF-8,其它都不支持这种字符集。所以那种情况下就要使用本土语言中使用最广泛的字符集,这也许是你工作环境中使用的默认字符集(比如中国大陆主要使用GBK/GB2312字符集,中国台湾和香港地区主要使用BIG5字符集,译者注)。
要注意模板使用的字符集和模板生成的输出内容的字符集是独立的(除非包含FreeMarker的软件故意将设置输出内容的字符集和模板字符集设置成相同的)。
4.2.2 输出的字符集
注意:
output_encoding设置/参数和内建函数url从FreeMarker 2.3.1版本开始才可以使用,而在2.3以前的版本中是不存在的。
原则上,FreeMarker不处理输出内容的字符集问题,因为FreeMarker将输出内容都写入了java.io.Writer对象中。而Writer对象是由封装了FreeMarker(比如Web应用框架)的软件生成的,那么输出内容的字符集就是由封装软件来控制的。而FreeMarker有一个称为output_encoding(开始于FreeMarker 2.3.1版本之后)的设置。封装软件应该使用这个设置(Writer对象使用的字符集)来通知FreeMarker在输出中(否则FreeMarker不能找到它)使用哪种字符集。有一些新的特性,如内建函数url,特殊变量output_encoding也利用这个信息。因此,如果封装软件没有设置字符集这个信息,那么FreeMarker需要知道输出字符集的特性就不能被利用了。
如果你使用FreeMarker来编写软件,你也许想知道在输出内容中到底选择了哪种字符集。当然这取决于运行FreeMarker输出内容的计算机本身,但是如果用户对这个问题可以变通,那么通用的实践是使用模板文件的字符集作为输出的字符集,或者使用UTF-8。通常使用UTF-8是最佳的实践,因为任意的文本可能来自数据模型,那就可能包含不能被模板字符集所编码的字符。
如果使用了Template.createProcessingEnvironment(...)和Environment.process(...)方法来代替Template.process(...)方法,FreeMarker的设置可以对任意独立执行的模板进行。因此,你可以对每个独立执行的模板设
置output_encoding信息:
4.3 多线程
在多线程运行环境中,Configuration实例,Template实例和数据模型应该是永远不能改变(只读)的对象。也就是说,创建和初始化它们(如使用set...方法)之后,就不能再修改它们了(比如不能再次调用set...方法)。这就允许我们在多线程环境中避免代价很大的同步锁问题。要小心Template实例;当你使用Configuration.getTemplate方法获得它的一个实例时,也许得到的是从模板缓存中缓存的实例,这些实例都已经被其他线程使用了,所以不要调用它们的set...方法(当然调用process方法还是不错的)。
如果你只从同一个线程中访问所有对象,那么上面所述的限制将不会起作用。
使用FTL来修改数据模型对象或者共享变量是不太可能的,除非将方法(或其他对象)放到数据模型中来做。我们不鼓励你编写修改数据模型对象或共享变量的方法。多试试使用存储在环境对象(这个对象是为独立的Template.process调用而创建的,用来存储模板处理的运行状态)中的变量,所以最好不要修改那些由多线程使用的数据。要获取更多信息,请阅读:本部分4.1章节-变量中的内容。
4.4 Bean的包装
freemarker.ext.beans.BeansWrapper是一个对象包装器,最初加到FreeMarker中是为了将任意的POJO(Plan Old Java Objects,普通的Java对象)包装成TemplateModel接口类型。这样它就可以以正常的方式来进行处理,事实上DefaultObjectWrapper本身是BeansWrapper的扩展类。这里描述的所有东西对DefaultObjectWrapper都是适用的,除了DefaultObjectWrapper会用到freemarker.template.SimpleXxx类包装的String,Number,Date,array,Collection(如List),Map,Boolean和Iterator对象,会用 freemarker.ext.dom.NodeModel来包装W3C的DOM节点,所以上述这些描述的规则不适用。
当出现下面这些情况时,你会想使用BeansWrapper包装器来代替DefaultObjectWrapper:
在模板执行期间,数据模型中的Collection和Map应该被允许修改。(DefaultObjectWrapper会阻止这样做,因为当它包装对象时创建了数据集合的拷贝,而这些拷贝都是只读的。)
如果array,Collection和Map对象的标识符当在模板中被传递到被包装对象的方法时,必须被保留下来。也就是说,那些方法必须得到之前包装好的同类对象。
如果在之前列出的Java API中的类(如String,Map,List等),应该在模板中可
Writer w = new OutputStreamWriter(out, outputCharset);
Environment env = template.createProcessingEnvironment(dataModel, w);
env.setOutputEncoding(outputCharset);
env.process();
见。还有,默认情况下它们是不可见的,但是可以设置获取的可见程度(后面将会介绍)。要注意这不是一个好的实践,尽量去使用内建函数(如foo?size,foo?upper,foo?replace('_', '-')等)来代替Java API的使用。
下面是对BeansWrapper创建的TemplateModel对象进行的总结。为了后续的讨论,这里我们假设在包装之前对象都称为obj,而包装的后称为model。
4.4.1 TemplateHashModel functionality 模板哈希表模型
所有的对象都将被包装成TemplateHashModel类型,进而可以获取出JavaBean对象中的属性和方法。这样,就可以在模板中使用model.foo的形式来调用obj.getFoo()方法或obj.isFoo()方法了。(要注意公有的属性直接是不可见的,必须为它们编写getter方法才行)公有方法通过哈希表模型来取得,就像模板方法模型那样,因此可以使用model.doBar()来调用object.doBar()。下面我们来更多讨论一下方法模型功能。
如果请求的键值不能映射到一个bean的属性或方法时,那么框架将试图定位到“通用的get方法”,这个方法的签名是public any-return-type get(String)或public any-return-type get(Object),使用请求键值来调用它们。这样就使得访问java.util.Map和其他类似类型的键值对非常便利。只要map的键是String 类型的,属性和方法名可以在映射中查到。(有一种解决方法可以用来避免在映射中遮挡名称,请继续来阅读。)要注意java.util.ResourceBundle对象的方法使用getObject(String) 方法作为通用的get方法。
如果在BeansWrapper实例中调用了setExposeFields(true)方法,那么它仍然会暴露出类的公有的,非静态的变量,用它们作为哈希表的键和值。即如果foo是类Bar的一个公有的,非静态的变量,而bar是一个包装了Bar实例模板变量,那么表达式bar.foo的值将会作为bar对象中foo变量的值。所有这个类的超类中公有变量都会被暴露出来。
4.4.2 说一点安全性
默认情况下,不能访问模板制作时认为是不安全的一些方法。比如,不能使用同步方法(wait,notify,notifyAll),线程和线程组的管理方法(stop,suspend,resume,setDaemon,setPriority),反射相关方法(Field setXxx,Method.invoke,Constructor.newInstance,Class.newInstance,Class.getClassLoader等),System和Runtime类中各种有危险性的方法(exec,exit,halt,load等)。BeansWrapper也有一些安全级别(被称作“方法暴露的级别”),默认的级别被称作为EXPOSE_SAFE,它可能对大多数应用程序来说是适用的。没有安全保证的级别称作是EXPOSE_ALL,它允许你调用上述的不安全的方法。一个严格的级别是EXPOSE_PROPERTIES_ONLY,它只会暴露出bean属性的getters方法。最后,一个称作是EXPOSE_NOTHING的级别,它不会暴露任何属性和方法。这种情况下,你可以通过哈希表模型接口访问的那些数据只是map和资源包中的项,还有,可以从通用get(Object) 方法和get(String)方法调用返回的对象,所提供的受影响的对象就有这样的方法。
4.4.3 TemplateScalarModel functionality 模板标量模型
对于java.lang.String对象的模型会实现TemplateScalarModel接口,这个接口中的getAsString()方法简单代替了toString()方法。要注意把String对象包装到Bean包装器中,要提供比它们作为标量时更多的功能:因为哈希表接口描述了上述所需功能,那么包装String的模型也会提供访问所有String的方法(indexOf,substring等)。
4.4.4 TemplateNumberModel functionality 模板数字模型
对于是java.lang.Number的实例对象的模型包装器,它们实现了TemplateNumberModel接口,接口中的getAsNumber()方法返回被包装的数字对象。要注意把Number对象包装到Bean包装器中,要提供比它们作为数字时更多的功能:因为哈希表接口描述了上述所需功能,那么包装Number的模型也会提供访问所有Number的方法。
4.4.5 TemplateCollectionModel functionality 模板集合模型
对于本地的Java数组和其他所有实现了java.util.Collection接口的类的模型包装器,都实现了TemplateCollectionModel接口,因此也增强了使用list指令的附加功能。
4.4.6 TemplateSequenceModel functionality 模板序列模型
对于本地的Java数组和其他所有实现了java.util.List接口的类的模型包装器,都实现了TemplateSequenceModel接口,这样,它们之中的元素就可以使用model[i]这样的语法通过索引来访问了。你也可以使用内建函数model?size来查询数组的长度和列表的大小。
而且,所有的方法都可指定的一个单独的参数,从java.lang.Integer(即int, long, float, double,java.lang.Object,java.lang.Number,java.lang.Integer)中通过反射方法调用,这些类也实现了这个接口。这就意味着你可以通过很方便的方式来访问被索引的bean属性:model.foo[i]将会翻译为obj.getFoo(i)。
4.4.7 TemplateMethodModel functionality 模板方法模型
一个对象的所有方法作为访问TemplateMethodModelEx对象的代表,它们在对象模型的方法名中使用哈希表的键。当使用model.method(arg1, arg2, ...)来调用方法时,形式参数被作为模板模型传递给方法。方法首先不会包装它们,后面我们会说到解包的详细内容。这些不被包装的参数之后被实际方法来调用。以防止方法被重载,许
多特定的方法将会被选择使用相同的规则,也就是Java编译器从一些重载的方法中选择一个方法。以防止没有方法签名匹配传递的参数,或者没有方法可以被无歧义地选择,将会抛出TemplateModelException异常。
返回值类型为void的方法返回TemplateModel.NOTHING,那么它们就可以使用${obj.method(args)} 形式的语法被安全地调用。
java.util.Map实例的模型仍然实现了TemplateMethodModelEx接口,作为调用它们get()方法的一种方式。正如前面所讨论的那样,你可以使用哈希表功能来访问“get”方法,但是它有一些缺点:因为第一个属性和方法名会被键名来检查,所以执行过慢;和属性,方法名相冲突的键将会被隐藏;最终这种方法中你只可使用String类型的键。对比一下,调用model(key) 方法,将直接翻译为model.get(key):因为没有属性和方法名的查找,速度会很快;不容易被隐藏;最终对非字符串的键也能正常处理,因为参数没有被包装,只是被普通的方法调用。实际上,Map中的model(key)和model.get(key)是相等的,只是写起来很短罢了。
java.util.ResourceBundle类的模型也实现了TemplateMethodModelEx接口,作为一种访问资源和信息格式化的方便形式。对资源包的单参数调用,将会取回名称和未包装参数的toString()方法返回值一致的资源。对资源包的多参数调用的情况和单参数一样,但是它会将参数作为格式化的模式传递给java.text.MessageFormat,在第二个和后面的作为格式化的参数中使用未包装的值。MessageFormat对象将会使用它们原本的本地化资源包来初始化。
4.4.8 解包规则
当从模板中调用Java方法时,它的参数需要从模板模型转换回Java对象。假设目标类型(方法常规参数被声明的类型)是用来T代表的,下面的规则将会按下述的顺序进行依次尝试:
 对包装器来说,如果模型是空模型,就返回Java中的null。
 如果模型实现了AdapterTemplateModel接口,如果它是T的实例,或者它是一个数字而且可以使用数字强制转换成T,那么model.getAdaptedObject(T)的结果会返回。由BeansWrapper创建的所有方法是AdapterTemplateModel的实现,所以由BeansWrapper为基本的Java对象创建的展开模型通常不如原本的Java对象。
 如果模型实现了已经废弃的WrapperTemplateModel接口,如果它是T的实例,或者它是一个数字而且可以使用数字强制转换成T ,那么model.getWrappedObject()方法的结果会返回。
 如果T是java.lang.String类型,那么如果模型实现了TemplateScalarModel接口,它的字符串值将会返回。注意如果模型没有实现接口,我们不能尝试使用String.valueOf(model)方法自动转换模型到String类型。这里不得不使用内建函数?string明确地用字符串来处理非标量。
 如果T是原始的数字类型或者是可由T指定的java.lang.Number类型,还有模型实现了TemplateNumberModel接口,如果它是T的实例或者是它的装箱类型(如果T是原始类型), 那么它的数字值会返回。否则,如果T是一个Java内建的数字类型(原始类型或是java.lang.Number的标准子类,包括BigInteger和BigDecimal),类型T的一个新对象或是它的装箱类型会由
数字模型的适当强制的值来生成。
 如果T是boolean值或java.lang.Boolean类型,模型实现了TemplateHashModel接口,那么布尔值将会返回。
 如果T是java.util.Map类型,模型实现了TemplateHashModel接口,那么一个哈希表模型的特殊Map表示对象将会返回。
 如果T是java.util.List类型,模型实现了TemplateSequenceModel接口,那么一个序列模型的特殊List表示对象将会返回。
 如果T是java.util.Set类型,模型实现了TemplateCollectionModel接口,那么集合模型的一个特殊Set表示对象将会返回。
 如果T是java.util.Collection或java.lang.Iterable类型,模型实现了TemplateCollectionModel或TemplateSequenceModel接口,那么集合或序列模型(各自地)一个特殊的Set或List表示对象将会返回。
 如果T是Java数组类型,模型实现了TemplateSequenceModel接口,那么一个新的指定类型的数组将会创建,它其中的元素使用数组的组件类型作为T,递归展开到数组中。
 如果T是char或者java.lang.Character类型,模型实现了TemplateScalarModel接口,它的字符串表示中包含精确的一个字符,那么一个java.lang.Character类型的值将会返回。
 如果T定义的是java.util.Date类型,模型实现了TemplateDateModel接口,而且它的日期值是T的实例,那么这个日期值将会返回。
 如果模型是数字模型,而且它的数字值是T的实例,那么数字值就会返回。你可以得到一个实现了自定义接口的java.lang.Number类型的自定义子类,也许T就是那个接口。
 如果模型是日期类型,而且它的日期值是T的实例,那么日期值将会返回。类似的考虑为*。
 如果模型是标量类型,而且T可以从java.lang.String类型来定义,那么字符串值将会返回。这种情况涵盖T是java.lang.Object, java.lang.Comparable和java.io.Serializable类型。
 如果模型是布尔类型,而且T可以从java.lang.Boolean类型来定义,那么布尔值将会返回。和**是相同的。
 如果模型是哈希表类型,而且T可以从freemarker.ext.beans.HashAdapter类型来定义,那么一个哈希表适配器将会返回。和**是形同的。
 如果模型是序列类型,而且T可以从freemarker.ext.beans.SequenceAdapter类型来定义,那么一个序列适配器将会返回。和**是形同的。
 如果模型是集合类型,而且T可以从freemarker.ext.beans.SetAdapter类型来定义,那么集合的set适配器将会返回。和**是形同的。
 如果模型是T的实例,那么模型本身将会返回。这种情况涵盖方法明确地声明一
个FreeMarker特定模型接口,而且允许返回指令,当java.lang.Object被请求时允许返回方法和转换的模型。
 意味着没有可能转换的异常被抛出。
4.4.9 访问静态方法
从BeansWrapper.getStaticModels()方法返回的TemplateHashModel对象可以用来创建哈希表模型来访问静态方法和任意类型的字段。
之后你就可以得到模板的哈希表模型,它会暴露所有java.lang.System类的静态方法和静态字段(final类型和非final类型)作为哈希表的键。设想你已经将之前的模型放到根模型中了:
从现在开始,你可以在模板中使用${File.SEPARATOR}来插入文件分隔符,或者你可以列出所有文件系统中的根元素,通过:
来进行。
当然,你必须小心这个模型所带来的潜在的安全问题。
你可以给模板作者完全的自由,不管它们通过将静态方法的哈希表放到模板的根模型中,来使用哪种类的静态方法,如用如下方式:
如果它被用作是以类名为键的哈希表,这个对象暴露的只是任意类的静态方法。那么你可以在模板中使用如${statics["java.lang.System"].currentTimeMillis()}这样的表达式。注意,这样会有更多的安全隐患,比如,如果方法暴露级别对EXPOSE_ALL是很弱的,那么某些人可以使用这个模型调用System.exit()方法。
注意在上述的示例中,我们通常使用默认的BeansWrapper实例。这是一个方便使用的静态包装器实例,你可以在很多情况下使用。特别是你想修改一些属性(比如模型缓存,安全级别,或者是空模型对象表示)时,你也可以自由地来创建自己的BeansWrapper实例,然后用它们来代替默认包装器。
4.4.10 访问枚举类型
在JRE 1.5版本之后,从方法BeansWrapper.getEnumModels()返回的
BeansWrapper wrapper = BeansWrapper.getDefaultInstance();
TemplateHashModel staticModels = wrapper.getStaticModels();
TemplateHashModel fileStatics =
(TemplateHashModel) staticModels.get("java.io.File");
root.put("File", fileStatics);
<#list File.listRoots() as fileSystemRoot>...</#list>
root.put("statics", BeansWrapper.getDefaultInstance().getStaticModels());
TemplateHashModel可以被用作创建访问枚举类型值的哈希表模型。(试图在之前JRE中调用这个方法会导致UnsupportedOperationException异常。)
这样你就可以得到模板哈希表模型,它暴露了java.math.RoundingMode类所有枚举类型的值,并把它们作为哈希表的键。设想你将之前的模型已经放入根模型中了:
从现在开始,你可以在模板中使用表达式RoundingMode.UP来引用枚举值UP。
你可以给模板作者完全的自由,不管它们使用哪种枚举类,将枚举模型的哈希表放到模板的根模型中,可以这样做:
如果它被用作是类名作为键的哈希表,这个对象暴露了任意的枚举类。那么你可以在模板中使用如${enums["java.math.RoundingMode"].UP}的表达式。
被暴露的枚举值可以被用作是标量(它们会委派它们的toString()方法),也可以用在相同或不同的比较中。
注意在上述的例子中,我们通常使用默认的BeansWrapper实例。这是一个方便使用的静态包装器实例,你可以在很多情况下使用。特别是你想修改一些属性(比如模型缓存,安全级别,或者是空模型对象表示)时,你也可以自由地来创建自己的BeansWrapper实例,然后用它们来代替默认包装器。
4.5 日志
FreeMarker整合了如下的日志包:SLF4,Apache Commons Logging,Log4J,Avalon LogKit和java.util.logging(Java2平台1.4版本之后)。默认情况下,FreeMarker会按如下顺序来查找日志包,而且会自动使用第一个发现的包:SLF4J,Apache Commons Logging,Log4J,Avalon,java.util.logging。然而,如果在freemarker.log.Logger类用合适的参数中调用静态的selectLoggerLibrary方法,而且在使用任何FreeMarker类之前记录信息,你可以明确地选择一个日志包,或者关闭日志功能。可以参见API文档获取详细信息。
注意:
在FreeMarker 2.4版本之前,因为向后兼容性的限制,SLF4J和Apache Commons Logging不会被自动搜索,所以要使用它们其中之一,你必须这样来做:
BeansWrapper wrapper = BeansWrapper.getDefaultInstance();
TemplateHashModel enumModels = wrapper.getEnumModels();
TemplateHashModel roundingModeEnums =
(TemplateHashModel)enumModels.get("java.math.RoundingMode");
root.put("RoundingMode", roundingModeEnums);
root.put("enums", BeansWrapper.getDefaultInstance().getEnumModels());
import freemarker.log.Logger;
...
// 重要:这应该在使用其它FreeMarker类之前被执行!
Logger.selectLoggerLibrary(Logger.LIBRARY_SLF4J);
//或者 Logger.LIBRARY_COMMONS
...
我们推荐这样来做,因为从FreeMarker 2.4版开始,如果SLF4J可用,它会被默认使用,否则使用Apache Commons Logging,否则会使用Log4J。
所有由FreeMarker产生的日志信息会被记录到顶级记录器是名为freemarker的记录器中。现在被使用的记录器是:
记录器名称
目标
freemarker.beans
记录Beans包装器模块的日志信息。
freemarker.cache
记录模板加载和缓存相关的日志信息。
freemarker.runtime
记录在模板执行期间抛出的模板异常日志信息。
freemarker.runtime.attempt
记录在模板执行期间抛出的模板异常日志信息,但是是由attempt/recover指令捕捉的。开启DEBUG严重级别来查看异常。
freemarker.servlet
记录由FreeMarkerServlet类产生的日志信息。
freemarker.jsp
记录FreeMarker对JSP支持时打出的日志信息。
你也可以调用freemarker.log.Logger类中的静态方法selectLoggerLibrary,传递一个字符串来用作是前缀,这是上述提到的日志包名称。如果你想给每个Web应用使用不同的日志包,这个基本操作会是很有用的(假设应用程序使用的是它本地的freemarker.jar包)。而且你也可以使用Logger.selectLoggerLibrary(Logger.LIBRARY_NONE)来关闭日志,不管哪种情况,selectLoggerLibrary必须在FreeMarker记录任何日志信息前面来调用,否则它就不会有任何效果。
4.6 在Servlet中使用FreeMarker
作为基础的了解,在Web应用领域中使用FreeMarker和其他没有什么不同。FreeMarker将输出内容写到你传递给Template.process方法的Writer对象中,它并不关心Writer对象将输出内容打印到控制台或是一个文件中,或是HttpServletResponse对象的输出流中。FreeMarker并不知道servlets和Web;它仅仅是使用模板文件来合并Java对象,之后从它们中间生成输出文本。从这里可知,如何创建一个Web应用程序都随你的习惯来。
但是,你可能想在已经存在的Web应用框架中使用FreeMarker。许多框架都是基于“Model 2”架构的,JSP页面来控制显示。如果你使用了这样的框架(不如Apache Struts),那么继续阅读本文。对于其他框架请参考它们的文档。
4.6.1 在“Model 2”中使用FreeMarker
许多框架依照HTTP请求转发给用户自定义的“action”类,将数据作为属性放在ServletContext,HttpSession和HttpServletRequest对象中,之后请求被框架派发到一个JSP页面中(视图层),使用属性传递过来的数据来生成HTML页面,这样的策略通常就是所指的Model 2模型。
使用这样的框架,你就可以非常容易的用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 设置: -->
<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>
<!-- 强制使用 UTF-8作为输出编码格式! -->
</init-param>
在这之后,你可以像使用JSP(*.jsp)文件那样使用FTL文件(*.ftl)了。(当然你可以选择除ftl之外的扩展名;这只是惯例)
注意
它是怎么工作的?让我们来看看JSP是怎么工作的。许多servlet容器处理JSP时使用一个映射为*.jsp的servlet请求URL格式。这样servlet就会接收所有URL是以.jsp结尾的请求,查找请求URL地址中的JSP文件,内部编译完后交给Servlet,然后调
<!-- FreeMarker 设置: -->
<init-param>
<param-name>template_update_delay</param-name>
<param-value>0</param-value>
<!-- 0 只对开发使用! 否则使用大一点的值. -->
</init-param>
<init-param>
<param-name>default_encoding</param-name>
<param-value>ISO-8859-1</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>
...
<!-- 为了阻止从Servlet容器外部访问MVC的视图层组件。
RequestDispatcher.forward/include应该起到作用。
移除下面的代码可能开放安全漏洞!
-->
<security-constraint>
<web-resource-collection>
<web-resource-name>
FreeMarker MVC Views
</web-resource-name>
<url-pattern>*.ftl</url-pattern>
</web-resource-collection>
<auth-constraint>
<!-- 不允许任何人访问这里 -->
</auth-constraint>
</security-constraint>
用生成信息的serlvet来生成页面。这里为URL类型是*.ftl映射的FreemarkerServlet也是相同功能,只是FTL文件不会编译给Servlet-s,而是给Template对象,之后Template对象的process方法就会被调用来生成页面。
比如,代替这个JSP页面(注意它使用了Struts标签库来保存设计,而不是嵌入可怕的Java代码):
你可以使用这个FTL文件(使用ftl扩展名而不是jsp):
警告!
在FreeMarker中,<html:form action="/query">...</html:form>仅仅被视为是静态文本,所以它会按照原本输出出来了,就像其他XML或HTML标记一样。JSP标签也仅仅是FreeMarker的指令,没有什么特殊之处,所以你可以使用FreeMarker语法形式来调用它们,而不是JSP语法:<@html.form action="/query">...</@html.form>。注意在FreeMarker语法中你不能像JSP那样在参数中使用${...},而且不能给参数值加引号。所以这样是不对的:
<%@ 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>
<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>
<#-- WRONG: -->
<@my.jspTag color="${aVariable}" name="aStringLiteral"
width="100" height=${a+b} />
但下面这样是正确的:
在这两个模板中,当你要引用user和latestProduct时,它会首先试图去查找一个名字已经在模板中创建的变量(比如prod;如果你使用JSP:这是一个page范围内的属性)。如果那样做不行,它会尝试在对HttpServletRequest象中查找那个名字的属性,如果没有找到就在HttpSession中找,如果还没有找到那就在ServletContext中找。FTL按这种情况工作是因为FreemarkerServlet创建数据模型由上面提到的3个对象中的属性而来。那也就是说,这种情况下根哈希表不是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文档。
4.6.2 包含其它Web应用程序资源中的内容
你可以使用由FreemarkerServlet(2.3.15版本之后)提供的客户化标签<@include_page path="..."/>来包含另一个Web应用资源的内容到输出内容中;这对于整合JSP页面(在同一Web服务器中生活在FreeMarker模板旁边)的输出到FreeMarker模板的输出中非常有用。使用:
和使用JSP指令是相同的:
注意:
<@include_page ...>不能和<#include ...>搞混,后者是为了包含FreeMarker模板而不会牵涉到Servlet容器。使用<#include ...>包含的模板和包
<#-- Good: -->
<@my.jspTag color=aVariable name="aStringLiteral"
width=100 height=a+b />
<@include_page path="path/to/some.jsp"/>
<jsp:include page="path/to/some.jsp">
含它的模板共享模板处理状态,比如数据模型和模板语言变量,而<@include_page ...>开始一个独立的HTTP请求处理。
注意:
一些Web应用框架为此提供它们自己的解决方案,这种情况下你就可以使用它们来替代。而一些Web应用框架不使用FreemarkerServlet,所以include_page是没有作用的。
路径可以是相对的,也可以是绝对的。相对路径被解释成相对于当前HTTP请求(一个可以触发模板执行的请求)的URL,而绝对路径在当前的servlet上下文(当前的Web应用)中是绝对的。你不能从当前Web应用的外部包含页面。注意你可以包含任意页面,而不仅仅是JSP页面;我们仅仅使用以.jsp结尾的页面作为说明。
除了参数path之外,你也可以用布尔值(当不指定时默认是true)指定一个名为inherit_params可选的参数来指定被包含的页面对当前的请求是否可见HTTP请求中的参数。
最后,你可以指定一个名为params的可选参数,来指定被包含页面可见的新请求参数。如果也传递继承的参数,那么指定参数的值将会得到前缀名称相同的继承参数的值。params的值必须是一个哈希表类型,它其中的每个值可以是字符串,或者是字符串序列(如果你需要多值参数)。这里给出一个完整的示例:
这会包含path/to/some.jsp页面,传递它的所有的当前请求的参数,除了“foo”和“bar”,这两个会被分别设置为“99”和多值序列“a”,“b”。如果原来请求中已经有这些参数的值了,那么新值会添加到原来存在的值中。那就是说,如果“foo”有值“111”和“123”,那么现在它会有“99”,“111”,“123”。
事实上使用params给参数传递非字符串值是可能的。这样的一个值首先会被转换为适合的Java对象(数字,布尔值,日期等),之后调用它们Java对象的toString()方法来得到字符串值。最好不要依赖这种机制,作为替代,明确参数值在模板级别不能转换成字符串类型之后,在使用到它的地方可以使用内建函数?string和?c。
4.6.3 在FTL中使用JSP客户化标签
FreemarkerServlet将一个哈希表类型的JspTaglibs放到数据模型中,就可以使用它来访问JSP标签库了。JSP客户化标签库将被视为普通用户自定义指令来访问。例如,这是使用了Struts标签库的JSP文件:
<@include_page path="path/to/some.jsp" inherit_params=true params={"foo": "99", "bar": ["a", "b"]}/>
这是一个(近似)等价的FTL:
因为JSP客户化标签是在JSP环境中来书写操作的,它们假设变量(在JSP中常被指代“beans”)被存储在4个范围中:page范围,request范围,session范围和application范围。FTL没有这样的表示法(4种范围),但是FreemarkerServlet给客户化标签提供仿真的环境,这样就可以维持JSP范围中的“beans”和FTL变量之间的对应关系。对于自定义的JSP标签,请求,会话和应用范围是和真实JSP相同的:javax.servlet.ServletContext,HttpSession和ServerRequest对象中的属性。从FTL的角度来看,这三种范围都在数据模型中,这点前面已经解释了。page范围和FTL全局变量(参见global指令)是对应的。那也就是,如果你使用global指令创建一个变量,通过仿真的JSP环境,它会作为page范围变量对自定义标签可见。而且,如果一个JSP标签创建了一个新的page范围变量,那么结果和用global指令创建的是相同的。要注意在数据模型中的变量作为page范围的属性对JSP标签是不可见的,尽管
<%@page contentType="text/html;charset=ISO-8859-2" language="java"%>
<%@taglib uri="/WEB-INF/struts-html.tld" prefix="html"%>
<%@taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%>
<html>
<body>
<h1><bean:message key="welcome.title"/></h1>
<html:errors/>
<html:form action="/query">
Keyword: <html:text property="keyword"/><br>
Exclude: <html:text property="exclude"/><br>
<html:submit value="Send"/>
</html:form>
</body>
</html>
<#assign html=JspTaglibs["/WEB-INF/struts-html.tld"]>
<#assign bean=JspTaglibs["/WEB-INF/struts-bean.tld"]>
<html>
<body>
<h1><@bean.message key="welcome.title"/></h1>
<@html.errors/>
<@html.form action="/query">
Keyword: <@html.text property="keyword"/><br>
Exclude: <@html.text property="exclude"/><br>
<@html.submit value="Send"/>
</@html.form>
</body>
</html>
它们在全局是可见的,因为数据模型和请求,会话,应用范围是对应的,而不是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文件中:
注意尽管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下才会有用。
4.6.4 在JSP页面中嵌入FTL
有一个标签库允许你将FTL片段放到JSP页面中。嵌入的FTL片段可以访问JSP 的4种范围内的属性(Beans)。你可以在FreeMarker发布包中找到一个可用的示例和这个标签库。
4.7 为FreeMarker配置安全策略
当FreeMarker运行在装有安全管理器的Java虚拟机中时,你不得不再授与一些权限,确保运行良好。最值得注意的是,你需要为对freemarker.jar的安全策略文件添加
<listener>
<listener-class>freemarker.ext.jsp.EventForwarding</listener-class>
</listener>
这些条目:
另外,如果从一个目录中加载模板,你还需要给FreeMarker授权来从那个目录下读取文件,使用如下的授权:
最终,如果你使用默认的模板加载机制,也就是从当前文件夹下加载模板,那么需要指定这些授权内容:(主要表达式${user.dir}将会在运行时被策略解释器处理,几乎它就是一个FreeMarker模板)
很自然地,如果你在Windows下运行,使用两个反斜杠来代替一个斜杠来分隔路径中的目录间隔。
4.8 遗留的XML包装实现
注意:
遗留的XML包装已经废弃了。FreeMarker 2.3已经引入了对新的XML处理模型的支持。新的XML包装包已经引入了,freemarker.ext.dom。对于新用法,我们鼓励你使用。它会在XML处理指南中来说明。
freemarker.ext.xml.NodeListModel类提供了来包装XML文档展示为节点树模板模型。每个节点列表可以包含零个或多个XML节点(文档类型,元素类型,文本类型,处理指令,注释,实体引用,CDATA段等)。节点列表实现了下面模板的语义模型接口:
grant codeBase "file:/path/to/freemarker.jar"
{
permission java.util.PropertyPermission "file.encoding", "read";
permission java.util.PropertyPermission "freemarker.*", "read";
}
grant codeBase "file:/path/to/freemarker.jar"
{ ...
permission java.io.FilePermission "/path/to/templates/-", "read";
}
grant codeBase "file:/path/to/freemarker.jar"
{
...
permission java.util.PropertyPermission "user.dir", "read";
permission java.io.FilePermission "${user.dir}/-", "read";
}
4.8.1 TemplateScalarModel模板标量模型
当使用一个标量时,节点列表将会呈现XML片段,表示其包含的节点。这使得使用XML到XML转换模板很方便。
4.8.2 TemplateCollectionModel 模板集合模型
当用list指令来使用一个集合时,它会简单枚举它的节点。每个节点将会被当作一个新的单一节点组的节点列表返回。
4.8.3 TemplateSequenceModel 模板序列模型
当被用作是序列时,它会返回第i个节点作为一个新的节点列表,包含单独的被请求的节点。也就是说,要返回<book>元素的第三个<chapter>元素,你可以使用下面的代码(节点索引是从零开始的):
4.8.4 TemplateHashModel 模板哈希表模型
当被用作是哈希表时,它基本上是用来遍历子节点。也就是,如果你有个名为book的节点列表,并包装了一个有很多chapter的元素节点,那么book.chapter将会产生一个book元素的所有chapter元素的节点列表。@符号常被用来指代属性:book.@title产生一个有单独属性的节点列表,也就是book元素的title属性。
意识到下面这样的结果是很重要的,比如,如果book没有chapter-s,那么book.chapter就是一个空序列,所以xmldoc.book.chapter??就不会是false,它会一直是true!相似地,xmldoc.book.somethingTotallyNonsense??也不会是false。为了检查是否发现子节点,可以使用xmldoc.book.chapter?size == 0。
哈希表定义了一些“魔力的键”。所有的这些键以下划线开头。最值得注意的是_text,可以得到节点的文本内容:${book.@title._text}将会给模板交出属性的值。相似地,_name将会取得元素或属性的名字。*或_allChildren返回所有节点列表元素中的直接子元素,而@*或_allAttributes返回节点列表中元素的所有属性。还有很多这样的键;下面给出哈希表键的详细总结:
键名
结果为
*或_children
所有当前节点(非递归)的直接子元素。
适用于元素和文档节点
@*或_attributes
当前节点的所有属性。仅适用于元素。
@attributeName
当前节点的命名属性。适用于元素,声明和处理指令。在声明中它支持属性publicId,systemId和elementName。在处理指令中,它支持属性target和
<#assign 3rdChapter = xmldoc.book.chapter[2]>
data,还有数据中以name="value"对出现的其他属性名。对于声明和处理指令的属性节点是合成的,因此它们没有父节点。要注意,@*不能在声明或处理指令上进行操作。
_ancestor
当前节点的所有祖先,直到根元素(递归)。
适用于类型和_parent相同的节点类型。
_ancestorOrSelf
当前节点和它的所有祖先节点。
适用于和_parent相同的节点类型。
_cname
当前节点(命名空间URI+本地名称)的标准名称,每个节点(非递归)一个字符串值。适用于元素和属性。
_content
当前节点的全部内容,包括子元素,文本,实体引用和处理指令(非递归)。适用于元素和文档。
_descendant
当前节点的所有递归的子孙元素。
适用于文档和元素节点。
_descendantOrSelf
当前节点和它的所有递归的子孙元素。
适用于文档和元素节点。
_document
当前节点所属的所有文档类型。
适用于所有除文本的节点。
_doctype
当前节点的声明。仅仅适用于文档类型节点。
_filterType
这是一种按类型过滤的模板方法模型。当被调用时,它会产生一个节点列表,仅仅包含它们当前节点,这些节点的类型和传递给它们参数的一种类型相匹配。你应该传递任意数量的字符串给这个方法,其中包含来保持类型的名字。合法的类型名称是:“attribute”,“cdata”,“comment”,“document”,“documentType”,“element”,“entity”,“entityReference”,“processingInstruction”,“text”。
_name
当前节点的名称。每个节点(非递归)一个字符串值。适用于元素和属性(返回它们的本地名称),实体,处理指令(返回它的目标),声明(返回它的公共ID)。
_nsprefix
当前节点的命名空间前缀,每个节点(非递归)一个字符串值。
适用于元素和属性。
_nsuri
当前节点的命名空间URI,每个节点(非递归)一个字符串值。
适用于元素和属性。
_parent
当前节点的父节点。
适用于元素,属性,注释,实体,处理指令。
_qname
当前节点在[namespacePrefix:]localName形式的限定名,每个节点(非递归)一个字符串值。
适用于元素和属性。
_registerNamespace
(prefix, uri)
注册一个对当前节点列表和从当前节点列表派生出的所有节点列表有指定前缀和URI的XML命名空间。注册之后,你可以使用nodelist["prefix:localname"]或nodelist["@prefix:localname"]语法来访问元素和属性,它们的名字是命名空间范围内的。注意命名空间
的前缀需要不能和当前XML文档它自己使用的前缀相匹配,因为命名空间纯粹是由URI来比较的。
_text
当前节点的文本内容,每个节点(非递归)一个字符串值。适用于元素,属性,注释,处理指令(返回它的数据)和CDATA段。保留的XML字符(‘<’和‘&’)不能被转义。
_type
返回描述节点类型的节点列表,每个节点包含一个字符串值。可能的节点名称是:合法的节点名称是:“attribute”,“cdata”,“comment”,“document”,“documentType”,“element”,“entity”,“entityReference”,“processingInstruction”,“text”。如果节点类型是未知的,就返回“unknown”。
_unique
当前节点的一个拷贝,仅仅保留每个节点第一次的出现,消除重复。重复可以通过应用对树的向上遍历出现在节点列表中,如_parent,_ancestor,_ancestorOrSelf和_document,也就是说会返回一个节点列表,它包含foo中重复的节点,每个节点会包含出现的次数,和它子节点数目相等。这些情况下,使用foo._children._parent._unique来消除重复。适用于所有节点类型。
其他的键
当前节点的子元素的名称和键相匹配。这允许以book.chapter.title这种风格语法进行方便的子元素遍历。注意在技术上nodeset.childname和nodeset("childname")相同,但是两者写法都很短,处理也很迅速。适用于文档和元素节点。
4.8.5 TemplateMethodModel 模板方法模型
当被用作方法模型,它返回一个节点列表,这个列表是处理节点列表中当前内容的XPath表达式的结果。为了使这种特性能够工作,你必须将Jaxen类库放到类路径下。比如:
4.8.6 命名空间处理
为了遍历有命名空间范围内名称的子元素这个目的,你可以使用节点列表注册命名空间前缀。你可以在Java代码中来做,调用:
方法,或者在模板中使用
语法。从那里开始,你可以在命名空间通过特定的URI来标记引用子元素,用这种语法nodelist["prefix:localName"]和
<#assign firstChapter=xmldoc("//chapter[first()]")>
public void registerNamespace(String prefix, String uri);
${nodelist._registerNamespace(prefix, uri)}
nodelist["@prefix:localName"]
和在XPath表达式中使用这些命名空间前缀一样。命名空间使用一个节点列表来注册并传播到所有节点列表,这些节点列表来自于原来的节点列表。要注意命名空间只可使用URI来进行匹配,所以你可以在你的模板中安全地使用命名空间的前缀,这和在实际XML中的不同,在模板和XML文档中,一个前缀只是一个对URI的本地别名。
4.9 和Ant一起使用FreeMarker
我们现在知道有两种“FreeMarker Ant tasks”:
 FreemarkerXmlTask:它来自于FreeMarker的发布包,打包到freemarker.jar中。这是使用FreeMarker模板转换XML文档的轻量级的,易于使用的Ant任务。它的入口源文件(输入文件)是XML文件,和生成的输出文件对应,这是通过单独模板实现的。也就是说,对于每个XML文件,模板会被执行(在数据模型中的XML文档),模板的输出会被写入到一个和原XML文件名相似名称的文件中。因此,模板文件扮演了一个和XSLT样式表相似的角色,但它是FTL,而不是XSLT。
 FMPP:这是一个重量级的,以很少的XML为中心,第三方Ant任务(和独立的命令行工具)。它主要的目的是用作为模板文件的源文件(输入文件)生成它们自己对应的输出文件,但它也对以XML-s为源文件的FreemarkerXmlTask进行支持。而且,相比于FreemarkerXmlTask,它还有额外的特性。那么它的缺点是什么?它太复杂太一般化了,不容易掌握和使用。
这一部分介绍了FreemarkerXmlTask,要了解FMPP更多的信息,可以访问它的主页:http://fmpp.sourceforge.net/。
为了使用FreemarkerXmlTask,你必须首先在你的Ant构建文件中定义freemarker.ext.ant.FreemarkerXmlTask,然后调用任务。假设你想使用假定的“xml2html.ftl”模板转换一些XML文档到HTML,XML文档在目录“xml”中而HTML文档生成到目录“html”中,你应该这样来写:
这个任务将会对每个XML文档调用模板。每个文档将会被解析成DOM树,然后包装成FreeMarker节点变量。当模板开始执行时,特殊变量.node被设置成XML文档节点的根。
注意,如果你正使用遗留的(FreeMarker 2.2.x和以前版本)XML适配器实现,也同样可以进行,而且XML树的根节点被放置在数据模型中,作为变量document。它包含了遗留的freemarker.ext.xml.NodeListModel类的实例。
注意所有通过构建文件定义的属性将会作为名为“properties”的哈希表模型来用。一些其他方法也会可用;对模板中什么样的可用变量的详细描述,还有什么样的属性可以被任
<taskdef name="freemarker" classname="freemarker.ext.ant.FreemarkerXmlTask">
<classpath>
<pathelement location="freemarker.jar" />
</classpath>
</taskdef>
<mkdir dir="html" />
<freemarker basedir="xml" destdir="html" includes="**/*.xml" template="xml2html.ftl" />
务接受,参见freemarker.ext.ant.FreemarkerXmlTask的JavaDoc文档。
4.10 Jython 包装器
freemarker.ext.jython包包含了启用任意Jython对象的模型,并被用作是TemplateModel。在一个很基础的示例中,你可以使用如下调用:
freemarker.ext.jython.JythonWrapper类的方法。这个方法会包装传递的对象,包装成合适的TemplateModel。下面是一个对返回对象包装器的属性的总结。为了下面的讨论,我们假设在模板模型根中,对对象obj调用JythonWrapper后模型名为model。
4.10.1 TemplateHashModel functionality 模板哈希表模型功能
PyDictionary和PyStringMap将会被包装成一个哈希表模型。键的查找映射到__finditem__方法;如果一个项没有被找到,那么就返回一个为None的模型。
4.10.2 TemplateScalarModel functionality 模板标量模型功能
每一个python对象会实现TemplateScalarModel接口,其中的getAsString()方法会委派给toString()方法。
4.10.3 TemplateBooleanModel functionality 模板布尔值模型功能
每一个python对象会实现TemplateBooleanModel接口,其中的getAsBoolean()方法会指派给__nonzero__()方法,符合Python语义的true/false。
4.10.4 TemplateNumberModel functionality 模板数字模型功能
对PyInteger,PyLong和PyFloat对象的模型包装器实现了TemplateNumberModel接口,其中的getAsNumber()方法返回__tojava__(java.lang.Number.class)。
4.10.5 TemplateSequenceModel functionality 模板序列模型功能
对所有扩展了PySequence的类的模型包装器会实现TemplateSequenceModel接口,因此它们中的元素可以通过使用model[i]语法形式的序列来访问,这会指派给__finditem__(i)。你也可以使用内建函数
public TemplateModel wrap(Object obj);
model?size查询数组的长度或者list的大小,它会指派给__len__()。
第三部分 XML处理指南
前言
尽管FreeMarker最初被设计用作Web页面的模板引擎,对于2.3版本来说,它的另外一个应用领域目标是:转换XML到任意的文本输出(比如HTML)。因此,在很多情况下,FreeMarker也是一个可选的XSLT。
从技术上来说,在转换XML文档上没有什么特别之处。它和你使用FreeMarker做其他事情都是一样的:你将XML文档丢到数据模型中(和其他可能的变量),然后你将FTL模板和数据模型合并来生成输出文本。对于更好的XML处理的额外特性是节点FTL变量类型(在通用的树形结构中象征一个节点,不仅仅是对XML有用)和用内建函数,指令处理它们,你使用的XML包装器会暴露XML文档,并将作为模板的FTL变量。
使用FreeMarker和XSLT有什么不同?FTL语言有常规的命令式/过程式的逻辑。另一方面,XSLT是声明式的语言,由很聪明的人设计出来,所以它并不能轻易吸收它的逻辑,也不会在很多情况下使用。而且它的语法也非常繁琐。然而,当你处理XML文档时,XSLT的“应用模板”方法可以非常方便,因此FreeMarker支持称作“访问者模式”的相似事情。所以在很多应用程序中,写FTL的样式表要比写XSLT的样式表容易很多。另外一个根本的不同是FTL转换节点树到文本,而XSLT转换一课树到另一棵树。所以你不能经常在使用XSLT的地方使用FreeMarker。
第一章 揭示XML文档
1.1 节点树
我们使用如下示例的XML文档:
W3C的DOM定义XML文档模型为节点树。上面XML的节点树可以被视为:
<book>
<title>Test Book</title>
<chapter>
<title>Ch1</title>
<para>p1.1</para>
<para>p1.2</para>
<para>p1.3</para>
</chapter>
<chapter>
<title>Ch2</title>
<para>p2.1</para>
<para>p2.2</para>
</chapter>
</book>
document
|
+- element book
|
+- text "\n "
|
+- element title
| |
| +- text "Test Book"
|
+- text "\n "
|
+- element chapter
| |
| +- text "\n "
| |
| +- element title
| | |
| | +- text "Ch1"
| |
| +- text "\n "
| |
| +- element para
| | |
| | +- text "p1.1"
| |
| +- text "\n "
| |
| +- element para
| | |
| | +- text "p1.2"
| |
| +- text "\n "
| |
| +- element para
| |
| +- text "p1.3"
|
+- element
|
+- text "\n "
|
+- element title
| |
要注意,烦扰的“\n”是行的中断(这里用\n指示,在FTL字符串中使用转义序列)和标记直接的缩进空格。
注意和DOM相关的术语:
 一棵树最上面的节点称为root根,在XML文档中,它通常是“文档”节点,而不是最顶层元素(本例中的book)。
 如果B是A的直接后继,我们说B节点是A节点的child子节点。比如,两个chapter元素是book元素的子节点,但是para元素就不是。
 如果A是B的直接前驱,也就是说,如果B是A的子节点,我们说节点A是节点B的parent父节点。比如,book元素是两个chapter元素的父节点,但是它不是para元素的父节点。
 XML文档中可以出现几种成分,比如元素,文本,注释,处理指令等。所有这些成分都是DOM树的节点,所以就有元素节点,文本节点,注释节点等。原则上,元素的属性也是树的节点—它们是元素的子节点--,但是,通常我们(还有其他XML相关的技术)不包含元素的子节点。所以基本上它们不被记为子节点。
程序员将DOM树的文档节点方法FreeMarker的数据模型中,那么模板开发人员可以以变量为出发点来使用DOM树。
FTL中的DOM节点和node variable节点变量对应。这是变量类型,和字符串,数字,哈希表等类型相似。节点变量类型使得FreeMarker来获取一个节点的父节点和子节点成为可能。这是技术上需要允许模板开发人员在节点间操作,也就是,使用节点内建函数或者visit和recurse指令;我们会在后续章节中展示它们的使用。
1.2 将XML放到数据模型中
注意:
这个部分是对程序员来说的:
创建一个简单的程序来运行下面的示例是非常容易的。仅仅用下面这个例子来替换程序开发指南中快速入门示例中的“Create a data-model”部分:
| +- text "Ch2"
|
+- text "\n "
|
+- element para
| |
| +- text "p2.1"
|
+- text "\n "
|
+- element para
|
+- text "p2.2"
然后你可以在基本的输出(通常是终端屏幕)中得到一个程序可以输出XML转换的结果。
注意:
 parse方法默认移除注释和处理指令节点。参见API文档获取详细信息。
 NodeModel也允许你直接包装org.w3c.dom.Node-s。首先你也许想用静态的实用方法清空DOM树,比如NodeModel.simplify或你自定义的清空规则。
注意有可用的工具,你可以使用它们从XML文档中来生成文件,所以你不需对通用任务来写自己的代码。参加“和Ant一起使用FreeMarker”部分。
/* Create a data-model */
Map root = new HashMap();
root.put(
"doc",
freemarker.ext.dom.NodeModel.parse(new File("the/path/of/the.xml")));
第二章 必要的XML处理
2.1 通过例子来学习
注意:
这部分我们使用的DOM树和变量都是前一章做的那个。
假设程序员在数据模型中放置了一个XML文档,就是名为doc的变量。这个变量和DOM树的根节点“document”对应。真实的变量doc之后结构是非常复杂的,大约类似DOM树。所以为了避免钻牛角尖,我们通过例子来看看如何使用。
2.1.1 通过名称来访问元素
这个FTL打印book的title:
输出是:
正如你所看到的,doc和book都可以当作哈希表来使用。你可以按照子变量的形式来获得它们的子节点。基本上,你用描述路径的方法来访问在DOM树中的目标(元素title)。你也许注意到了上面有一些是假象:使用${doc.book.title},就好像我们指示FreeMarker打印title元素本身,但是我们应该打印它的子元素文本(看看DOM树)。那也可以办到,因为元素不仅仅是哈希表变量,也是字符串变量。元素节点的标量是从它的文本子节点级联中获取的字符串结果。然而,如果元素有子元素,尝试使用一个元素作为标量会引起错误。比如${doc.book}将会以错误而终止。
FTL打印2个chapter的title:
这里,book有两个chapter子元素,doc.book.chapter是存储两个元素节点的序列。因此,我们可以概括上面的FTL,所以它以任意chapter的数量起作用:
但是如果只有一个chapter会怎么样呢?实际上,当你访问一个作为哈希表子变量的元素时,通常也可以是序列(不仅仅是哈希表和字符串),但如果序列只包含一个项,那么变量也作为项目自身。所以,回到第一个示例中,它也会打印book的title:
但是你知道那里就只有一个book元素,而且book也就只有一个title,所以你可以忽略那些[0]。如果book恰好有一个chapter-s(否则它就是模糊的:它怎么知道你想要
<h1>${doc.book.title}</h1>
<h1>Test Book</h1>
<h2>${doc.book.chapter[0].title}</h2>
<h2>${doc.book.chapter[1].title}</h2>
<#list doc.book.chapter as ch>
<h2>${ch.title}</h2>
</#list>
<h1>${doc.book[0].title[0]}</h1>
的是哪个chapter的title?所以它就会以错误而停止),${doc.book.chapter.title}也可以正常进行。但是因为一个book可以有很多chapter,你不能使用这种形式。如果元素book没有子元素chapter,那么doc.book.chapter将是一个长度为零的序列,所以用FTL<#list ...>也可以进行。
知道这样一个结果是很重要的,比如,如果book没有chapter-s,那么book.chapter就是一个空序列,所以doc.book.chapter就不会是false,它就一直是true!类似地,doc.book.somethingTotallyNonsense??也不会是false。来检查是否有子节点,可以使用doc.book.chapter[0]??(或doc.book.chapter?size == 0)。当然你可以使用类似所有的控制处理操作符(比如doc.book.author[0]!"Anonymous"),只是不要忘了那个[0]。
注意:
序列的大小是1的规则是方便XML包装的方便特性(通过多类型的FTL变量来实现)。其他普通序列将不会起作用。
现在我们完成了打印每个chapter所有的para-s示例:
这将打印出:
上面的FTL可以书写的更加漂亮:
最终,一个广义的子节点选择机制是:模板列出所有示例XML文档的para-s。
<h1>${doc.book.title}</h1>
<#list doc.book.chapter as ch>
<h2>${ch.title}</h2>
<#list ch.para as p>
<p>${p}
</#list>
</#list>
<h1>Test</h1>
<h2>Ch1</h2>
<p>p1.1
<p>p1.2
<p>p1.3
<h2>Ch2</h2>
<p>p2.1
<p>p2.2
<#assign book = doc.book>
<h1>${book.title}</h1>
<#list book.chapter as ch>
<h2>${ch.title}</h2>
<#list ch.para as p>
<p>${p}
</#list>
</#list>
输出将会是:
这个示例说明了哈希表变量选择序列子节点的做法(在前面示例中的序列大小为1)。在这个具体的例子中,子变量chapter返回一个大小为2的序列(因为有两个chapter -s),之后子变量para在那个序列中选择para所有的子节点。
这种机制的一个负面结果是类似于doc.somethingNonsense.otherNonsesne.totalNonsense这样的东西会被算作是空序列,而且你也不会得到任何错误信息。
2.1.2 访问属性
这个XML和原来的那个是相同的,除了它使用title属性,而不是元素:
一个元素的属性可以通过和元素的子元素一样的方式来访问,除了你在属性名的前面放置一个@符号:
<#list doc.book.chapter.para as p>
<p>${p}
</#list>
<p>p1.1
<p>p1.2
<p>p1.3
<p>p2.1
<p>p2.2
<!-- THIS XML IS USED FOR THE "Accessing attributes" CHAPTER ONLY! -->
<!-- Outside this chapter examples use the XML from earlier. -->
<book title="Test">
<chapter title="Ch1">
<para>p1.1</para>
<para>p1.2</para>
<para>p1.3</para>
</chapter>
<chapter title="Ch2">
<para>p2.1</para>
<para>p2.2</para>
</chapter>
</book>
这会打印出和前面示例相同的结果。
按照和获取子节点一样的逻辑来获得属性,所以上面的ch.@title结果就是大小为1的序列。如果没有title属性,那么结果就是一个大小为0的序列。所以要注意,这里使用内建函数也是有问题的:如果你很好奇是否foo含有属性bar,那么你不得不写foo.@bar[0]??来验证。(foo.@bar??是不对的,因为它总是返回true)。类似地,如果你想要一个bar属性的默认值,那么你就不得不写foo.@bar[0]!"theDefaultValue"。
正如子元素那样,你可以选择多节点的属性。例如,这个模板将打印所有chapter的title属性。
2.1.3 探索DOM树
这个FTL将会枚举所有book元素的子节点:
这会打印出:
?node_type的意思可能没有解释清除。有一些在DOM树中存在的节点类型,比如"element","text","comment","pi"等。
?node_name返回节点的节点名称。对于其他的节点类型,也会返回一些东西,但
<#assign book = doc.book>
<h1>${book.@title}</h1>
<#list book.chapter as ch>
<h2>${ch.@title}</h2>
<#list ch.para as p>
<p>${p}
</#list>
</#list>
<#list doc.book.chapter.@title as t>
${t}
</#list>
<#list doc.book?children as c>
- ${c?node_type} <#if c?node_type = 'element'>${c?node_name}</#if>
</#list>
- text
- element title
- text
- element chapter
- text
- element chapter
- text
是它对声明的XML处理更有用,这会在后面章节中讨论。
如果book元素有属性,由于实际的原因它可能不会在上面的列表中出现。但是你可以获得包含元素所有属性的列表,使用变量元素的子变量@@。如果你将XML的第一行修改为这样:
然后运行这个FTL:
然后得到这个输出(或者其他相似的结果):
要返回子节点的列表,有一个方便的子变量来仅仅列出元素的子元素:
将会打印:
你可以使用内建函数parent来获得元素的父节点:
这会打印出:
在最后一行你访问到了DOM树的根节点,文档节点。它不是一个元素,这就是为什么得到了一个奇怪的名字;现在我们不来处理它,文档节点没有父节点。
你可以使用内建函数root来快速返回到文档节点:
<book foo="Foo" bar="Bar" baaz="Baaz">
<#list doc.book.@@ as attr>
- ${attr?node_name} = ${attr}
</#list>
- baaz = Baaz
- bar = Bar
- foo = Foo
<#list doc.book.* as c>
- ${c?node_name}
</#list>
- title
- chapter
- chapter
<#assign e = doc.book.chapter[0].para[0]>
<#-- Now e is the first para of the first chapter -->
${e?node_name}
${e?parent?node_name}
${e?parent?parent?node_name}
${e?parent?parent?parent?node_name}
para
chapter
book
@document
这会打印:
在内建函数完整的列表中你可以用来在DOM树中导航,可以阅读参考文档中的内建函数-节点部分。
2.1.4 使用XPath表达式
注意:
XPath表达式仅在Jaxen(推荐使用,但是使用至少Jaxen 1.1-beta-8版本,不能再老了)或Apache Xalan库可用时有效。(Apache Xalan库在Sun J2SE 1.4,1.5和1.6(也许在后续版本)中已经包含了;不需要独立的Xalan的jar包)
如果哈希表的键使用了节点变量而不能被解释(下一部分对此精确定义),那么它就会被当作Xpath表达式被解释。要获得XPath的更多信息,可以访问http://www.w3.org/TR/xpath。
例如,这里我们列出标题为”Ch1”的chapter的para元素:
它会打印:
长度为1(在前面部分解释过了)的序列的规则也代表XPath的结果。也就是说,如果结果序列仅仅包含1个节点,它也会当作节点自身。例如,打印chapter元素”Ch1”第一段:
这也会打印相同内容:
XPath表达式的内容节点(或者是节点序列)是哈希表子变量被用作发布XPath表达式的节点。因此,这将打印和上面例子相同的内容:
注意现在你可以使用0序列或多(比1多)节点作为内容,这只在程序员已经建立FreeMarker使用Jaxen而不是Xalan时才可以。
<#assign e = doc.book.chapter[0].para[0]>
${e?root?node_name}
${e?root.book.title}
@document
Test Book
<#list doc["book/chapter[title='Ch1']/para"] as p>
<p>${p}
</#list>
<p>p1.1
<p>p1.2
<p>p1.3
${doc["book/chapter[title='Ch1']/para[1]"]}
${doc["book/chapter[title='Ch1']/para[1]"][0]}
${doc.book["chapter[title='Ch1']/para[1]"]}
也要注意XPath序列的项索引从1开始,而FTL的序列项索引是用0开始的。因此,要选择第一个chapter,XPath表达式是"/book/chapter[1]",而TL表达式是book.chapter[0]。
如果程序员建立使用Jaxen而不是Xalan,那么FreeMarker的变量在使用XPath变量引用时是可见的:
注意$currentTitle不是FreeMarker的插值,因为那里没有{和}。那是XPath表达式。
一些XPath表达式的结果不是节点集,而是字符串,数字或者布尔值。对于那些XPath表达式,结果分别是FTL字符串,数字或布尔值变量。例如,下面的例子将会计算XML文档中para元素的总数,所以结果是一个数字:
输出是:
2.1.5 XML命名空间
默认来说,当你编写如doc.book这样的东西时,那么它会选择属于任何XML命名空间(和XPath相似)名字为book的元素。如果你想在XML命名空间中选择一个元素,你必须注册一个前缀,然后使用它。比如,如果元素book是命名空间http://example.com/ebook,那么你不得不关联一个前缀,要在模板的顶部使用ftl指令的the ns_prefixes参数:
现在你可以编写如doc["e:book"]的表达式。(因为冒号会混淆FreeMarker,方括号语法的使用是需要的)
ns_prefixes的值作为哈希表,你可以注册多个前缀:
ns_prefixes参数影响整个FTL命名空间。这就意味着实际中,你在主页面模板中注册的前缀必须在所有的<#include ...>-d模板中可见,而不是<#imported ...>-d模板(经常用来引用FTL库)。从另外一种观点来说,一个FTL库可以注册XML命名空间前缀来为自己使用,而前缀注册不会干扰主模板和其他库。
要注意,如果一个输入模板是给定XML命名空间域中的,为了方便你可以设置它为默
<#assign currentTitle = "Ch1">
<#list doc["book/chapter[title=$currentTitle]/para"] as p>
...
${x["count(//para)"]}
5
<#ftl ns_prefixes={"e":"http://example.com/ebook"}>
<#ftl ns_prefixes={
"e":"http://example.com/ebook",
"f":"http://example.com/form",
"vg":"http://example.com/vectorGraphics"}
>
认命名空间。这就意味着如果你不使用前缀,如在doc.book中,那么它会选择属于默认命名空间的元素。这个默认命名空间的设置使用保留前缀D,例如:
现在表达式doc.book选择属于XML命名空间http://example.com/ebook的book元素。不幸的是,XPath不支持默认命名空间。因此,在XPath表达式中,没有前缀的元素名称通常选择不输入任何XML命名空间的元素。然而,在默认命名空间中访问元素你可以直接使用前缀D,比如:doc["D:book/D:chapter[title='Ch1']"]。
注意当你使用默认命名空间时,那么你可以使用保留前缀N来选择不属于任意节点空间的元素。比如doc.book["N:foo"]。这对XPath表达式不起作用,上述的都可以写作doc["D:book/foo"]。
2.1.6 不用忘了转义!
我们在所有的示例中都犯了大错。我们生成HTML格式的输出,HTML格式保留如<,&等的字符。所以当我们打印普通文本(比如标题和段落)时,我们不得不转义它,因此,示例的正确版本是:
所以如果book的标题是“Romeo & Julia”,那么HTML输出的结果就是正确的:
2.2 形式化描述
每一个和DOM树中单独节点对应的变量都是节点和哈希表类型的多类型的变量(对于程序员:实现TemplateNodeModel和TemplateHashModel两个接口)。因此,你可以使用和节点有关的内建函数来处理它们。哈希表的键被解释为XPath表达式,除了在下面表格中显示的特殊键。一些节点变量也有字符串类型,所以你可以使用它们作为字符串变量(对于程序员:他们需要实现TemplateScalarModel接口)。
节点类型(?node_type)
节点名称
(?node_name)
字符串值
(比如 <p>${node})
特殊的哈希表的键
<#ftl ns_prefixes={"D":"http://example.com/ebook"}>
<#escape x as x?html>
<#assign book = doc.book>
<h1>${book.title}</h1>
<#list book.chapter as ch>
<h2>${ch.title}</h2>
<#list ch.para as p>
<p>${p}
</#list>
</#list>
</#escape>
...
<h1>Romeo &amp; Julia</h1>
...
"document"
"@document"
没有字符串值。
(当你尝试用字符串来使用时发生错误。)
"elementName", "prefix:elementName", "*", "**", "@@markup", "@@nested_markup", "@@text"
"element"
"name":元素的名称。这是本地命名(也就是没有命名空间前缀的名字。)
如果它没有子元素,所有子节点的文本串联起来。当你尝试用它当字符串时会发生错误。
"elementName", "prefix:elementName", "*", "**", "@attrName", "@prefix:attrName", "@@", "@*", "@@start_tag", "@@end_tag", "@@attributes_markup", "@@markup", "@@nested_markup", "@@text", "@@qname"
"text"
"@text"
文本本身。
"@@markup", "@@nested_markup", "@@text"
"pi"
"@pi$target"
在目标名称和?>之间的部分
"@@markup", "@@nested_markup", "@@text"
"comment"
"@comment"
注释的文本,不包括分隔符<!--和-->。
"@@markup", "@@nested_markup", "@@text"
"attribute"
"name":属性的名字。这是本地命名(也就是没有命名空间前缀的名字。)
属性的值。
"@@markup", "@@nested_markup", "@@text", "@@qname"
"document_type"
"@document_type$name": name是文档元素的名字。
没有字符串值。(当你尝试用字符串来使用时发生错误。)
"@@markup", "@@nested_markup", "@@text"
注意:
 没有CDATA类型,CDATA节点被透明地认为是文本节点。
 变量不支持?keys和?values。
 元素和属性节点的名字是本地命名,也就是,它们不包含命名空间前缀。节点所属的命名空间的URI可以使用内建函数?node_namespace来获得。
 XPath表达式需要Jaxen(推荐使用,但是请使用1.1-beta-8之后的版本;可以从http://jaxen.org/下载。)或者Apache的Xalan库,否则会有错误并且终止模板的执行。要注意,隐藏在XPath表达式中一些特殊哈希表键有相同的意思,尽管没有
XPath可用的实现,但那些XPath表达式仍然会起作用。如果Xalan和Jaxen两者都可用,FreeMarker将会使用Xalen,除非你在Java代码中通过调用freemarker.ext.dom.NodeModel.useJaxenXPathSupport()方法,才会使用Jaxen。
 如果Jaxen被用来支持XPath(而不是Xalan),那么FreeMarker变量通过XPath变量的引用是可见的(比如doc["book/chapter[title=$currentTitle]"])。
特殊哈希表键的含义:
 "elementName","prefix:elementName":返回元素名为elementName的子节点的序列。(注意这里的子节点就是直接后继)选择是XML命名空间的标识,除非XML文档用不再命名空间标识节点中的XML解析器解析。在XML命名空间标识节点中,不含前缀的名字(元素名)仅仅选择不属于任何XML命名空间的元素(除非你已经注册了默认的XML命名空间),有前缀的名字(前缀:元素名)选择属于前缀代表命名空间的元素。前缀的注册和默认XML命名空间的设置是由ftl指令的ns_prefixes参数完成的。
 "*":返回所有子元素(直接后继)节点的序列。这个序列会按“文档顺序”包含元素,也就是说,按照每个XML节点的表现形式的第一个字符的顺序出现(在一般实体的扩展之后)。
 "**":返回所有后继节点的序列。这个序列按文档顺序包含元素。
 "@attName","@prefix:attrName":作为一个大小为1,包含属性节点的序列的形式,返回元素的属性名attName,如果属性不存在时,作为一个空序列返回(所以来检查属性是否存在,可以使用foo.@attName[0]??,而不是foo.@attName??)。正如"elementName"这个特殊的键,如果序列的长度为1,那么它也当作是第一个子变量。如果没有使用prefix,那么它只返回属性,而不使用XML命名空间(尽管你已经设置了以恶搞默认的XML命名空间)。如果使用了prefix,它返回仅仅属于关联的prefix的XML命名空间的属性。前缀的注册是由ftl指令的ns_prefixes参数完成的。
 "@@"或"@*":返回属于父节点的节点的属性序列,这和XPath中的@*是相同。
 "@@qname":返回元素的完全限定名(比如e:book,和由?node_name返回的本地名book形成对比)。使用的前缀(如e)是基于在当前命名空间用ftl指令的ns_prefixes参数注册的前缀而选择的,而不受源XML文档中使用的前缀影响。如果你已经设置了一个默认的XML命名空间,那么节点会使用默认的,前缀D就会被使用了。不属于任何XML命名空间的节点,不使用前缀(尽管你设置过默认的命名空间)。如果节点没有为命名空间注册的前缀,那结果是不存在的变量(node.@@qname??是false)。
 "@@markup":这会以字符串形式返回一个节点的完整XML标记。(完整的XML标记表示它也包含子节点的标记,而且包含子节点的子节点的标记,以此类推)你得到的标记和在源XML文档中的标记相比不是必须的,它只是语义上的相同。特别地,注意CDATA段将会解释成普通文本。也要注意基于你是怎么用包装原生的XML文档的,注释或处理指令节点可能被移除,而且它们将会从源文件的输出中消失。对于在输出XML段的每个XML命名空间,第一个被输出的开始标记将会包含xmlns:prefix属性,而且那些前缀将会在输出的元素和属性名中使用。这些前缀和使用ftl指令的ns_prefixes参数注册的前缀相同(没有前缀使用D,因为它会和xmlns属性被用来注册默认的命名空间),如果XML命名空间没有定
义前缀,那么会使用一个任意未被选择使用的前缀。
 "@@nested_markup":这个和"@@markup"相似,但是它返回不包括开放和封闭标记元素的XML标记。对于文档节点,它返回和"@@markup"相同的内容。对于其他类型节点(文本,处理指令等),它返回一个空字符串。不像"@@markup",在输出中没有xmlns:prefix属性,但是关于在元素和属性名中使用前缀规则是相同的。
 "@@text":它返回文本节(所有后继文本节点,而不是直接子节点)点的值,连接成一个单独的字符串。如果节点没有子文本节点,那么返回的是空字符串。
 "@@start_tag":返回元素节点开始标记的标记。正如@@markup,输出和源XML文档相比不是必须的,但是在语义上是相同的。关于XML命名空间(输出中的xmlns:prefix属性等),规则和@@markup是相同的。
 "@@end_tag":返回元素节点结束标记的标记,正如@@markup,输出和源XML文档相比不是必须的,但是在语义上是相同的。
 @@attributes_markup:返回元素节点属性的标记,正如@@markup,输出和源XML文档相比不是必须的,但是在语义上是相同的。
2.2.1 节点序列
许多特殊哈希表的键(指的示上面所有的列表),和XPath表达式的结果是节点集(参考XPath介绍http://www.w3.org/TR/xpath/),返回节点的序列。
这些节点序列,如果它们只存储一个子变量,也会当作子变量本身。例如,如果book元素只有一个title子节点,${book.title[0]}和${book.title}是相同的。
返回一个空节点序列是普通的情形。例如,如果一个确定的XML文档,book元素没有子元素chapter,那么book.chapter的结果就是一个空的节点序列。要小心!这也意味着,book.chapter(注意类型)也会返回空的节点序列,而且会抛出错误而停止执行。同时,book.chapter??(注意类型)将会返回true,因为空的序列存在,所以你不得不使用book.chapter[0]??来检查。
节点序列存储的不是1个节点(但是0或者比1多的节点)也会支持上面所描述的哈希表的键。那就是,下面这些特殊的键都是支持的:
 "elementName","prefix:elementName"
 "@attrName","@prefix:attrName"
 "@@markup","@@nested_markup"
 "@@text"
 "*", "**"
 "@@", "@*"
当你在一个包含多于1个节点或0个节点序列里应用上面这些特殊的键中之一的时候,那么对于序列(就是特殊的键可以起作用,比如文本节点对于键*或@foo将会被略过)中的每个节点,这些特殊的键将会被应用于单独的节点,结果将会从最终的结果中被连接。结果会被以和节点在序列中出现的对应顺序来连接。连接就意味着字符串或序列的连接是基于结果类型之上的。如果特殊的键会得到单独节点的字符串结果,那么对于多个节点的结果就是一个单独的字符串(对单独节点的结果进行连接),而如果特殊的键返回单独节点的序列,那么对于多个节点,结果就是一个单独的序列。如果在你应用特殊键的序列中没有节点,那么字符串结果就是空串,而序列结果就是空序列。
XPath表达式可以和节点序列一同使用。然而,对于0或者大于1的节点,因为Xalan对XPath实现的限制,仅仅你使用Jaxen代替Xalan时它才会起作用。
第三章 声明的XML处理
3.1 基础内容
注意:
这部分使用的DOM树和变量是之前章节中做好的。
因为XML处理的方法非常必要—这在前面章节中已经展示了—你编写一个FTL程序来遍历树,为了找到不同种类的节点。而使用声明的方法,你宁愿定义如何控制不同种类的节点,之后让FreeMarker遍历那个树,调用你定义的处理器。这个方法对于复杂的XML模式非常有用,相同元素可以作为其他元素的子元素出现。这样的模式的示例就是XHTML和XDocBook。
你最经常使用来处理声明方式的指令就是recurse指令,这个指令获取节点变量,并把它作为是参数,从第一个子元素开始,一个接一个地“访问”所有它的子元素。“访问”一个节点意味着它调用了用户自定义的指令(比如宏),它的名字和子节点(?node_name)的名字相同。我们这么说,用户自定义指令操作节点。使用用户自定义指令处理的节点作为特殊变量.node是可用的。例如,这个FTL:
会打印(这里已经移除了输出内容中一些烦扰的空白):
如果你调用recurse而不用参数,那么它使用.node,也就是说,它访问现在处理这个节点的所有子节点。所以这个FTL:
会打印(这里已经移除了输出内容中一些烦扰的空白):
<#recurse doc>
<#macro book>
I'm the book element handler, and the title is: ${.node.title}
</#macro>
I'm the book element handler, and the title is: Test Book
<#recurse doc>
<#macro book>
Book element with title ${.node.title}
<#recurse>
End book
</#macro>
<#macro title>
Title element
</#macro>
<#macro chapter>
Chapter element with title: ${.node.title}
</#macro>
你已经看到了如何来为元素节点定义处理器,但不是为文本节点定义处理器。因为处理器的名字是和它处理的节点名字相同的,作为所有文本节点的节点名字是@text(参考形式化描述的表格中内容), 你为文本节点定义处理器,可以是这样的:
注意?html。你不得不转义HTML文本,因为你生成的是HTML格式的输出。
这个模板就是转换XML到完整的HTML:
那么输出是(这里已经移除了输出内容中一些烦扰的空白):
Book element with title Test Book
Title element
Chapter element with title: Ch1
Chapter element with title: Ch2
End book
<#macro @text>${.node?html}</#macro>
<#recurse doc>
<#macro book>
<html>
<head>
<title><#recurse .node.title></title>
</head>
<body>
<h1><#recurse .node.title></h1>
<#recurse>
</body>
</html>
</#macro>
<#macro chapter>
<h2><#recurse .node.title></h2>
<#recurse>
</#macro>
<#macro para>
<p><#recurse>
</#macro>
<#macro title>
<#--
We have handled this element imperatively,
so we do nothing here.
-->
</#macro>
<#macro @text>${.node?html}</#macro>
注意你可以在输出中使用trim指令,如<#t>来大幅减少多余的空白。参考:模板开发指南/杂项/空白处理。
你也许会说FTL处理它的这些必要方法可以更短些。那是对的,但是示例XML使用了非常简单的模式,正如之前说过的,声明方法带和XML模式一同来了它的格式,而这个模式关于这里可以出现什么元素是不固定的。所以,介绍元素mark, 应该把文本标记为红色,而和你在哪儿使用它无关;在title或在para中。对于这点,使用声明的方法,你可以增加一个宏:
那么<mark>...</mark>将会自动起作用。所以对于命令式的XML模式,声明的XML处理确实将会很短,而且更重要的是,对于必要的XML处理,FTL-s会更清晰。但这都依赖于你的决定,什么时候使用哪种方法;不要忘记你可以自由混合两种方法。也就是说,在一个元素处理器中,你可以使用命令式的方法来处理元素的内容。
3.2 详细内容
3.2.1 默认处理器
对于一些XML节点类型,有默认的处理器,这会处理你不给这些节点定义处理器的节点(也就是说,如果没有可用的,和节点名称相同的用户自定义指令)。这里的节点类型,默认的处理器会做:
 文本节点:打印其中的文本。要注意,在很多应用程序中,这对你来说并不好,因为你应该在你发送它们到输出(使用?html或?xml或?rtf等,这基于输出的格式)前转义这些文本。
 处理指令节点:如果你定义了自定义指令,可以通过调用处理器调用@pi,否则将什么都不做(忽略这些节点)。
 注释节点,文档类型节点:什么都不做(忽略这些节点)。
<html>
<head>
<title>Test Book</title>
</head>
<body>
<h1>Test Book</h1>
<h2>Ch1</h2>
<p>p1.1
<p>p1.2
<p>p1.3
<h2>Ch2</h2>
<p>p2.1
<p>p2.2
</body>
</html>
<#macro mark><font color=red><#recurse></font></#macro>
 文档节点:调用recurse,也就是说,访问文档节点的所有子节点。
元素和属性节点通常将会被XML独立机制处理。也就是,@node_type将会被调用作为处理器,如果它没有被定义,那么错误会阻止模板的处理。
元素节点的情形,这意味着如果你定义了一个称为@element的宏(或其他种类的用户自定义指令),没有其他特定的处理器时,那么它会捕捉所有元素节点。如果你没有@element处理器,那么你必须为所有可能的元素定义处理器。
属性节点在recurse指令中不可见,所以不需要为它们编写处理器。
3.2.2 访问单独节点
使用visit指令你可以访问单独的节点,而不是节点的子节点:<#visit nodeToVisist>。有时这会很有用。
3.2.3 XML命名空间
我们说过对于一个元素的处理器,用户自定义指令(比如宏)的名字就是元素的名字。事实上,它是元素的完全限定名:prefix:elementName。这个关于prefix-es的使用规则和命令式处理是相同的。因此,用户自定义指令book仅仅处理不属于任何XML命名空间(除非你已经定义了默认的XML命名空间)的book元素。所以示例XML将会使用XML命名空间http://example.com/ebook:
那么FTL就会像这样:
<book xmlns="http://example.com/ebook">
...
<#ftl ns_prefixes={"e":"http://example.com/ebook"}>
<#recurse doc>
<#macro "e:book">
<html>
<head>
<title><#recurse .node["e:title"]></title>
</head>
<body>
<h1><#recurse .node["e:title"]></h1>
<#recurse>
</body>
</html>
</#macro>
<#macro "e:chapter">
<h2><#recurse .node["e:title"]></h2>
<#recurse>
</#macro>
<#macro "e:para">
<p><#recurse>
</#macro>
或者你可以定义一个默认的XML命名空间,那后面部分的模板保持和源XML命名空间相同,比如:
但是这种情形下不要忘了在XPath表达式(我们在默认中没有使用)中,默认的XML命名空间必须通过明确的D:来访问,因为在XPath中没有前缀的名称通常指代没有XML命名空间的节点。而且注意到命令式的XML处理也是相同的逻辑,如果(当且仅当)没有默认XML命名空间时,元素处理器的名字没有XML命名空间是N:elementName。然而,对于不是元素类型的节点(比如文本节点),你不能在处理器名称中使用前缀N,因为这些节点在XML命名空间中是没有这些概念的。所以对于示例,文本节点的处理器通常就是@text。
对于更详细的内容,请阅读参考文档中的recurse和visit指令。
<#ftl ns_prefixes={"D":"http://example.com/ebook"}>
<#recurse doc>
<#macro book>
...
<#macro "e:title">
<#--
We have handled this element imperatively,
so we do nothing here.
-->
</#macro>
<#macro @text>${.node?html}</#macro>
第四部分 参考文档
第一章 内建函数参考文档
1.1 处理字符串的内建函数
1.1.1 substring 取子串
注意:
这个内建函数从FreeMarker 2.3.7开始可用。
概要:exp?substring(from, toExclusive),也可以作为exp?substring(from)调用
一个字符串的子串。from是第一个字符开始的索引。它必须是一个数字而且至少是0,而且要小于或等于toExclusive,否则错误就会中断模板的处理。toExclusive是子串中最后一个字符之后的位置索引,换句话说,它比最后一个字符的索引大1。它必须是数字,至少是0,要小于或等于字符串的长度,否则错误就会中止模板的处理。如果toExclusive被忽略了,那么它默认就是字符串的长度。如果参数不是整型的数字,那么数值中只有整型的部分会被使用。
例如:
输出为:
- ${'abc'?substring(0)}
- ${'abc'?substring(1)}
- ${'abc'?substring(2)}
- ${'abc'?substring(3)}
- ${'abc'?substring(0, 0)}
- ${'abc'?substring(0, 1)}
- ${'abc'?substring(0, 2)}
- ${'abc'?substring(0, 3)}
- ${'abc'?substring(0, 1)}
- ${'abc'?substring(1, 2)}
- ${'abc'?substring(2, 3)}
1.1.2 cap_first 首字母大写
字符串中的第一个单词的首字母大写。更精确的“单词”的意思可以查看内建函数word_list。比如:
输出为:
在"- green mouse"的情形下,第一个单词是-。
1.1.3 uncap_first 首字母小写
这和cap_first是相反的。字符串第一个单词的首字母小写。
1.1.4 capitalize 首字母大写
字符串的所有单词都大写。更精确的“单词”的意思可以查看内建函数word_list。比如:
输出为:
- abc
- bc
- c
-
-
- a
- ab
- abc
- a
- b
- c
${" green mouse"?cap_first}
${"GreEN mouse"?cap_first}
${"- green mouse"?cap_first}
Green mouse
GreEN mouse
- green mouse
${" green mouse"?capitalize}
${"GreEN mouse"?capitalize}
Green Mouse
Green Mouse
1.1.5 chop_linebreak 切断换行符
如果在末尾没有换行符的字符串,那么可以换行,否则不改变字符串。
1.1.6 date,time,datetime 日期,时间,时间日期
字符串转换成日期值。建议指定一个确定格式个参数。比如:
将会打印出(基于当地(语言)和其他设置决定输出)如下内容:
要注意日期根据date_format,time_format和datetime_format的设置转换回字符串(对于日期转换成字符的更多内容请阅读:日期内建函数,日期插值)。这和你在转换字符串到日期类型时使用什么格式没有关系。
如果你了解模板处理时默认的日期/时间/时间日期格式,你可以不用格式化参数:
如果字符串不在适当的格式,当你尝试使用这些内建函数时,错误将会中断模板执行。
1.1.7 ends_with 以…结尾
返回是否这个字符串以指定的子串结尾。比如"redhead"?ends_with("head")返回布尔值true。而且"head"?ends_with("head")也返回true。
1.1.8 html HTML格式的转义文本
字符串按照HTML标记输出。也就是说,下面字符串将会被替代:
<#assign test1 = "10/25/1995"?date("MM/dd/yyyy")>
<#assign test2 = "15:05:30"?time("HH:mm:ss")>
<#assign test3 = "1995-10-25 03:05 PM"?datetime("yyyy-MM-dd hh:mm a")>
${test1}
${test2}
${test3}
Oct 25, 1995
3:05:30 PM
Oct 25, 1995 3:05:00 PM
<#assign test1 = "Oct 25, 1995"?date>
<#assign test2 = "3:05:30 PM"?time>
<#assign test3 = "Oct 25, 1995 03:05:00 PM"?datetime>
${test1}
${test2}
${test3}
 <用&lt替换;
 >用&gt替换;
 &用&amp替换;
 "用&quot替换;
注意如果你想安全地插入一个属性,你必须在HTML模板中使用引号标记(是",而不是')为属性值加引号:
注意在HTML页面中,通常你想对所有插值使用这个内建函数。所以你可以使用escape指令来节约很多输入,减少偶然错误的机会。
1.1.9 group 分组
这个函数只和内建函数matches的结果使用。请参考matches函数。
1.1.10 index_of 索引所在位置
返回第一次字符串中出现子串时的索引位置。例如"abcabc"?index_of("bc")将会返回1(不要忘了第一个字符的索引是0)。而且,你可以指定开始搜索的索引位置:将"abcabc"?index_of("bc", 2)会返回4。这对第二个参数的数值没有限制:如果它是负数,那就和是0是ige效果了,如果它比字符串的长度还大,那么就和它是字符串长度那个数值是一个效果。小数会被切成整数。
如果第一个参数作为子串没有在该字符串中出现时(如果你使用了第二个参数,那么就从给定的序列开始。),那么就返回-1.
1.1.11 j_string Java语言规则的字符串转义
根据Java语言字符串转义规则来转义字符串,所以它很安全的将值插入到字符串类型中。要注意它不会在被插入的值的两侧添加引号;你需要在这里使用字符串值。
所有UCS编码下指向0x20的字符会被转义。当它们在Java语言中(比如\n,\t等)没有专门的转义序列时,将会被用UNICODE进行转义替换(\uXXXX)。
例如:
将会打印:
<input type=text name=user value="${user?html}">
<#assign beanName = 'The "foo" bean.'>
String BEAN_NAME = "${beanName?j_string}";
String BEAN_NAME = "The \"foo\" bean.";
1.1.12 js_string JavaScript语言规则的字符串转义
根据JavaScript语言字符串转义规则来转义字符串,所以它很安全的将值插入到字符串类型中。要注意,它不会在被插入的值两侧添加引号;而是在字符串中间使用。
引号(")和单引号(')要被转义。从FreeMarker 2.3.1开始,也要将>转义为\>(为了避免</script>)。
所有在UCS编码下指向0x20的字符将会被转义。当它们在JavaScript中没有专用的转义序列时(比如\n,\t等),它们会被UNICODE字符代替(\uXXXX)。
例如:
将会打印:
1.1.13 json_string JSON规则的字符串转义
根据JSON语言的字符串规则来转义字符串,所以在字符串中插入值是安全的。要注意它不会在被插入的值两侧添加引号;而是在字符串中间使用。
这不会转义'字符,因为JSON字符串必须使用"来括起来。它会在<之后直接出现的/(斜杠)字符转义为\/,来避免</script>等。它也会在]]之后转义>字符为\u003E,来避免退出XML的CDATA段。
所有在UCS编码下指向0x20的字符会被转义。当在JSON中没有专用的转义序列时(比如\n,\t等),它们会被UNICODE字符代替(\uXXXX)。
1.1.14 last_index_of 最后的索引所在位置
返回最后一次(最右边)字符串中出现子串时的索引位置。它返回子串第一个(最左边)字符所在位置的索引。例如"abcabc"?last_index_of("ab"):将会返回3。而且你可以指定开始搜索的索引。例如:"abcabc"?last_index_of("ab", 2),将会返回0。要注意第二个参数暗示了子串开始的最大索引。对第二个参数的数值没有限制:如果它是负数,那么效果和是零的一样,如果它比字符串的长度还大,那么就和它是字符串长度那个数值是一个效果。小数会被切成整数。
如果第一个参数作为子串没有在该字符串中出现时(如果你使用了第二个参数,那么就从给定的序列开始。),那么就返回-1.
<#assign user = "Big Joe's \"right hand\"">
<script>
alert("Welcome ${user?js_string}!");
</script>
<script>
alert("Welcome Big Joe\'s \"right hand\"!");
</script>
1.1.15 length 字符串长度
字符串中字符的数量
1.1.16 lower_case 小写形式
字符串的小写形式。比如"GrEeN MoUsE?lower_case"将会是"green mouse"。
1.1.17 left_pad 距左边
注意:
这个内建函数从FreeMarker 2.3.1版本开始可用。在2.3版本中是没有的。
如果它仅仅用1个参数,那么它将在字符串的开始插入空白,直到整个串的长度达到参数指定的值。如果字符串的长度达到指定数值或者比指定的长度还长,那么就什么都不做了。比如这样:
将会打印:
如果使用了两个参数,那么第一个参数表示的含义和你使用一个参数时的相同,第二个参数指定用什么东西来代替空白字符。比如:
[${""?left_pad(5)}]
[${"a"?left_pad(5)}]
[${"ab"?left_pad(5)}]
[${"abc"?left_pad(5)}]
[${"abcd"?left_pad(5)}]
[${"abcde"?left_pad(5)}]
[${"abcdef"?left_pad(5)}]
[${"abcdefg"?left_pad(5)}]
[${"abcdefgh"?left_pad(5)}]
[ ]
[ a]
[ ab]
[ abc]
[ abcd]
[abcde]
[abcdef]
[abcdefg]
[abcdefgh]
将会打印:
第二个参数也可以是个长度比1大的字符串。那么这个字符串会周期性的插入,比如:
将会打印:
第二个参数必须是个字符串值,而且至少有一个字符。
1.1.18 right_pad 距右边
注意:
这个内建函数从FreeMarker 2.3.1版本开始可用。在2.3版本中是没有的。
这个和left_pad相同,但是它从末尾开始插入字符而不是从开头。
比如:
[${""?left_pad(5, "-")}]
[${"a"?left_pad(5, "-")}]
[${"ab"?left_pad(5, "-")}]
[${"abc"?left_pad(5, "-")}]
[${"abcd"?left_pad(5, "-")}]
[${"abcde"?left_pad(5, "-")}]
[-----]
[----a]
[---ab]
[--abc]
[-abcd]
[abcde]
[${""?left_pad(8, ".oO")}]
[${"a"?left_pad(8, ".oO")}]
[${"ab"?left_pad(8, ".oO")}]
[${"abc"?left_pad(8, ".oO")}]
[${"abcd"?left_pad(8, ".oO")}]
[.oO.oO.o]
[.oO.oO.a]
[.oO.oOab]
[.oO.oabc]
[.oO.abcd]
这将输出:
1.1.19 contains 包含
注意:
这个内建函数从FreeMarker 2.3.1版本开始可用。在2.3版本中是没有的。
如果函数中的参数可以作为源字符串的子串,那么返回true。比如:
将会输出:
[${""?right_pad(5)}]
[${"a"?right_pad(5)}]
[${"ab"?right_pad(5)}]
[${"abc"?right_pad(5)}]
[${"abcd"?right_pad(5)}]
[${"abcde"?right_pad(5)}]
[${"abcdef"?right_pad(5)}]
[${"abcdefg"?right_pad(5)}]
[${"abcdefgh"?right_pad(5)}]
[${""?right_pad(8, ".oO")}]
[${"a"?right_pad(8, ".oO")}]
[${"ab"?right_pad(8, ".oO")}]
[${"abc"?right_pad(8, ".oO")}]
[${"abcd"?right_pad(8, ".oO")}]
[ ]
[a ]
[ab ]
[abc ]
[abcd ]
[abcde]
[abcdef]
[abcdefg]
[abcdefgh]
[.oO.oO.o]
[aoO.oO.o]
[abO.oO.o]
[abc.oO.o]
[abcdoO.o]
<#if "piceous"?contains("ice")>It contains "ice"</#if>
It contains "ice"
1.1.20 matches 匹配
这是一个“超级用户”函数。不管你是否懂正则表达式。
注意:
这个函数仅仅对使用Java2平台的1.4版本之后起作用。否则它会发生错误并中止模板的处理。
这个函数决定了字符串是否精确匹配上模式。而且,它返回匹配的子串列表。返回值是一个多类型的值:
 布尔值:如果字符串精确匹配上模式返回true,否则返回false。例如,"fooo"?matches('fo*')是true,但是"fooo bar"?matches('fo*')是false。
 序列:字符串匹配子串的列表。可能是一个长度为0的序列。
比如:
将会打印:
如果正则表达式包含分组(括号),那么你可以使用内建函数groups来访问它们:
这会打印:
matches接受两个可选的标记参数,要注意它不支持标记fr,而且也会忽略标记r。
<#if "fxo"?matches("f.?o")>Matches.<#else>Does not match.</#if>
<#assign res = "foo bar fyo"?matches("f.?o")>
<#if res>Matches.<#else>Does not match.</#if>
Matching sub-strings:
<#list res as m>
- ${m}
</#list>
Matches.
Does not match.
Matching sub-strings:
- foo
- fyo
<#assign res = "aa/rx; ab/r;"?matches("(\\w[^/]+)/([^;]+);")>
<#list res as m>
- ${m} is ${m?groups[1]} per ${m?groups[2]}
</#list>
- aa/rx; is aa per rx
- ab/r; is ab per r
1.1.21 number 数字格式
字符串转化为数字格式。这个数字必须是你在FTL中直接指定数值的格式。也就是说,它必须以本地独立的形式出现,小数的分隔符就是一个点。此外这个函数认识科学记数法。(比如"1.23E6","1.5e-8")。
如果这恶搞字符串不在恰当的格式,那么在你尝试访问这个函数时,错误会抛出并中止模板的处理。
已知的问题:如果你使用比Java2平台1.3版本还早的版本,这个函数将不会被识别,前缀和科学记数法都不会起作用。
1.1.22 replace 替换
在源字符串中,用另外一个字符穿来替换原字符串中出现它的部分。它不处理词的边界。比如:
将会打印:
替换是从左向右执行的。这就意味着:
将会打印:
如果第一个参数是空字符串,那么所有的空字符串将会被替换,比如"foo"?replace("","|"),就会得到"|f|o|o|"。
replace接受一个可选的标记参数,作为第三个参数。
1.1.23 rtf 富文本
字符串作为富文本(RTF 文本),也就是说,下列字符串:
 \替换为\\
 {替换为\{
 }替换为\}
1.1.24 url URL转义
注意:
这个内建函数从FreeMarker 2.3.1版本开始可用。在2.3版本中是没有的。
在URL之后的字符串进行转义。这意味着,所有非US-ASCII的字符和保留的URL字符
${"this is a car acarus"?replace("car", "bulldozer")}
this is a bulldozer abulldozerus
${"aaaaa"?replace("aaa", "X")}
Xaa
将会被%XX形式来转义。例如:
输出将是(假设用来转义的字符集是US-ASCII兼容的字符集):
注意它会转义所有保留的URL字符(/,=,&等),所以编码可以被用来对查询参数的值进行,比如:
注意:
上面的没有HTML编码(?htm)是必要的,因为URL转义所有保留的HTML编码。但是要小心:通常引用的属性值,用普通引号(")包括,而不是单引号('),因为单引号是不被URL转义的。
为了进行URL转义,必须要选择字符集,它被用来计算被转义的部分(%XX)。如果你是HTML页面设计者,而且你不懂这个,不要担心:程序员应该配置FreeMarker,则它默认使用恰当的字符集(程序员应该多看看下面的内容)。如果你是一个比较热衷于技术的人,那么你也许想知道被url_escaping_charset设置的指定字符集,它可以在模板的执行时间设置(或者,更好的是,由程序员之前设置好)。例如:
此外,你可以明确地指定一个为单独URL转义的字符集,作为内建函数的参数:
如果内建函数url没有参数,那么它会使用由url_escaping_charset设置的字符集。这个设置应该被软件设置,包括FreeMarker(比如一个Web应用框架),因为它不会被默认设置为null。如果它没有被设置,那么FreeMarker退回使用output_encoding的设置,这个也会被默认设置,所以它也是又软件设置的。如果output_encoding也没有被设置,那么没有参数的内建函数url将不会被执行,二期它会引起运行时错误。当然,有参数的url函数将会执行。
用setting指令在模板中设置url_escaping_charset是可能的。至少在真实的MVC应用中,这是一个不好的实践行为。output_encoding不能由setting指令来设置,所以它应该是软件的工作。你可以阅读程序开发指南/其它/字符集问题来获取
<#assign x = 'a/b c'>
${x?url}
a%2Fb%20c
<a href="foo.cgi?x=${x?url}&y=${y?url}">Click here...</a>
<#--
This will use the charset specified by the programmers
before the template execution has started.
-->
<a href="foo.cgi?x=${x?url}">foo</a>
<#-- Use UTF-8 charset for URL escaping from now: -->
<#setting url_escaping_charset="UTF-8">
<#-- This will surely use UTF-8 charset -->
<a href="bar.cgi?x=${x?url}">bar</a>
<a href="foo.cgi?x=${x?url('ISO-8895-2')}">foo</a>
更多信息。
1.1.25 split 分割
它被用来根据另外一个字符串的出现将原字符串分割成字符串序列。比如:
将会打印:
既然已经假设所有分隔符的出现是在新项之前,因此:
将会打印:
split函数接受一个可选的标记参数作为第二个参数。
1.1.26 starts_with 以…开头
如果字符串以指定的子字符串开头,那么返回true。比如"redhead"?starts_with("red")返回布尔值true,而且"red"?starts_with("red")也返回true。
1.1.27 string(当被用作是字符串值时)
什么也不做,仅仅返回和其内容一致的字符串。例外的是,如果值是一个多类型的值(比如同时有字符串和序列两种),那么结果就只是一个简单的字符串,而不是多类型的值。这可以被用来防止多种人为输入。
<#list "someMOOtestMOOtext"?split("MOO") as x>
- ${x}
</#list>
- some
- test
- text
<#list "some,,test,text,"?split(",") as x>
- "${x}"
</#list>
- "some"
- ""
- "test"
- "text"
- ""
1.1.28 trim 修整字符串
去掉字符串首尾的空格。例如:
输出是:
1.1.29 upper_case 大写形式
字符串的大写形式。比如"GrEeN MoUsE"将会是"GREEN MOUSE"。
1.1.30 word_list 词列表
包含字符串词的列表,并按它们在字符串中的顺序出现。词是连续的字符序列,包含任意字符,但是不包括空格。比如:
将会输出:
1.1.31 xhtml XHTML格式
字符串作为XHTML格式文本输出,下面这些:
 <替换为&lt;
 >替换为&gt;
 &替换为&amp;
 "替换为&quot;
 '替换为&#39;
这个函数和xml函数的唯一不同是xhtml函数转义'为&#39;,而不是&apos;,但是一些老版本的浏览器不正确解释&apos;。
1.1.32 xml XML格式
字符串作为XML格式文本输出,下面这些:
 <替换为&lt;
 >替换为&gt;
(${" green mouse "?trim})
(green mouse)
<#assign words = " a bcd, . 1-2-3"?word_list>
<#list words as word>[${word}]</#list>
[a][bcd,][.][1-2-3]
 &替换为&amp;
 "替换为&quot;
 '替换为&apos;
1.1.33 通用标记
很多字符串函数接受一个可选的被称为“标记”的字符串参数。在这些字符串中,每一个字符都影响着内建函数行为的一个特定方面。比如,字母i表示内建函数不应该在同一个字母的大小写上有差异。标记中字母的顺序并不重要。
下面是标记字母的完整列表:
 i:大小写不敏感:不区分同一个字母大小写之间的差异。
 f:仅仅是第一。也就是说,替换/查找等,只是第一次出现的东西。
 r:查找的子串是正则表达式。FreeMarker使用变化的正则表达式,在http://java.sun.com/j2se/1.4.1/docs/api/java/util/regex/Pattern.html中描述。只有你使用Java2平台的1.4版本以后,标记才会起作用。否则它会发生错误导致模板处理停止。
 m:正则表达式多行模式。在多行模式下,表达式^和$仅仅匹配前后,分别是一行结尾或者是字符串的结束。默认这些表达式仅仅匹配整个字符串的开头和结尾。注意^和$不会匹配换行符本身。
 s:启用正则表达式的do-tall模式(和Perl的单行模式一样)。在do-tall模式下,表达式.匹配任意字符串,包括行结束符。默认这个表达式不匹配行结束符。
 c:在正则表达式中许可空白和注释。
示例:
这会输出:
这是内建函数使用通用标记的表格,哪些支持什么样的标记。
内建函数
i(忽略大小写)
r(正则表达式)
m(多行模式)
s(dot-all模式)
c(whitesp和注释)
f(仅第一个)
replace


只和r
只和r
只和r

<#assign s = 'foo bAr baar'>
${s?replace('ba', 'XY')}
i: ${s?replace('ba', 'XY', 'i')}
if: ${s?replace('ba', 'XY', 'if')}
r: ${s?replace('ba*', 'XY', 'r')}
ri: ${s?replace('ba*', 'XY', 'ri')}
rif: ${s?replace('ba*', 'XY', 'rif')}
foo bAr XYar
i: foo XYr XYar
if: foo XYr baar
r: foo XYAr XYr
ri: foo XYr XYr
rif: foo XYr baar
split


只和r
只和r
只和r

match

忽略




1.2 处理数字的内建函数
相关的FAQs:如果你有和1,000,000或1 000 000而不是1000000类似的东西,或者是3.14而不是3,14的东西,反之亦然,请参考FAQ中相关内容,也要注意内建函数c的所有内容。
1.2.1 c 数字转字符
注意:
这个内建函数从FreeMarker 2.3.3以后开始存在。
这个函数将数字转换成字符串,这都是对计算机来说的,而不是对用户。也就是说,它根据程序语言的用法来进行格式化,这对于FreeMarker的所有本地数字格式化设置来说是独立的。它通常使用点来作为小数分隔符,而且它从来不用分组分隔符(像3,000,000),指数形式(比如5E20),多余的在开头或结尾的0(比如03或1.0),还有+号(比如+1)。它最多在小数点后打印16位,因此数值的绝对值小于1E-16将会显示为0。这个函数非常严格,因为作为默认(像${x}这样)数字被本地(语言,国家)特定的数字格式转换为字符串,这是让用户来看的(比如3000000可能会被打印为3,000,000)。当数字不对用户打印时(比如,对于一个数据库记录ID,用作是URL的一部分,或者是HTML表单中的隐藏域,或者打印CSS/JavaScript的数值文本),这个函数必须被用来打印数字(也就是使用${x?c}来代替${x}),否则输出可能由于当前数字格式设置,本地化(比如在一些国家中,小数点不是点,而是逗号)和数值(像大数可能被分组分隔符“损坏”)而损坏。
1.2.2 string(当用作是数值类型时) 数字转字符串
将一个数字转换成字符串。它使用程序员已经指定的默认格式。你也可以明确地用这个函数再指定一个数字格式,这在后面会展示。
有四种预定义的数字格式:computer,currency,number和percent。这些格式的明确含义是本地化(国家)指定的,受Java平台安装环境所控制,而不是FreeMarker,除了computer,用作和函数c是相同的格式。你可以这样来使用预定义的格式:
如果你本地是US English,那么就会生成:
<#assign x=42>
${x}
${x?string} <#-- the same as ${x} -->
${x?string.number}
${x?string.currency}
${x?string.percent}
${x?string.computer}
前三个表达式的输出是相同的,因为前两个表达式是默认格式,这里是数字。你可以使用一个设置来改变默认设置:
现在会输出:
因为默认的数字格式被设置成了“货币”。
除了这三种预定义格式,你可以使用Java中数字格式语法写的任意的数字格式(http://java.sun.com/j2se/1.4/docs/api/java/text/DecimalFormat.html):
输出这些:
42
42
42
$42.00
4,200%
42
<#setting number_format="currency">
<#assign x=42>
${x}
${x?string} <#-- the same as ${x} -->
${x?string.number}
${x?string.currency}
${x?string.percent}
$42.00
$42.00
42
$42.00
4,200%
<#assign x = 1.234>
${x?string("0")}
${x?string("0.#")}
${x?string("0.##")}
${x?string("0.###")}
${x?string("0.####")}
${1?string("000.00")}
${12.1?string("000.00")}
${123.456?string("000.00")}
${1.2?string("0")}
${1.8?string("0")}
${1.5?string("0")} <-- 1.5, rounded towards even neighbor
${2.5?string("0")} <-- 2.5, rounded towards even neighbor
${12345?string("0.##E0")}
在金融和统计学中,四舍五入都是根据所谓的一半原则,这就意味着对最近的“邻居”进行四舍五入,除非离两个邻居距离相等,这种情况下,它四舍五入到偶数的邻居。如果你注意看1.5和2.5的四舍五入的话,这在上面的示例中是可以看到的,两个都被四舍五入到2,因为2是偶数,但1和3是奇数。
除了Java小数语法模式之外,你可以编写如${aNumber?string("currency")}这样的代码,它会做和${aNumber?string.currency}一样的事情。
正如之前展示的预定义格式,默认的数字格式也可以在模板中进行设置:
输出这个:
要注意数字格式是本地化敏感的:
输出这个:
1.2.3 round,floor,ceiling 数字的舍入处理
注意:
1
1.2
1.23
1.234
1.234
001.00
012.10
123.46
1
2
2 <-- 1.5, rounded towards even neighbor
2 <-- 2.5, rounded towards even neighbor
1.23E4
<#setting number_format="0.##">
${1.234}
1.23
<#setting locale="en_US">
US people write: ${12345678?string(",##0.00")}
<#setting locale="hu">
Hungarian people write: ${12345678?string(",##0.00")}
US people write: 12,345,678.00
Hungarian people write: 12 345 678,00
内建函数round从FreeMarker 2.3.13版本之后才存在。
使用确定的舍入法则,转换一个数字到整数:
 round:返回最近的整数。如果数字以.5结尾,那么它将进位(也就是说向正无穷方向进位)
 floor:返回数字的舍掉小数后的整数(也就是说向服务穷舍弃)
 ceiling:返回数字小数进位后的整数(也就是说向正无穷进位)
示例:
打印:
这些内建函数在分页处理时也许有用。如果你仅仅想展示数字的舍入形式,那么你应该使用内建函数string和numer_format设置。
1.3 处理日期的内建函数
1.3.1 string(当用作日期值时)日期转字符串
这个内建函数以指定的格式转换日期类型到字符串类型。(当默认格式由FreeMarker的date_format,time_format和datetime_format设置来支配时是很好的选择,那么你就不需要使用这个内建函数了。)
格式可以是预定义格式中的一种,或者你可以指定明确的格式化模式。
预定义的格式是short,medium,long和full,它们定义了冗长的结果文本输出。例如,如果输出的本地化是U.S. English,而且时区是U.S. Pacific,那么下面的代码:
<#assign testlist=[
0, 1, -1, 0.5, 1.5, -0.5,
-1.5, 0.25, -0.25, 1.75, -1.75]>
<#list testlist as result>
${result} ?floor=${result?floor} ?ceiling=${result?ceiling} ?round=${result?round}
</#list>
0 ?floor=0 ?ceiling=0 ?round=0
1 ?floor=1 ?ceiling=1 ?round=1
-1 ?floor=-1 ?ceiling=-1 ?round=-1
0.5 ?floor=0 ?ceiling=1 ?round=1
1.5 ?floor=1 ?ceiling=2 ?round=2
-0.5 ?floor=-1 ?ceiling=0 ?round=0
-1.5 ?floor=-2 ?ceiling=-1 ?round=-1
0.25 ?floor=0 ?ceiling=1 ?round=0
-0.25 ?floor=-1 ?ceiling=0 ?round=0
1.75 ?floor=1 ?ceiling=2 ?round=2
-1.75 ?floor=-2 ?ceiling=-1 ?round=-2
将会打印这样的内容:
short,medium,long和full的精确含义是以当前本地(语言)设置为主的。此外,它不是由FreeMarker确定的,而是由你运行FreeMarker的Java平台实现的。
对于包含日期和时间两部分的日期类型,你可以分别指定日期和时间部分的长度:
将会输出:
注意?string.short和?string.short_short是相同的,?string.medium和?string.medium_medium是相同的,等等。
${openingTime?string.short}
${openingTime?string.medium}
${openingTime?string.long}
${openingTime?string.full}
${nextDiscountDay?string.short}
${nextDiscountDay?string.medium}
${nextDiscountDay?string.long}
${nextDiscountDay?string.full}
${lastUpdated?string.short}
${lastUpdated?string.medium}
${lastUpdated?string.long}
${lastUpdated?string.full}
12:45 PM
12:45:09 PM
12:45:09 PM CEST
12:45:09 PM CEST
4/20/07
Apr 20, 2007
April 20, 2007
Friday, April 20, 2007
4/20/07 12:45 PM
Apr 20, 2007 12:45:09 PM
April 20, 2007 12:45:09 PM CEST
Friday, April 20, 2007 12:45:09 PM CEST
${lastUpdated?string.short_long} <#-- short date, long time -->
${lastUpdated?string.medium_short} <#-- medium date, short time -->
4/8/03 9:24:44 PM PDT
Apr 8, 2003 9:24 PM
警告!
不幸的是,由于Java平台的限制,你可以在数据模型中保存日期变量,那里FreeMarker不能决定变量是否存储的是日期部分(年,月,日),还是时间部分(时,分,秒,毫秒),还是两者都有。这种情况下,当你编写如${lastUpdated?string.short},或简单的${lastUpdated}时FreeMarker不知道如何来显示日期,因此模板会中止执行并抛出错误。要阻止这些发生,你可以使用内建函数?date,?time和?datetime来帮助FreeMarker。比如${lastUpdated?datetime?string.short}。要询问程序员数据模型中确定的变量是否有这个问题,或通常使用内建函数?date,?time和?datetime来处理。
要代替使用预定义的格式,你可以使用?string(pattern_string)来精确指定格式的模式。这个模式使用Java日期格式的语法。比如:
将会输出:
注意:
不像预定义格式,你不能和精确的给定模式使用内建函数?date,?time和?datetime,因为使用这个模式你就告诉FreeMarker来显示哪部分的日期。然而,FreeMarker将盲目地信任你,如果你显示的部分不存在于变量中,所以你可以显示“干扰”。比如${openingTime?string("yyyy-MM-dd hh:mm:ss a")},而openingTime中只存储了时间,将会显示1970-01-01 09:24:44 PM。
定义模式的字符串也可以是"short","medium","short_medium"等。这和你使用预定义格式语法:someDate?string("short")和someDate?string.short是相同的。
也可以参考:模板开发指南/模板/插值中的日期部分。
1.3.2 date,time,datetime (当使用日期值时)
这些内建函数用来指定日期变量中的哪些部分被使用:
 date:仅仅年,月和日的部分被使用。
 ime:仅仅时,分,秒和毫秒的部分被使用。
 datetime:日期和时间量部分都使用。
在最佳情况下,你不需要使用这些内建函数。不幸的是,由于Java平台上的技术限制,FreeMarker有时不能发现日期中的哪一部分在使用(也就是说,仅仅年+月+日在使用,或仅仅时+分+秒+毫秒在使用,或两种都用);询问程序员哪些变量会有这个问题。如果FreeMarker不得不执行需要这些信息的操作-比如用文本显示日期-但是它不知道哪一部分在使用,它会以错误来中止运行。这就是你不得不使用内建函数的时候了。比如,假设openingTime
${lastUpdated?string("yyyy-MM-dd HH:mm:ss zzzz")}
${lastUpdated?string("EEE, MMM d, ''yy")}
${lastUpdated?string("EEEE, MMMM dd, yyyy, hh:mm:ss a '('zzz')'")}
2003-04-08 21:24:44 Pacific Daylight Time
Tue, Apr 8, '03
Tuesday, April 08, 2003, 09:24:44 PM (PDT)
是一个有这样问题的变量:
这些函数的另外一种用法:来截短日期。比如:
将会输出这样的东西:
如果?的左边是字符串,那么这些内建函数会将字符串转换为日期变量。(参考处理字符串的内建函数date)
1.3.3 iso_...内建函数族
这些内建函数转换日期,时间或时间日期值到字符串,并遵循ISO 8601的扩展格式。这些内建函数有很多表形式: iso_utc,iso_local,iso_utc_nz,iso_local_nz,iso_utc_m,iso_utc_m_nz等。名称的构成由下列单词顺序组成,每部分由一个_(下划线)分隔开:
1. iso(必须的)
2. 是utc或local的二者之一(必须的(除了给定一个参数,这个后面再来说)):来指定你想根据UTC来打印日期或根据当前时区来打印。当前时区是根据FreeMarker的设置项time_zone来确定的,它通常是由程序员在模板外配置的(当然它也可以在模板内设置,比如使用<#setting time_zone="America/New_York">)。
3. h,m或ms(可选的):时间部分的精度。当忽略的时候,就默认设置到秒的精度(比如12:30:18)。h表示小时的精度(比如12),m表示分钟的精度(比如12:30),ms就表示毫秒的精度(12:30:18.25,这里表示250毫秒)。要注意当使用ms时,毫秒会显示为百分制的形式(遵循标准)而且不会去尾到0秒。因此,如果毫秒的部分变成0的话,整个的毫秒的部分就会被忽略掉了。同时也要注意毫秒的部分是由一个点来分隔的,而不是逗号(遵循Web约定和XML Schema的日期/时间格式)。
4. nz(可选的):如果有的话,时区偏移(比如+02:00或-04:30或Z)就不会显示出来了。否则是会显示出来的,仅仅对日期值有效(因为有时区偏移的日期在ISO 8601:2004中没有出现)。从FreeMarker 2.3.19版开始,出于遵守XML Schema的日期/时间格式的考虑,偏移通常包含分钟。
示例:
<#assign x = openingTime> <#-- no problem can occur here -->
${openingTime?time} <#-- without ?time it would fail -->
<#-- For the sake of better understanding, consider this: -->
<#assign openingTime = openingTime?time>
${openingTime} <#-- this will work now -->
Last updated: ${lastUpdated} <#-- assume that lastUpdated is a date-time value -->
Last updated date: ${lastUpdated?date}
Last updated time: ${lastUpdated?time}
Last updated: 04/25/2003 08:00:54 PM
Last updated date: 04/25/2003
Last updated time: 08:00:54 PM
可能输出为(基于当前的时间和时区):
还有另外一组iso_...内建函数形式,你可从名称中以忽略掉local或utc,但是要指定时区作为内建函数的参数。比如:
可能输出为(基于当前的时间和时区):
<#assign aDateTime = .now>
<#assign aDate = aDateTime?date>
<#assign aTime = aDateTime?time>
Basic formats:
${aDate?iso_utc}
${aTime?iso_utc}
${aDateTime?iso_utc}
Different accuracies:
${aTime?iso_utc_ms}
${aDateTime?iso_utc_m}
Local time zone:
${aDateTime?iso_local}
Basic formats:
2011-05-16
21:32:13Z
2011-05-16T21:32:13Z
Different accuracies:
21:32:13.868Z
2011-05-16T21:32Z
Local time zone:
2011-05-16T23:32:13+02:00
<#assign aDateTime = .now>
${aDateTime?iso("UTC")}
${aDateTime?iso("GMT-02:30")}
${aDateTime?iso("Europe/Rome")}
The usual variations are supported:
${aDateTime?iso_m("GMT+02")}
${aDateTime?iso_m_nz("GMT+02")}
${aDateTime?iso_nz("GMT+02")}
如果该时区参数不能被解释,那么模板处理就会出错并且终止。
参数也可以是java.util.TimeZone对象类型(会返回Java中方法或在数据模型中的值),而不仅仅是字符串。
1.4 处理布尔值的内建函数
1.4.1 string(当被用作是布尔值时) 转换布尔值为字符串
转换布尔值到字符串。你也以两种方式来使用:
以foo?string:这样会使用代表true和false值的默认字符串来转换布尔值为字符串。默认情况,true被翻译为"true",而false被翻译为"false"。如果你用FreeMarker来生成源代码,这是很有用的,因为这个值不是对本地化(语言,国家)敏感的。为了改变这些默认的字符串,你可以使用boolean_format设置。注意,如果变量是多类型的变量,也就是有布尔值和字符串,那么变量的字符串值会被返回。
以foo?string("yes", "no"):如果布尔值是true,这会返回第一个参数(这里是:"yes"),否则就返回第二个参数(这里是:"no")。注意返回的的值是一个字符串;如果参数是数字类型,首先它会被转换成字符串。
1.5 处理序列的内建函数
1.5.1 first 第一个子变量
序列的第一个子变量。如果序列为空,那么模板处理将会中止。
1.5.2 last 最后一个子变量
序列的最后一个子变量。如果序列为空,那么模板处理将会中止。
2011-05-16T21:43:58Z
2011-05-16T19:13:58-02:30
2011-05-16T23:43:58+02:00
The usual variations are supported:
2011-05-16T23:43+02:00
2011-05-16T23:43
2011-05-16T23:43:58
1.5.3 seq_contanis 序列包含…
注意:
这个内建函数从FreeMarker 2.3.1版本开始可用。而在2.3版本中不存在。
注意:
seq_前缀在这个内建函数中是需要的,用来和contains区分开。contains函数用来在字符串中查找子串(因为变量可以同时当作字符串和序列)。
辨别序列中是否包含指定值。它包含一个参数,就是来查找的值。比如:
输出是:
为了查找值,这个函数使用了FreeMarker的比较规则(就像你使用的==运算符),除了比较两个不同类型的值,或FreeMarker不支持的类型来比较,其他都不会引起错误,只是为认为两个值不相等。因此,你可以使用它来查找标量值(也就是字符串,数字,布尔值,或日期/时间类型)。对于其他类型结果通常都是false。
对于容错性,这个函数还对collections起作用。
1.5.4 seq_index_of 第一次出现…时的位置
注意:
这个内建函数从FreeMarker 2.3.1版本开始可用。而在2.3版本中不存在。
注意:
seq_前缀在这个内建函数中是需要的,用来和index_of区分开。index_of函数用来在字符串中查找子串(因为变量可以同时当作字符串和序列)。
返回序列中第一次出现该值时的索引位置,如果序列不包含指定的值时返回-1。要查找的值作为第一个参数。比如这个模板:
将会输出:
<#assign x = ["red", 16, "blue", "cyan"]>
"blue": ${x?seq_contains("blue")?string("yes", "no")}
"yellow": ${x?seq_contains("yellow")?string("yes", "no")}
16: ${x?seq_contains(16)?string("yes", "no")}
"16": ${x?seq_contains("16")?string("yes", "no")}
"blue": yes
"yellow": no
16: yes
"16": no
<#assign colors = ["red", "green", "blue"]>
${colors?seq_index_of("blue")}
${colors?seq_index_of("red")}
${colors?seq_index_of("purple")}
2
0
-1
为了查找值,这个函数使用了FreeMarker的比较规则(就像你使用的==运算符),除了比较两个不同类型的值,或FreeMarker不支持的类型来比较,其他都不会引起错误,只是为认为两个值不相等。因此,你可以使用它来查找标量值(也就是字符串,数字,布尔值,或日期/时间类型)。对于其他类型结果通常是-1。
搜索开始的地方可以由第二个可选的参数来确定。如果在同一个序列中相同的项可以多次出现时,这是很有用的。第二个参数的数值没有什么限制:如果它是负数,那么就和它是零的效果一样,而如果它是比序列长度还大的数,那么就和它是序列长度值的效果一样。小数值会被切成整数。比如:
将会输出:
1.5.5 seq_last_index_of 最后一次出现..的位置
注意:
这个内建函数从FreeMarker 2.3.1版本开始可用。而在2.3版本中不存在。
注意:
seq_前缀在这个内建函数中是需要的,用来和last_index_of区分开。last_index_of用于在字符串中搜索子串(因为一个变量可以同时是字符串和序列)。
返回序列中最后一次出现值的索引位置,,如果序列不包含指定的值时返回-1。也就是说,和seq_index_of相同,只是在序列中从最后一项开始向前搜索。它也支持可选的第二个参数来确定从哪里开始搜索的索引位置。比如:
<#assign names = ["Joe", "Fred", "Joe", "Susan"]>
No 2nd param: ${names?seq_index_of("Joe")}
-2: ${names?seq_index_of("Joe", -2)}
-1: ${names?seq_index_of("Joe", -1)}
0: ${names?seq_index_of("Joe", 0)}
1: ${names?seq_index_of("Joe", 1)}
2: ${names?seq_index_of("Joe", 2)}
3: ${names?seq_index_of("Joe", 3)}
4: ${names?seq_index_of("Joe", 4)}
No 2nd param: 0
-2: 0
-1: 0
0: 0
1: 2
2: 2
3: -1
4: -1
将会输出这个:
1.5.6 reverse 反转序列
序列的反序形式。
1.5.7 size 序列大小
序列中子变量的数量(作为一个数值)。假设序列中至少有一个子变量,那么序列s中最大的索引是s?size - 1(因为第一个子变量的序列是0)。
1.5.8 sort 排序
以升序方式返回。(要使用降序排列时,使用它之后还要使用reverse内建函数)这仅在子变量都是字符串时有效,或者子变量都是数字,或者子变量都是日期值(日期,时间,或日期+时间),或者所有子变量都是布尔值时(从2.3.17版本开始)。如果子变量是字符串,它使用本地化(语言)的具体单词排序(通常是大小写不敏感的)。比如:
将会打印(至少是US区域设置):
<#assign names = ["Joe", "Fred", "Joe", "Susan"]>
No 2nd param: ${names?seq_last_index_of("Joe")}
-2: ${names?seq_last_index_of("Joe", -2)}
-1: ${names?seq_last_index_of("Joe", -1)}
0: ${names?seq_last_index_of("Joe", 0)}
1: ${names?seq_last_index_of("Joe", 1)}
2: ${names?seq_last_index_of("Joe", 2)}
3: ${names?seq_last_index_of("Joe", 3)}
4: ${names?seq_last_index_of("Joe", 4)}
No 2nd param: 2
-2: -1
-1: -1
0: 0
1: 0
2: 2
3: 2
4: 2
<#assign ls = ["whale", "Barbara", "zeppelin", "aardvark", "beetroot"]?sort>
<#list ls as i>${i} </#list>
aardvark Barbara beetroot whale zeppelin
1.5.9 sort_by 以…来排序
返回由给定的哈希表子变量来升序排序的哈希表序列。(要降序排列使用这个内建函数后还要使用reverse内建函数)这个规则和内建函数sort是一样的,除了序列中的子变量必须是哈希表类型,而且你不得不给哈希变量的命名,那会用来决定排序顺序。比如:
将会打印(至少是US区域设置):
如果你用来排序的子变量的层次很深(也就是说,它是子变量的子变量的子变量,以此类推),那么你可以使用序列来作为参数,它指定了子变量的名字,来向下引导所需的子变量。比如:
<#assign ls = [
{"name":"whale", "weight":2000},
{"name":"Barbara", "weight":53},
{"name":"zeppelin", "weight":-200},
{"name":"aardvark", "weight":30},
{"name":"beetroot", "weight":0.3}
]>
Order by name:
<#list ls?sort_by("name") as i>
- ${i.name}: ${i.weight}
</#list>
Order by weight:
<#list ls?sort_by("weight") as i>
- ${i.name}: ${i.weight}
</#list>
Order by name:
- aardvark: 30
- Barbara: 53
- beetroot: 0.3
- whale: 2000
- zeppelin: -200
Order by weight:
- zeppelin: -200
- beetroot: 0.3
- aardvark: 30
- Barbara: 53
- whale: 2000
<#assign members = [
{"name": {"first": "Joe", "last": "Smith"}, "age": 40},
{"name": {"first": "Fred", "last": "Crooger"}, "age": 35},
{"name": {"first": "Amanda", "last": "Fox"}, "age": 25}]>
Sorted by name.last:
<#list members?sort_by(['name', 'last']) as m>
- ${m.name.last}, ${m.name.first}: ${m.age} years old
</#list>
将会打印(至少是US区域设置):
1.5.10 chunk 区块
注意:
这个内建函数从FreeMarker 2.3.3版本以后可用。
这个内建函数分割序列到多个大小为函数的第一个参数给定的序列(就像mySeq?chunk(3))。结果是包含这些序列的一个序列。最后一个序列可能比给定的大小要小,处分第二个参数也给定了(比如mySeq?chunk(3, '-')),那个就是用来填充最后一个序列,以达到给定的大小。比如:
这会输出:
这个函数通常在输出的序列中使用表格/柱状的格式。当被用于HTML表格时,第二个参数通常是"\xA0"(也就是不换行的空格代码,也就是我们所知的“nbsp”),所以空TD的边界就不会不显示。
第一个参数必须是一个数字,而且至少是1.如果这个数字不是整数,那么它会被静默地去掉小数部分(也就是说3.1和3.9都会被规整为3)。第二个参数可以是任意类型的值。
Sorted by name.last:
- Crooger, Fred: 35 years old
- Fox, Amanda: 25 years old
- Smith, Joe: 40 years old
<#assign seq = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']>
<#list seq?chunk(4) as row>
<#list row as cell>${cell} </#list>
</#list>
<#list seq?chunk(4, '-') as row>
<#list row as cell>${cell} </#list>
</#list>
a b c d
e f g h
i j
a b c d
e f g h
i j - -
1.6 处理哈希表的内建函数
1.6.1 keys 键的集合
一个包含哈希表中查找到的键的序列。注意并不是所有的哈希表都支持这个(询问程序员一个指定的哈希表是否允许这么操作)。
输出:
因为哈希表通常没有定义子变量的顺序,那么键名称的返回顺序就是任意的。然而,一些哈希表维持一个有意义的顺序(询问程序员指定的哈希表是否是这样)。比如,由上述{...}语法创建的哈希表保存了和你指定子变量相同的顺序。
1.6.2 值的集合
一个包含哈希表中子变量的序列。注意并不是所有的哈希表都支持这个(询问程序员一个指定的哈希表是否允许这么操作)。
至于返回的值的顺序,和函数keys的应用是一样的;看看上面的叙述就行了。
1.7 处理节点(XML)的内建函数
注意由这些内建函数返回的变量是由用于节点变量的实现生成的。意思就是返回的变量可以有更多的特性,附加于它这里的状态。比如,由内建函数children返回的序列和XML DOM节点也可以被用作是哈希表或字符串,这在第三部分XML处理指南中有解释。
1.7.1 children 子节点序列
一个包含该节点所有子节点(也就是直接后继节点)的序列。
XML:这和特殊的哈希表的键*几乎是一样的。除了它返回所有节点,而不但是元素。所以可能的子节点是元素节点,文本节点,注释节点,处理指令节点等,而且还可能是属性节点。属性节点排除在序列之外。
1.7.2 parent 父节点
在节点树中,返回该节点的直接父节点。根节点没有父节点,所以对于根节点,表达式
<#assign h = {"name":"mouse", "price":50}>
<#assign keys = h?keys>
<#list keys as key>${key} = ${h[key]}; </#list>
name = mouse; price = 50;
node?parent??的值就是false。
XML:注意通过这个函数返回的值也是一个序列(当你编写someNode[".."]时,和XPath表达式..的结果是一样的)。也要注意属性节点,它返回属性所属的元素节点,尽管属性节点不被算作是元素的子节点。
1.7.3 root 根节点
该节点所属节点树的根节点。
XML:根据W3C,XML文档的根节点不是最顶层的元素节点,而是文档本身,是最高元素的父节点。例如,如果你想得到被称为是foo的XML(所谓的“文档元素”,不要和“文档”搞混了)的最高元素,那么你不得不编写someNode?root.foo。如果你仅仅写了someNode?root,那么你得到的是文档本身,而不是文档元素。
1.7.4 ancestors 祖先节点
一个包含所有节点祖先节点的序列,以直接父节点开始,以根节点结束。这个函数的结果也是一个方法,你可以用它和元素的完全限定名来过滤结果。比如以名称section用node?ancestors("section")来获得所有祖先节点的序列,
1.7.5 node_name 节点名称
当被“访问”时,返回用来决定哪个用户自定义指令来调用控制这个节点的字符串。可以参见visit和recurse指令。
XML:如果节点是元素或属性,那么字符串就会是元素或属性的本地(没有前缀)名字。否则,名称通常在节点类型之后以@开始。可以参见XML处理指南中2.2节的形式化描述。要注意这个节点名称与在DOM API中返回的节点名称不同;FreeMarker节点名称的目标是给要处理节点的用户自定义指令命名。
1.7.6 node_type节点类型
描述节点类型的字符串。FreeMarker没有对节点类型定义准确的含义;它依赖于变量是怎么建模的。也可能节点并不支持节点类型。在这种情形下,该函数就返回未定义值,所以你就不能使用返回值。(你可以用node?node_type??继续检查一个节点是否是支持类型属性)
XML:可能的值是"attribute","text","comment","document_fragment","document","document_type","element","entity","entity_reference","notation","pi"。注意没有"cdata"类型,因为CDATA被认为是普通文本元素。
1.7.7 node_namespace 节点命名空间
返回节点命名空间的字符串。FreeMarker没有为节点命名空间定义准确的含义;它依赖于变量是怎么建模的。也可能节点没有定义任何节点命名空间。这种情形下,该函数应该返回未定义的变量(也就是node?node_namespace??的值是false),所以你不能使用这个返回值。
XML:这种情况下的XML,就是XML命名空间的URI(比如"http://www.w3.org/1999/xhtml")。如果一个元素或属性节点没有使用XML命名空间,那么这个函数就返回一个空字符串。对于其他XML节点这个函数返回未定义的变量。
1.8 很少使用的和专家级的内建函数
这些是你通常情况下不应该使用的内建函数,但是在特殊情况下(调试,高级宏)它们会有用。如果你需要在普通页面模板中使用这些函数,你可能会重新访问数据模型,所以你不要使用它们。
1.8.1 byte,double,float,int,long,short
返回一个包含原变量中相同值的SimpleNumber,但是在内部表示值中使用了java.lang.Type。如果方法被重载了,这是有用的,或者一个TemplateModel解包器在自动选择适合的java.lang.*类型有问题时。注意从2.3.9版本开始,解包器有本质上改进,所以你将基本不会使用到这些内建函数来转换数字类型了,除非在重载方法调用中来解决一些含糊的东西。
内建函数long也可以用于日期,时间和时间日期类型的值来获取返回为java.util.Date.getTime()的值。如果你不得不调用使用long类型时间戳的Java方法时,这是非常有用的。这个转换不是自动进行的。
1.8.2 number_date,number_to_time,number_to_datetime
它们被用来转换数字(通常是Java的long类型)到日期,时间或时间日期类型。这就使得它们和Java中的new java.util.Date(long)是一致的。那也就是说,现在数字可以被解释成毫秒数进行参数传递。数字可以是任意内容和任意类型,只要它的值可以认为是long就行。如果数字不是完整的,那么它就会根据“五入”原则进行进位。这个转换不是自动进行的。
示例:
输出就会是像这样的(基于当前的本地化设置和时区):
${1305575275540?number_to_datetime}
${1305575275540?number_to_date}
${1305575275540?number_to_time}
1.8.3 eval 求值
这个函数求一个作为FTL表达式的字符串的值。比如"1+2"?eval返回数字3。
1.8.4 has_content 是否有内容
如果变量(不是Java的null)存在而且不是“空”就返回true,否则返回false。“空”的含义靠具体的情形来决定。它是直观的常识性概念。下面这些都是空:长度为0的字符串,没有子变量的序列或哈希表,一个已经超过最后一项元素的集合。如果值不是字符串,序列,哈希表或集合,如果它是数字,日期或布尔值(比如0和false是非空的),那么它被认为是非空的,否则就是空的。注意当你的数据模型实现了多个模板模型接口,你可能会得到不是预期的结果。然而,当你有疑问时你通常可以使用expr!?size > 0或expr!?length > 0来代替expr?has_content。
这个函数是个特殊的函数,你可以使用像默认值操作符那样的圆括号手法。也就是说,你可以编写product.color?has_content和(product.color)?has_content样的代码。第一个没有控制当product为空的情形,而第二个控制了。
1.8.5 interpret 将字符串解释为FTL模板
这个函数解释字符串作为FTL模板,而且返回一个用户自定义指令,也就是当应用于任意代码块中时,执行模板就像它当时被包含一样。例如:
输出为:
正如你看到的,inlineTemplate是用户自定义指令,也就是当被执行时,运行当时使用interpret函数生成的模板。
你也可以在两个元素的序列中应用这个函数。这种情况下序列的第一个元素是模板源代码,第二个元素是内联模板的名字。在调试时它可以给内联模板一个确定的名字,这是很有用的。所以你可以这么来写代码:
<#assign x=["a", "b", "c"]>
<#assign templateSource = r"<#list x as y>${y}</#list>">
<#-- Note: That r was needed so that the ${y} is not interpreted above -->
<#assign inlineTemplate = templateSource?interpret>
<@inlineTemplate />
abc
<#assign inlineTemplate = [templateSource, "myInlineTemplate"]?interpret>
May 16, 2011 3:47:55 PM
May 16, 2011
3:47:55 PM
在上述的模板中,要注意给内联模板一个名字没有及时的效果,它仅仅在你得到错误报告时可以得到额外的信息。
1.8.6 is_... 判断函数族
这些内建函数用来检查变量的类型,然后根据类型返回或。下面是is_...判断函数族的列表:
内建函数
如果值是…时返回true
is_string
字符串
is_number
数字
is_boolean
布尔值
is_date
日期(所有类型:仅日期,仅时间和时间日期)
is_method
方法
is_transform
变换
is_macro

is_hash
哈希表
is_hash_ex
扩展的哈希表(也就是支持?keys和?values)
is_sequence
序列
is_collection
集合
is_enumerable
序列或集合
is_indexable
序列
is_directive
指令的类型(比如宏,或TemplateDirectiveModel,TemplateTransformModel等)
is_node
节点
1.8.7 namespace 命名空间
这个函数返回和宏变量关联的命名空间(也就是命名空间的入口哈希表)。你只能和宏一起来用它。
1.8.8 new 创建TemplateModel实现
这是用来创建一个确定TemplateModel实现的变量的内建函数。
在?的左边你可以指定一个字符串,是TemplateModel实现类的完全限定名。结果是调用构造方法生成一个方法变量,然后将新变量返回。
比如:
对于更多的关于构造方法参数被包装和如何选择重载的构造方法信息,请阅读:程序开发指南/其它/Bean 包装器部分内容。
这个内建函数可以是出于安全考虑的,因为模板作者可以创建任意的Java对象,只要它们实现了TemplateModel接口,然后来使用这些对象。而且模板作者可以触发没有实现TemplateModel接口的类的静态初始化块。你可以(从FreeMarker 2.3.17版开始)限制这个内建函数对类的访问,通过使用Configuration.setNewBuiltinClassResolver(TemplateClassResolver)或设置new_builtin_class_resolver。参考Java API文档来获取详细信息。如果您允许并不是很可靠的用户上传模板,那么你一定要关注这个问题。
<#-- Creates an user-defined directive be calling the parameterless constructor of the class -->
<#assign word_wrapp = "com.acmee.freemarker.WordWrapperDirective"?new()>
<#-- Creates an user-defined directive be calling the constructor with one numerical argument -->
<#assign word_wrapp_narrow = "com.acmee.freemarker.WordWrapperDirective"?new(40)>
第二章 指令参考文档
如果你没有在这里发现模板中的指令,可能你需要在废弃的FTL结构中来查找它了。
2.1 if,else,elseif指令
2.1.1 概要
<#if condition>
...
<#elseif condition2>
...
<#elseif condition3>
...
...
<#else>
...
</#if>
这里:
 condition,condition2等:表达式将被计算成布尔值。
2.1.2 描述
你可以使用if,elseif和else指令来条件判断是否越过模板的一个部分。这些condition-s必须计算成布尔值,否则错误将会中止模板处理。elseif-s和else-s必须出现在if的内部(也就是,在if的开始标签和技术标签之间)。if中可以包含任意数量的elseif-s(包括0个)而且结束时else是可选的。
比如:
只有if,没有elseif和else:
只有if和else,没有elseif:
if和两个elseif,没有else:
<#if x == 1>
x is 1
</#if>
<#if x == 1>
x is 1
<#else>
x is not 1
</#if>
if和3个elseif,还有else:
要了解更多布尔表达式,可以参考:模板开发指南/模板/表达式部分内容。
你(当然)也可以嵌套if指令:
注意:
如何测试x比1大?<#if x > 1>是不对的,因为FreeMarker将会解释第一个>作为结束标记。因此,编写<#if (x > 1)>或<#if x &gt; 1>是正确的。
<#if x == 1>
x is 1
<#elseif x == 2>
x is 2
<#elseif x == 3>
x is 3
</#if>
<#if x == 1>
x is 1
<#elseif x == 2>
x is 2
<#elseif x == 3>
x is 3
<#elseif x == 4>
x is 4
<#else>
x is not 1 nor 2 nor 3 nor 4
</#if>
<#if x == 1>
x is 1
<#if y == 1>
and y is 1 too
<#else>
but y is not
</#if>
<#else>
x is not 1
<#if y < 0>
and y is less than 0
</#if>
</#if>
2.2 switch,case,default,break指令
2.2.1 概要
<#switch value>
<#case refValue1>
...
<#break>
<#case refValue2>
...
<#break>
...
<#case refValueN>
...
<#break>
<#default>
...
</#switch>
这里:
 value,refValue1等:表达式将会计算成相同类型的标量。
2.2.2 描述
这个指令的用法是不推荐的,因为向下通过的行为容易出错。使用elseif-s来代替,除非你想利用向下通过这种行为。
Switch被用来选择模板中的一个片段,如何选择依赖于表达式的值:
在switch中间必须有一个或多个<#case value>,在所有case标签之后,有一个可选的<#default>。当FreeMarker到达指令时,它会选择一个refValue和
<#switch being.size>
<#case "small">
This will be processed if it is small
<#break>
<#case "medium">
This will be processed if it is medium
<#break>
<#case "large">
This will be processed if it is large
<#break>
<#default>
This will be processed if it is neither
</#switch>
value相等的case指令来继续处理模板。如果没有和合适的值匹配的case指令,如果default指令存在,那么就会处理default指令,否则就会继续处理switch结束标签之后的内容。现在有一个混乱的事情:当它选择一个case指令后,它就会继续处理case指令中的内容,直到遇到break指令。也就是它遇到另外一个case指令或<#default>标记时也不会自动离开switch指令。比如:
如果x是1,那么它会打印1 2 d;如果x是2,那么就会打印2 d;如果x是3,那么它会打印d。这就是前面提到的向下通过行为。break标记指示FreeMarker直接略过剩下的switch代码段。
2.3 list,break 指令
2.3.1 概要
<#list sequence as item>
...
</#list>
这里:
 sequence:表达式将被算作序列或集合
 item:循环变量(不是表达式)的名称
2.3.2 描述
你可以使用list指令来处理模板的一个部分中的一个序列中包含的各个变量。在开始标签和结束标签中的代码将会被处理,首先是第一个子变量,然后是第二个子变量,接着是第三个子变量,等等,直到超过最后一个。对于每个变量,这样的迭代中循环变量将会包含当前的子变量。
在list循环中,有两个特殊的循环变量可用:
 item_index:这是一个包含当前项在循环中的步进索引的数值。
 item_has_next:来辨别当前项是否是序列的最后一项的布尔值。
示例1:
<#switch x>
<#case x = 1>
1
<#case x = 2>
2
<#default>
d
</#switch>
<#assign seq = ["winter", "spring", "summer", "autumn"]>
<#list seq as x>
${x_index + 1}. ${x}<#if x_has_next>,</#if>
</#list>
将会打印:
示例2:你可以使用list在两个数字中来计数,使用一个数字范围序列表达式:
输出是:
注意上面的示例在你希望x是0的时候不会有作用,那么它打印0和-1。
你可以使用break指令在它通过最后一个序列的子变量之前离开list循环。比如这会仅仅打印“winter”和“spring”。
注意如果你开启经典的兼容模式,那么list也接受一个标量而且将它视为单元素的序列。
通常来说,避免list中使用无论何时可能包装了Iterator作为参数的集合和使用包装了java.util.Collection或序列的集合是最好的。但是在某些情况,当你处理时仅仅有一个Iterator。要注意如果你传递了一个包装了Iterator的集合给list,你仅仅可以迭代一次元素,因为Iterators是由它们一次性对象的特性决定的。当你尝试第二次列出这样一个集合变量时,错误会中止模板的处理。
2.4 include指令
2.4.1 概要
<#include path>
or
<#include path options>
这里:
 path:要包含文件的路径;一个算作是字符串的表达式。(用其他话说,它不用是一个固定的字符串,它也可以是像profile.baseDir + "/menu.ftl"这样的东西。)
1. winter,
2. spring,
3. summer,
4. autumn
<#assign x=3>
<#list 1..x as i>
${i}
</#list>
1
2
3
<#list seq as x>
${x}
<#if x = "spring"><#break></#if>
</#list>
 options:一个或多个这样的选项:encoding=encoding, parse=parse
 encoding:算作是字符串的表达式
 parse:算作是布尔值的表达式(为了向下兼容,也接受一部分字符串值)
2.4.2 描述
你可以使用它在你的模板中插入另外一个FreeMarker模板文件(由path参数指定)。被包含模板的输出格式是在include标签出现的位置插入的。被包含的文件和包含它的模板共享变量,就像是被复制粘贴进去的一样。include指令不能由被包含文件的内容所替代,它只是当FreeMarker每次在模板处理期间到达include指令时处理被包含的文件。所以对于如果include在list循环之中的例子,你可以为每个循环周期内指定不同的文件名。
注意:
这个指令不能和JSP(Servlet)的include搞混,因为它不涉及到Servlet容器中,只是处理应外一个FreeMarker模板,不能“离开”FreeMarker。关于如何处理“JSP include”,可以参考FAQ中的内容。
path参数可以是如"foo.ftl"和"../foo.ftl"一样的相对路径,或者是如"/foo.ftl"这样的绝对路径。相对路径是相对于使用import指令的模板文件夹。绝对路径是相对于程序员在配置FreeMarker时定义的基路径(通常指代“模板的根路径”)。
注意:
这和FreeMarker 2.1版本之前的处理方式不同,之前的路径通常是绝对路径。为了保留原来的行为,要在Configuration对象中开启经典的兼容模式。
通常使用/(斜杠)来分隔路径成分,而不是\(反斜杠)。如果你从你本地的文件系统加载模板,要使用反斜杠(像Windows操作系统)。FreeMarker会自动转换它们。
比如:
假设/common/copyright.ftl包含:
那么这个:
会打印出:
Copyright 2001-2002 ${me}<br>
All rights reserved.
<#assign me = "Juila Smith">
<h1>Some test</h1>
<p>Yeah.
<hr>
<#include "/common/copyright.ftl">
<h1>Some test</h1>
<p>Yeah.
<hr>
Copyright 2001-2002 Juila Smith
All rights reserved.
支持的options选项有:
 parse:如果它为真,那么被包含的文件将会当作FTL来解析,否则整个文件将被视为简单文本(也就是说不会在其中查找FreeMarker的结构)。如果你忽略了这个选项,那么它默认是true。
 encoding:被包含文件从包含它的文件继承的编码方式(实际就是字符集),除非你用这个选项来指定编码方式。编码名称要和java.io.InputStreamReader中支持的那些一致(对于Java API 1.3版本:MIME 希望的字符集是从IANA字符集注册处得到的)。合法的名字有:ISO-8859-2,UTF-8,Shift_JIS,Big5,EUC-KR,GB2312。
比如:
注意,对于所有模板可能会用Configuration的“自动包含”设置自动处理通用的包含物。
2.4.2.1 使用获得机制
有一个特殊的路径组成,是用一个星号(*)来代表的。它被解释为“当前目录或其他任意它的父目录”。因此,如果模板在/foo/bar/template.ftl位置上,有下面这行:
那么引擎就会在下面的位置上寻找模板,并按这个顺序:
 /foo/bar/footer.ftl
 /foo/footer.ftl
 /footer.ftl
这种机制被称为acquisition获得并允许设计者在父目录中放置通用的被包含的文件,而且当需要时在每个子路径基础上重新定义它们。我们说包含它们的模板获得了从包含它的第一个父目录中的模板。注意你不但可以在星号的右面指定一个模板的名字,也可以指定一个子路径。也就是说,如果前面的模板由下面这个所替代:
那么引擎将会从下面的路径开始寻找模板,并按这个顺序:
 /foo/bar/commons/footer.ftl
 /foo/commons/footer.ftl
 /commons/footer.ftl
然而,在路径中最大只能有一个星号。指定多余一个星号会导致模板不能被发现。
2.4.2.2 本地化查找
无论何时模板被加载,它都被分配了一个本地化环境。本地化环境是语言和可选的国家或方言标识。模板一般是由程序员编写一些代码来加载的,出于一些方面的考虑,程序员为模板选择一种本地化环境。比如:当FreemarkerServlet加载模板时,它经常用本
<#include "/common/navbar.html" parse=false encoding="Shift_JIS">
<#include "*/footer.ftl">
<#include "*/commons/footer.ftl">
地化环境匹配浏览器请求Web页面的语言偏好来请求模板。
当一个模板包含另一个模板时,它试图加载以相同的本地化环境加载模板。假定你的模板以本地化en_US来加载,那就意味着是U.S. English。当你包含另外一个模板:
那么引擎实际上就会寻找一些模板,并按照这个顺序:
 footer_en_US.ftl
 footer_en.ftl
 footer.ftl
要注意你可以使用Configuration的setLocalizedLookup方法关闭本地化查找特性。
当你同时使用获得机制和本地化查找时,在父目录中有指定本地化的模板优先于在子目录中有很少本地化的模板。假设你使用下面的代码来包含/foo/bar/template.ftl:
引擎将会查找这些模板,并按照这个顺序:
 /foo/bar/footer_en_US.ftl
 /foo/footer_en_US.ftl
 /footer_en_US.ftl
 /foo/bar/footer_en.ftl
 /foo/footer_en.ftl
 /footer_en.ftl
 /foo/bar/footer.ftl
 /foo/footer.ftl
 /footer.ftl
2.5 import 指令
2.5.1 概要
<#import path as hash>
这里:
 path:模板的路径。这是一个算作是字符串的表达式。(换句话说,它不是一个固定的字符串,它可以是这样的一些东西,比如,profile.baseDir + "/menu.ftl"。)
 hash:哈希表变量的结束名称,你可以由它来访问命名空间。这不是表达式。
2.5.2 描述
引入一个库。也就是说,它创建一个新的命名空间,然后在那个命名空间中执行给定path参数中的模板,所以模板用变量(宏,函数等)填充命名空间。然后使得新创建的命名空间对哈希表的调用者可用。这个哈希表变量将会在命名空间中,由import(就像你可以用assign指令来创建一样。)的调用者被创建成一个普通变量,名字就是hash参
<include "footer.ftl">
<include "*/footer.ftl">
数给定的。
如果你用同一个path多次调用import,它会创建命名空间,但是只运行第一次import的调用。后面的调用仅仅创建一个哈希表变量,你只是通过它来访问同一个命名空间。
由引入的模板打印的输出内容将会被忽略(不会在包含它的地方被插入)。模板的执行是来用变量填充命名空间,而不是写到输出中。
例如:
path参数可以是一个相对路径,比如"foo.ftl"和"../foo.ftl",或者是像"/foo.ftl"一样的绝对路径。相对路径是相对于使用import指令模板的目录。绝对路径是程序员配置FreeMarker时定义的相对于根路径(通常指代“模板的根目录”)的路径。
通常使用/(斜杠)来分隔路径组成,而不是\(反斜杠)。如果你从你本地的文件系统中加载模板,那么它使用反斜杠(比如在Windows环境下),FreeMarker将会自动转换它们。
像include指令一样,获得机制和本地化擦找也可以用来解决路径问题。
注意,对于所有模板来说,它可以自动做通用的引入操作,使用Configuration的“自动引入”设置就行了。
如果你命名空间不是很了解,你应该阅读:模板开发指南/其他/命名空间部分的内容。
2.6 noparse指令
2.6.1 概要
<#noparse>
...
</#noparse>
2.6.2 描述
FreeMarker不会在这个指令体中间寻找FTL标签,插值和其他特殊的字符序列,除了noparse的结束标记。
例如:
<#import "/libs/mylib.ftl" as my>
<@my.copyright date="1999-2002"/>
Example:
--------
<#noparse>
<#list animals as being>
<tr><td>${being.name}<td>${being.price} Euros
</#list>
</#noparse>
将会输出:
2.7 compress指令
2.7.1 概要
<#compress>
...
</#compress>
2.7.2 描述
当你使用了对空白不敏感的格式(比如HTML或XML)时压缩指令对于移除多余的空白是很有用的。它捕捉在指令体(也就是在开始标签和结束标签中)中生成的内容,然后缩小所有不间断的空白序列到一个单独的空白字符。如果被替代的序列包含换行符或是一段空间,那么被插入的字符也会是一个换行符。开头和结尾的不间断的空白序列将会完全被移除。
会输出:
Example:
--------
<#list animals as being>
<tr><td>${being.name}<td>${being.price} Euros
</#list>
<#assign x = " moo \n\n ">
(<#compress>
1 2 3 4 5
${moo}
test only
I said, test only
</#compress>)
(1 2 3 4 5
moo
test only
I said, test only)
2.8 escape,noescape指令
2.8.1 概要
<#escape identifier as expression>
...
<#noescape>...</#noescape>
...
</#escape>
2.8.2 描述
当你使用escape指令包围模板中的一部分时,在块中出现的插值(${...})会和转义表达式自动结合。这是一个避免编写相似表达式的很方便的方法。它不会影响在字符串形式的插值(比如在<#assign x = "Hello ${user}!">)。而且,它也不会影响数值插值(#{...})。
例如:
事实上它等同于:
注意它和你在指令中用什么样的标识符无关 - 它仅仅是作为一个转义表达式的正式参数。
当你在include指令中调用宏时,理解在模板文本中转义仅仅对出现在<#escape ...>和</#escape>中的插值起作用是很重要的。也就是说,它不会转义文本中<#escape ...>之前的东西或</#escape>之后的东西,也不会从escape-d部分中来调用。
<#escape x as x?html>
First name: ${firstName}
Last name: ${lastName}
Maiden name: ${maidenName}
</#escape>
First name: ${firstName?html}
Last name: ${lastName?html}
Maiden name: ${maidenName?html}
输出将是:
从更深的技术上说,escape指令的作用是用在模板解析的时间而不是模板处理的时间。这就表示如果你调用一个宏或从一个转义块中包含另外一个模板,它不会影响在宏/被包含模板中的插值,因为宏调用和模板包含被算在模板处理时间。另外一方面,如果你用一个转义区块包围一个或多个宏声明(算在模板解析时间,和宏调用想法),那么那些宏中的插值将会和转义表达式合并。
有时需要暂时为一个或两个在转义区块中的插值关闭转义。你可以通过关闭,过后再重新开启转义区块来达到这个功能,但是那么你不得不编写两遍转义表达式。你可以使用非转义指令来替代:
和这个是等同的:
转义可以被嵌套(尽管你不会在罕见的情况下来做)。因此,你可以编写如下面代码(这个例子固然是有点伸展的,正如你可能会使用list来迭代序列中的每一项,但是我们现在所做的是阐述这个观点)的东西:
<#assign x = "<test>">
<#macro m1>
m1: ${x}
</#macro>
<#escape x as x?html>
<#macro m2>m2: ${x}</#macro>
${x}
<@m1/>
</#escape>
${x}
<@m2/>
&lt;test&gt;
m1: <test>
<test>
m2: &lt;test&gt;
<#escape x as x?html>
From: ${mailMessage.From}
Subject: ${mailMessage.Subject}
<#noescape>Message: ${mailMessage.htmlFormattedBody}</#noescape>
...
</#escape>
From: ${mailMessage.From?html}
Subject: ${mailMessage.Subject?html}
Message: ${mailMessage.htmlFormattedBody}
...
实际上和下面是等同的:
当你在嵌入的转义区块内使用非转义指令时,它仅仅不处理一个单独层级的转义。因此,为了在两级深的转义区块内完全关闭转义,你需要使用两个嵌套的非转义指令。
2.9 assign 指令
2.9.1 概要
<#assign name=value>
or
<#assign name1=value1 name2=value2 ... nameN=valueN>
or
<#assign same as above... in namespacehash>
or
<#assign name>
capture this
</#assign>
or
<#assign name in namespacehash>
capture this
</#assign>
这里:
 name:变量的名字。不是表达式。而它可以本写作是字符串,如果变量名包含保留字符这是很有用的,比如<#assign "foo-bar" = 1>。注意这个字符串没有展开插值(如"${foo}")。
<#escape x as x?html>
Customer Name: ${customerName}
Items to ship:
<#escape x as itemCodeToNameMap[x]>
${itemCode1}
${itemCode2}
${itemCode3}
${itemCode4}
</#escape>
</#escape>
Customer Name: ${customerName?html}
Items to ship:
${itemCodeToNameMap[itemCode1]?html}
${itemCodeToNameMap[itemCode2]?html}
${itemCodeToNameMap[itemCode3]?html}
${itemCodeToNameMap[itemCode4]?html}
 value:存储的值。是表达式。
 namespacehash:(通过import)为命名空间创建的哈希表。是表达式。
2.9.2 描述
使用这个指令你可以创建一个新的变量,或者替换一个已经存在的变量。注意仅仅顶级变量可以被创建/替换(也就是说你不能创建/替换some_hash.subvar,除了some_hash)。
关于变量的更多内容,请阅读:模板开发指南/其他/在模板中定义变量
例如:seasons变量可以存储一个序列:
比如:变量test中存储增长的数字:
作为一个方便的特性,你可以使用一个assign标记来进行多次定义。比如这个会做上面两个例子中相同的事情:
如果你知道什么是命名空间:assign指令在命名空间中创建变量。通常它在当前的命名空间(也就是和标签所在模板关联的命名空间)中创建变量。但如果你是用了in namespacehash,那么你可以用另外一个命名空间来创建/替换变量。比如,这里你在命名空间中/mylib.ftl创建/替换了变量bgColor。
assign的极端使用是当它捕捉它的开始标记和结束标记中间生成的输出时。也就是说,在标记之间打印的东西将不会在页面上显示,但是会存储在变量中。比如:
将会打印:
<#assign seasons = ["winter", "spring", "summer", "autumn"]>
<#assign test = test + 1>
<#assign
seasons = ["winter", "spring", "summer", "autumn"]
test = test + 1
>
<#import "/mylib.ftl" as my>
<#assign bgColor="red" in my>
<#macro myMacro>foo</#macro>
<#assign x>
<#list 1..3 as n>
${n} <@myMacro />
</#list>
</#assign>
Number of words: ${x?word_list?size}
${x}
Number of words: 6
1 foo
2 foo
3 foo
请注意你不应该使用它来往字符串中插入变量:
你可以这么来写:
2.10 global 指令
2.10.1 概要
<#global name=value>
or
<#global name1=value1 name2=value2 ... nameN=valueN>
or
<#global name>
capture this
</#global>
这里:
 name:变量的名称。它不是表达式。但它可以被写作是字符串形式,如果变量名包含保留字符这是很有用的,比如<#global "foo-bar" = 1>。注意这个字符串没有扩展插值(如"${foo}")。
 value:存储的值,是表达式。
2.10.2 描述
这个指令和assign相似,但是被创建的变量在所有的命名空间中都可见,但又不会存在于任何一个命名空间之中。精确地说,正如你会创建(或替换)一个数据模型变量。因此,这个变量是全局的。如果在数据模型中,一个相同名称的变量存在的话,它会被使用这个指令创建的变量隐藏。如果在当前的命名空间中,一个相同名称的变量存在的话,那么会隐藏由global指令创建的变量。
比如<#global x = 1>,用创建了一个变量,那么在所有命名空间中x都可见,除非另外一个称为x的变量隐藏了它(比如你已经用<#assign x = 2>创建了一个变量)。这种情形下,你可以使用特殊变量globals,比如${.globals.x}。注意使用globals你看到所有全局可访问的变量;不但由global指令创建的变量,而且是数据模型中的变量。
自定义JSP标记的用户请注意:用这个指令创建的变量集合和JSP页面范围对应。这就意味着,如果自定义JSP标记想获得一个页面范围的属性(page-scope bean),在当前命名空间中一个相同名称的变量,从JSP标记的观点出发,将不会隐藏。
<#assign x>Hello ${user}!</#assign> <#-- BAD PRACTICE! -->
<#assign x="Hello ${user}!">
2.11 local 指令
2.11.1 概要
<#local name=value>
or
<#local name1=value1 name2=value2 ... nameN=valueN>
or
<#local name>
capture this
</#local>
这里:
 name:在root中局部对象的名称。它不是一个表达式。但它可以被写作是字符串形式,如果变量名包含保留字符这是很有用的,比如<#global "foo-bar" = 1>。注意这个字符串没有扩展插值(如"${foo}")。
 value:存储的值,是表达式。
2.11.2 描述
它和assign指令类似,但是它创建或替换局部变量。这仅仅在宏和方法的内部定义才会有作用。
要获得更多关于变量的信息,可以阅读:模板开发指南/其他/在模板中定义变量部分内容。
2.12 setting 指令
2.12.1 概要
<#setting name=value>
这里:
 name:设置的名称。不是表达式!
 value:设置的值,是表达式。
2.12.2 描述
为进一步的处理而设置。设置是影响FreeMarker行为的值。新值仅仅在被设置的模板处理时出现,而且不触碰模板本身。设置的初始值是由程序员设定的(参加:程序开发指南/配置/设置信息部分的内容)。
支持的设置有:
 locale:输出的本地化(语言)。它可以影响数字,日期等显示格式。它的值是由语言编码(小写两个字母的ISO-639编码)和可选的国家码(大写的两个字母ISO-3166编码)组成的字符串,它们以下划线相分隔,如果我们已经指定了国家那么一个可选的不同编码(不是标准的)会以下划线分隔开国家。合法的值示例:en,en_US,en_US_MAC。FreeMarker会尝试使用最特定可用的本地化设置,所以如果你指定了en_US_MAC,但是它不被知道,那么它会尝试en_US,然后尝试en,然后是计算机(可能是由程序员设置的)默认的本地化设置。
 number_format:当没有指定确定的格式化形式时,用来转化数字到字符串形式的数字格式化设置。可以是下列中的一个预定义值number(默认的),computer,currency,或percent。此外,以Java小数数字格式化语法书写的任意的格式化形式也可以被指定。更多的格式形式请参考处理数字的内建函数string。
 boolean_format:以逗号分隔的一对字符串来分别展示true和false值,当没有指定确定的格式时,转换布尔值到字符串。默认值是"true,false"。也可以参考处理布尔值的内建函数string。
 date_format,time_format,datetime_format:,当没有指定确定的格式时,用来转换日期到字符串的日期/时间格式形式。如${someDate}. date_format这个情形,它只影响和日期相关的日期(年,月,日),time_format只影响和时间相关的日期(时,分,秒,毫秒),datetime_format只影响时间日期类型的日期(年,月,日,时,分,秒,毫秒)。这个设置的可能值和处理日期的内建函数string的参数相似;可以在那部分参考更多内容。比如"short","long_medium","MM/dd/yyyy"。
 time_zone:时区的名称来显示并格式化时间。默认情况下,使用系统的时区。也可以是Java时区API中的任何值。比如:"GMT","GMT+2","GMT-1:30","CET","PST","America/Los_Angeles"。
 url_escaping_charset:用来URL转义(比如${foo?url})的字符集,来计算转义(%XX)的部分。通常包含FreeMarker的框架应该设置它,所以你不应该在模板中来设置。(程序员可以阅读程序开发指南/其他/字符集问题部分来获取更多内容)
 classic_compatible:这是对专业人员来说的,它的值应该是一个布尔值。参见freemarker.template.Configurable的文档来获取更多信息。
示例:假设模板的初始本地化是hu(Hungarian,匈牙利),那么这个:
将会输出:
因为匈牙利人使用逗号作为小数的分隔符,而美国人使用点。
${1.2}
<#setting locale="en_US">
${1.2}
1,2
1.2
2.13 用户自定义指令(<@...>)
2.13.1 概要
<@user_def_dir_exp param1=val1 param2=val2 ... paramN=valN/>
(注意XML风格,>之前的/)
如果你需要循环变量,请参考2.13.2.2节内容。
<@user_def_dir_exp param1=val1 param2=val2 ... paramN=valN ; lv1, lv2, ..., lvN/>
或者和上面两个相同但是使用结束标签,请参考2.13.2.1节内容。
<@user_def_dir_exp ...>
...
</@user_def_dir_exp>

<@user_def_dir_exp ...>
...
</@>
或和上面的相同但是使用位置参数传递,请参考2.13.2.3节内容
<@user val1, val2, ..., valN/>
等…
这里:
 user_def_dir_exp:表达式算作是自定义指令(比如宏),将会被调用。
 param1,param2等:参数的名称,它们不是表达式。
 val1,val2等:参数的值,它们是表达式。
 lv1,lv2等:循环变量的名称,它们不是表达式。
参数的数量可以是0(也就是没有参数)。
参数的顺序并不重要(除非你使用了位置参数传递)。参数名称必须唯一。在参数名中小写和大写的字母被认为是不同的字母(也就是Color和color是不同的)。
2.13.2 描述
这将调用用户自定义指令,比如宏。参数的含义,支持和需要的参数的设置依赖于具体的自定义指令。
你可以阅读模板开发指南/其他/定义你自己的指令部分。
示例1:调用存储在变量html_escape中的指令:
输出:
<@html_escape>
a < b
Romeo & Juliet
</@html_escape>
a &lt; b
Romeo &amp; Juliet
示例2:调用有参数的宏
输出:
2.13.2.1 结束标签
你可以在结束标签中忽略user_def_dir_exp。也就是说,你可以写</@>来替代</@anything>。这个规则当表达式user_def_dir_exp太复杂时非常有用,因为你不需要在结束标签中重复表达式。此外,如果表达式包含比简单变量名和点还多的表达式,你就不能再重复它们了。比如<@a_hash[a_method()]>...</@a_hash[a_method()]>就是错的,你必须写为<@a_hash[a_method()]>...</@>。但是<@a_hash.foo>...</@a_hash.foo>是可以的。
2.13.2.2 循环变量
一些自定义指令创建循环变量(和list指令相似)。正如预定义指令(如list)一样,当你调用这个指令(如<#list foos as foo>...</#list>中的foo)时循环变量的名称就给定了,而变量的值是由指令本身设置的。在自定义指令的情形下,语法是循环变量的名称在分号之后给定。比如:
<@list items=["mouse", "elephant", "python"] title="Animals"/>
...
<#macro list title items>
<p>${title?cap_first}:
<ul>
<#list items as x>
<li>${x?cap_first}
</#list>
</ul>
</#macro>
<p>Animals:
<ul>
<li>Mouse
<li>Elephant
<li>Python
</ul>
...
<@myRepeatMacro count=4 ; x, last>
${x}. Something... <#if last> This was the last!</#if>
</@myRepeatMacro>
注意由自定义指令创建的循环变量数量和分号之后指定的循环变量数量需要不匹配。也就是说,如果你对重复是否是最后一个不感兴趣,你可以简单来写:
或者你可以:
此外,如果你在分号之后指定更多循环变量而不是自定义指令创建的,也不会引起错误,只是最后的循环变量不能被创建(也就是在嵌套内容中那些将是未定义的)。尝试使用未定义的循环变量,就会引起错误(除非你使用如?default这样的内建函数),因为你尝试访问了一个不存在的变量。
请参考模板开发指南/其他/定义你自己的指令部分来获取更多内容。
2.13.2.3 位置参数传递
位置参数传递(如<@heading "Preface", 1/>)是正常命名参数传递(如<@heading title="Preface" level=1/>)的速记形式,这里忽略了参数的名称。如果自定义指令只有一个参数,或者对于经常使用的自定义指令它参数的顺序很好记忆,速记形式应该被应用。为了应用这种形式,你不得不了解声明的命名参数的顺序(如果指令只有一个参数这是很琐碎的)。也就是,如果heading被创建为<#macro heading title level>...,那么<@heading "Preface", 1/>和<@heading title="Preface" level=1/>(或<@heading level=1 title="Preface"/>;如果你使用参数名称,那顺序就不重要了)是相等的。要注意位置参数传递现在仅仅支持宏定义。
2.14 macro,nested,return 指令
2.14.1 概要
<#macro name param1 param2 ... paramN>
...
<#nested loopvar1, loopvar2, ..., loopvarN>
...
<#return>
...
</#macro>
这里:
 name:宏变量的名称,它不是表达式。然而,它可以被写成字符串的形式,如果
<@myRepeatMacro count=4 ; x>
${x}. Something...
</@myRepeatMacro>
<@myRepeatMacro count=4>
Something...
</@myRepeatMacro>
宏名称中包含保留字符时这是很有用的,比如<#macro "foo-bar">...。注意这个字符串没有扩展插值(如"${foo}")。
 param1,param2等: 局部变量的名称,存储参数的值(不是表达式),在=号后面和默认值(是表达式)是可选的。默认值也可以是另外一个参数,比如<#macro section title label=title>。
 paramN,最后一个参数,可以可选的包含一个尾部省略(...),这就意味着宏接受可变的参数数量。如果使用命名参数来调用,paramN将会是包含给宏的所有未声明的键/值对的哈希表。如果使用位置参数来调用,paramN将是额外参数的序列。
 loopvar1,loopvar2等:可选的循环变量的值,是nested指令想为嵌套内容创建的。这些都是表达式。
return和nested指令是可选的,而且可以在<#macro>和</#macro>之间被用在任意位置和任意次数。
没有默认值的参数必须在有默认值参数(paramName=defaultValue)之前。
2.14.2 描述
创建一个宏变量(在当前命名空间中,如果你知道命名空间的特性)。如果你对宏和自定义指令不了解,你应该阅读模板开发指南/其他/定义你自己的指令部分。
宏变量存储模板片段(称为宏定义体)可以被用作自定义指令。这个变量也存储自定义指令的被允许的参数名。当你将这个变量作为指令时,你必须给所有参数赋值,除了有默认值的参数。默认值当且仅当你调用宏而不给参数赋值时起作用。
变量会在模板开始时被创建;而不管macro指令放置在模板的什么位置。因此,这样也可以:
然而,如果宏定义被插在include指令中,它们直到FreeMarker执行include指令时参会可用。
例如:没有参数的宏:
输出:
<#-- call the macro; the macro variable is already created: -->
<@test/>
...
<#-- create the macro variable: -->
<#macro test>
Test text
</#macro>
<#macro test>
Test text
</#macro>
<#-- call the macro: -->
<@test/>
示例:有参数的宏:
输出:
示例:有参数和默认值参数的宏:
输出:
示例:一个复杂的宏。
输出:
Test text
<#macro test foo bar baaz>
Test text, and the params: ${foo}, ${bar}, ${baaz}
</#macro>
<#-- call the macro: -->
<@test foo="a" bar="b" baaz=5*5-2/>
Test text, and the params: a, b, 23
<#macro test foo bar="Bar" baaz=-1>
Test text, and the params: ${foo}, ${bar}, ${baaz}
</#macro>
<@test foo="a" bar="b" baaz=5*5-2/>
<@test foo="a" bar="b"/>
<@test foo="a" baaz=5*5-2/>
<@test foo="a"/>
Test text, and the params: a, b, 23
Test text, and the params: a, b, -1
Test text, and the params: a, Bar, 23
Test text, and the params: a, Bar, -1
<#macro list title items>
<p>${title?cap_first}:
<ul>
<#list items as x>
<li>${x?cap_first}
</#list>
</ul>
</#macro>
<@list items=["mouse", "elephant", "python"] title="Animals"/>
<p>Animals:
<ul>
<li>Mouse
<li>Elephant
<li>Python
</ul>
示例:一个支持多个参数和命名参数的宏:
输出:
2.14.2.1 nested
nested指令执行自定义指令开始和结束标签中间的模板片段。嵌套的片段可以包含模板中合法的任意内容:插值,指令…等。它在上下文环境中被执行,也就是宏被调用的地方,而不是宏定义体的上下文中。因此,比如,你不能看到嵌套部分的宏的局部变量。如果你没有调用nested指令,自定义指令开始和结束标记中的部分将会被忽略。
示例:
输出:
嵌套指令可以对嵌套内容创建循环变量。比如:
<#macro img src extra...>
<img src="/context${src?html}"
<#list extra?keys as attr>
${attr}="${extra[attr]?html}"
</#list>
>
</#macro>
<@img src="/images/test.png" width=100 height=50 alt="Test"/>
<img src="/context/images/test.png"
alt="Test"
height="50"
width="100"
>
<#macro do_twice>
1. <#nested>
2. <#nested>
</#macro>
<@do_twice>something</@do_twice>
1. something
2. something
<#macro do_thrice>
<#nested 1>
<#nested 2>
<#nested 3>
</#macro>
<@do_thrice ; x>
${x} Anything.
</@do_thrice>
这会打印:
一个更为复杂的示例:
输出将是:
2.14.2.2 return
使用return指令,你可以在任意位置留下一个宏或函数定义。比如:
输出:
2.15 function,return 指令
2.15.1 概要
<#function name param1 param2 ... paramN>
...
<#return returnValue>
1 Anything.
2 Anything.
3 Anything.
<#macro repeat count>
<#list 1..count as x>
<#nested x, x/2, x==count>
</#list>
</#macro>
<@repeat count=4 ; c, halfc, last>
${c}. ${halfc}<#if last> Last!</#if>
</@repeat>
1. 0.5
2. 1
3. 1.5
4. 2 Last!
<#macro test>
Test text
<#return>
Will not be printed.
</#macro>
<@test/>
Test text
...
</#function>
这里:
 name:方法变量的名称(不是表达式)
 param1,param2等:局部变量的名称,存储参数的值(不是表达式),在=号后面和默认值(是表达式)是可选的。
 paramN,最后一个参数,可以可选的包含一个尾部省略(...),这就意味着宏接受可变的参数数量。局部变量paramN将是额外参数的序列。
 returnValue:计算方法调用值的表达式。
return指令可以在<#function ...>和</#function>之间被用在任意位置和任意次数。
没有默认值的参数必须在有默认值参数(paramName=defaultValue)之前。
2.15.2 描述
创建一个方法变量(在当前命名空间中,如果你知道命名空间的特性)。这个指令和macro指令的工作方式一样,除了return指令必须有一个参数来指定方法的返回值,而且视图写入输出的将会被忽略。如果到达</#function>(也就是说没有return returnValue),那么方法的返回值就是未定义变量。
示例1:创建一个方法来计算两个树的平均值:
将会打印:
示例2:创建一个方法来计算多个数字的平均值:
会打印:
<#function avg x y>
<#return (x + y) / 2>
</#function>
${avg(10, 20)}
15
<#function avg nums...>
<#local sum = 0>
<#list nums as num>
<#local sum = sum + num>
</#list>
<#if nums?size != 0>
<#return sum / nums?size>
</#if>
</#function>
${avg(10, 20)}
${avg(10, 20, 30, 40)}
${avg()!"N/A"}
2.16 flush 指令
2.16.1 概要
<#flush>
2.16.2 描述
当FreeMarker生成输出时,他/她通常存储生成的输出内容然后以一个或几个大片段送到客户端。这种发送的行为被称为冲洗(就像冲厕所)。尽管冲洗是自动发生的,有时你想在模板处理时的一点强制执行,这就是flush指令要做的。如果需要它,那么程序员会告诉你;通常你不必使用这个指令。自动冲洗的机制和flush指令的明确效果是在程序员的控制之下的。
冲洗简单调用当前使用java.io.Writer实例的flush方法。整体的缓冲和冲洗机制在writer(就是传递给Template.process方法的参数)中已经实现了;FreeMarker不用来处理它。
2.17 stop 指令
2.17.1 概要
<#stop>

<#stop reason>
这里:
 reason:关于终端原因的信息化消息。表达式被算做是字符串。
2.17.2 描述
中止模板的处理。这是一种像紧急中断的机制:不要在普通情况下使用。抛出StopException异常会发生中止,而且StopException会持有reason参数的值。
15
25
N/A
2.18 ftl 指令
2.18.1 概要
<#ftl param1=value1 param2=value2 ... paramN=valueN>
这里:
 param1,param2等:参数的名字,不是表达式。允许的参数有encoding,strip_whitespace,strip_text等。参加下面。
 value1,value2等:参数的值。必须是一个常量表达式(如true,或"ISO-8859-5",或{x:1, y:2})。它不能用变量。
2.18.2 描述
告诉FreeMarker和其他工具关于模板的信息,而且帮助程序员来自动检测一个文本文件是否是FTL文件。这个指令,如果存在,必须是模板的第一句代码。该指令前的任何空白将被忽略。这个指令的老式语法(#-less)格式是不被支持的。
一些设置(编码方式,空白剥离等)在这里给定的话就有最高的优先级,也就是说,它们直接作用于模板而不管其他任何FreeMarker配置的设置。
参数:
 encoding:使用这个你可以在模板文件中为模板指定编码方式(字符集)(也就是说,这是新创建Template的encoding设置,而且Configuration.getTemplate中的encoding参数不能覆盖它。)。要注意,FreeMarker会尝试会和自动猜测的编码方式(这依赖于程序员对FreeMarker的配置)找到ftl指令并解释它,然后就会发现ftl指令会让一些东西有所不同,之后以新的编码方式来读取模板。因此,直到ftl标记使用第一个编码方式读取到结尾,模板必须是合法的FTL。这个参数的合法值是从IANA字符集注册表中参考MIME中的字符集名称。比如ISO-8859-5,UTF-8或Shift_JIS。
 strip_whitespace:这将开启/关闭空白剥离。合法的值是布尔值常量true和flase(为了向下兼容,字符串"yes","no","true","false"也是可以的)。默认值(也就是当你不使用这个参数时)是依赖于程序员对FreeMarker的配置,但是对新的项目还应该是true。
 strip_text:当开启它时,当模板被解析时模板中所有顶级文本被移除。这个不会影响宏,指令,或插值中的文本。合法值是布尔值常量true和flase(为了向下兼容,字符串"yes","no","true","false"也是可以的)。默认值(也就是当你不使用这个参数时)是flase。
 strict_syntax:这会开启/关闭“严格的语法”。合法值是布尔值常量true和flase(为了向下兼容,字符串"yes","no","true","false"也是可以的)。默认值(也就是当你不使用这个参数时)是依赖于程序员对FreeMarker的配置,但是对新的项目还应该是true(程序员:对于config.setStrictSyntaxMode(true);,你应该明确设置它为true)。要获取更多信息,可以参考:废弃的FTL结构/老式FTL语法部分。
 ns_prefixes:这是关联节点命名空间前缀的哈希表。比如:
{"e":"http://example.com/ebook", "vg":"http://example.com/vektorGraphics"}。这通常是用于XML处理的,前缀可以用于XML查询,但是它也影响visit和recurse指令的工作。相同节点的命名空间只能注册一个前缀(否则会发生错误),所以在前缀和节点命名空间中是一对一的关系。前缀D和N是保留的。如果你注册前缀D,那么除了你可以使用前缀D来关联节点命名空间,你也可以设置默认的节点命名空间。前缀N就不能被注册;当且仅当前缀D被注册时,N被用来表示在特定位置没有节点命名空间的节点。(要参考默认节点命名空间的用法,N,一般的前缀,可以看XML处理中visit和recurse指令。)ns_prefixes的作用限制在单独的FTL命名空间内,换句话说,就是为模板创建的FTL命名空间内。这也意味着ns_prefixes当一个FTL命名空间为包含它的模板所创建时才有作用,否则ns_prefixes参数没有效果。FTL命名空间当下列情况下为模板创建:(a)模板是“主”模板,也就是说它不是被<#include ...>来调用的模板,但是直接被调用的(和process一起的Template或Environment类的方法);(b)模板直接被<#import ...>调用。
 attributes:这是关联模板任意属性(名-值对)的哈希表。属性的值可以是任意类型(字符串,数字,序列等)。FreeMarker不会尝试去理解属性的含义。它是由封装FreeMarker(比如一个Web应用框架)的应用程序决定的。因此,允许的属性的设置是它们依赖的应用(Web应用框架)的语义。程序员:你可以通过关联Template对象的getCustomAttributeNames和getCustomAttribute方法(从freemarker.core.Configurable继承而来)获得属性。如当模板被解析时,关联Template对象的模板属性,属性可以在任意时间被读取,而模板不需要被执行。上面提到的方法返回未包装的属性值,也就是说,使用FreeMarker独立的类型,如java.util.List。
这个指令也决定模板是否使用尖括号语法(比如<#include 'foo.ftl'>)或方括号语法(如[#include 'foo.ftl'])。简单而言,这个指令使用的语法将会是整个模板使用的语法,而不管FreeMarker是如何配置的。
2.19 t,lt,rt 指令
2.19.1 概要
<#t>
<#lt>
<#rt>
<#nt>
2.19.2 描述
这些指令,指示FreeMarker去忽略标记中行的特定的空白
 t(整体削减):忽略本行中首和尾的所有空白。
 lt(左侧削减):忽略本行中首部所有的空白。
 rt(右侧削减):忽略本行中尾部所有的空白。
这里:
 “首部空白”表示本行所有空格和制表符(和其他根据UNICODE中的空白字符,除了换行符)在第一个非空白字符之前。
 “尾部空白”表示本行所有的空格和制表符(和其他根据UNICODE中的空白字符,除了换行符)在最后一个非空白字符之后,还有行末尾的换行符。
理解这些检查模板本身的指令是很重要的,而不是当你合并数据模型时,模板生成的输出。(也就是说,空白的移除发生在解析阶段)
比如这个:
将会输出:
这些指令在行内的放置不重要。也就是说,不管你是将它们放在行的开头,或是行的末尾,或是在行的中间,效果都是一样的。
2.20 nt 指令
2.20.1 概要
<#nt>
--
1 <#t>
2<#t>
3<#lt>
4
5<#rt>
6
--
--
1 23
4
5 6
--
2.20.2 描述
“不要削减”。这个指令关闭行中出现的空白削减。它也关闭其他同一行中出现的削减指令(t,lt,rt的效果)。
2.21 attempt,recover 指令
2.21.1 概要
<#attempt>
attempt block
<#recover>
recover block
</#attempt>
这里:
 attempt block:任意内容的模板块。这是会被执行的,但是如果期间发生了错误,那么这块内容的输出将会回滚,之后recover block就会被执行。
 recover block: 任意内容的模板块。这个仅在attempt block执行期间发生错误时被执行。你可以在这里打印错误信息或其他操作。
recover是命令的。attempt/recover可以嵌套在其他attempt s或recover s中。
注意:
上面的格式是从2.3.3版本开始支持的,之前它是<#attempt>...<#recover>...</#recover>,也支持向下兼容。此外,这些指令在FreeMarker 2.3.1版本时引入的,在2.3版本中是不存在的。
2.12.2 描述
如果你想让页面成功输出内容,尽管它在页面特定位置发生错误也这样,那么这些指令就是有用的。如果一个错误在attempt block执行期间发生,那么模板执行就会中止,但是recover block会代替attempt block执行。如果在attempt block执行期间没有发生错误,那么recover block就会忽略。一个简单的示例如下:
如果thisMayFails变量不存在,那么输出:
Primary content
<#attempt>
Optional content: ${thisMayFails}
<#recover>
Ops! The optional content is not available.
</#attempt>
Primary content continued
如果thisMayFails变量存在而且值为123,那么输出:
attempt block块有多或没有的语义:不管attempt block块的完整内容是否输出(没有发生错误),或者在attempt block(没有发生错误)块执行时没有输出结果。比如,上面的示例,发生在“Optional content”之后的失败被打印出来了,而没有在“Ops!”之前输出。(这是在attempt block块内,侵入的输出缓冲的实现,就连flush指令也会送输出到客户端。)
为了阻止来自上面示例的误解:attempt/recover不仅仅是处理未定义变量(对于那个可以使用不存在变量控制符来处理)。它可以处理发生在块执行期间的各种类型的错误(而不是语法错误,这会在执行之前被检测到)。它的目的是包围更大的模板段,错误可能发生在很多地方。比如,你在模板中有一个部分,来处理打印广告,但是它不是页面的主要内容,所以你不想你的页面因为一些打印广告(也可能是短暂的数据库故障)的错误而挂掉。所以你将整个广告区域放在attempt block块中。
在一些环境下,程序员配置FreeMarker,所以对于特定的错误,它不会中止模板的执行,在打印一些错误提示信息到输出(请参考:程序开发指南/配置/错误处理部分)中之后,而是继续执行。attempt指令不会将这些抑制的错误视为错误。
在recover block块中,错误的信息存在特殊变量error中。不要忘了以点开始引用特殊变量(比如:${.error})
在模板执行期间发生的错误通常被被日志记录,不管是否发生在attempt block块中。
2.22 visit,recurse,fallback 指令
2.22.1 概要
<#visit node using namespace>

<#visit node>
<#recurse node using namespace>

<#recurse node>

<#recurse using namespace>

<#recurse>
Primary content
Ops! The optional content is not available.
Primary content continued
Primary content
Optional content: 123
Primary content continued
<#fallback>
这里:
 node:算作节点变量的表达式。
 namespace:一个命名空间,或者是命名空间的序列。命名空间可以以命名空间哈希表(又称为根哈希表)给定,或者可以引入一个存储模板路径的字符串。代替命名空间哈希表,你也可以使用普通哈希表。
2.22.2 描述
visit和recurse指令是用来递归处理树的。在实践中,这通常被用来处理XML。
2.22.2.1 visit 指令
当你调用了<#visit node>时,它看上去像用户自定义指令(如宏)来调用从节点名称(node?node_name)和命名空间(node?node_namesoace)中有名称扣除的节点。名称扣除的规则:
 如果节点不支持节点命名空间(如XML中的文本节点),那么这个指令名仅仅是节点的名称(node?node_name)。如果getNodeNamespace方法返回null时节点就不支持节点命名空间了。
 如果节点支持节点命名空间(如XML中的元素节点),那么从节点命名空间中的前缀扣除可能在节点名称前和一个做为分隔符(比如e:book)的冒号追加上去。前缀,以及是否使用前缀,依赖于何种前缀在FTL命名空间中用ftl指令的ns_prefixes参数注册的,那里visit寻找控制器指令(visit调用的相同FTL命名空间不是重要的,后面你将会看到)。具体来说,如果没有用ns_prefixes注册默认的命名空间,那么对于不属于任何命名空间(当getNodeNamespace返回"")的节点来说就不使用前缀。如果使用ns_prefixes给不属于任意命名空间的节点注册了默认命名空间,那么就使用前缀N,而对于属于默认节点命名空间的节点就不使用前缀了。否则,这两种情况下,用ns_prefixes关联节点命名空间的前缀已经被使用了。如果没有关联节点命名空间的节点前缀,那么visit仅仅就好像没有以合适的名称发现指令。
自定义指令调用的节点对于特殊变量.node是可用的。比如:
输出就像:
<#-- 假设nodeWithNameX?node_name是"x" -->
<#visit nodeWithNameX>
Done.
<#macro x>
Now I'm handling a node that has the name "x".
Just to show how to access this node: this node has ${.node?children?size} children.
</#macro>
如果使用可选的using从句来指定一个或多个命名空间,那么visit就会在那么命名空间中寻找指令,和先前列表中指定的命名空间都获得优先级。如果指定using从句,对最后一个未完成的visit调用的用using从句指定命名空间的命名空间或序列被重用了。如果没有这样挂起的visit调用,那么当前的命名空间就被使用。比如,如果你执行这个模板:
这是n1.ftl:
这是n2.ftl:
这会打印:
Now I'm handling a node that has the name "x".
Just to show how to access this node: this node has 3 children.
Done.
<#import "n1.ftl" as n1>
<#import "n2.ftl" as n2>
<#-- 这会调用n2.x (因为没有n1.x): -->
<#visit nodeWithNameX using [n1, n2]>
<#-- 这会调用当前命名空间中的x: -->
<#visit nodeWithNameX>
<#macro x>
Simply x
</#macro>
<#macro y>
n1.y
</#macro>
<#macro x>
n2.x
<#-- 这会调用call n1.y,因为它 从等待访问调用中继承了"using [n1, n2]"-->
<#visit nodeWithNameY>
<#-- 这会调用n2.y: -->
<#visit nodeWithNameY using .namespace>
</#macro>
<#macro y>
n2.y
</#macro>
n2.x
n1.y
n2.y
Simply x
如果visit既没有在和之前描述规则的名称扣除相同名字的FTL命名空间发现自定义指令,那么它会尝试用名称@node_type来查找,又如果节点不支持节点类型属性(也就是node?node_type返回未定义变量),那么使用名称@default。对于查找来说,它使用和之前描述相同的机制。如果仍然没有找到处理节点的自定义指令,那么visit停止模板执行,并抛出错误。一些XML特定的节点类型在这方面有特殊的处理;参考:XML处理指南/声明的XML处理/详细内容部分。示例:
这会打印:
2.22.2.2 recurse 指令
<#recurse>指令是真正纯语义上的指令。它访问节点的所有子节点(而没有节点本身)。所以来写:
和这个是相等的:
而目标节点在recurse指令中是可选的。如果目标节点没有指定,那就仅仅使用.node。因此,<#recurse>这个精炼的指令和下面这个是相同的:
对于熟悉XSLT的用户的评论,<#recurse>是和XSLT中<xsl:apply-templates/>指令相当类似的。
Handling node x
There was no specific handler for node y
<#-- 假设nodeWithNameX?node_name是"x" -->
<#visit nodeWithNameX>
<#-- 假设 nodeWithNameY?node_type是"foo" -->
<#visit nodeWithNameY>
<#macro x>
Handling node x
</#macro>
<#macro @foo>
There was no specific handler for node ${node?node_name}
</#macro>
<#recurse someNode using someLib>
<#list someNode?children as child><#visit child using someLib></#list>
<#list .node?children as child><#visit child></#list>
2.22.2.3 fallback 指令
正如前面所学的,在visit指令的文档中,自定义指令控制的节点也许在多个FTL命名空间中被搜索。fallback指令可以被用在自定义指令中被调用处理节点。它指挥FreeMarker在更多的命名空间(也就是,在当前调用列表中自定义指令命名空间之后的命名空间)中来继续搜索自定义指令。如果节点处理器被发现,那么就被调用,否则fallback不会做任何事情。
这个指令的典型用法是在处理程序库之上写定制层,有时传递控制到定制的库中:
<#import "/lib/docbook.ftl" as docbook>
<#-- 我们使用docbook类库,但是我们覆盖一些这个命名空间中的处理器-->
<#visit document using [.namespace, docbook]>
<#-- 覆盖"programlisting"处理器,但是要注意它的"role"属性是"java"
-->
<#macro programlisting>
<#if .node.@role[0]!"" == "java">
<#-- 在这里做一些特殊的事情... -->
...
<#else>
<#-- 仅仅使用原来的(覆盖的)处理器 -->
<#fallback>
</#if>
</#macro>
第三章 特殊变量参考文档
特殊变量是由FreeMarker引擎自己定义的变量。要访问它们,你可以使用.variable_name语法。比如,你不能仅仅写version,而必须写.version。
支持的特殊变量有:
 data_model:你可以使用来直接访问数据模型的哈希表。也就是,你使用global指令定义在这里不可见的的变量。
 error(从FreeMarker 2.3.1版本开始可用):这个变量在recover指令体中可以访问,它存储了我们恢复错的错误信息。
 globals:你可以使用来访问全局可访问的变量的哈希表:数据模型和由global指令创建的变量。注意用assign或macro创建的变量不是全局的。因此当你使用globals时你不能隐藏变量。
 language:返回当前本地设置的语言部分的值。比如.locale是en_US,那么.lang是en。
 locale:返回当前本地设置的值。这是一个字符串,比如en_US。要获取关于本地化字符串值的更多内容,请参考setting指令。
 locales:你可以访问本地化变量的哈希表(由local指令创建的变量,还有宏的参数)。
 main:你可以用来访问主命名空间的哈希表。注意像数据模型中的全局变量通过这个哈希表是不可见的。
 namespace:你可以用来访问当前命名空间的哈希表。注意像数据模型中的全局变量通过这个哈希表是不可见的。
 node(由于历史原因重命名为current_node): 你可以用访问者模式(也就是用visit,recurse等指令)处理的当前节点。而且,当你使用FreeMarker XML的Ant 任务时,它初始存储根节点。
 now:返回当前的时间日期。使用示例:" Page generated: ${.now}","Today is ${.now?date}","The current time is ${.now?time}"。
 output_encoding(从FreeMarker 2.3.1版本开始可用):返回当前输出字符集的名称。如果框架封装FreeMarker却没有为FreeMarker指定输出字符集时这个特殊变量是不存在的。(程序员可以阅读关于字符集问题的更多内容,在:程序开发指南/其他/字符集问题部分。)
 template_name:当前模板的名称(从FreeMarker 2.3.14版本开始可用)。
 url_escaping_charset(从FreeMarker 2.3.1版本开始可用):如果存在,它存储了应该用于URL转义的字符集的名称。如果这个变量不存在就意味着没有人指定URL编码应该使用什么样的字符集。这种情况下,url内建函数使用特殊变量output_encoding指定的字符集来进行URL编码。处理机制和它是相同的。(程序员可以阅读关于字符集问题的更多内容,在:程序开发指南/其他/字符集问题部分。)
 vars:表达式.vars.foo返回和表达式foo相同的变量。出于某些原因你不得不使用方括号语法时这是有用的,因为它只对哈希表子变量有用,所以你需要一个人工的父哈希表。比如,要读取有特殊名称的顶层变量可能会把FreeMarker弄糊涂,你可以写.vars["A strange name!"]。或者,使用和变量
varName给定的动态名称访问顶层变量你可以写.vars[varName]。注意这个哈希表由.vars返回,并不支持?keys和?values。
 version:返回FreeMarker版本号的字符串形式,比如2.2.8。这可以用来检查你的应用程序使用的是哪个版本的FreeMarker,但是要注意这个特殊变量在2.3-final或2.2.8版本之前不存在。非最终发行版本号包含缩写形式的“preview”,是“pre”(比如2.3pre6),或缩写形式的“release candidate”,是“rc”。
第四章 FTL中的保留名称
下面的这些名称不能在非方括号语法中被用作顶层变量(比如vars["in"]),因为这是FTL中的关键字。
 true:布尔值“true”
 false:布尔值“false”
 gt:比较运算符“大于”
 gte:比较运算符“大于或等于”
 lt:比较运算符“小于”
 lte:比较运算符“小于或等于”
 as:由少数指令使用
 in:由少数指令使用
 using:由少数指令使用
第五章 废弃的FTL结构
5.1 废弃的指令列表
下面这些指令是废弃的,但是仍然可以运行:
 call:使用自定义指令来代替调用
 comment:这是<#--...-->的老式格式。在<#comment>和</#comment>之间的任何东西都会被忽略。
 foreach:它是list指令的代名词,有着轻微不同的参数语法。它的语法结构是<#foreach item in sequence>,和<#list sequence as item>是相同的。
 transform:使用自定义指令来代替调用
下面这些指令不在可以运行:
 遗留的function:起初function是被用作定义宏,由macro指令的支持,它就被废弃了。对于FreeMarker 2.3版本来说,这个指令由不同的意义而再次引入:它被用来定义方法。
5.2 废弃的内建函数列表
下面这些内建函数是被废弃的,但是仍可以运行:
default:由默认值运算符的引入,它被废弃了。exp1?default(exp2)和exp1!exp2是相同的,(exp1)?default(exp2)和(exp1)!exp2.是相同的。唯一的不同是在FreeMarker 2.4版本之前,内建函数default通常算作是exp2,而默认值运算符仅仅当默认值真的需要时才算。从FreeMarker 2.4版本之后,内建函数default被改进了,和默认值运算符的行为非常像了。
exists:由空值测试运算符的引入,它被废弃了。exp1?exists和exp1??是一样的,(exp1)?exists和(exp1)??也是一样的。
if_exists:由默认值运算符的引入,它被废弃了。exp1?if_exists和exp1!相似,(exp1)?if_exists和(exp1)!相似。不同之处在于,用if_exists的默认值不仅仅同时是空字符串,空序列和空哈希表,而且布尔值false和不做任何事情的变换,还有忽略所有参数。
web_safe:和html相同。
5.3 老式的macro和call指令
5.3.1 概要
<#macro name(argName1, argName2, ... argNameN)>
...
</#macro>
<#call name(argValue1, argValue2, ... argValueN)>
这里:
 name:宏的名称(不是表达式)
 argName1,argName2等:存储参数值局部变量的名称(不是表达式)
 argValue1,argValue2等:表达式,参数的值
5.3.2 描述
注意:
这是FreeMarker 2.1版本的文档中宏还有宏它相关的指令。这仍然可以用,但是已经被废弃了。你也许想阅读FreeMarker2.2+版本的参考:就是指令参考中的macro,return部分和用户自定义指令部分。
宏是关联名称的模板段。你可以在你的模板中的很多位置使用命名的代码段,所以它可以在重复的任务中帮助你。宏可以有参数,这会在你调用它的时候影响生成的输出。
你可以使用macro指令来定义宏,之后你可以在整个模板中定义宏。macro指令本身不往输出中写任何东西,它只是用来定义宏。例如这会定义一个称为warning的宏:
无论何时你使用call指令来调用这个宏时,宏定义体(在宏的开始标签和结束标签之间的部分)将会被处理。比如这个调用了名为warning的宏:
作为call指令参数传递的参数将会在宏定义体中可以作为局部变量来访问。
当你调用一个宏,你必须指定和在宏定义时参数数量相同的参数。比如如果这个宏这么来定义:
那么这些是合法的调用:
<#macro warning(message)>
<div align=center>
<table border=1 bgcolor=yellow width="80%"><tr><td align=center>
<b>Warning!</b>
<p>${message}
</td></tr></table>
</div>
</#macro>
<div align=center>
<table border=1 bgcolor=yellow width="80%"><tr><td align=center>
<b>Warning!</b>
<p>Unplug the machine before opening the cover!
</td></tr></table>
</div>
<#macro test(a, b, c)>Nothing...</#macro>
如果宏没有定义参数,那么你可以忽略圆括号中的内容:
当你定义宏时,那么它在模板中就是可用的,你也只能在模板中来定义宏。但是你可能想在更多模板中使用相同的宏。这种情况下你可以在公共文件中存储你定义的宏,之后在所有你需要这些宏的模板中包含那个文件。
调用定义在模板下部的宏是不错的(因为宏在解析时间定义,而不是执行时间)。然而,如果宏定义被插入到include指令中了,它们知道FreeMarker执行include指令时才会可用。
你可以用return指令在</#macro>标签之前留下宏定义体。
5.4 转换指令
5.4.1 概要
<transform transVar>
...
</transform>
or
<transform transVar name1=value1 name2=value2 ... nameN=valueN>
...
</transform>
这里:
 transVar:要来改变的表达式
 name1,name2,…nameN:参数的名称。文字值,不是表达式
 value1,value2,…valueN:算作参数值的表达式
5.4.2 描述
注意:
这个指令仍然可用,但是已经被废弃了。你也许想阅读用户自定义指令部分来查看它的替代物。
捕捉生成在它体内(也就是开始标签和结束标签之间)的输出,之后让给定的转换物在写入最终的输出之前改变。
示例:
<#call test(1, 2, 3)>
<#call test("one", 2 + x, [1234, 2341, 3412, 4123])>
<#macro test>mooo</#macro>
<#call test>
输出为:
一些转换可能需要参数。参数的名称和意义依赖于转换的问题。比如这里我们给出一个名为“var”的参数:
这是程序员在数据模型中放置必要转换的任务。对于可访问转换的名称和用法请问程序员。最初对在freemarker.template.utility包中的大多数转换来说有共享变量。要获取更多信息,请阅读:程序开发指南/配置/共享变量部分的内容。
5.5 老式FTL语法
在FTL标签中使用#形式的FTL语法已经是不要求(在2.1版本之前是不允许的)的了。比如,你可以这样写代码:
<p>A very simple HTML file:
<pre>
<transform html_escape>
<html>
<body>
<p>Hello word!
</body>
</html>
</transform>
</pre>
<p>A very simple HTML file:
<pre>
&lt;html&gt;
&lt;body&gt;
&lt;p&gt;Hello word!
&lt;/body&gt;
&lt;/html&gt;
</pre>
<#-- This transform stores the output in the variable x,
rather than sending it to the output -->
<transform capture_output var="x">
some test
</transform>
而没有#样式语法的代码对于HTML作者来说更加自然,它有很多的缺点,所以最终我们决定废弃它。使用新式语法(又称为“严格的语法”),#是严格要求的。也就是说,像<include "common_footer.html">这样的东西将会原样出现在输出中,因为它们不被认为是FTL标签。注意用户自定义指令使用@代替#。
然而,为了给用户时间来准备这种改变,在FreeMarker 2.1和2.2版本中,#的用法是可选的,除非程序员调用Configuration的setStrictSyntaxMode(true)在FreeMarker配置中开启严格语法模式。事实上,我们把这个强烈建议给程序员。从后续释出版本开始,这个设置将会初始设置为true。而且,如果你在模板文件中想使用严格语法或老式语法,你可以用ftl指令来指定。
“严格语法”比遗留的FTL语法的好处是:
 由于对于FTL来说,所有<#...>和</#...>都是保留的。
 我们可以引入新的指令而不破坏向后兼容。
 我们可以检测你是否创建了一个类型,也就是<#inculde ...>被视为解析时的错误,而不是被静默地视为简单文本。
 对于第三方工具来处理模板(比如高亮语法显示)来说是简单的,特别是因为它们不需要知道新释出版本中被引入的新指令。
 模板更易于阅读,因为很容易辨认嵌入在HTML或其他标记中的<#...>标签。
 <#和</#是合法的XML(除了在CDATA段中),而且其他大多数SGML应用中也是合法的,所以它们不能妨碍用在静态文本部分(比如你在生成的XML中有include元素)的标签。
5.6 #{…}式的数字插值
已经被废弃了:使用number_format设置和内建函数string来代替。对于计算机使用(也就非本地的格式化)的格式化,使用内建函数c(像number?c)。
<html>
<head>
<title>Welcome!</title>
</head>
<body>
<h1>Welcome ${user}!</h1>
<p>We have there animals:
<ul>
<list animals as being>
<li>${being.name} for ${being.price} Euros
</list>
</ul>
<include "common_footer.html">
</body>
</html>
5.6.1 概要
#{expression}
or
#{expression; format}
这里:
 expression:可以算作是数字的表达式
 format:可选的格式说明符
5.6.2 描述
数字插值被用来输出数值。如果表达式不能算成数字,那么计算过程就会以抛出错误而终止。
可选的格式说明符指定了使用mminMmax语法显示的小数位数的最小和最大值。比如,m2M5表示“最少两位,最多5位小数位”。最小值和最大值说明符部分可以被忽略。如果仅指定最小值,那么最大值和最小值相等。如果仅指定最大值,那么最小值是0。
输出的小数点字符是国际化的(根据当前本地设置),这表示它可能不是一个点。
不像${...},#{...}忽略number_format设置。实际上这是向后兼容的一个怪点,但是当你在如<a href="quertyDatabase?id=#{id}">这些情况打印数字时它可能是有用的,这里你肯定不想分组分隔符或像那些幻想的东西。然而,从FreeMarker 2.3.3版本开始,而使用内建函数c来达到这个目的,比如<a href="quertyDatabase?id=${id?c}">。
示例:假设x是2.582而y是4:
<#-- If the language is US English the output is: -->
#{x} <#-- 2.582 -->
#{y} <#-- 4 -->
#{x; M2} <#-- 2.58 -->
#{y; M2} <#-- 4 -->
#{x; m1} <#-- 2.6 -->
#{y; m1} <#-- 4.0 -->
#{x; m1M2} <#-- 2.58 -->
#{y; m1M2} <#-- 4.0 -->
第五部分 附录
附录A FAQ
1. JSP和FreeMarker的对比
注意:JSP 1.x版本作为MVC模板引擎是非常糟糕的,因为它不是为模板引擎而开发的,所以这里我们就不管它了。我们仅仅比较FreeMarker和JSP 2.0与JSTL的组合。
FreeMarker的优点:
 FreeMarker不依赖于Servlet,网络或Web环境;它仅仅是通过合并模板和Java对象(数据模型)来生成文本输出的类库。你可以在任意地方任意时间来执行模板;不需要HTTP的请求转发或类似的手段,也不需要Servlet环境。出于这些特点你可以轻松的将它整合到任何系统中去。
 在模板中没有servlet特定的范围和其它高级技术。FreeMarker一开始就是为MVC设计的,它仅仅专注于展示。
 你可以从任意位置加载模板;从类路径下,从数据库中等。
 默认情况下,数字和日期格式是本地化敏感的。因为我们对用户输出,你所做的仅仅是书写${x},而不是<fmt:formatNumber value="${x}" />。你也可以很容易就改变这个行为,默认输出没有本地化的数字。
 易于定义特设的宏和函数。
 隐藏错误并假装它不存在。丢失的变量默认会引起错误,也不会默认给任意值。而且null-s也不会默认视为0/false/空字符串。参见FAQ第三点来获取更多信息。
 “对象包装”允许你在模板中以自定义,面向表现的方式来展示对象。(比如:参见XML处理指南/必要的XML处理/通过例子来学习部分,来看看使用这种技术时W3C的DOM节点是如何通过模板展现出来的。)
 宏和函数仅仅是变量(和JSP的自定义标记工作方式来比较),就像其它任意值一样,所以它们可以很容易的作为参数值来传递,放置到数据模型中等。
 当第一次访问一个页面时几乎察觉不到的延迟(或在它改变之后),因为没有更高级的编译发生。
FreeMarker的缺点:
 不是一个标准。很少的工具和IDE来集成它,少数的开发者知道它,很少的工业化的支持。(然而,如果没有使用.tag文件,JSP标签库在FreeMarker模板中工作不需要改变)
 因为宏和函数仅仅是变量,不正确的指令,参数名和丢失的必须变量仅仅在运行时会被检测到。
 除了一些视觉上的相似性,它的语法不同于HTML/XML语法规则,这会使得新用户感到混乱。(这就是简洁的价值所在)
 不能和JSF一起使用。(这在技术上可行,但是没有人来实现它)
如果你认为可以用FreeMarker来代替JSP,你或许可以阅读这部分内容:程序开发指南/其他/在servlets中使用FreeMarker/在“Model 2”中使用FreeMarker。
2. Velocity和FreeMarker的对比
Velocity是一个很简单,更轻量级的工具。因此,它没有强调很多FreeMarker可以做到的事情,而且通常来说它的模板语言没有高深之处,但是它很简单;访问http://freemarker.org/fmVsVel.html 来获取更多信息,当前(2005年时),Velocity已经有很多第三方的支持而且有用很大的用户群体。
3. 为什么FreeMarker对null-s和不存在的变量很敏感,如何来处理它?
概括一下这点是关于什么的:默认情况下,FreeMarker将试图访问一个不存在的变量或null值(这两点是一样的,参看FAQ-17)视为一个错误,这会中断模板的执行。
首先,你应该理解敏感的原因。很多脚本语言和模板语言都能容忍不存在的变量(还有null-s),通常它们将这些变量视为空字符串和/或0,还有逻辑false。这些行为主要有以下几点问题:
 它潜在隐藏了一些可能偶然发生的错误,就像变量名中的一个错字,或者当模板编写者引用程序员没有放到数据模型中的变量时,或程序员使用了一个不同的名字。人们是容易犯下这种偶然的错误的,而计算机则不会,所以失去这些机会,模板引擎可以显示这些错误则是一个很糟糕的运行方式。尽管你很小心地检查了开发期间模板输出的内容,那也很容易就忽略了如<#if hasDetp>print dept here...</#if>这样的错误,可能永远不会对访问者打印部门信息,因为你已经搞乱了变量名(这里应该是hasDept)。也考虑一下后期的维护,当你以后修改你的应用时,你可能不会每次都重新来仔细检查模板。
 做了危险的假设。脚本语言或模板引擎对应用领域是一无所知的,所以当它决定一些它不知道是0或false值时,这是一个相当不负责而且很武断的事情。仅仅因为它不知道你当前银行账户的余额,我们能说是0(意外地写下:Balance: ${balanace}是多么容易啊)么?仅仅因为不知道一个病人是否对青霉素过敏,我们就能说他/她不对青霉素过敏(意外地写下:hasPenicilinAllergy>Warning...<#else>Allow...</#if>是多磨容易啊;这里有一个错误的变量名,你发现了么)?想一想这样错误的暗示信息。它们的问题相当严重,令人担忧。展示一个错误提示页面通常要比直接显示错误信息好很多。
这种情况下(不面对这种问题),不对其敏感那就是隐藏错误假装它不存在了,这样很多用户会觉得方便,但是我们仍然相信在大多数情况下严格对待这个问题,从长远考虑会节省你更多时间并提高软件的质量。
另外一方面,我们意识到你有更好的原因在有些地方不想FreeMarker对错误敏感,那么对于这种情况的解决方案如下:
 通常数据模型中含有null-s或可选变量。这种情况下使用模板开发指南/模板/表达式/处理不存在的值部分介绍的操作符。如果你使用它们很频繁的话,那么就要重新考虑一下你的数据模型了,因为过分依赖它们并不是那些难以使用的详细模板的结果,但是会增加隐藏错误和打印任意不正确输出(原因前面已经说过了)的可能性。
在产品运行的服务器上你可能宁可显示一个不完整/受损的页面也不愿显示错误页面。这种情况下你可以使用错误控制而不是默认处理。错误控制可以用来略过会出问题的部分而不是中止整个页面的呈现。然而要注意,尽管错误控制不会给变量随意的默认值,对于页面显示关键的信息,也可能要好过显示错误页面。(另外一个你也许感兴趣的特性:attempt/recover指令)
4. 文档编写了特性X,但是好像FreeMarker并不知道它,或者它的行为和文档描述的不同,或者一个据称已经修改的BUG依然存在。
你确定你使用的文档和FreeMarker是相同的版本吗?特别要注意我们的在线文档是针对最新的稳定版本的FreeMarker,你可以使用老版本的。
你确定Java的类加载器发现了你期望使用版本的freemarker.jar?可能一个有更高优先级的老版本的freemarker.jar被使用了(比如在Ant的lib目录下的)。要检查版本信息,可以使用${.version}在模板中打印版本号。如果报出“未知的内建函数变量:版本”的错误信息,那么你使用的是2.3-final或2.2.8之前的版本,但是你任然可以在应用的Java代码中使用Configuration.getVersionNumber()方法来获得版本号。如果这个方法也不存在的话,你使用的就是2.3-preview或2.2.6之前的版本了。
如果你认为文档或FreeMarker本身有问题,请使用bug跟踪系统或邮件列表或http://sourceforge.net/projects/freemarker/上的论坛报告给我们。谢谢你!
5. 为什么FreeMarker打印奇怪的数字数字格式(比如1,000,000或1 000 000而不是1000000)?
FreeMarker使用Java平台的本地化敏感的数字格式信息。默认的本地化数字格式可能是分组或其他不想要的格式。为了避免这种情况,你不得不使用number_format设置来重写Java平台建议的数字格式,比如:
注意人们通常在没有分组分隔符时阅读大数是有些困难的。所以通常建议保留分隔符,而在对计算机处理时的情况,分组分隔符会使其混乱,就要使用内建函数c了。比如:
cfg.setNumberFormat("0.######"); // now it will print 1000000
// where cfg is a freemarker.template.Configuration object
<a href="/shop/productdetails?id=${product.id?c}">Details...</a>
6. 为什么FreeMarker会打印不好的小数和/或分组分隔符号(比如3.14而不是3,14)
不同的国家使用不同的小数/分组分隔符号。如果你看到不正确的符号,那么可能你的本地化设置不太合适。设置Java虚拟机的默认本地化或使用FreeMarker的设置选项locale来重写默认本地化。比如:
然而有时你想输出一个数字,这个数字不是对用户的,而是对计算机的(比如你想在CSS中打印大小),不管页面的本地化(语言)是怎么样的,这种情况你必须使用点来作为小数分隔符。这样就可以使用内建函数c,比如:
7. 为什么当我想用如格式打印布尔值时,FreeMarker会抛出错误,又如何来修正呢?
不像是数字,布尔值没有通用的可接受的格式,在相同页面中也没有一个通用的格式。就像当你在HTML页面中展示一件产品是可洗的,你可能不会想给访问者看“Washable:true”,而是“Washable:yes”。所以我们强制模板作者(由${washable}引起错误)去探索用户的感知,在确定的地方布尔值应该来显示成什么。通常我们格式化布尔值的做法是:${washable?string("yes", "no")},${caching?string("Enabled", "Disabled")},${heating?string("on", "off")}等。而对于生成源代码${washable?string("true", "false")}应该很经常使用,所以${washable?string}(也就是忽略参数列表)和原先的形式是相等的。
8. FreeMarker标签中的<和>混淆了编辑器或XML处理器,应该怎么做?
从FreeMarker 2.3.4版本开始,你可以使用[和]来<代替>。更多的细节,可以阅读模板开发指南/其它/替换(方括号)语法部分中的内容
9. 什么是合法的变量名?
关于在变量名中使用的字符和变量名的长度,FreeMarker没有限制,但是为了你的方便,在选择变量名时最好是简单变量引用表达式(参见模板开发指南/模板/表达式/检索变量/顶层变量部分的内容)。如果你不得不选择一个非常极端的变量名,那也不是一个问题:参见
cfg.setLocale(java.util.Locale.ITALY);
// where cfg is a freemarker.template.Configuration object
font-size: ${fontSize?c}pt;
第10点(下面这部分,译者注)的内容。
10. 如何使用包含空格,或其他特殊字符的变量(宏)名?
如果你的变量名很奇怪,比如foo-bar,当你编写如${foo-bar}的形式时,FreeMarker会曲解你可能想要的东西。在这种确定的情况下,它会相信你想从foo中减去bar的值,这个FAQ例子解释了如何控制这样的情况。
首先应该清理这些句法问题。关于变量名中使用的字符和变量名的长度,FreeMarker没有限制,但是有时你需要使用一些句法技巧。
如果你想读取变量:使用方括号语法。一个使用了方括号语法的示例是baaz["foo"],这和baaz.foo是相等的。在方括号内的子变量名是一个字符串(事实上,可以是任意表达式),它允许你写baaz["foo-bar"]。现在你或许会说这是仅仅对哈希表子变量使用的。是的,但是顶级变量可以通过特殊的哈希表变量.vars来访问。比如foo和.vars["foo"]是相等的。所以你可以写vars["foo-bar"]。自然地,这个技巧对宏的调用也是起作用的:<@.vars["foo-bar"]/>。
如果你想创建或修改变量:所有你可以用来创建或修改变量的指令(如assign,local,global,macro,function等)允许对目的变量名的引用。比如,<#assign foo = 1>和<#assign "foo" = 1>是相同的。所以你可以编写如<#assign "foo-bar" = 1>和<#macro "foo-bar">的代码。
11. 当我试图使用JSP客户标签时为什么会得到非法参数异常:形式参数类型不匹配?
在JSP页面中,你引用的所有参数(属性)值是和参数的类型无关的,比如字符串,布尔值或数字。但是因为在FTL模板中,自定义标签可以被当作普通的自定义FTL指令来访问,那么就不得不在自定义标签中使用FTL的语法规则,而不是JSP的语法规则。因此,根据FTL的语法规则,你必须不能对布尔值和数字值参数加引号,否则它们将会被解释成字符串值,这也就会在FreeMarker试图将这些参数值传递给不使用字符串的自定义标记时引发类型不匹配的错误。
比如,在Struts框架的Tiles标签库的insert标签中,flush参数是布尔值。在JSP页面中,正确的语法形式为:
但是在FT中,你就应该这么来写:
而且,出于相同的原因,这么写是不对的:
<tiles:insert page="/layout.jsp" flush="true"/>
...
<@tiles.insert page="/layout.ftl" flush=true/>
...
<tiles:insert page="/layout.jsp" flush="${needFlushing}"/>
...
而应该这么来写:
(而不是flush=${needFlushing}!)
12. 如何像jsp:include一样的方式引入其它的资源?
而不是<#include ...>,仅仅是包含另外一个FreeMarker模板而不涉及Servlet容器。
因为你看到的包含方法是和Servlet相关的,而纯FreeMarker是不知道Servlet设置HTTP存在的,那是Web应用框架来决定你是否可以这样做和如何来做。比如,在Struts2中,你可以这么来做:
如果Web应用框架对FreeMarker的支持是基于freemarker.ext.servlet.FreemarkerServlet的,那么你可以这样来做(从FreeMarker 2.3.15版本之后):
但是如果Web应用框架提供它自己的解决方案,那么你就可以参考,毕竟它可能会做一些特殊的处理。
对于include_page更多的信息,可以阅读程序开发指南的4.6.2小节包含其它Web应用程序资源中的内容的内容。
13. 如何给普通Java方法/ TemplateMethodModelEx/ TemplateTransformModel/ TemplateDirectiveModel的实现传递普通java.lang.* / java.util.*对象的参数?
不幸地是,对于这个问题没有简单的通用解决方案。问题在于FreeMarker的对象包装是很灵活的,当从模板中访问变量时是很棒的,但是会使得在Java端解包时变成一个棘手的问题。比如,很可能将一个非java.util.Map对象包装称为TemplateHashModel(FTL哈希表变量)。但是它就不能被解包成java.util.Map,因为没有包装过的java.util.Map。
所以该怎么做呢?基本上有下面两种情况:
 对演示目的(比如一种“工具”用来帮助FreeMarker模板)指令和方法应该声明它们的形式参数为TemplateModel类型和它的更确切的子接口类型。毕竟,对象包装是对于表面转换数据模型,并服务于展示层的,而这些方法是展示层的一部分。
 和展示任务(比如,对于逻辑层)不相关的方法应该被实现成普通的Java方法,而且不能使用任何FreeMarker特定的类,因为根据MVC范例,它们必须独立于展
<tiles:insert page="/layout.jsp" flush=needFlushing/>
...
<@s.include value="/WEB-INF/just-an-example.jspf" />
<@include_page path="/WEB-INF/just-an-example.jspf" />
示技术(FreeMarker)。如果这样的方法是从模板中调用的,那么对象包装器的责任就是要保证参数转换到合适的类型。如果你使用了DefaultObjectWrapper或BeansWrapper(参看程序开发指南/其它/Bean的包装部分),那么这就会自动发生(但是要保证你使用的FreeMarker版本至少是2.3.3)。此外,如果你使用了BeansWrapper,那么这个方法将会得到之前包装过的相同实例(因为它是被BeansWrapper包装的)。
14. 为什么在myMap[myKey]表达式中不能使用非字符串的键?那现在应该怎么做?
FreeMarker模板语言(FTL)的“哈希表”类型和Java的Map是不同的。FTL的哈希表也是一个关联数组,但是它仅仅使用字符串的键。这是因为它是为子变量而引入的(比如user.password中的password,它和user["password"]是相同的),而变量名是字符串。
所以FTL的哈希表不是通常目的上的,可以被用来使用任意类型的键查找值的关联数组。而且,它有方法。方法是数据模型的一部分,它们也可以执行所有和计算有关的各种数据模型,当然你可以为Map查找在数据模型中添加一些方法。不好的消息是数据模型的构建,作为一个应用程序的具体问题,是使用FreeMarker程序员的任务,所以他们的任务是保证这样的方法可以服务于模板设计者并呈现出来。(然而,当模板开发人员需要调用不是用来呈现内容的方法时,那么就要考虑数据模型是否足够的简单。也许你应该在数据模型构建阶段做一些后台运算。在理想的情况下,数据模型包含应该被展示的内容,而不是那些为后期运算而服务的基本内容。)
如果你阅读过程序开发指南,那么在技术上你就明白了,数据模型是freemarker.template.TemplateModel对象的树。通常(非必须)由自动包装(包含)普通Java对象到TemplateModel对象方式的数据模型的构建。进行对象包装的对象是对象包装器,当你配置FreeMarker时它就被指定了。FreeMarker本身有一部分即装即用的对象包装器实现,也许它们当中使用最多的一个就是freemarker.ext.beans.BeansWrapper(可以阅读程序开发指南/其它/Bean的包装部分获取更多内容,译者注)。如果你使用了它的实例作为对象包装器,那么你向数据模型中放置的java.util.Map将会作为一个方法,那么你可以在模板中编写myMap(myKey),在内部就会调用Map.get(myKey)方法。对myKey的类型没有严格的限制,正如Map.get(Object key)也没有这样的限制。如果myKey的值由BeansWrapper来包装或其他支持解包的对象包装器,或在模板中是以文字形式给出的,那么值在真正调用Map.get(Object key)方法之前将会被自动解包成普通Java对象,所以它不会和TemplateModel一起调用。
但是那仍然会有问题。Java的Map对键的确切的类非常专注,所以对于在模板中计算的数字类型的键,你不得不将它们转换成合适的Java类型,否则其中的项就不能被发现。比如,如果你在Map中使用Integer类型的键,那么你就必须书写${myMap.get(123?int)}。这是由FTL的有意简化的仅有单独数字类型的类型系统导致的非常丑陋的写法,而Java区分很多数字类型。(理想情况下,在上述情况中,程序员保证get方法自动转换键到Integer类型,所以这不是模板开发者的问题。这可以由定制包装器来完成,但是包装器必须知道特定的使用Integer类型键的Map对象,假设是应用程序的特定知识。)注意,当键值直接从数据模型(也就是说,你不用在模板中使用算
数运算来改变它的值)中获取时是不需要转换的,包含当它是方法返回值的情况,而且在包装之前要是合适的类,因为这样解包的结果将会是原始的类型。
15. 当使用?keys/?values遍历Map(哈希表)的内容时,得到了混合真正map条目的java.util.Map的方法。当然,只是想获取map的条目。
当然是使用了BeansWrapper或者你自己的对象包装器,或者是自定义的BeansWrapper的子类,而它的simpleMapWrapper属性将会置成false。不幸的是,这是默认(出于向下兼容的考虑)的情况,所以在你创建对象包装器的地方,你不得不明确地设置它为true。
16. 在FreeMarker的模板中如何改变序列(lists)和哈希表(maps)?
首先,你也许不想修改序列/哈希表,仅仅是连接(增加)它们中的两个或多个,这就会生成一个新的序列/哈希表,而不是修改了已经存在的那个。这种情况下使用序列连接和哈希表连接符(参考模板开发指南/模板/表达式部分获取详细内容,译者注)。而且,你也可以使用子序列操作符来代替移除序列项。然而,要注意性能的影响:这些操作很快,但是这些哈希表/序列都是后续操作的结果(也就是说,当你使用操作的结果作为另外一个操作的输入等情况时),而这些结果的读取是比较慢的。
现在,如果你仍然想修改序列/哈希表,那么继续阅读…
FreeMarker模板语言并不支持序列/哈希表的修改。它是用来展示已经计算好的东西的,而不是用来计算数据的。要保持模板简洁。但是不要放弃,下面你会看到一些建议和技巧。
如果你能在数据模型构建器的程序和模板之间分离这些工作是最好的,那么模板就不需要来改变序列/哈希表。也许你想重新构思一下你的数据模型了,你会明白这是可能的。但是,很少有对一些复杂但都是和纯展示相关的算法进行需要修改序列/哈希表的这种情况。它很少发生,所以要三思那些计算(或它们其中的部分)是属于数据模型领域的而不是展示领域的。我们假设它们确实是输入展示领域的。比如,你想以一些非常精妙的方式展示一个关键词的索引,这些算法需要你来创建,还有编写一些序列变量。那么你应该做这样的一些事情(糟糕的情况包含糟糕的方案):
<#assign caculatedResults =
'com.example.foo.SmartKeywordIndexHelper'?new().calculate(keywords)>
<#-- 一些简单的算法放到这里,比如: -->
<ul>
<#list caculatedResults as kw>
<li><a href="${kw.link}">${kw.word}</a>
</#list>
</ul>
也就是说,你从模板中去除了展示任务中的复杂部分,而把它们放到了Java代码中。要注意它不会影响数据模型,所以展示层仍然会和其它的应用逻辑相分离。当然这种处理问题的缺陷就是模板设计者会需要Java程序员的帮助,但是对于复杂的算法这可能也是需要的。
现在,如果你仍然坚持说你需要直接使用FreeMarker模板来改变序列/哈希表,这里有两种解决方案,但是请阅读它们之后的警告:
 你可以通过编写TemplateMethodModelEx和TemplateDirectiveModel的实现类来修改特定类型的序列/哈希表。仅仅只是特定的类型,因为TemplateSequenceModel和TemplateHashModel没有用来修改的方法,所以你需要序列或哈希表来实现一些额外的方法。这个解决方案的一个示例可以在FMPP(FMPP是FreeMarker-based text file PreProcessor,即基于FreeMarker的文本文件与处理器,用于生成文本文件。可以参考FMPP项目的主页获取更多信息http://fmpp.sourceforge.net ,译者注)中看到。它允许你这样来进行操作(pp存储由FMPP为模板提供的服务):
pp.add指令仅仅作用于由pp.newWritableSequence()方法创建的序列。因此,模板设计者不能修改一个来自于数据模型的序列。
 如果你使用了定制的包装器(比如你可以使用<@myList.append foo />),那么序列可以有一些方法/指令。(而且,如果你使用来配置它,那么它就会暴露出公有的方法,你可以对变量来使用作用于java.util.Map和java.util.List对象的Java API。就像Apache的Velocity一样。)
但是要小心,这些解决方案有一个问题:序列连接,序列切分操作符(比如seq[5..10])(请参考模板开发指南/模板/表达式/序列操作部分获取更多内容,译者注)和内建函数?reverse不会复制原来的序列,仅仅只是包装了一下(为了效率),所以,如果源序列后期(一种不正常的混叠效应)改变了,那么结果序列将也会改变。相同的问题也存在于哈希表连接(请参考模板开发指南/模板/表达式/哈希表操作部分获取更多内容,译者注)的结果;它只是包装了两个哈希表,所以,如果你之前修改了要添加的哈希表,那么结果哈希表将会神奇地改变。作为一种变通方式,在你执行了上述有问题的操作之后,要保证你没有修改作为输入的对象,或者没有使用由上述两点(比如,在FMPP中,你可以这样来做:<#assign b = pp.newWritableSequence(a[5..10])>和<#assign c = pp.newWritableHash(hashA + hashB)>)描述的解决方案提供的方法创建结果的拷贝。当然这很容易丢失,所以再次重申,宁可创建数据模型而不用去修改集合,也不要使用上面展示的显示层的任务助手类。
17. 关于null在FreeMarker模板语言是什么样的?
FreeMarker模板语言并不知道Java语言中的null。它也没有关键字null,而且它也不能测试变量是否是null。当在技术上面对null时,那么它会将其视作是不存在的变量。比如,如果x在数据模型中是null,而且不会被呈现出来,那么${x!'missing'}将会打印出“missing”,你无法辨别其中的不同。而且,比如你想测试是否Java代码中的一个方法返回了null,仅仅像<#if foo.bar()??>这样来写即可。
你也许对这后面实现的理由感兴趣,出于展示层的观点,null和不存在的东西通常是
<#assign a = pp.newWritableSequence()>
<@pp.add seq=a value="red" />
一样的。这二者之间的不同仅仅是技术上的细节,是实现细节的结果而不是应用逻辑。你也不能将null和其它东西来比较(不像Java语言);在模板中,null和其它东西来比较是没有意义的,因为模板语言不进行标识比较(当你想比较两个对象时,像Java中的==操作符),但更常见的是内容的比较(像Java语言中的Object.equals(Object);它也不会对null起作用)。而FreeMarker如何来别变一些具体的东西和不存在的或未知的东西来比较?或两个不存在(未知的)东西是相等的?当然这些问题无法来回答。
这个不了解null的方法至少有一个问题。当你从模板中调用Java代码的方法时,你也许想传递null值作为参数(因为方法是设计用于Java语言的,那里是有null这个概念的)。这种情况下你可以利用FreeMarker的一个bug(在我们提供一个传递null值给方法正确的方案前,这个bug我们是不会修复的):如果你指定一个不存在的值作为参数,那么它不会引发错误,但是null就会被传递给这个方法。就像foo.bar(nullArg)将会使用null作为参数调用bar方法,假设没有名为“nullArg”的参数存在。
18. 我该怎么在表达式(作为另外一个指令参数)中使用指令(宏)的输出?
使用assign或local指令捕捉输出到变量中。比如:
19. 在输出中为什么用“?”来代替字符X?
这是因为你想打印的字符不能用输出流的字符集(编码)(字符集可以参考词汇表/Charset部分来获取更多内容,译者注)来表现,所以Java平台(而不是FreeMarker)用问号来代替了会有问题的字符。通常情况,对于输出和模板(使用模板对象的getEncoding()方法)应该使用相同的字符集,或者是更安全的,你通常对输出应该使用UTF-8字符集。对输出流使用的字符集不是由FreeMarker决定的,而是你决定的,当你创建Writer对象时,传递了模板的process方法。
示例:这里在Servlet中使用了UTF-8字符集:
注意问号(或其他替代符号)可能在FreeMarker环境之外产生,这种情况上面的做法都没有用了。比如一种糟糕的/错误配置的数据库连接或者JDBC驱动可能带来已经替代过的字符文本。HTML形式是另外一种编码问题的潜在来源。在很多地方打印字符串中字符的数字编码是个很好的想法,首先来看看问题是在哪里发生的。
你可以阅读有关字符集和FreeMarker的更多信息:在程序开发指南/其它/字符集问题部
<#assign capturedOutput><@outputSomething /></#assign>
<@otherDirective someParam=capturedOutput />
...
resp.setContentType("text/html; charset=utf-8");
Writer out = resp.getWriter();
...
t.process(root, out);
...
分。
20. 在模板执行完成后,怎么在模板中获取计算过的值?
首先,要确定你的应用程序设计得很好:模板应该展示数据,而不是来计算数据。如果你仍确定你想这么做,请继续阅读…
当你使用<#assign x = "foo">时,那么你不会真正修改数据模型(因为它是只读的,参考:程序开发指南/其它/多线程部分内容),但是在处理(参考程序开发指南/其它/变量部分)的运行时环境(参考词汇表/Environment部分)创建x变量。这个问题就是当Template.process返回时,运行时环境将会被丢弃,因为它是为单独Template.process调用创建的:
要阻止这个,你可以做下面的事情,是和上面相同的,除了你有机会返回在模板中创建的变量。
21. 我能允许用户上传模板吗?又如何保证安全呢?
通常来说,你不能允许用户上传模板,除非它们是系统管理员或其他受信任的人。因为模板是源代码的一部分,就像*.java文件一样。如果你仍然想让用户上传模板,这里有些问题你是需要考虑的:
 Denial-of-Service(DoS,拒绝服务,译者注)攻击:这是轻而易举地就可以创建模板,几乎要永远运行下去(使用循环),或者耗尽内存(通过在循环中来连接字符串)。FreeMarker不能强制CPU或内存的使用限制,所以这并不是在FreeMarker中可以解决的问题。
 数据模型和包装(Configuration.setObjectWrapper):默认情况下数据模型给予访问全部放置于其中的公有的Java对象的API的能力(有一些是例外的)。要避免这个问题,你不得不构建只对模板来说是必须使用的API的数据模型。出于这样的考虑,可以使用SimpleObjectWrapper来纯粹从Map(映射集合),List(列表集合),Array(数组),String(字符串),Number(数字),Boolean(布尔值)和Date(日期)来创建数据模型。或者,你可以实现你自己的非常限制的ObjectWrapper,这样就可以安全访问你的POJO了。
 模板加载器(Configuration.setTemplateLoader):模板可以使用名称(或路径)来加载其它模板,比如<#include "../secret.txt">。为了避免加载敏感的数据,你不得不使用TemplateLoader来二次检查要加载的文件是可以访问的。FreeMarker会来阻止从模板根路径之外来加载文件的,不管模板加载器,但是根据底层的存储机制,可能存在FreeMarker考虑不到的漏洞(比
// 在内部会创建运行时环境,之后会被丢弃
myTemplate.process(root, out);
Environment env = myTemplate.createProcessingEnvironment(root, out);
env.process(); // 处理模板
TemplateModel x = env.getVariable("x"); // 获取参数x
如,仅仅作为一个例子,~会跳到用户目录)。要注意freemarker.cache.FileTemplateLoader检查符合规范的路径,所以,这也许是这个目的的不错选择,但增加文件扩展名的检查(文件必须是*.ftl)也是一个不错的想法。
 内建函数new(Configuration.setNewBuiltinClassResolver,Environment.setNewBuiltinClassResolver):在模板中如"com.example.SomeClass"?new()样来使用,这对于部分在Java中实现的FTL类库来说很重要,但是在普通模板中是不需要的。而new并不会实例化不是TemplateModel类型的类,FreeMarker中包含的TemplateModel类,就是用来创建任意的Java对象。其它“危险的”TemplateModel可以存在于你的类路径中。即便它们没有实现TemplateModel接口,它的静态初始化块也会执行。为了避免这些问题,你应该使用TemplateClassResolver来限制可以访问的类(可能是基于它们要求的模板)。
22. 如何在Java语言中实现方法或宏而不是在模板语言中?
这是不可能的,如果你想编写一个类分别实现freemarker.template.TemplateMethodModelEx或freemarker.template.TemplateDirectiveModel,但还是有相似的东西可以使用,那么在你编写<#function my ...>...</#function>或<#macro my ...>...</#macro>的地方,可以使用<#assign my = "your.package.YourClass "?new()>来代替。注意对这种方法使用assign指令,因为在FreeMarker中函数(和方法)和宏仅仅是普通变量。(出于相同的原因,你可以在调用模板之前,在数据模型中放置TemplateMethodModelEx或TemplateDirectiveModel实例,或者当你实例化应用程序时,将它们放置到共享变量的map中去。(可以参考如下方法实现:freemarker.template.Configuration.setSharedVariable(String, TemplateModel)))
23. 为什么FreeMarker的日志压制了我的应用程序?
这是因为FreeMakrer没有发现任何日志系统。要修正这个问题,你必须为你的应用程序使用下列日志系统的其中之一:SLF4J(推荐使用的),Apache Commons Logging,Log4J(org.apache.log4j),Avalon(org.apache.log)或使用J2SE 1.4或更高版本(它包含java.util.logging)。也就是说,你的应用程序使用的类加载器必须发现这些提到的类之一。到FreeMarker 2.4版时,如果你想使用SLF4J或Commons Logging,那么你需要参考一些额外的步骤(参考程序开发指南/其它/日志部分)。
24. 在基于Servlet的应用程序中,如何在模板执行期间发生错误时,展示一个友好的错误提示页面,而不是堆栈轨迹?
首先,使用来RETHROW_HANDLER代替默认的DEBUG_HANDLER(要获取更多关于模板异常处理的信息,请阅读程序开发指南/配置/错误控制部分,译者注)。现在,当错误发生时,FreeMarker也不会打印任何东西到输出中了,所以控制权是在你手中的。在你捕捉到Template.process(...)抛出的异常之后,基本上你可以执行下面两种策略:
 调用httpResp.isCommitted(),如果它返回了false,那么你可以调用httpResp.reset(),之后给访问者打印一个“友好的错误提示页面”。如果返回值是true,那么尝试去完成页面,打印一些清晰的东西给访问者,因为Web服务器的错误,页面的生成被突然中断。那么你不得不打印很多冗余的HTML结束标签,设置颜色和字体大小来保证错误信息在客户端浏览器中是可读的(检查在src\freemarker\template\TemplateException.java中的HTML_DEBUG_HANDLER源代码,去看一个示例)。
 使用全页面的缓冲。这就意味着Writer对象不会逐步将输出送到客户端,而是在内存中缓冲整个页面。因为你给Template.process(...)方法提供了Writer实例,这是你的责任,FreeMarker不会做什么事情的。比如,你想使用StringWriter,如果Template.process(...)方法以抛出异常而返回,那么就忽略由StringWriter积累的内容,之后发送一个替代的错误页面,否则你就要在输出中打印StringWriter对象中的内容。使用这种方法你就可以确定不用去处理部分发送的页面,但是由于页面(比如,在生成很长的页面时会很慢,用户会感到很大的响应延迟,而且服务器也会占用大量的内存)的特征,它也会有负面的性能影响。要注意使用StringWriter对象并不是最有效的解决方案,在积累的内容增长时,它通常也会重新分配自己的缓冲。
25. 我正使用一个可视化的HTML割裂模板标记的编辑器。你们可以改变模板语言的语法来兼容我的编辑器么?
我们不会修改标准的版本,因为有很多模板都依赖于它。
我们的观点是这个打断模板代码的编辑器是它自己打断的。一个优秀的编辑器应该可以忽略,而不是割裂它不能理解的东西。
从FreeMarker 2.3.4版本开始,你应该感兴趣你可以使用[和]来代替<和>了。
要获取更多信息,可以阅读模板开发指南/其它/替换(方括号)语法部分。
26. FreeMarker有多快?真的是2.X版本的要比1.X版本(经典的FreeMarker)的慢吗?
首先,不要忘了在MVC(可以阅读词汇表/MVC模式部分获取更多内容,译者注)系统中FreeMarker仅仅是一个和视图相关的组件。此外MVC模板趋于简化:许多含有很少插值
和循环块,条件块的静态文本。所以它不像PHP或Model 1模型下的JSP;你的应用程序性能是不受模板执行时间过多影响的。
FreeMarker已经相当快了,在你的应用程序中,它不会称为瓶颈的。想法,其他诸如数据库操作的速度或网络带宽等因素可能会占据制约应用速度的主导地位。FreeMarker性能的影响仅仅对于真正繁忙的站点(也就说,每台服务器承受每秒30次以上的访问)来说才是要注意的,这种情况下,几乎所有的数据库数据要被缓存。如果你发现FreeMarker很慢,要保证被解析的模板缓存运转地很好(Configuration.getTemplate是默认使用缓存的)。解析一个模板文件相对来说是开销很大的步骤,在很多长时间运行的服务器端应用程序中,你会只想解析一次模板而多次使用。(注意FreeMarker 2.1.4和之前的版本有会销毁缓存的有bug。)
FreeMarker 2.1版本是要比1.7.1版本慢。这依赖于你的模板,但是它可能慢了2或3倍。但再次重申,它不意味着响应时间会慢2或3倍;很多FreeMarker用户是不会感知出最终响应时间的改变的。
27. 我的Java类怎么才能获取到关于模板结构的信息(比如所有变量的列表)?
在FreeMarker 2.2版本中,Template有一个未公开的方法可以来检验解析过的模板:getRootTreeNode。
但是列出所有可访问的方法是不现实的,因为变量名可以动态地从数据中生成。然而,为什么FreeMarker不支持这个却有一个更重要的原因。FreeMarker的设计是基于分离业务逻辑和显示对象的想法。这种分离有两种形式:
1. 模板知道想要的数据是什么,但是它们不知道数据是如何生成的。
2. 业务对象知道数据生成什么,但是它们不知道数据是如何显示的。因此,它们不知道模板的任何事情。
因为业务对象不依赖于模板,如果你需要和其它的展示系统来使用它们,你不需要重写你的应用程序。
28. 你会一直提供向后的兼容性吗?
FreeMarker 2.0版本对于FreeMarker 1.X来说是由一个新的作者完全重写的。1.X系列仍然作为一个分离的项目:经典的FreeMarker。因为2.X版本依照的是和1.X版本不同的原理,2.0X发行的版本不是由于高一点的版本号而成熟的。这造成了进一步在2.01和2.1版本之间的彻底改变。对于2.1版本,一些东西更加成熟,而2.2版本也几乎完全向下兼容2.1版本。我们希望这种趋势一直保持下去。
当前,不同的第二版本号发行规则(比如2.1.x和2.2.x)是不(完全)兼容的。发行的版本仅在第三版本号不同时(比如2.2.1和2.2.6)是兼容的。
我们通常对发行的版本提供向后兼容的bug修复发行包。所以基本上,在已经写好的版本中,你不需要换到新的版本,不会有不向后兼容的发行包。具体问题要具体分析,FreeMarker和其它一些项目相比,修复设计错误是很快的,但是这中间的代价主要是不向后兼容。这不是最佳的做法,也许有很少的设计错误也是不错的。但是,这就是现实。
29. 如果我们把FreeMarker和我们的产品一起发行,我们需要发布我们产品的源代码么?
不需要。对于2.0版本的FreeMarker,本事是基于BSD规则许可的。这就意味着源代码或二进制发布包是免费制作的,它可以被包含在其它产品中,不管是商业的还是开源的项目。
FreeMarker本身仅有的应用于版权的限制,还有FreeMarker的使用或作为你的项目的贡献或支持。请参考附录E许可部分来获取更多信息。
如果你使用FreeMarker,我们希望你会给我们一个关于你的产品的连接,但这也不是必须的。
附录B 安装FreeMarker
不需要真正的安装过程。仅仅是拷贝lib/freemarker.jar到你Java应用程序的路径中,让类加载器可以发现它。比如,如果你在Web使用了FreeMarker,那么你就要将freemarker.jar放在你Web应用程序的WEB-INF/lib目录中。(如果你想和JSP的Model 2模型(这也意味着你在模板中还可以使用JSP客户化标签库)一起使用FreeMarker,那就需要一些额外的步骤。要了解更多内容,可以参考程序开发指南/其它/在Servlet中使用FreeMarker部分)
但是,如果你想开启一些FreeMarker可选的特性,对于类加载器来说,可能还需要一些第三方类库:
 对于正则表达式的内建函数至少需要J2SE 1.4版本。
 对于XML包装需要至少J2SE 1.4版本或JAXP+DOM实现+SAX实现。
 对于XML的XPath支持,需要Jaxen(推荐,在http://jaxen.org/ 下载)或者Apache的Xalan。请使用至少Jaxen 1.1-beta-8版本,而不要老的版本!Apache Xalan库包含在Sun J2SE 1.4,1.5和1.6中(也许在后续版本中还会有),所以在这些版本中,不需要分开的Xalan的jar包。
 很显然,对于FreemarkerServlet来说,javax.servlet类库是必须的。Servlet的版本至少在2.2以上。
 对于JSP客户化标签库的支持,你需要使用JSP 1.2的API。不需要JSP的实现,仅仅是API。要了解更多内容,请参考程序开发指南/其它/在Servlet中使用FreeMarker部分。
 很显然,对于Jython包装器来说,Jython类库是必须的。
 对于废弃的freemarker.ext.jdom包来说,JDOM是必须的。
附录C 构建FreeMarker
如果你想修改源代码来重构freemarker.jar,你需要Ant(就是Apache的Ant项目,在http://ant.apache.org/ 可下载和了解详细内容,译者注)1.6.1(或更新版本)和JDK 5(或更新版本)。如果这些条件满足了,运行发行包根目录下的Ant构建脚本,就会创建新的freemarker.jar。要注意对于第一次构建你的计算机必须联网,因为创建任务需要下载必备的依赖(大约20MB)到发行包根目录的lib子目录中去。
也许你应该在我们的测试套件中测试新的jar文件。这可以由运行Ant的test目标(到发行包的根目录下,然后执行“ant test”)来完成。如果测试失败了,阅读build/testcase目录中的.txt文件来获取更多内容。
要注意这样是构建了一个全部的发行包,也包含了FreeMarker手册和离线的Web站点,而要从发行包中只获取源代码部分是不可能的。你将不得不从FreeMarker的SVN资源库中去检查“文档的生成”和“站点”子项目。
附录D 版本
2.3.19 版
发布日期:2012-02-29
不要忘了安全相关的修复,它可能会影响到你的应用程序!
FTL部分的改变
 注意:在2.3.17版引入的ISO 8601 日期/时间格式内建函数的输出,有轻微的调整。从现在开始,时区偏移,用于显示而且不是Z时,通常包含了分钟。比如在模板中,15:30:15+02现在就成为15:30:15+02:00了。而且格式会根据ISO 8601(所以期望ISO 8601 日期/时间格式的内容应该不会有问题)来进行验证,但是最后的格式需要使用了XML Schema 日期/时间格式来编译,因此做了这个修改。
 新的内建函数,用来转义JSON字符串。
 Bug修复:如果在同一个模板之前没有正确的#标记,错误的#标记被打印成静态的文本,而不会引发解析错误。因为这个修复并不会100%的向后兼容,老版本中的行为会被保留下来,除非你设置incompatible_enhancements(也就是Configuration.setIncompatibleEnhancements(String))成"2.3.19"或更高版本。
Java部分的改变
 注意:本次发布包含两个重要的安全修复,很明显,这些安全问题会导致一些应用程序被利用。FreeMarker不能在所有的配置中解决这个问题,所以请阅读下面的细节而不仅仅是升级FreeMarker!理论上,这些修改并不是100%向后兼容的,但是破坏任何东西也是不可能的。这两个修改是:
 含有字符0(\u0000)的字符已经不允许出新在模板路径中了。如果路径包含了它,FreeMarker会找不到模板。
这是修复模板路径如"secret.txt\u0000.ftl"的安全问题的。它会在应用程序中绕过过滤器。FreeMarker本身并不关系过滤器的存在,但是一些应用程序会基于过滤器来过滤FreeMarker的路径。当它们作用于这样的路径时,在存储机制之后的C/C++实现会将路径视作"secret.txt",因为在C/C++中0是字符串的终止符,那么就会加载一个非FTL文件作为模板了,将文件的内容反馈给攻击者。
注意一些HTTP服务器,尤其是Tomcat和Apache HTTP Server会阻塞在查询字符串(query string,译者注)外包含0(%00)的URL,这并不会被这样的Web URL所利用。一些其它HTTP服务器,就不会阻塞这样的URL,比如Jetty。
 ClassTemplateLoader,当基于路径"/"(就像new ClassTemplateLoader(someClass, "/"))创建时,就不允许模板路径在任何/之前包含冒号,在这种情况下,也就好像找不到模板。
这个修复是修改模板路径如"file:/etc/secret"或"http://example.com/malware.ftl"被类加载器结构中的java.net.URLClassLoader解释成完整的URL,因此就允许从这些URL中来加载文件作为模板。这是java.net.URLClassLoader的一个怪癖(或者说是bug),那么这个问题就会存在于使用了这样的类加载器的系统中。
要小心,一些框架使用它们自己的TemplateLoader实现。如果那些实现很脆弱的话,那么上述的安全问题在升级FreeMarker后也还是会存在的!注意这些可利用点仅仅作用于包含URLClassLoader的类加载器结构和类加载器用于加载模板而不在模板路径(而不是"/")前添加任何前缀的情况中。
这些安全问题大多数情况是会在用户(访问者)给应用程序提供任意模板路径的情况下来影响应用程序的。这不是合理构建MVC应用程序的情况,因为只有MVC的控制器可以直接处理,并且控制器指定模板路径。但是基于JSP模型2(参考第二部分4.6.1节内容)的遗留MVC应用程序通常会以公共的以.ftl结尾的URL暴露出MVC视图层组件,那么就会允许用户给出任意的FreeMarker模板路径。这样的应用程序应该在web.xml中添加security-constratint来加强安全,这部分请参考第二部分的4.6.1节。无论当前的安全修复,在程序中,这一点都是必须要做的。
通常来说,你不应允许用户去指定任意的模板路径,如果你允许的话,那么就要特别小心TemplateLoader的使用了。
 Configuration有了新的方法:removeTemplateFromCache(...)。这个方法会从缓存中移除给定的模板。所以它会重新加载而不管下次请求时模板更新的延迟。
 从现在开始,当检查类的时候BeansWrapper会忽略setter方法。它们不再被使用了,所以它们不会引发“java.beans.IntrospectionException异常:也就是在读写方法之间类型不匹配”的错误。
 TemplateClassResolver.SAFER_RESOLVER现在不允许创建freemaker.template.utility.JythonRuntime和freemarker.template.utility.Execute了。这个修改会影响到内建函数new的行为,如果FreeMarker配置使用SAFER_RESOLVER,在2.4版之前不是默认的。
 Bug修复:现在可以调用含有可变参数的方法了。(之前仅对重载的方法有效)
 Bug修复:[1837697],[2831150],[3039096],[3165425]。对Jython的支持,现在仅仅对Jython 2.2和2.5版本有效。
 Bug修复:[3325103],TemplateException相关异常和ParseException相关异常现在是可以序列化的了。
2.3.18版
发布日期:2011-05-21
Java部分的改变
 Bug修复:[3304568],2.3.17版中,WEB-INF\lib\*.jar文件里没有TLD文件,除非它们在web.xml中使用taglib元素被明确指出来。这是2.3.17版中引入的一个bug。
其它改变
 在freemarker.jar的META-INF文件夹下添加了LICENSE.txt和NOTICE.txt。
2.3.17版
发布日期:2011-05-17
因为一些安全上的修复,你可能会想迫切升级到这个版本。
FTL部分的改变
 内建函数seq_index_of和seq_last_index_of现在可以用于集合对象(freemarker.template.TemplateCollectionModel-s),而不仅仅是序列(freemarker.template.TemplateSequenceModel-s)了。
 内建函数long现在可以作用于日期,时间日期或时间类型的值了,并且返回一个毫秒数(即java.util.Date.getTime()的返回值)。
 要转换数字(通常是Java long类型)到日期或时间日期或时间值,添加内建函数?number_to_date,?number_to_time,?number_to_datetime(可以参考内建函数部分的数字转日期部分获取更多内容)。(不幸的是,内建函数?date没有扩展来支持这个特性,是因为考虑到向后兼容的问题。)
 新的内建函数来使用ISO 8601的扩展格式来格式化数字,无论当前的日期/时间格式设置,也不管当前时区的设置。比如${myTimeStamp?iso_utc}会打印出如2010-05-16T23:05:45Z的东西(可以参考iso内建函数族部分获取更多内容)。
 新的特殊变量,now。它会返回当前的时间日期。使用示例:" Page generated: ${.now}","Today is ${.now?date}","The current time is ${.now?time}"。
 内建函数sort和sort_by现在可以支持布尔值排序了。
 当使用不支持或位置的字符串内建函数标记时,FreeMarker现在会记录警告(warn,译者注)等级(每个类加载器最大25次,是为了阻止日志更新太快)的日志。从FreeMarker 2.4版开始这里才会错误(error,译者注)级别的日志。
 Bug修复:[3047201],使用正则表达式(比如内建函数?match)会引发在多线程环境中的查找,而且当动态生成正则表达式时会引发内存泄漏。
 Bug修复:内建函数seq_contains,seq_index_of和
seq_last_index_of对使用纯BeansWrapper(而不是DefaultObjectWrapper)包装作为TemplateSequenceModel的非java.util.List和java.util.Collection对象会失效。(也可以参考下面的getSupportsIndexedAccess())
Java部分的改变
 安全修复:使用精心设计的包含代码点0('\u0000')模板名称(模板路径),如果它们是FreeMarker模板的话,那就可能从模板根目录外部来加载文件了。这个问题的根源就是底层的C/C++部分(属于Java平台或操作系统)将0解释成字符串了,而Java(因此还有FreeMarker和Servlet容器)却不是这么做的。那么在底层上,看起来对FreeMarker安全的路径就变得不安全了。目前通过名称(Configuration.getTemplate(...),<#include ...>,<#import ...>)加载模板的所有方式都有问题。
如果你不允许用户上传模板那么就不会有影响,或至少包含以下一个方面:
 在你的系统中,用户不能提供任意的字符串作为模板名称(模板路径)。比如,如果用户只允许访问属于MVC控制器的URL(就像他们不能访问*.ftl),那么他们就不会写出任意的模板名称。
 模板名称是Web页面URL的一部分,而Web服务器或Servlet容器不允许URL中包含%00,或者在传给Servlet之前就终止了URL的执行。
 你正在使用FileTemplateLoader,而链接不允许放在里面(默认也是不允许的)。
 FreeMarker现在直接使用SLF4J或Apache Commons Logging来记录本身的信息。然而,它不会自动使用这些日志库,直到2.4版本发布;可以阅读程序开发指南/其它/日志部分来获取详细信息。但是我们现在建议使用SLF4J。
 新的设置信息:”auto_flush”,Configurable.setAutoFlush(boolean)。设置输出的Writer是否自动在Template.process(Object,Writer)(和它的重载方法)的结束时刷新。默认是true,这和之前的做法是对应的。需要时使用false,比如当Web页面由几个盒子(比如Portlet,UI面板等)组成,它们不会用#include(或其它类似的指令)来插入到一个主FreeMarker模板中,而且它们都由独立的Template.process(…)调用来处理。在这种情况下,自动刷新将会在每个盒子之后提交HTTP响应对象,因此干扰整个页面的缓冲,而且可能会因为太频繁和过早的响应缓冲刷新而降低性能。
 添加了新的设置项: Configuration.setNewBuiltinClassResolver(TemplateClassResolver),或使用new_builtin_class_resolver属性。这允许你指定内建函数new(比如"com.example.SomeClass"?new())如何来处理类,还有哪些类可以访问等。如果你不是允许很受信任的用户上传模板,那么你绝对对这个设置感兴趣;可以参考freemarker.core.Configurable.setSetting和freemareker.template.Configuration.setNewBuiltinClassResolver的Java API文档。另外,我们还是建议你设置它为TemplateClassResolver.SAFER_RESOLVER(或safer,如果你使
用属性配置时),因为它并不是100%的向后兼容(参考Java API文档)。
 添加了freemarker.cache.NullCacheStorage:在Configuration中设置它作为关闭缓存存储。
 在freemarker.ext.beans.CollectionModel中添加了getSupportsIndexedAccess(),所以可以检查TemplateSequenceModel.get(int)是否作用于特定的CollectionModel实例。
 Bug修复:[2992265]:JSP FreeMarkerPageContext.include行为不正确。
 Bug修复:在JSP标签中使用FreeMarker对JSP的支持时使用了javax.servlet.jsp.PageContext.pushBody(比如一些条纹标签),在中会发生"ArrayIndexOutOfBoundsException:-1"异常。
 Bug修复:[3033015],AllHttpScopesHashModel原先使用了WrappingTemplateModel.getDefaultObjectWrapper()方法来包装页面范围内的变量,同时使用了用户指定的ObjectWrapper来处理其它范围(request请求,session会话,等等)。而现在,在页面范围内它也使用用户指定的包装器了。
 Bug修复:[3128073],对于一个哈希表中的某key不存在的情况,当解包key失败时,HashAdapther.containsKey(...)返回true。这个修复的一个副作用就是,BeansWrapper.CAN_NOT_UNWRAP现在是私有的属性了;之前由于失误它是公有的属性。
 Bug修复:[3151085],freemarker.jsp.TaglibFactory不会准确定位到tld文件。这次修复更好的遵循了JSP规范,来解决和定位tld文件。
 Bug修复:使用BeansWrapper来解包null,会有一个自定义的空模型而并没有导致为空。现在解压null和自定义的空模型都会返回空。
 日志信息不再包含换行符(CR或LF),和Java中字符串语法的引用路径和其它任意的文本,也会将字符&lt;转义为\u003C。这些解决了的安全问题和低质量的日志记录器和错误读取器有关。这在模板处理错误项中是要格外注意的,这将引用异常信息。注意堆栈跟踪路径(Throwable对象)是如何被日志记录的仍然是根据你使用的日志框架决定的。
其它改变
 DTD和XSD包含在freemarker.jar的freemarker/ext/jsp下,现在是基于阿帕奇2.0版协议的。这也LICENSE.txt在中说明白了。之前这些文件并没有明确的协议说明。
2.3.16 版
发布日期:2009-12-07
Java部分的改变
 修正了引发序列向Java数组的错误包装的bug(点击http://sourceforge.net/tracker/?func=detail&aid=2105310&group_id=794&atid=100794 查看bug报告)
 修正了单精度和双精度浮点数的四舍五入问题(点击https://sourceforge.net/tracker/?func=detail&aid=2503124&group_id=794&atid=100794 查看bug报告)
 创建了新的freemarker.runtime.attempt分类和在DEBUG安全下的<#attempt>块中的异常捕获日志。
 修正了(很长时间了)RhinoWrapper不能和所有版本Rhino一起工作的问题,因为由Undefined.instance的改变导致的二进制文件不兼容。
 修正了TextUtil.XMLEncNQG不转义]]>到]]&gt的bug。
 修正了根目录不能作为模板基路径的bug,因为FileTemplateLoader认定模板在基路径之外。
 宏的名字已经不能通过API来改变了。
 FreeMarkerServlet现在在Spring Security框架中和Session固定攻击保护协同工作了,参考http://sourceforge.net/projects/freemarker/forums/forum/2345/topic/3475868 来获取更多内容。
 大幅度提高<#return>指令的性能。
FTL部分的改变
 修正了anXMLNode.@@markup和@@nested_markup不转义]]>到]]&gt的bug。
2.3.15 版
发布日期:2008-12-16
FTL部分的改变
 Bug修复:哈希表连接(比如hash1 + hash2)打乱了键/值的顺序,即便两者都是有顺序的。
 在基于FreemarkerServlet的Web页面中, 你可以使用<@include_page path="..."/>来使用Servlet的包含功能,参考程序开发指南/其它/在Servlet中使用FreeMarker/包含其它Web应用程序资源中的内容部分来获取详细内容。
Java部分的改变
 BeansWrapper可以自动检测由JavaRebel重新加载的类。
 修正了从Environment.getCurrentEnvironment()返回值中引发null的bug,同时处理自动包含和自动引入。(参考https://sourceforge.net/forum/message.php?msg_id=5531621 阅读bug报告)
 修复了导致在POJO上的getObject(Object)方法不被视作是普通get方法的bug
 大幅度提升了<#break>指令的性能。
 DeepUnwrap现在可以解包定制的当前对象包装器的null模型到Java的null类型。
2.3.14 版
发布日期:2008-09-01
FTL部分的改变
 新的内建函数:xhtml,可以阅读参考文档/内建函数参考文档/处理字符串的内建函数部分来获取更多内容。
 新的特殊变量:template_name,可以阅读参考文档/特殊变量参考文档部分来获取更多内容。
 现在你可以使用参数值作为其它参数的默认值,比如<#macro section title label=title>。在之前的版本中它并不可靠。没有关于参数顺序的限制,就像<#macro section label=title title>也能正常工作。
 添加了一个新的数字格式说明符(可以阅读参考文档/内建函数参考/处理数字的内建函数部分获取更多内容),computer。这个使用和exp?c一样的格式。
Java部分的改变
 freemarker.ext.servlet.AllHttpScopesHashModel类的构造方向现在是公有的了,允许它被第三方Web框架来重用。
 bug修复:当传递一个单独字符的字符串作为键时,freemarker.ext.beans.SimpleMapModel (不像freemarker.ext.beans.MapModel或freemarker.template.SimpleHash)不允许通过java.lang.Character的键来查找。
 bug修复:在freemarker.template.utility.DeepUnwrap类中的解包更宽泛了,而对于递归序列或哈希表的元素不再宽泛。
 bug修复:对于要绑定到map的明确的null值来说,freemarker.ext.beans.MapModel返回
BeansWrapper.wrap(null)来代替null。
 bug修复:修了有合理getter方法且实现了类型参数化接口的类的一个细小的bug。
 bug修复:[1939742],一个报告的很深的边角问题。
2.3.13 版
发布日期:2008-05-05
FTL部分的改变
 处理四舍五入问题的内建函数:round,floor,ceiling。可以阅读参考文档/内建函数参考/处理数字的内建函数部分获取更多内容
Java部分的改变
 [1898300],[1818742],[1780882]:由Azul系统的工程师的帮助,对于根本地并发性能改进,重新启用了模板缓存机制。(和2.3.12版本相比,在一台128-CPU的Azul设备上,在Struts2应用程序中,有了20倍速的提升。)而且,模板加载(包括解析)的错误现在被缓存了,在应用中提升了常常获取不到模板的性能。
 [1892546],在FreemarkerServlet中允许定制TemplateLoader。
 Bug修复:[1725107]在Servlet 2.4中使用FreeMarker的JSP标签库支持可能生成XML验证的警告。
 Bug修复:[1939742]在循环中,ConcurrentModificationException访问一个不存在的SimpleHash项。
 Bug修复:[1902012] IteratorModel吃掉异常原因。
 Bug修复:<#assign x></#assign>(空的嵌套内容)会导致NullPointerException。
 Bug修复:[1926150] CachedTemplate应该序列化。
2.3.12 版
发布日期:2008-02-03
Java部分的改变
 Bug修复:[1857161]在2.3.11版中失效的JSP的SimpleTag的支持。
 在模板中,使用了Java 5的可变参数功能(可变长度参数列表),现在你可以很方便地调用Java代码的方法。而且,重载的方法选择逻辑现在对可变参数方法也更加智能了。
 枚举常量现在由它们的name()方法来标识了,而不是以前的toString()方
法(因为后者可以在子类中重写)。这不会影响枚举常量的打印方式;当然那会仍然使用toString()方法。
 在解析异常中的信息现在展示了模板的名称。
2.3.11 版
发布日期:2007-12-04
这个发行包包含了一些性能和可用性的提升。
FTL部分的改变
 Bug修复:[1687248]警告!这个修复可能会打破一些模板!修复了内建函数c(?c)有时引起整个数字末尾被格式化成“.0”(比如:1.0)的bug,而且有时会引起数字格式化成指数形式(比如4E-20)。从现在开始,整个数字不会使用小数点(即便是没有包装的数字是double类型的;要记得,模板语言仅仅知道单一的数字类型),而且指数类型永远也不会被使用。而在小数点之后的数字的最大位数为限制在16,所以小于1E-16的数字会被显示为0。
Java部分的改变
 FreeMarker现在在有了更好的JSP 2.0和JSP 2.1的规则支持。最值得注意的是,JSP 2.0的SimpleTag接口现在被支持了。此外,尽管运行在一个没有它自己JSP实现的环境中,FreeMarker的JSP运行时环境,在JSP 2.0的API JAR在类路径下可以被找到时,它将会使得自己的JspFactory和JspEngineInfo实现成为可用的资源,而JspApplicationContext的实现也是如此。
 一个新的模型接口,TemplateDirectiveModel为实现用户自定义指令,而不是之前的TemplateTransformModel提供一个简单的范例。而TemplateTransformModel也将被废弃了。
 FreeMarker现在发现基于Xalan的XPath支持包含在Sun的JRE/JDK 5和6中,所以不需要单独的Xalan的jar包来对XPath进行必须的支持。(然而,我们建议Jaxen而不是Xalan,因为FreeMarker对XPath的支持用Jaxen更加完整。当然对于Jaxen来说,jar包是需要的)
 BeansWrapper的包装性能通过消除各种重复执行的测试类有一个重大的提升。
对BeansWrapper定制的额外注意:BeansWrapper的子类以前覆盖了getInstance(Object, ModelFactory)方法的,现在也应该覆盖getModelFactory(Class)方法来获得提升的优点。覆盖老的方法仍然可以执行,但是不能得到性能提升的优点。
 一个由BeansWrapper创建的包装器的内存占用已经降低了(通过一个默认大小的HashMap的容量),直到方法或索引属性来访问它(简单属性可以在访问时而不增长内存占用)。
 Rhino对象可以在模板中被用作标量,数字和布尔值,对这些类型遵守JavaScript的类型转换。
 .data_model现在是TemplatHashModelEx了。这就是说数据模型变量名的列表通常可以通过.data_model?keys来获得。
 FileTemplateLoader现在可以可选地允许跟随的符号连接来指向基路径。由于向后兼容性,默认情况下它是关闭的。
 Bug修复:[1670887] TaglibFactory标签库的匹配不按照JSP 1.2 FCS。
 Bug修复:[1754320]在setXPathSupportClass中的bug阻止用户提供XPathSupport实现的插件。
 Bug修复:[1803298]使用循环变量解析宏时的解析器错误。
 Bug修复:[1824122]从JAR文件中加载文件可能导致文件句柄泄漏(因为Sun的Java API实现的一个bug)。
 Bug修复:现在,如果重新加载修改过的模板文件失败,那么缓存的模板就从缓存中移除了。
文档的改变
 在模板开发指南中大量重写(之前叫做设计者指南),特别是在入门部分。
 从现在开始,#{...}在文档中被视为是废弃的结构了。
 现在,术语“变换”已经从文档中移除了。取而代之的是现在通常使用的是“用户自定义指令”,这也包括宏,TemplateTransformModel和新的TemplateDirectiveModel,这也仅仅是实现用户自定义指令的不同方式。
 手册中一些小的改进。
2.3.10 版
发布日期:2007-04-20
这个发行版包含很多重要的bug修复。
Java部分的改变
 [1589245]当Configuration.clearTemplateCache()被调用时,MultiTemplateLoader清理了它的内部缓存数据(用于同一个模板优化后查找)。
 [1619257]当strict_bean_model用在FreeMarker配置Properties对象或用在<#setting .../>指令中时会导致异常的bug修复。
 [1685176]在Sun的Java 6的Java虚拟机下使用MRU缓存的垃圾收集的特定作用下,引发StackOverflowError的bug修复。
 [1686955]当ResourceBundleModel创建MessageFormat对象时,它传递它们自己的本地化信息。可以阅读程序开发指南/其它/Bean的包装/模板方法模型部分来获取更多信息。
 [1691432]导致BeansWrapper.EXPOSE_SAFE,而不比BeansWrapper.EXPOSE_ALL安全的bug修复。
FTL部分的改变
 [1628550]现在你可以使用dateExp?string.full来格式化使用了Java的java.util.Date.FULL格式的日期。可以阅读参考文档/内建函数参考/处理日期的内建函数部分来获取更多信息。
2.3.9 版
发布日期:2007-01-23
这个发行版包含访问JDK 1.5枚举类型的支持,还有通过BeansWrapper从模板中访问类的公有字段的支持。
Java部分的改变
 如果你在BeansWrapper上调用setExposeFields(true)方法,现在它可以暴露出对象的公有字段给模板。可以阅读程序开发指南/其它/Bean的包装/模板哈希表模型部分来获取更多信息。
 BeansWrapper现在可以传递任意序列模型给Java代码的方法,它期望得到java.util.Collection类型或者本地的Java数组(包含原生数组)。可以阅读程序开发指南/其它/Bean的包装/模板哈希表模型部分来获取更多信息。
 BeansWrapper现在可以传递任意的序列和集合模型给Java代码的方法,它期望得到一个java.lang.Iterable类型。可以阅读程序开发指南/其它/Bean的包装/模板哈希表模型部分来获取更多信息。
 BeansWrapper现在当传递给Java代码方法时,可以解包数字模型到正确的目标类型,它期望得到原生或包装过的数字。使用各种专业的内建函数(可以阅读参考文档/内建函数参考文档/很少使用的和专家级的内建函数)来手动强制那些类型大多称为不必要的。
 修复了在特定少数情况下,当BeansWrapper传递java.util.Collection给一个方法时,它期待java.util.Set类型的bug。可以阅读程序开发指南/其它/Bean的包装/模板哈希表模型部分来获取更多信息。
 在BeansWrapper和DefaultObjectWrapper中支持了JDK 1.5的枚举类型。通过调用getEnumModels()方法,你可以检索以类名来命名的哈希模型,而且允许访问枚举的值。也就是说,如果你在数据模型中,以enums命名绑定了这个哈希模型,那么你可以在模板中书写如enums["java.math.RoundingMode"].UP这样的表达式。枚举值可以被用作是标量,来支持相等或不等比较。可以阅读程序开发指南/其它/Bean的包装/访问枚举类型部分来获取更多信息。
 freemarker.ext.rhino.RhinoWrapper现在可以给FreeMarker的未定义值正确翻译Rhino的Undefined实例,UniqueTag.NOT_FOUND和UniqueTag.NULL。
2.3.8 版
发布日期:2006-07-09
这个发行包大大提升了对JSP 2.0的兼容性。(对于那些在2.3.7版本中看到相同点的用户:对不起,版本历史有错误,和JSP 2.0相关的改变不在那个发行包中。)
Java部分的改变
 JSP支持改进:[1326058]添加对DynamicAttributes的支持(JSP 2.0中的新特性)。
 JSP支持改进:在FreemarkerPageContext中添加pushBody()/popBody()对的支持。
 JSP支持改进:添加对getVariableResolver()的支持(JSP 2.0中的新特性)。
 JSP支持改进:添加对include(String, boolean)的支持(JSP 2.0中的新特性)。
 JSP支持改进:添加对getExpressionEvaluator()的支持(JSP 2.0中的新特性)。然而,这却需要在类路径下放置Apache的commons-el包,否则这个方法是没有用的(它是可选的)。要注意原则上EL的支持是不需要的,因为FreeMarker有它自己的表达式语言。尽管客户化的JSP 2标签在JSP 2的API中,但是它也许仍然想要这个方法。
2.3.7 版
发布日期:2006-06-23
和2.3.7 RC1版相比,这个发行包包含新的控制null/不能存在变量的操作符,内建函数substring,还有一些bug修复。注意2.3.7 RC1版也有一个更改日志,所以你也许想要阅读它(就在下面,译者注)。
Java部分的改变
 内建函数seq_contains现在可以控制TemplateCollectionModel了。
 Bug修复:在2.3.7 RC1中,FreemarkerServlet常常会在实例化时由于NullPointerException而终止。
FTL部分的改变
 3个新的操作符被加入到了测试不存在变量的控制中。这些操作符将内建函数default,和if_exists废弃了。(当你使用这些废弃的内建函数时,解析器
不会报告任何警告信息,它们仍然有效):
 exp1!exp2和exp1?default(exp2)是基本相等的,而且(exp1)!exp2和(exp1)?default(exp2)也是基本相等。仅有的不同是当默认值不需要的时候,新的操作符不去计算exp2。
 exp1!和exp1?if_exists是相似的,而且(exp1)!和(exp1)?if_exists也是相似的。不同的是,使用这个新的操作符,默认值可以同时是空的字符串,空的list和空的哈希表(多类型参数),而使用if_exists时,默认值同时可以是空字符串,空list,空哈希表,布尔值false,不执行任何操作的变换和忽略所有参数。
 exp1??和exp1?exists是相等的,而且(exp1)??和(exp1)?exists也是相等的。
 新的内建函数:exp?substring(from, toExclusive),也可以被调用做substring(from)。获得子串可能会需要相当长的时间,比如myString[from..toInclusive]和myString[from..]。这个语法现在因为获取子串而废弃了(但仍然可以使用),作为替代,你应该使用myString?substring(from, toExclusive)和myString?substring(from)。因为substring仍然应用于字符串,所以序列(list)分割仍然需要使用老式语法。要注意新的内建函数多了一个参数,它用作是独占的索引。更深的不同是内建函数substring要求索引“from”的值比“to”的值要小。所以长度为0的子串是可能的,而不会反转子串。
 Bug修复:[1487694]当recover指令没有嵌套内容时会发生问题。
2.3.7 RC1版
发布日期:2006-04-27
这个发行包包含了很多bug修复和一些FreemarkerServlet相关的改进。它是一个候选发布版,也就是说它不能用在生产环境中。我们建议这个版本用来开发,同时也要测试它。
Java部分的改变
 FreemarkerServlet改进:AllHttpScopesHashModel现在是公有的了,所以你可以向数据模型中添加非列表变量了。
 FreemarkerServlet改进:当它抛出ServletException异常时,异常被控制在J2SE 1.4下了。
 Bug修复:[1469275]当和重新加载的类使用BeansWrapper时的NullPointerException异常
 Bug修复:[1449467] HttpSessionHashModel不是Serializable(可序列化,译者注)的
 Bug修复:[1435113] BeanWrapper中和索引属性的错误
 Bug修复:[1411705] TemplateCache中的获取功能bug
 Bug修复:[1459699]标签语法不能用Configuration.setSetting(String,String)设置
 Bug修复:[1473403] ReturnInstruction.Return应该是公有的
FTL部分的改变
 Bug修复:[1463664] [/#noparse]被打印出来了
2.3.6 版
发布日期:2006-03-15
2.3.5版发布几天之后,修复一系列bug之后的快速发布版本。所以到目前新添加的特性请阅读2.3.5版本。
Java部分的改变
 Bug修复:仅在FreeMarker 2.3.5版中,当你第二次读取一个bean的属性时,FreeMarker会认为它不存在(null)。
2.3.5 版
发布日期:2006-03-11
由于其中的一系列bug,这个版本被撤回了。请不要使用它!当然,所有新的特性也在FreeMarker的2.3.6版本中包含了。
一些新的特性和一些bug修复。
FTL部分的改变
 Bug修复:[1435847]替代语法对注释不起作用
 Bug修复:使用新的方括号语法,标签可以使用>来结束。现在只能是]了。
 Bug修复:[1324020] ftl指令如果它不再自己的那行上,会报出ParseException异常
 Bug修复:[1404033]内建函数eval在哈希表连接时会失效
Java部分的改变
 一个新的Configuration级(配置级别,译者注)的设置,tagSyntax被加入了。这就决定了模板的语法(尖括号语法和方括号语法(可以参考模板开发指南/其它/替换(方括号)语法部分获取详细内容,译者注))可以没有ftl指令在其中了。所以现在你可以默认选择使用新的方括号语法。我们建议使用自动探测(yourConfig.setTagSyntax(Configuration.AUTO_DETECT_TAG_SYNTAX)),因为从2.4版本开始那就是默认的了。自动探测选择语法是基
于FreeMarker模板(可以是任意的FreeMarker标签,不仅仅是ftl)的第一个标签的。要注意,之前的版本如果模板使用了ftl指令,那么ftl语法指令的语法就决定了模板的语法,而且tagSyntax设置也是被忽略的。
 现在,BeansWrapper,DefaultObjectWrapper和SimpleObjectWrapper支持在Map(比如myHash["a"])中查找使用了Character键的一个字符的字符串。简单而言,作为一种特殊的情况,当一次哈希表查找一个字符的字符串失败时,它会在低层的map中检查Character键。(Bug跟踪记录[1299045]FreeMarker不支持字符键的map查找。)
 一个新的属性,strict被添加到BeansWrapper,DefaultObjectWrapper和SimpleObjectWrapper中了。如果这个属性是true,那么尝试在模板中(就像myBean.aProperty)读取一个不存在bean类(而不仅仅是持有null值)中的bean的属性时,将会引起InvalidPropertyException异常,这也不能在模板中(也不能使用myBean.noSuchProperty?default('something')这种方法)抑制。使用?default('something')和?exists和类似的内建函数的这种方式,不能用来控制值为null的存在的属性,这样就没有隐藏属性名拼写错误的风险了。拼写错误通常会引起错误。但是要提醒自己,这是基于基本的FreeMarker的方法的,所以当且仅当你真正明白自己在做什么的时候才能使用这个特性。
 Bug修复:[1426227] printStackTrace(…)中的NullPointerException异常
 Bug修复:[1386193] ArithmeticEngine中的算数除0异常
2.3.4 版
发布日期:2005-10-10
一些新特性和bug修复。
FTL部分的改变
 现在你可以在FreeMarker标签中使用[和]来代替<和了>,比如你可以编写[#if loggedIn]...[/#if]和[@myMacro /]了,可以参考模板开发指南/其它/替换(方括号)语法部分获取详细内容。
 Bug修复:内建函数has_content对数字,日期和布尔值(如果这个值不是多类型的如序列,集合,哈希表或字符串值)处理时返回false。现在通常对数字,日期或布尔值(除了值是序列,集合,哈希表或字符串,因为那样它就会被检查)返回true。
Java部分的改变
 Bug修复:ClassTemplateLoader类的无参数的构造方法没有作用。
 Bug修复:Jython包装器不能很好的包装java.util.Date对象。现在使用BeanWrapper来包装,并包装成TemplateDateModel。
 Bug修复:当被包含的文件有语法错误时,include指令被指责有问题。
其它改变
手册很小的修改。
2.3.3 版
发布日期:2005-06-23
一些新的特性加入和很多bug修复。
要注意:
 如果你使用Log4J日志,从现在开始,至少需要Log4J 1.2以上版本。这是因为Log4J的API中有不匹配的改变。
 如果你自己构建FreeMarker:从现在开始至少需要使用JavaCC 3.2版本(代替JavaCC 2.1版本),还有Ant 1.6.1版本(代替Ant 1.5.x版本)。使用这个发布包不影响使用freemarker.jar的用户。
FTL部分的改变
 相对于用户,为计算机格式化数据创建新的内建函数:c(可以阅读参考文档/内建函数参考文档/处理数字的内建函数部分来获取详细信息,译者注)。它应该被用于数字而且必须使用Java语言来格式化,而不考虑数字本身的格式和本地化设置,比如数据库记录的ID,作为URL中的一部分或在HTML的表单中用作隐藏域,或者打印CSS/JavaScript数字的值。
 为柱状/表格显示序列而创建的新的内建函数:chunk(可以阅读参考文档/内建函数参考文档/处理序列的内建函数部分来获取详细信息,译者注)。
 序列分割(可以阅读模板开发指南/模板/表达式/序列操作/序列分割部分来获取详细信息,译者注)和取子串操作现在允许忽略最后的索引,这种情况下默认使用序列项或字符中的最后一个的索引。比如:products[2..]。(而且,数字值范围现在允许忽略最后的数字,这种情况默认为无限大。比如:5..。)
 Bug修复:如果字符串被替换成"",现在内建函数?replace也能正常执行了。
Java部分的改变
 新的模板加载器:freemarker.cache.StringTemplateLoader。它和字符串String一起使用Map来作为它的模板的源码。可以参加Java Doc文档获取详细内容。
 实验Rhino支持:FreeMarker现在有一个对Rhino(Java ECMAScript的实现)而做的实验对象:freemarker.ext.rhino.RhinoWapper。
 为SimpleXxx类创建的一些新的工具方法:SimpleHash.toMap(),SimpleSequence.toList()。
 Bug修复:FTL文字和其它SimpleSequnce,现在可以由DefaultWrapper或BeansWrapper暴露出的,被用作于FreeMarker不能察觉的Java代码中的方法的参数。也就是说,方法参数是各自自动由TemplateTypeModel转换成java.util.Map和java.util.List的。
 Bug修复:JSP支持现在可以作用于JSP 2的兼容容器中了。对了,现在它还不支持JSP 2的新特性,也就是JSP 1.2标签库支持在JSP 2的容器中不好用。
 Bug修复:当你试图设置属性值为null时,这个值对这些设置也是合法的。但是,Configuration.setOutputEncoding和setURLEscapingCharset方法可能会由于NullPointerException异常而中止。
 Bug修复:如果代替的字符串是"",freemarker.template.utility.StringUtil.replace(...)也能正常工作了。
 Bug修复:Log4J日志已经更新到可以兼容即将发布的Log4J 1.3版本。注意现在FreeMarker还是需要Log4J 1.2版本。
 Bug修复:因为关键字enum的引用,FreeMarker还没有在J2SE 1.5上构建源代码。
 Bug修复:SimpleSequence.synchronizedWrapper()方法的返回值还没有适当同步。和SimpleHash.synchronizedWrapper()是相同的。
 Bug修复:BeansWrapper和覆盖bean方法/属性的问题。(详细:bug跟踪记录#1217661和#1166533)
 Bug修复:如果Request属性定义在数据模型中的话,不能访问JSP标签库的问题。(详细:bug跟踪记录#1202918)
 Bug修复:各种小的解析故障被修复了。
其它改变
 手册改进,特别是FAQ部分。
2.3.2 版
发布日期:2005-01-22
Bug修复发布版
Java部分的改变
 Bug修复:如果你在FreeMarker模板中使用JSP标签库,由于在FreeMarker 2.3.1 版本中引入了一个bug,FreeMarker会尝试从Sun的Web站点获取DTD文件。这是一个非常严重的问题,因为如果你的服务器不在线或者Sun的Web站点暂时无法访问的话,那么使用了JSP标签库的模板可能会由于错误而中止执行。
 Bug修复:DefaultObjectWrapper已经忽略了nullModel属性的值。(要注意,我们不鼓励使用“null model(空模型)”。)
2.3.1 版
发布日期:2005-01-04
维护和bug修复发行版
可能存在的向后兼容问题
如果你在FreeMarker模板中使用JSP标签的话,那么有一个bug修复可能会影响Web应用程序的行为:javax.servlet.jsp.PageContext.getSession()的FreeMarker实现是不正确的。getSession()方法是一个很方便的方法,客户化标签可以通过它获得当前的HttpSession对象(如果没有session的话,那么返回值可能是null)。直到现在,如果session不存在的话,那么它会自动创建,所以不会返回null。这是一个bug,所以从2.3.1版本开始,它不会创建session了,如果session不存在的话,只是返回null。如果这个方法在页面局部刷新之后调用,那么老的不正确的做法可能引起页面呈现失败。但是要小心,老的做法也可能会在忘记创建session的地方隐藏一些Web应用的bug,所以用新的正确的做法你可能要面对一些由之前隐藏下来的bug引发的故障。(这是MVC模式中控制器创建session的任务,除了JSP标签需要自动创建的session,但是那样它就期望getSession()方法来做了。)
FTL部分的改变
 新的内建函数:url。这个内建函数可以用于URL转义。要注意的是,使用这个内建函数很方便,封装了FreeMarker的软件应该升级到2.3.1版本(程序员可以在下面获取更多信息…)。
 新的特殊变量:output_encoding和url_escaping_charset。要注意的是,使用这些特殊变量,封装了FreeMarker的软件应该升级到2.3.1版本(程序员可以在下面获取更多信息…)。
 处理序列的新的内建函数:seq_contains,seq_index_of,seq_last_index_of。
 处理字符串的内建函数:left_pad,right_pad和contains。
 新的指令:attempt/recover
 内建函数js_string现在可以转义>为\>(来避免</script>)。
 内建函数sort和sort_by现在可以对日期值进行排序。而且,sort_by现在可以对子变量的子变量的子变量…进行排序,可以作用于任意深度的数据(详细信息可以阅读参考文档/内建函数参考/处理序列的内建函数部分)。
 freemarker.template.TemplateExceptionHandler.HTML_DEBUG_HANDLER现在可以打印更多不能透入HTML上下文的信息。
Java部分的改变
 新的设置:output_encoding。这个设置用来通知FreeMarker,包含在软件
中(比如Web应用程序框架)的关于字符集作用于输出内容。默认它是不被设置的,而且它也不强制来设置,而是包含它的软件来做这件事情。如果模板想使用新的特殊变量output_encoding,或想使用新的内建函数url,那么这个就必须被设置。注意FreeMarker API允许你对每个模板的执行来设置这个信息(可以参考Template.createProcessingEnvironment(...))。
 新的设置:url_escaping_charset。这是当你使用新的内建函数url进行URL转义时,用于计算转义部分(%XX)的字符集设置。如果它没有被设置,那么内建函数url将会使用output_encoding值的设置,而且如果output_encoding也没有被设置,那么没有参数的url函数(${foo?url})将不能使用。
 很明显,使用单例(静态)的Configuration实例是不好的做法,所以相关的方法都已经废弃了,手册也已经做了相应的调整,而且FreemarkerXmlTask也被同步更新了。
 加入了freemarker.template.utility.Constants类,它包含很多常量字段来存储使用很频繁的TemplateModel常量值,如EMPTY_SEQUENCE,ZERO等也是这样。
 当在FreeMarker中使用SecurityManager时,访问系统属性可能会引起访问控制异常。现在,这个异常会被缓存而且使用警告级的日志来记录,而且返回属性的默认值。
 InvocationTargetException已经从特定情况的异常引发跟踪路径中去除了。
 如果ServletException是引起TemplateException异常的直接原因,那么在TemplateException的堆栈跟踪信息中添加一个很不好的跟踪工具来打印ServletException的本质原因,尽管ServletException类本身写的不是很好。
 Bug修复: javax.servlet.jsp.PageContext.getSession()的FreeMarker实现是不正确的。通过方便的getSession()方法,自定义标签可以获取当前的对象(如果没有session的话,很可能是null)。直到现在,如果session不存在的话,那么它会自动,所以不会返回null。这是一个bug,所以从2.3.1版本开始,它不会创建session了,如果session不存在的话,只是返回null。如果这个方法在页面局部刷新之后调用,那么老的不正确的做法可能引起页面呈现失败。但是要小心,老的做法也可能会在忘记创建session的地方隐藏一些Web应用的bug,所以用新的正确的做法你可能要面对一些由之前隐藏下来的bug引发的故障。(这是MVC模式中控制器创建session的任务,除了JSP标签需要自动创建的session,但是那样它就期望getSession()方法来做了。)
 Bug修复:BeansWrapper不会一直正确处理Java类有公有静态字段和公有静态方法同名的情况。
 Bug修复:SimpleMethodModel有时不会正确传递异常,引起空指针异常。
 Bug修复:当你很多时候处理相同的Environment时,模板执行可能使用过期缓存的值,而且在两个处理之间改变设置的值。注意这仅当单独线程环境时可能发生,因为此时允许修改这样的设置。
 Bug修复:如果模板开发者忘记指定确定的参数时,一些处理字符串的内建函数会由于IndexOutOfBounds异常而失败。现在在处理失败时会有更多的错误提示信息了。
 Bug修复:如果freemarker.ext.dom.NodeModel.equals(...)的形参是null时,它会由于空指针异常而处理失败。
 Bug修复:导致的TemplateException异常的信息有时在J2SE 1.4或更高版本的堆栈信息中打印两次。
 Bug修复:StringUtil.FTLStringLiteralEnc(String)方法已经完成了。
其它改变
 修改和完善手册和JavaDoc API
在最终版本之前发布的历史
预览版和最终版的区别
 如果ServletException是引起TemplateException异常的直接原因,那么在TemplateException的堆栈跟踪信息中添加一个很不好的跟踪工具来打印ServletException的本质原因,尽管ServletException类本身写的不是很好。
 Bug修复:如果freemarker.ext.dom.NodeModel.equals(...)的形参是null时,它会由于空指针异常而处理失败。
 Bug修复:导致的TemplateException异常的信息有时在J2SE 1.4或更高版本的堆栈信息中打印两次。
 手册中的小改动和完善。
2.3 版
发布日期:2004-06-15
和2.2.x系列版本相比,FreeMarker 2.3版本引入了很多小的新特性和质量的提升。最值得注意的改进是在模板中定义函数(方法)的能力,在字符串文字中插入变量的能力,支持多个宏的参数,还有更加智能的默认对象包装器。尽管这些变更都不是很大的修改,2.3.x版本是没有向后兼容2.2.x版的(请参考下面的列表),所以你可能要为新项目选择来使用了。
可能新特性中最大的推动是完全重新设计的XML包装器。使用新的XML包装器,FreeMarker以一个新的领域作为目标,这个XSLT的应用领域是相似的:转化复杂的XML到任意的文本输出中。尽管这个子项目刚刚诞生,但在实践中肯定是可用的。可以参考第三部分:XML处理指南获取详细内容。
没有向后兼容的改变!
 因为插值(${...}和#{...})现在可以在字符串文本中起作用了,在字符串
中的字符序列${和#{是该保留的。所以如果你有一些如<#set x = "${foo}">这样的代码,那么你不得不使用<#set x = r"${foo}">来替换,要注意,在原生(r)字符串中,如\n这样的转义是不起作用的。
 strict_syntax设置的默认(初始)值已经从false改变成true了。当strict_syntax是true时,使用老式语法如<include "foo.ftl">这样的标签将会被视作静态文本(所以它们就会按照原样出现在输出中,如HTML标记那样),而不是FTL标签。这样的标签不得不写作<#include "foo.ftl">,因为以<#,</#,<@或</@开头的标记被算作是FTL标记。如果要回复老式的过渡行为,让遗留语法和新的语法都能被识别,你可以明确地在cfg.setStrictSyntaxMode(false)中对strict_syntax设置成false。而且,对于独立的模板你可以在模板中以<#ftl strict_syntax=false>开头来强制使用老式语法。(要获得更多严格语法的详细信息,可以阅读参考文档/废弃的FTL结构/老式FTL语法)
 几个类从freemarker.template包中移出,移入新的freemarker.core包中:
 普通类:ArithmeticEngine,Configurable,Environment
 异常类:InvalidReferenceException,NonBooleanException,NonNumericalException,NonStringException,ParseException,StopException
 错误类: TokenMgrError
分割freemarker.template包的主要原因是很多专家级的公有类和接口已经太多了,正如我们为第三方工具引入API,不如调试API。
 freemarker.template.TemplateMethodModel.exec现在返回Object而不是TemplateModel了。
 空白剥离现在更加强烈了:如果一行上仅仅包含FTL标签,它会移除头和尾部的空白。(之前,如果标签是<#include ...>或用户自定义空白指令标签,如<@myMacro/>(或它的相同写法:<@myMacro></@myMacro>和<@myMacro></@>),空白是不会移除的。现在空白在这些情况中也可以被移除了)而且,夹在连个不输出元素中的空白,比如宏定义,定义变量,引入或属性设置,现在是被忽略的。更多信息:可以参考模板开发指南/其它/空白处理/剥离空白部分。
 function指令现在用于定义方法。你应该在模板中使用macro来替换function。要注意,如果你不在其中使用return指令,老式的function仍然可用,你可以使用废弃的call指令来调用它们。
 在模板语言中,表达式as,in和using现在是关键字了,除了方括号语法,它们已经不能用于顶级变量名了。如果你在一些情况下,顶级变量名使用了这些名字的其中之一,你将不得不将它们重命名,或者和特殊变量.vars一起使用方括号语法:.vars["in"]。
 内建函数?new,正如它的实现,是一个安全隐患。现在,它只允许你实例化一个实现了freemarker.template.TemplateModel接口的Java对象。如果你想得到内建函数?new和之前版本中存在的功能,要在模板中有一个可用的freemarker.template.utility.ObjectConstructor类的实例。(比如:myDataModel.put("objConstructor", new ObjectConstructor());,之后在模板中你可以这么来做:<#assign
aList = objConstructor("java.util.ArrayList", 100)>)
 FreemarkerServlet的改变:
 默认情况下,FreemarkerServlet使用ObjectWrapper.DEFAULT_WRAPPER而不是ObjectWrapper.BEANS_WRAPPER。这就意味着,默认情况下,java.lang.String,java.lang.Number,java.util.List和java.util.Map类型的对象将会各自通过SimpleScalar,SimpleNumber,SimpleSequence和SimpleHash类被包装成TemplateModels。因此,那些对象中的java方法将都会不可用了。FreeMarker 2.3中的默认包装器实现会自动探知如何包装Jython对象,而且包装org.w3c.dom.Node对象成freemarker.ext.dom.NodeModel的实例。
 FreemarkerServlet的基本实现不再使用HttpRequest.getLocale()对模板推导本地化设置。而只是代表新的受保护的方法,deduceLocale。这个方法的默认实现只是返回配置locale的值。
FTL部分个改变
 字符串中的插值。为了简便,插值现在支持在字符串中使用了。比如:<@message "Hello ${user}!" />和<@message "Hello" + user + "!" />是相同的。
 原始字符串:字符串中的前缀为r,插值和转义序列将不会被解释成特殊记号。比如:r"\n${x}"会被解释成字符序列'\','n','$','{','x','}',变量x的值和其本身都不会换行。
 使用function指令,方法变量可以在FTL中定义了。
 支持多个宏参数。如果宏声明中的最后一个参数以...结尾,所有传递到宏中的额外参数将可以通过那个参数来访问。对于调用宏的位置参数,参数将会是一个序列。对于命名参数,函数将会是一个哈希表。要注意对于新的function指令它们都能很好工作。
 一个新的头部信息参数,strip_text,它从一个模板中移除了所有顶级文本。这对于“include files(包含文件,译者注)”来说是很有用的,来压制分开宏定义的新的一行。可以参见ftl指令。
 新的特殊变量:.vars。这对于使用方括号语法读取顶级变量来说是很有用的。比如:<#macro "name-with-hyphens">...或<#macro "foo bar" = 123>。
 处理哈希表的内建函数?keys和?values现在返回序列了。在实际条件下这就意味着你可以访问它们的大小或者通过序列获取它们的子变量,使用所用的处理序列的内建函数。(对于程序员要注意:TemplateHashModelEx接口没有改变。老的代码仍然有用。请参见API文档看看这是为什么。)
 存在性的内建函数(?default,?exists等)现在可以作用于序列子变量了。请阅读关于内建函数default的文档来获取更多信息。
 剥离空白现在比之前更加强烈了:如果这一行仅仅包含FTL标签的话,它通常移除
头部和尾部的空白。(之前,如果标签是<#include ...>或用户自定义空白指令标签,如<@myMacro/>(或它的相同写法:<@myMacro></@myMacro>和<@myMacro></@>),空白是不会移除的。现在空白在这些情况中也可以被移除了)而且,夹在连个不输出元素中的空白,比如宏定义,定义变量,引入或属性设置,现在是被忽略的。更多信息:可以参考模板开发指南/其它/空白处理/剥离空白部分。
 空白剥离对于单独的一行可以使用nt指令来关闭(不修剪)。
 哈希表可以使用+操作符来连接了。哈希表中的右半部分的键优先。
 新的处理Java和JavaScript字符串转义的内建函数:j_string和js_string。
 内建函数replace和split现在支持大小写敏感的比较和正则表达式(仅J2SE 1.4以上版本),还有一些其它新的选项。更多信息可以阅读参考文档/内建函数参考/处理字符串的内建函数/通用标记部分。
 处理正则表达式匹配的新的内建函数(仅J2SE 1.4以上版本):maches
 新的内建函数eval,来评定一个字符串作为FTL表达式。比如"1+2"?eval返回数字3。
 新的读取本地化设置值的特殊变量:locale,lang。可以阅读参考文档获取更多信息。
 新的读取FreeMarker版本数字的特殊变量:version。可以阅读参考文档获取更多信息。
 三个新的指令recurse,visit和fallback被引入来支持声明节点树的处理。这就是处理XML输入的典型(虽然不完全是)使用。和这一起,一种新的变量类型被引入了,这就是节点类型。可以阅读第三部分中声明的XML处理部分获取更多内容。
 内建函数,正如它的实现,是一个安全隐患。现在,它只允许你实例化一个实现了freemarker.template.TemplateModel接口的Java对象。如果你想得到内建函数?new和之前版本中存在的功能,要在模板中有一个可用的freemarker.template.utility.ObjectConstructor类的实例。(比如:myDataModel.put("objConstructor", new ObjectConstructor());,之后在模板中你可以这么来做:<#assign aList = objConstructor("java.util.ArrayList", 100)>)
 变量名可以在任意位置(除了使用引号括号语法)包含@。比如<#assign x@@@ = 123>是合法的。
 在模板语言中,表达式as,in和using现在是关键字了,除了方括号语法(如.vars["in"]),它们已经不能用于顶级变量名了。
 ftl指令的新参数:attributes。这个属性的值是关联模板任意属性(名-值对)的哈希表。这个参数的值也可以是任意类型(字符串,数字,序列等)。FreeMarker不会去理解属性的意义。这都是随封装了FreeMarker的应用程序(比如Web应用程序框架)。因此,允许参数的集合和它们的语义是依赖于应用程序(Web应用程序框架)的。
 其它的小质量的提升…
Java部分的改变
 更智能的默认对象包装:默认对象包装器现在是freemarker.template.DefaultObjectWrapper,专注于包装任意对象,正如bean使用freemarker.ext.beans.BeansWrapper包装器。当然,包装org.w3c.dom.Node对象会使用新的DOM包装器。而且,它也知道Jython对象,如果传入的对象是Jython对象,它会使用freemarker.ext.jython.JythonWrapper包装器。(我们把它认为是一个向后兼容的改变,因为这个新的对象包装器是不同的是,对于那些老的包装器不能包装的,会抛出异常的对象。)
 freemarker.template.TemplateMethodModel.exec现在返回Object而不是TemplateModel了。
 strict_syntax设置的默认(初始)值已经从false改变成true了。当strict_syntax是true时,使用老式语法如<include "foo.ftl">这样的标签将会被视作静态文本(所以它们就会按照原样出现在输出中,如HTML标记那样),而不是FTL标签。这样的标签不得不写作<#include "foo.ftl">,因为以<#,</#,<@或</@开头的标记被算作是FTL标记。如果要回复老式的过渡行为,让遗留语法和新的语法都能被识别,你可以明确地在cfg.setStrictSyntaxMode(false)中对strict_syntax设置成false。而且,对于独立的模板你可以在模板中以<#ftl strict_syntax=false>开头来强制使用老式语法。(要获得更多严格语法的详细信息,可以阅读参考文档/废弃的FTL结构/老式FTL语法)
 新的CacheStorage实现:freemarker.cache.MruCacheStorage。这个缓存存储实现了二级最近使用的缓存。在第一级,每一项强烈引用并会达到指定的最大数目。当超过最大数目时,最近很少使用的项会被送到二级缓存中,那里的引用会减轻,直到达到另外一个指定的最大数目。freemarker.cache.SoftCachseStorage和StrongCachseStorage已经废弃了,MruCacheStorage已经在各处替代了它们。默认的缓存存储现在是0大小的MruCacheStorage对象,并有无限大的容量。对于cache_storage的Configuration.setSetting现在可以理解字符串值,比如"strong:200, soft:2000"。
 对于BeansWrapper生成的模型,现在你可以使用${obj.method(args)}语法来调用返回值为void的方法。void方法现以TemplateModel.NOTHING作为返回值。
 freemarker.template.SimpleHash现在可以包装只读的Map,比如Servlet API中的封装HTTP请求参数的map。
 TemplateNodeModel接口现在被引入来支持节点树的递归处理。通常这是用于和XML相关的应用。
 新的包:freemarker.ext.dom。这个包含了新的XML包装器,它们使用访问者模式(也就是使用<#visit ...>和相似的指令)支持XML文档,作为传统的包装器提供更方便的XML遍历。要获取更多内容可以参考第三部分XML处理指南。
 新的包:freemarker.core。一些高级用户使用的
freemarker.template包中的大多数类现在移到这里了。分割freemarker.template包的主要原因是大量的“专家级”的公有类和接口增长的太多了,如我们引入的调试API等第三方工具。
 新的包:freemarker.debug。这个包提供了调试API,通过它你可以在网络(RMI)上调试执行模板。你需要编写一个前端(客户端),因为API只是服务器端的。要获取更多信息可以阅读freemarker.debug包的JavaDoc文档。
 你可以使用静态方法Configuration.getVersionNumber()来查询FreeMarker的版本号。而且在freemarker.jar中包含的Manifest.mf现在也包含FreeMarker的版本号了,此外,使用java -jar freemarker.jar来执行它将会在标准输出中打印版本号。
 增加了一个新的受保护的FreemarkerServlet方法:Configuration getConfiguration()。
 日期支持现在被标记为最终版了。(之前是实验阶段的。)
 BeansWrapper已经改进了,当自我检查时可以阻挡一些安全异常。
 其它小的质量提升和扩展…
其它改变
 修改和改进了手册和JavaDoc API。
在最终版之前发行包的历史
最终发布版和候选发布4的区别
 增加了一个新的特殊变量version来打印FreeMarker的版本号。可以阅读参考文档来获取更多内容。
 文档的小修复和改进。
候选发布4和3的区别
 BeansWrapper已经改进了,当自我检查时可以阻挡一些安全异常。
 FreemarkerXmlTask有两个新的子任务,可以用Jython脚本来准备模板执行:prepareModel和prepareEnvironment。jython子任务和prepareEnvironment现在都被废弃了。可以参考Java API文档来获取详细信息。
 新的特殊变量来读取FreeMarker的版本号:version。可以阅读参考手册中的内容获取更多信息。
 Bug修复:大于符号不再混淆内建函数eval了。
 Bug修复:BeansWrapper现在更加适当地包装方法的null返回值了。
 Bug修复:FreemarkerXmlTask不再需要Jython类了,除非你真的要使用Jython脚本。还修复了一些Jython相关特性的bug。
 Bug修复:如果模板执行控制器忽略了发生在FTL标签(比如<#if "foo${badVar}" != "foobar">)中的插值异常和错误,那么就和发生在FTL标签外中插值的错误以相同的方法来处理。因此,指令调用不会被略过,可能有问题的插值会被替换为空字符串。(这和<#if "foo"+badVar != "foobar">的行为是不相符的,应该和之前示例中的代码是100%相等的)
 Bug修复:当根据本地文件系统收到异常路径时,FileTemplateLoader现在更加健壮了。在之前的版本中,当你使用FileTemplateLoader时,这样的路径有时引起不希望发生的IOException异常,并中止在之后的FileTemplateLoader寻找模板。
候选发布3和2的区别
 Bug修复:修复了模板缓存中的一个致命bug,它是由最新的缓存修改而引入的。模板缓存通常当更新延迟失效时重新加载不改变的模板,直到模板真正的改变了,这种情况下它是不会重新加载模板的。
候选发布2和1的区别
 Bug修复:当被老的版本替换时,模板缓存不重新加载模板的问题。
 JavaDoc API修改:日期/时间相关的类/接口被标记为实验阶段的。
 很小的改进。
候选发布1和预览版16的区别
 警告!没有向后兼容的改变!strict_syntax设置的默认值(初始值)已经从false改为true了。当strict_syntax是true时,用老式语法如<include "foo.ftl">编写的标签将会被视为是静态文本(所以它们就会按照原样出现在输出中,如HTML标记那样),而不是FTL标签。这样的标签不得不写作<#include "foo.ftl">,因为以<#,</#,<@或</@开头的标记被算作是FTL标记。如果要回复老式的过渡行为,让遗留语法和新的语法都能被识别,你可以明确地在cfg.setStrictSyntaxMode(false)中对strict_syntax设置成false。而且,对于独立的模板你可以在模板中以<#ftl strict_syntax=false>开头来强制使用老式语法。(要获得更多严格语法的详细信息,可以阅读参考文档/废弃的FTL结构/老式FTL语法)
 ftl指令的新参数:attributes。这个属性的值是关联模板任意属性(名-值对)的哈希表。这个参数的值也可以是任意类型(字符串,数字,序列等)。FreeMarker不会去理解属性的意义。这都是随封装了FreeMarker的应用程序(比如Web应用程序框架)。因此,允许参数的集合和它们的语义是依赖于应用程序(Web应用程序框架)的。
 Bug修复:freemarker.template.utility.DeepUnwrap解包序列到空ArrayList的问题。
 Bug修复:如果你在路径(获得的)中使用*/包含/引入一个模板,而且模板本身
反过来在路径中使用*/包含/引用另外一个模板,那可能会失败。
 freemarker.core.Environment中的新方法:importLib(Template loadedTemplate, java.lang.String namespace)和getTemplateForImporting(...), getTemplateForInclusion(...)。
 include和import指令中和java.io.IOException异常相关的错误信息的改进。
 文档中的小的改进。
预览版16和预览版15的区别
 新的包:freemarker.debug。这个包提供了调试API,通过它你可以在网络(RMI)上调试执行模板。你需要编写一个前端(客户端),因为API只是服务器端的。要获取更多信息可以阅读freemarker.debug包的JavaDoc文档。
 Bug修复:新的XML包装器中,@@markup和相似的特殊键:
 对空元素返回<foo></foo>而不是<foo />。这是不必要的冗长,如果你生成HTML时这会让浏览器迷惑。
 在源文档中展示了没有明确给定值的属性,仅仅是来自DTD的默认值。
 在<!DOCTYPE ...>中,忘记在系统标识符之前放置空格。
 Bug修复:如果内容是空节点集合的话,使用Jaxen的XPath会由于NullPointerException异常而中止执行。
 更智能一点的Xalan XPath错误信息。
 从模板缓存中撤销后备的类加载器逻辑。
 从现在开始,如果没有可用的XPath引擎,而且在一个“XML 查询”中的哈希表的键没有XPath不能解释,那么错误将会清楚地说明原因,而不是静默地返回未定义的变量(null)。
 Bug修复:一些模板会引起解析器处理中止。
 其它小的改进。
预览版15和预览版14的区别
 Bug修复:通常当JVM的内存使用很高时,新的默认模板缓存存储(MruCacheStorage)就会不定时出现NullPointerException持续异常而处理中止。
 Bug修复:当被引用的FTL指令有嵌套内容时,在错误信息也被引用时,而这个引用可能会很长,而暴露嵌套内容也是不需要的。
预览版14和预览版13的区别
 freemarker.template.TemplateMethodModel.exec现在返回Object而不是了TemplateModel。
 对Jaxen(而不是Xalan)修复和提升了XPath的处理。没有节点集合的XPath表达
式现在就可以使用了。在XPath表达式中,FreeMarker的变量就可以使用XPath变量引用来访问了(比如doc["book/chapter[title=$currentTitle]"])。
 freemarker.cache.SoftCachseStorage和StrongCachseStorage已经废弃了。现在使用更加灵活的MruCachseStorage。现在的默认的缓存存储是长度为0的MruCachseStorage对象,还有无限的长度。对于cache_storage的设置Configuration.setSetting现在可以理解字符串值了,比如"strong:200, soft:2000"。
 Bug修复:freemarker.cache.MruCachseStorage有时会因为ClassCastException异常而中止执行。
 新的处理Java和JavaScript字符串转义的内建函数:j_string和js_string
 freemarker.template.TemplateExceptionHandler.HTML_DEBUG_HANDLER现在可以打印出更多的阻止HTML内容的信息。
 可以使用静态方法Configuration.getVersionNumber()来查询FreeMarker的版本号码。而且包含在freemarker.jar文件中的Manifest.mf文件现在也包括了FreeMarker的版本号码,而且,执行java -jar freemarker.jar命令将会在控制台的输出中打印版本号码。
 为FreemarkerServlet增加了新的受保护的方法:Configuration getConfiguration()
 Bug修复:在某些情况下,FreeMarker冻结使用空的条件块儿。
 Bug修复:使用list指令对一个对象调用两次方法,如parent.getChildren()和<#list parent.children as child>...</#list>
预览版13和预览版12的区别
 空白剥离现在更加强烈了:如果一行上仅仅包含FTL标签,它会移除头和尾部的空白。(之前,如果标签是<#include ...>或用户自定义空白指令标签,如<@myMacro/>(或它的相同写法:<@myMacro></@myMacro>和<@myMacro></@>),空白是不会移除的。现在空白在这些情况中也可以被移除了)而且,夹在连个不输出元素中的空白,比如宏定义,定义变量,引入或属性设置,现在是被忽略的。更多信息:可以参考模板开发指南/其它/空白处理/剥离空白部分。
 空白剥离对于单独的一行可以使用nt指令来关闭(不修剪)。
 声明的XML处理的新指令:fallback
 freemarker.template.SimpleHash现在可以包装只读的Map,比如Servlet API中的封装HTTP请求参数的map。
预览版12和预览版11的区别
这个预览版和之前预览版的唯一改变是预览版11中有一个bug,就是DOM树不会被垃圾回收机制回收。
预览版11和预览版10的区别
 许多XML相关的改变。它们中的一部分和之前的发行包并不兼容!要获得“关于现在XML相关的特性是怎样进行的”更详细的内容,可以参考:XML处理指南部分。
 注意!如foo.@bar的属性查找现在返回序列(和子元素查询,XPath查询相似),而不是单独节点了。因为长度为1的节点序列的规则,那么仍然推荐书写${foo.@bar},但是如?exists,?if_exists或?default的内建函数就不像之前那么使用了。比如,代替foo.@bar?default('black'),你现在可以书写foo.@bar[0]?default('black')。所以,如果你对属性使用了存在性检验的内建函数,你将不得不在模板中将它们找出来并添加[0]。
 注意!XML命名空间控制已经完全重写,而且已经完全不和预览版10兼容了。如果你输入的XML文档没有使用xmlns属性,那么就不用担心。如果在比较元素本地名称的地方你使用了“松散模式”,而现在已经不可用了,那么,对不起…
 注意!特殊变量@@和@*现在返回属性节点的序列而不是它们的哈希表了。
 一些哈希表的键现在对存储多个节点的节点序列可用了。比如,要获取所有chapter的元素para的列表,只要书写doc.book.chapter.para。或者获取所有chapter的title属性,只要书写doc.book.chapter.@title。
 新的特殊哈希表键:**,@@start_tag,@@end_tag,@@attribute_markup,@@text,@@qname。
 处理属性节点的内建函数?parent现在返回属性节点所属的元素节点。
 如果你一旦调用了静态方法freemarker.ext.dom.NodeModel.useJaxenXPathSupport(),那么现在可以使用Jaxen来代替Xalan来处理XPath表达式了。如果Jaxen可用,我们计划自动使用Jaxen而不是Xalan,尽管Jaxen支持还没有实现全部功能。
 新的特殊变量:.vars。这对使用方括号语法来读取顶级元素非常有用,比如:.vars["name-with-hyphens"]和.vars[dynamicName]。
 新的内建函数eval,来评定一个字符串作为FTL表达式。比如"1+2"?eval返回数字3。
 要设置模板的本地化信息,FreemarkerServlet现在使用configuration的locale设置,而不是Locale.getDefault()方法了。而且,deduceLocale方法的方法前面也已经改变了。
 我们有一个新的(beta版本的)CacheStorage实现:freemarker.cache.MruCacheStorage。这个缓存存储实现了一个二级最近最新使用缓存。在第一级,每一项都强烈地被引用直到达到指定的最大数目。当超过最大数目时,最近最少使用的项目就被送到二级缓存中,在那里它们被少量引用,直到达到另外一个指定的最大数目。如果你想使用,那么可以尝试使用cfg.setCacheStorage(new freemarker.cache.MruCacheStorage(maxStrongSize,
maxSoftSize))。
预览版10和预览版9的区别
 特殊键@@xmlns被移除了,因为新的FTL指令<#xmlns...>有利于相同目的的实现。
 默认情况下,系统在关于命名空间前缀的使用上是很严格的。通常来说,你必须使用一个前缀来限定和XML命名空间相关的子元素。你可以使用新的<#xmlns...>指令来做这件事情,但是在输出的XML文件中的前缀声明在没有声明时也能使用。
 引入了一个新的特殊键@@text,它会返回所有包含(递归地)在一个连接在一起的元素内的文本节点。
 Jaxen或Xalan可以被用来提供XPath功能。之前的版本仅仅支持Xalan。
 FreemarkerServlet使用ObjectWrapper.DEFAULT_WRAPPER作为默认的包装器,代替了ObjectWrapper.BEANS_WRAPPER。这就意味着,默认情况下,java.lang.String,java.lang.Number,java.util.List和java.util.Map类型的对象将会各自通过SimpleScalar,SimpleNumber,SimpleSequence和SimpleHash类包装成TemplateModels。因此,这些对象中的Java方法就会不可用了。FreeMarker 2.3版中默认包装器的实现知道怎么去包装Jython对象,而且会把org.w3c.dom.Node对象包装成freemarker.ext.dom.NodeModel的实例。
 FreemarkerServlet的基本实现不再推断从HttpRequest.getLocale()方法钩住的本地化来使用。而是会代理钩住一个deduceLocale(),它可以在子类中重写。基本的实现仅仅使用Locale.getDefault()方法。
预览版9和预览版8的区别
 修改了在预览版8中引入的bug:XPath,现在@@markup和@@nested_markup可以作用于文档节点了。
预览版8和预览版7的区别
 macro和定义变量指令现在可以使用引号语法来接受任意目标变量的名称。比如:<#macro "foo-bar">...或<#assign "this+that" = 123>。这一点很重要,因为XML元素名称可以包含连字符号,而且现在不能为这些元素定义控制宏。
 特殊键@@content被重新命名为@@nested_markup。
 修改了过时的XML相关的手册内容(它们在预览版7中就已经过时了)。
 更好的解析错误消息。
 其它的各种小bug修复…
预览版7和预览版6的区别
 对XML处理时,至少当XPath已经大量使用时,XPath查询的缓存应该导致巨大的性能提升。
 在XML处理功能中,控制XML命名空间的限制条件。在2.3 预览版6中引入的新的strict_namespace_handling设置被移除了。一个通用的解决方案得出这样的配置是不需要的。
 特殊键@xmlns被重命名为@@xmlns。保留的命名空间前缀default被重命名为@@default。
 ftl指令现在接受非字符串类型。
 在freemarker.ext.dom包中,新的特殊键被引入处理XML节点的包装。@@markup键返回标记该元素的文字标记,而@@content键返回所有元素的标记,包括打开的和关闭的标记。
 其它的各种小bug修复…
预览版6和预览版5的区别
 判断存在性的内建函数(?default,?exists等)现在可以作用于序列子变量了。阅读内建函数default部分可以获取更多信息。
 内建函数matches现在返回序列而不是集合了。
 在XML处理功能中的处理XML命名空间的限制条件。一个新的设置strict_namespace_handling被引入了。如果这个被设置了(默认是不设置的),那么任意用于访问/递归机制的节点处理宏必须来自于一个在ftl头部声明的宏定义库,来处理有问题的命名空间。
 其它的各种小bug修复…
预览版5和预览版4的区别
 内建函数replace和split现在支持大小写不敏感的比较和正则表达式(仅在J2SE 1.4或更高版本)了,而且还有一些其它新的可选项。更多信息可以参考内建函数参考/处理字符串的内建函数/通用标记部分。
 处理正则表达式匹配的新的内建函数(仅在J2SE 1.4或更高版本):matches
 其它的各种小bug修复…
 手册:更多浏览器安全的HTML内容更新。
预览版4和预览版3的区别
 Bug修复:处理多类型的变量,对于哈希表类型重载+操作符比一些老的重载方式有更高的优先级。
 API文档在发行包tar.gz中去除。
预览版3和预览版2的区别
 XML处理:很多不同的bug修复,特别是声明式的处理。
 XML处理:内建函数namespace_uri,头部信息参数xmlnsuri,和方法TemplateNodeModel.getNodeNamespace分别被重命名为node_namespace和getNodeNamespace了。
 XML处理:更好的文档。特别要注意:XML处理指南部分
 一个新的头部信息参数strip_text,这会将所有顶级文本从模板中去除。参考ftl指令部分。
 支持可变数目的宏参数。如果在宏定义中的最后一个参数以...结尾,所有额外传递给宏的参数都将可以通过这个参数来访问到。对于使用位置参数的宏调用,参数将会是一个序列。对于命名参数,参数将会是一个哈希表。
 对于BeansWrapper生成的方法,你可以使用${obj.method(args)}语法来调用那些返回类型是void的方法。方法现在返回TemplateModel.NOTHING作为返回值。
预览版2和预览版1的区别
 freemarker.ext.dom.NodeModel的API轻微地改变了。因为老式的模式是线程不安全的,所以setDocumentBuilder()方法变成setDocumentBuilderFactory()了。stripComments和stripPIs方法被重命名为removeComments和removePIs了。并添加了一个新方法simplify。
 现在,在模板语言中,表达式as,in和using是关键字了,就不能被用作顶级变量名了,除非使用使用方括号语法(比如.vars["in"])。如果在某些情况下,有使用这些名字其中之一的顶级变量,那么就必须重命名它们(或者使用方括号语法)。对于这个不便的改进非常抱歉。
 内建函数?new,正如它的实现,是一个安全问题。现在,它仅允许你实例化一个实现了freemarker.template.TemplateModel接口的java对象。如果你想得到内建函数?new在之前版本中的功能,那么就在模板中准备一个新的freemarker.template.utility.ObjectConstructor类。
 <#recurse>指令不能使用了。和子句在一起不能使用了。现在这个问题解决了。
2.2.8 版
发布日期:2004-06-15
Bug修复和维护版本
FTL部分的改变
 添加了一个新的特殊变量:version,用于打印FreeMarker的版本号码。可以阅
读参考文档/特殊变量参考部分获取详细内容。
Java部分的改变
 BeansWrapper已经得到改进并可以阻止一些在自检时安全上的异常。
 Bug修复:当收到由本地文件系统抛出的异常路径时,FileTemplateLoader现在更加健壮了。在之前的版本中,当使用MultiTemplateLoader时,这样的路径有时会引起不希望得到的IOException异常,进而中止在之后的FileTemplateLoader中寻找模板。
 一部分FreeMarker代码被标识为特权代码段,所以当你使用安全管理器(这是从2.3版开始移植的)时你可以给FreeMarker授予额外的特权。可以阅读程序开发指南/其它/为FreeMarker配置安全策略部分获取详细信息。
其它改变
 文档的小的修改和改进。
2.2.7 版
发布日期:2004-03-17
重要bug修复包
Java部分的改变
 Bug修复:在模板缓存中修复了一个致命的bug,它是在最近缓存bug修复中引入的。当更新延迟时间达到时,模板缓存通常加载没有改变的模板,直到模板真正发生变化为止。这种情况下它就不会重新加载模板了。
2.2.6 版
发布日期:2004-03-13
维护和bug修复发布。一些改进是从FreeMarker 2.3rc1版开始移入的。
FTL部分的改变
 新的特殊变量:.vars。在使用方括号语法来读取顶级变量时,这是很有用的,比如.vars["name-with-hyphens"]和.vars[dynamicName]。
 处理Java和JavaScript字符串转义的新的内建函数:j_string和js_string。
Java部分的改变
 Bug修复:当模板被一个老的版本替换时,模板缓存不重新加载模板的问题。
 Bug修复:freemarker.template.utility.DeepUnwrap解包序列到空的ArrayList。
 Bug修复:在错误消息中,当引用的FTL指令有嵌入内容时,内容也被引用了。所以引用可能会非常长而且获取嵌套的行也不是必须的。
 freemarker.template.TemplateExceptionHandler.HTML_DEBUG_HANDLER现在打印更多的阻止HTML内容的消息。
 你可以使用静态的Configuration.getVersionNumber()方法来查询FreeMarker的版本号。而且包含在freemarker.jar中的Manifest.mf现在也包含FreeMarker的版本号了,此外,使用java -jar freemarker.jar执行将会在控制台的输出中打印版本号。
 日期支持现在被标记为最终的。(之前是实验状态)从FreeMarker2.2.1版开始就没有变化了。
其它改变
 修改和改进了手册和JavaDoc API文档。现在文档可以通过Eclipse的提示帮助插件(在FreeMarker网站的“编辑器/IDE插件”部分可以访问)来查看了。
 小的地方的改进。
2.2.5 版
发布日期:2003-09-19
维护和bug修复发布包
Java部分的改变
 如果由于I/O问题,缺少安全授权或其它异常而导致的当前目录不可访问时,使用默认构造方法创建Configuration实例时也不会失败了。
2.2.4 版
发布日期:2003-09-03
维护和bug修复发布包
Java部分的改变
 提升了对JSP标签库的支持。如果一些第三方标签库在FreeMarker中不可用时,那
么现在也许会
 JSP的PageContext现在实现了forward和include方法。
 从doStartTag的返回值中接受EVAL_PAGE作为SKIP_BODY的别名。在一些使用很广泛的标签库中这是一个公共的bug,现在FreeMarker限制更少,更容易接受它了。
 修复了一些关于宏的命名空间的很罕见的问题。
其它改变
 文档的小的改进
2.2.3 版
发布日期:2003-07-19
Bug修复发布
FTL部分的改变
 添加了内建函数is_date。
 Bug修复:各种内建函数is_xxx当应用于未定义的表达式时返回false。现在它们能正确地处理失败。
Java部分的改变
 Bug修复:JSP标签库支持现在可以读取JSP 1.2兼容的TLD XML文件。
 Bug修复:当指定的TLD XML文件没有发现时(之前是抛出NullPointerException异常),JSP标签库支持现在给出更多的有用的异常信息。
 Bug修复:JSP标签库支持现在初始化一个定制标签在它的父标签之后,而且页面上下文也如一些标签期望的那样,当属性的setter方法被调用时来设置了。
 Bug修复:在很少数的情况下,由于过早的存储优化,BeansWrapper可能分析类时会失败。
2.2.2 版
发布日期:2003-05-02
Bug修复包
Java部分的改变
 Bug修复:当使用W3C的DOM树时,freemarker.ext.xml.NodeListModel的_text键不返回元素的文本内容了。
 FreeMarker类库现在基于JDK 1.2.2的类库来构建了,保证了FreeMarker发布包对JDK 1.2的二进制兼容。
2.2.1 版
发布日期:2003-04-11
这个版本包含了重要的新特性,比如本地的FTL 日期/时间类型,自动包含和自动引入的设置。
日期/时间支持现在是实验阶段,但是我们希望它不会大量的修改。我们希望尽快将它标志为最终版本,所以我们鼓励大家在这个主题功能上发送反馈信息到邮件列表中。
FTL部分的改变
 新的标量类型:日期。要获取更多信息,可以阅读:模板开发指南/数值和类型/类型/标量,模板开发指南/模板/插值和处理日期的内建函数部分来获取详细信息。
Java部分的改变
 新的TemplateModel子接口:TemplateDateModel。要获取更多信息可以阅读程序开发指南/数据模型/标量部分。
 自动包含和自动引入:使用这些新的配置级的设置,你可以在所有模板中包含和引入通用的模板(通常是宏定义的集合),而不用再在模板中输入<#include ...>或<#import ...>了。要获取更多信息,可以阅读Java API文档中关于Configuration的部分。
 新的模板方法:createProcessingEnvironment。这个方法使得在Environment上,并在模板处理之前或在模板处理之后读取环境信息,做一些特殊的初始化成为可能。要获取更多信息可以阅读Java API文档。
 freemarker.ext.beans包的改变:BeanModel,MapModel和ResourceModel现在实现了TemplateHashModelEx接口。
 Bug修复:Configurable.setSettings(Properties)不移除属性值尾部的冗余空格/制表符。
2.2 版
发布日期:2003-03-27
这个发布包引入了一些重要的新特性。而不幸的是,变革也是很痛苦的;我们有一些非向后兼容的改变(参考下面的列表)。而且,对于那些等待所需的原生日期/时间类型的用户,对不起,这个发布中还是没有(因为开发团队一些内部的混乱…要坚信,它马上就会做好)。
没有向后兼容的改变!
 宏变量现在是普通变量。这就是说如果不幸你有同名的宏变量和其它类型变量,那么其它变量将会覆盖宏,所以你的老式模板将会发生故障。如果你有一个通用的宏的集合,你应该使用命名空间特性(可以参考模板开发指南/其它/命名空间部分获取详细内容,译者注)来避免在模板中和其它变量的意外碰撞。
 随着新的命名空间支持的引入,global和assign指令不再是同义的了。assign在当前的namespace中创建变量,而创global建的变量从所有命名空间(就像变量是在数据模型中)中都是可见的。因此,使用assign创建的变量更确定一些,而且会隐藏用global创建的同名变量。如果在模板中使用global和assign混合创建相同的变量,现在就会出现问题了。解决的方法就是查找老的模板中的所有global,用assign来替换。
 保留的哈希表root已经不作为预定义变量(我们已经没有保留变量了)存在了。使用特殊变量表达式来达到相似的效果。但是,我们没有和root相等的替换,因为在变量范围内的改变是由命名空间的引入而引起的。你也许应该使用.globals或.namespace。
 不能再通过BeansWrapper获取原生的Java数组,布尔值,数字值,枚举值,迭代器和用作TemplateScalarModel的资源包了。这种方式,通过BeansWrapper来包装的数字对象限制于FreeMarker的数字格式化机制。而且,布尔值可以使用内建函数?string来格式化。
 Configuration.setServletContextForTemplateLoading方法的方法签名已经改变了:第一个参数现在是Object而不是javax.servlet.ServletContext了。因此,你不得不重新编译调用了这个方法的类。这个改变用来防止当javax.servlet的类不可用,而你需要调用这个方法时的类加载失败是必须的。
 这个发布包引入了一个编译时空白移除器(可以参考模板开发指南/其它/空白处理部分获取详细内容,译者注),它会去掉一些在FreeMarker标签和注释周围典型的多余空白。这个属性默认是开启的!如果你生成的是像HTML那样中性的空白,很多情况下这不会引起问题。但是如果它在你生成的输出中引起了不合意的格式化,你就可以使用config.setWhitespaceStripping(false)方法来关闭它。而且,你可以使用新的ftl指令在每个模板上来开启/关闭它。
 引入了一些新的指令:nested,import,escape,t,rt,lt。这就是说如果你很不幸在模板中包含了一些比如<nested>的东西,那么它就会被误当作是指令。将来要避免这种问题,我们建议每个人将老式语法都转换为新式语法(“严格的语法”)。从后续的发布包开始,严格的语法将会是默认的语法。我们计划发布一个转换工具来转换老式模板。要获取更多内容,请阅读:参考文档/废弃的FTL结构/老式FTL语法部分。
 由FreemarkerServlet创建的数据模型现在使用自动的范围,所以编写Application.attrName,Session.attrName,
Request.attrName不再是强制的了;编写attrName就足够了(要获取更多内容,可以阅读程序开发指南/其它/在Servlet中使用FreeMarker)。如果老式模板依赖不存在的某些顶级变量,那么模板就会失效。
 FreemarkerServlet现在使用模板文件输出的编码,除非你在初始化参数ContentType中指定编码,比如text/html; charset=UTF-8。
 模板路径的格式比以往更加限制了。路径绝对不能使用/,./,../和://这种在URL路径中(或在UN*X路径)有其它含义的字符。字符*和?是保留的。而且,模板加载器也不想路径是以/开头的。要获取更多内容,请阅读:程序开发指南/配置/模板加载部分。
 如果转换的调用没有参数,那么到现在为止,TemplateTransformModel.getWriter已经可以接受null作为参数映射了。从现在开始,它会接受一个空的Map作为替代。要注意之前的API文档没有说明如果没有参数时,它通常接受null,所以没有关系,只有极少数的类使用了这个设计上的失误。
FTL(FreeMarker模板语言)部分的改变
 用户自定义指令:转换和宏调用的语法已经统一了;它们可以以用户自定义指令的相同方式来调用。这也就意味着宏开始支持命名参数和嵌套内容(比如--现在被废弃了-- transform指令)。例如,如果你有一个称为sect的宏,你可以通过编写<@sect title="Blah" style="modern">Blah blah...</@sect>来调用它。要获取更多信息,可以阅读:模板开发指南/其它/定义你自己的指令部分。
 宏现在是普通变量了。这大大简化了FreeMarker的语义,而且可以提供更多的灵活性;比如,你可以传递宏作为变量给其它的宏和转换。对于碰撞的问题,通常使用宏和变量名,我们提供了一个强大的解决方案:命名空间。
 命名空间:如果你想组合宏和转换(还有其它变量)的集合(“类库”),命名空间就是无价的,那么在任何模板中使用它们就不用担心偶然和应用程序中具体或临时变量的名称碰撞,或者是你想在同一模板中使用的其它集合。如果FreeMarker用户共享宏/转换的集合,则这一点是非常重要的。要获取更多信息,可以阅读:模板开发指南/其它/命名空间部分。
 随着命名空间的引入,和变量有关的术语要改变了。因此,assign不再是global的代名词了。assign指令没有废弃,应该可以在任何地方来代替global。在新的做法中,assign在当前命名空间中创建变量。而global指令创建的变量则在所有的命名空间(就像是在数据模型根上的变量)中都是可见的。在当前命名空间中使用assign创建的变量会隐藏使用global指令创建的同名变量。
 ftl指令:使用这个指令,你可以为FreeMarker提供关于模板的信息,比如模板的编码(字符集),改变所使用的语法等。而且,这个指令可以帮助你去编写更少依赖FreeMarker配置信息的模板,而且也会帮助第三方工具来标识和正确解析FreeMarker模板。要获取更多信息,请参考:参考文档/指令参考文档/ftl指令部分。
 空白剥离:FreeMarker现在可以自动移除一些在FreeMarker标签或注释周围典型的多余空白,就像缩进空格前和<#if ...>标签之后的换行符。更多信息请阅
读:模板开发指南/其它/空白处理/空白剥离部分。
 在一个区块中对所有插值应用一个普通的(转义的)表达式的新指令:escape。名称来自于这个指令对自动的HTML插值转义的普通用法。
 使用内建函数string处理数字格式化的新的更好的方式,foo?string(format)用来代替不太自然的foo?string[format]。
 string内建函数也可以作用于布尔值了。比如:${spamFilter?string("enabled", "disabled")}。要获取更多信息,请阅读参考文档/内建函数参考文档/处理布尔值的内建函数部分。
 对于使用内建函数string输出的布尔值默认的字符串可以使用boolean_format设置来配置。
 注释可以在FTL标记和插值内放置了。比如:<#assign <#-- a comment --> x=3>。
 嵌入在变量名中所有字母和数字,还有$已经被允许了(就像在Java程序语言中一样)。因此你可以使用口音,阿拉伯字母,中文字符等。
 字符串文字可以使用撇号来引用。"foo"和'foo'是相等的。
 新的处理字符串的内建函数:index_of,last_index_of,starts_with,ends_with,replace,split,chop_linebreak,uncap_first。
 新的处理序列的内建函数:sort,sort_by。
 对于专家级用户检查变量类型的内建函数。请看参考文档/内建函数参考/很少使用的和专家级的内建函数/is_...判断函数族部分。
 对于专家级用户来创建特定Java TemplateModel实现的变量的新的内建函数:请看参考文档/内建函数参考/很少使用的和专家级的内建函数/new函数
 新的内建函数,namespace,来获取宏的命名空间。
 新的表达式类型:特殊变量表达式。当我们引入新的预定义变量时,为了避免向后兼容性的问题。从现在开始,特殊变量表达式可以用来访问它们了。
 新的指令:t,rt和lt指令允许你在极端的FTL应用程序中做一些明确的空白移除。要获取更多信息,可以阅读参考文档/指令参考文档/t,lt,rt部分。
 assign,local和global现在可以捕捉生成的输出的变量的嵌套模板片段。这废弃了capture_output转换。更多信息可以阅读:参考文档/指令参考文档/assign指令部分。
 批量定义(比如<#assign x=1, y=2, z=3>)不再需要逗号来分隔定义(比如),尽管这仍然允许保留向后的兼容性。
 包含//:的路径被认为是绝对路径。
 include和transform指令不再需要分号来分隔模板或从参数列表中分隔转换变量的名称,尽管这仍然允许保留向后的兼容性。
 无#标记的语法已经废弃了(但是仍然可以使用)。也就是说,你应该编写<#directive ...>来代替<directive ...>,</#directive ...>来代替<directive ...>。
 foreach已经被废弃了(当仍然可以使用)。使用来list代替。
 Bug修复:在哈希表和序列的构造方法(比如[a, b, c])中的未定义变量不会引起错误了。
 Bug修复:如果有很多的字符串链式串接,比如"a"+x+"a"+x+"a"+x+"a"+x+"a"+x,字符串的连接有性能问题。
Java部分的改变
 任意的JSP自定义标记可以在FreemarkerServlet驱动的模板中用作FreeMarker转换变量。更多信息可以阅读:程序开发指南/其它/在Servlet中使用FreeMarker部分。
 BeansWrapper的很多改进:
 BeansWrapper不再暴露任意的对象成为TemplateScalarModel,只是java.lang.String和Character对象。这样的方式,由BeansWrapper包装的数字对象受限于FreeMarker的数字格式化机制。有一个副作用,非字符串和非数字对象以前接受平等和不平等的操作(因为它们有字符串的表示形式)现在会引起引擎在试图比较时抛出异常。
 java.lang.Character对象通过BeansWrapper作为标量被暴露出来。
 实验失败:使用BeansWrapper的setSimpleMapWrapper方法,你可以配置它来包装java.util.Map成TemplateHashModelEx,而且不会暴露出对象的方法。
 TransformControl接口(之前是实验阶段):如果由TemplateTransformModel.getWriter方法返回的Writer实现了这个接口,它可以指示引擎来跳过或重复计算嵌入的内容,还能获取有关在计算嵌入内容期间抛出的异常通知。要注意onStart和afterBody方法现在允许抛出IOException异常。要获取更多信息可以阅读API文档。
 本地化查找可以通过Configuration新的方法来关闭:set/getLocalizedLookup,clearTemplateCache。
 新的freemarker.cache.CacheStorage接口允许用户自定义使用cache_storage设置模板缓存策略插件。核心包现在附带两个实现:SoftCacheStorage和StrongCacheStorage。要获取更多信息可以阅读:程序开发指南/配置/模板加载部分。
 可以使用字符串名和字符串值通过Configurable超类(比如Configuration)的新方法setSetting(String key, String value)来设置。而且你可以使用setSettings方法从.properties文件中来加载配置信息。
 其它新的Configuration方法:clearTemplateCache,clearSharedVariables,getTemplateLoader和clone。
 TemplateTransformModel接口的改变:getWriter现在抛出IOException异常,而且如果转换变量不支持内容体则返回null。
 到现在为止,如果不用参数来调用转换变量,TemplateTransformModel.getWriter已经接受null作为参数Map。从现在开始,它会接受一个空的Map。要注意之前API文档中没有强调如果没有参数时,它会一直接受null,所以只有极少数的类使用了这个设计失误。
 FreemarkerServlet的多种改进:
 数据模型现在可以使用自动的披露范围,所以编写Application.attrName,Session.attrName,Request.attrName不再是强制的了;编写attrName就足够了。要
获取更多内容,可以阅读程序开发指南/其它/在Servlet中使用FreeMarker部分。
 FreemarkerServlet现在使用模板文件输出的编码,除非你在初始化参数ContentType中指定编码,比如text/html; charset=UTF-8。
 所有Configuration级的设置可以使用Servlet的初始参数来配置(template_exception_handler,locale,number_format等)。
 Servlet内部使用的对象包装器现在可以对Configuration实例设置为默认的对象包装器。
 不再关注于不属于存在session的请求的session的创建,提高了可扩展性。
 JDOM独立了XML包装:freemarker.ext.xml.NodeListModel是freemarker.ext.jdom.NodeListModel的重新实现而不再依赖于JDOM;你不再需要JDOM.jar了。新的NodeListModel会自动使用W3C DOM,dom4j或JDOM,这依赖于哪个类库是可用的(也就是说,依赖于你传递哪个对象给它的构造方法)。
 Bug修复:WebappTemplateLoader,使用Tomcat时由于资源缓存导致的模板更新不正常。现在WebappTemplateLoader会将资源直接作为File访问,如果可能的话,就会绕过缓存。
 FreemarkerServlet的各种Bug修复:
 如果通过RequestDispatcher.include调用,Servlet现在加载正确的模板。
 HttpServletRequest对象的缓存目前符合Servlet的规范。
 TemplateException异常在某些情况下被抑制了,导致页面上半部分的呈现是没有错误消息的。
 Bug修复:如果javax.servlet类不可用时,FreeMarker不会正常工作,因为Configuration明确地引用了javax.servlet.ServletContext。
 Bug修复:如果类仅仅在WEB-INF,中可用时,它们可能不会被发现,FreeMarker会尝试去动态加载类。
 Bug修复:Template构造方法(就是Configuration.getTemplate)有时会抛出TokenMgrError错误(一个不检查的异常)而不是ParseException异常。
其它改变
Web应用程序相关的示例都被替换了。
在最终版之前发行包的历史
最终发布版和候选发布2的区别
 你可以用Configuration和其它Configurable子类的setSettings方法从.properties文件中加载配置信息。
 新的处理字符串的内建函数:uncap_first
 Bug修复:当给一个模板暴露XML文档,而且和使用了Jaxen的XPath来访问它时,会发生ClassCastException异常。
 Bug修复:在某些条件下,如果你使用了非静态的默认Configuration实例,模板缓存已经使用了不好的Configuration实例加载模板。
候选发布2和候选发布1的区别
 没有向后兼容的改变!:FreemarkerServlet现在使用模板文件输出的编码,除非你在初始化参数ContentType中指定编码,比如text/html; charset=UTF-8。
 和RC1相比没有向后兼容的改变!:转换变量capture_output在当前模板中(比如assign指令)使用var参数创建变量,就不是全局变量。
 使用内建函数string格式化数字新的更好的方式是foo?string(format),代替了原来的不太自然的foo?string[format]。
 内建函数string现在也可以作用于布尔值了。比如:${spamFilter?string("enabled", "disabled")}。要获取更多信息可以阅读参考手册/内建函数参考/处理布尔值的内建函数部分。
 使用内建函数string输出布尔值的默认字符串可以使用来boolean_format设置。
 字符串文本可以使用撇号来引用。"foo"和'foo'是相等的。
 新的freemarker.cache.CacheStorage接口允许用户自定义使用cache_storage设置模板缓存策略插件。核心包现在附带两个实现:SoftCacheStorage和StrongCacheStorage。要获取更多信息可以阅读:程序开发指南/配置/模板加载部分。
 可以使用字符串名和字符串值通过Configurable超类(比如Configuration)的新方法setSetting(String key, String value)来设置。
 其它新的Configuration方法:getTemplateLoader,clone。
 assign,local和global现在可以捕捉生成的输出的变量的嵌套模板片段。这废弃了capture_output转换。更多信息可以阅读:参考文档/指令参考文档/assign指令部分。
 TemplateTransformModel接口的改变:getWriter现在抛出IOException异常,而且如果转换变量不支持内容体则返回null。
 如果转换的调用没有参数,那么到现在为止,TemplateTransformModel.getWriter已经可以接受null作为参数映射了。从现在开始,它会接受一个空的Map作为替代。要注意之前的API文档没有说明如果没有参数时,它通常接受null,所以没有关系,只有极少数的类使用了这个设计上的失误。
 TemplateControl接口的改变:onStart和afterBody方法现在允许抛出IOException异常。
 包含//:的路径被认为是绝对路径。
 新的处理字符串的内建函数:index_of,last_index_of,starts_with,ends_with,replace,split,chop_linebreak。
 新的处理序列的内建函数:sort,sort_by。
 所有Configuration级的设置可以使用Servlet的初始参数来配置(template_exception_handler,locale,number_format等)。
 Bug修复:如果类仅仅在WEB-INF,中可用时,它们可能不会被发现,FreeMarker会尝试去动态加载类。
 Bug修复:当你调用setTemplateLoader方法时Configuration对象的setLocalizedLookup(false)方法会被覆盖。
 Bug修复:如果有很多的字符串链式串接,比如"a"+x+"a"+x+"a"+x+"a"+x+"a"+x,字符串的连接有性能问题。
 Bug修复:空白剥离不能作用于多行之间跨越的标签了。
 Bug修复:移除了一些JDK 1.3上的依赖,所以FreeMarker可以在JDK 1.2.2上构建了。
预览版2和候选发布1的区别
 ftl现在更加严格了,而且不允许自定义参数了。要关联模板的自定义属性,如果有需求的话,我们后期可能要添加一个新的指令。
 escape指令不再影响数字插值(#{...})了,因为它会和如?html的字符串转义引起错误。
 freemarker.cache.TemplateLoader的normalizeName方法已经被移除了,因为它已经引发了很多并发症了。相反,正常化发生在TempateCache中的一个单独点。结果,FreeMarker现在对格式化模板路径,比如由核心解释的/../就更加严格了。
 实验失败:使用BeansWrapper的setSimpleMapWrapper方法,你可以配置它来包装java.util.Map成TemplateHashModelEx,而且不会暴露出对象的方法。
 新的Configuration方法:set/getLocalizedLookup,clearTemplateCache,clearSharedVariables。
 Environment API的很多清理。
 更好的JSP标准承诺:JSP页面范围变量是在模板中(而不是在数据模型中)创建的全局变量。
 JDOM独立了XML包装:freemarker.ext.xml.NodeListModel是freemarker.ext.jdom.NodeListModel的重新实现而不再依赖于JDOM;你不再需要JDOM.jar了。新的NodeListModel会自动使用W3C DOM,dom4j或JDOM,这依赖于哪个类库是可用的(也就是说,依赖于你传递哪个对象给它的构造方法)。
 Bug修复:WebappTemplateLoader,使用Tomcat时由于资源缓存导致的模板更新不正常。现在WebappTemplateLoader会将资源直接作为File访问,如果可能的话,就会绕过缓存。
 Bug修复:使用MultiTemplateLoader子类加载的模板在模板更新延迟时间到达后(默认是5秒),不管模板文件是否改变,模板也从模板缓存中移除了。
如果你有很多模板或者模板更新延迟时间设置为0的话,这可以引起很多额外的服务器负载。
 Bug修复:在哈希表和序列构造方法(如[a, b, c])中的未定义变量不会引起错误了。
预览版1和预览版2的区别
 所有的16位Unicode字符和数字,还有$字符(正如在Java语言中)现在允许出现在标识符中了。因此你可以使用口音的字母,阿拉伯字母,中文字符等在模板中用作标识符。
 宏现在可以对嵌套内容创建循环变量。要获取更多信息,可以阅读模板开发指南/其它/自定义指令/宏和循环变量部分。
 新的指令:t,rt和lt指令允许你在极端的FTL应用中进行明确的空白移除。要获取更多信息可以阅参考文档/指令参考文档/t,lt,rt指令读部分。
 联合命名空间的定义语法已经从<#assign foo=123 namespace=myLib>改变成了<#assign foo=123 in myLib>,因为之前的语法和批量定义很相似而会引起混乱。
 批量定义(比如<#assign x=1, y=2, z=3>)不再需要逗号来分隔定义(比如),尽管这仍然允许保留向后的兼容性。
 位置参数传递已经支持宏作为普通命名参数传递的速记形式调用。要获取详细信息可以阅读参考文档/指令参考文档/用户自定义指令部分。
 新的内建函数,namespace,获取当前执行宏的命名空间。
 TransformControl接口(之前是实验阶段):如果由TemplateTransformModel.getWriter方法返回的Writer实现了这个接口,它可以指示引擎来跳过或重复计算嵌入的内容,还能获取有关在计算嵌入内容期间抛出的异常通知。要注意onStart和afterBody方法现在允许抛出IOException异常。要获取更多信息可以阅读API文档。
 Jython包装器现在可以包装任意的Java对象,而不仅仅是PyObject了。如果一个既不是TemplateModel也不是PyObject对象被传递到包装器,首先要强制转换成使用了Jython自己包装机制的PyObject,之后包装成TemplateModel作为任意的PyObject。
 Environment API中的一些清理。
 和Web应用程序相关的示例已经替换了。
 Bug修复:使用URLTemplateLoader子类加载的模板在模板更新延迟时间到达后(默认是5秒),不管模板文件是否改变,模板也从模板缓存中移除了。如果你有很多模板或者模板更新延迟时间设置为0的话,这可以引起很多额外的服务器负载。
 Bug修复:即使TemplateException调试处理器在使用中(所以你可能会得到500的错误报告而不是调试信息),FreeMarkerServlet也抛出ServletException异常。
2.1.5 版
发布日期:2003-02-08
Java部分的改变
 Bug修复:修改了一个强制缓存频繁通过访问URL和多个模板加载器来重新加载模板bug。使用URLTemplateLoader和MultiTemplateLoader子类加载的模板在模板更新延迟时间到达后(默认是5秒),不管模板文件是否改变,模板也从模板缓存中移除了。如果你有很多模板或者模板更新延迟时间设置为0的话,这可以引起很多额外的服务器负载。
 Bug修复:很多JythonWrapper中的异常被解决了,和Jython的整合也更顺畅了。Jython包装器现在可以包装任意的Java对象,而不仅仅是PyObject了。如果一个既不是TemplateModel也不是PyObject对象被传递到包装器,首先要强制转换成使用了Jython自己包装机制的PyObject,之后包装成TemplateModel作为任意的PyObject。
2.1.4 版
发布日期:2002-12-26
Java部分的改变
 Bug修复:当自动查找使用日志类库时,Log4J现在可以被发现了。
 Bug修复:如果目录在系统变量"user.dir"中被指定但是不可读时,在Configuration的静态初始化器中一个异常不再被抛出。
2.1.3 版
发布日期:2002-12-09
FTL部分的改变
 Bug修复:内建函数cap_first也可以做double所做的事情了。
其它改变
 从现在开始,FreeMarker模板的官方扩展名是ftl了,而不再是fm。(这也是模板语言的名称;FTL,FreeMarker Template Language。)
 Web应用程序示例又调整了,在明确的(命名的)包中,JDK 1.4以下的类可以不再引入从默认(未命名)包中的类了。我们的Web应用示例使用了默认包下的类,现在都被移入到命名的包中了。
2.1.2 版
发布日期:2002-11-28
FTL(FreeMarker模板语言)的改变
 FreeMarkerServlet现在有一个设置响应头部信息Content-Type的配置项,默认是text/html。之前是不设置内容类型的,这就当和希望有这个属性的软件(比如OpenSymphony的SiteMesh)整合时不是很好。
 当映射到URL的扩展名而不是URL路径前缀时,FreeMarkerServlet现在可以正确工作了。
 你可以在Java代码中通过调用Environment.include(templateName, charset, parse)仿照include指令的调用。
 Bug修复:当Template.process()从另外一个模板处理中被调用时,那么当返回时就设置currentEnvironment为null,因此就损坏了父模板的处理。
 Bug修复:在JDOM支持中的_descendant键,在当应用于文档节点时,不正确地在结果外面留下文档根元素。
 Bug修复:因为我们错误的假设了JDK 1.4的benan introspector的特定行为,在非公有且实现了这个接口的类中调用了公共接口方法引起了JDK 1.4的异常。
其它改变
 手册中很多小的补充。
 文档HTML页面不会再尝试从Internet去加载SourceForge的Logo。
 默认的Ant任务是jar而不是dist。
2.1.1 版
发布日期:2002-11-04
FTL(FreeMarker模板语言)的改变
 多类型的字符串和数字变量或字符串和日期变量,现在当在${...}插值中使用时,变量使用数字或日期值作为输出,而不是字符串值了。这实际上是使得字符串/数字或字符串/日期变量的字符串部分变得没有用了。
 Bug修复:操作符“or”(||)当它左边的操作数是一个复合表达式时(比如在false
|| true || false中的第二个||;这会被算为false,但却应该是true)会出现错误。
 Bug修复:在注释中的小于符号会混淆FTL解析器(比如<#-- blah < blah -->);在有问题的注释之后,一切都被注释掉了。
 Bug修复:比较两个数字常量(比如3 == 3)会引起FTL解析器内部的错误,而且以错误中止模板的处理。
 实验阶段的日期/时间类型支持被移除了,因为看起来这个初始实现会有误导。FreeMarker 2.2将会支持日期/时间。
Java部分的改变
 Bug修复:使用BEANS_WRAPPER包装的Number会被用包装对象的toString()方法来展示。现在它们根据number_format设置来呈现,因为既是字符串又是数字的多类型变量现在使用数字值来代替字符串值输出了。
 实验阶段的日期/时间类型支持被移除了,因为看起来这个初始实现会有误导。FreeMarker 2.2将会支持日期/时间。
2.1 版
发布日期:2002-10-17
模板和Java API并不完全兼容2.0版。你需要重温已有的代码和模板,或者对新的项目使用2.1版。对这个不便非常抱歉;FreeMarker从1.x系列已经经历了一些革命性的改变。我们希望一切都能尽快足够成熟并提供(基本)向后的兼容性。要注意有一个向后兼容的标志,可以通过Configuration.setClassicCompatible(true)设置,它可以让新的FreeMarker来仿真FreeMarker 1.x的奇怪之处。
FTL(FreeMarker模板语言)部分的改变
 在模板中更严格地发现意外错误,当一些信息出现问题时避免展示不正确的信息:
 试图访问一个未定义的变量引起的错误并中止模板处理(至少是默认情况;详情见后)。在更早的版本中,未定义的变量会被静默的视为空(长度为零)的字符串。然而,你可以在模板中使用一些新的内建函数来处理未定义的变量。看待这个问题的另外一种方式是从模板设计者的角度来说,null值不再存在了。任何引用的变量必须是一个已经定义的变量。
要注意不论程序员如何配置FreeMarker来忽视特定的错误(比如说,未定义变量),然后跳过有问题的部分继续模板处理。这个“宽松的”策略应该仅仅用于不用来展示严格信息的站点。
 新的变量类型:boolean(布尔值,译者注)。在if/elseif和逻辑操作符(&&,||,!)的操作数的地方必须是布尔值。空的字符串不再被视为逻辑false。
 局部和全局变量。更多信息可以阅读:模板开发指南/其它/在模板中定义变量部分。
 对于宏的局部变量。你可以在宏定义体中使用local指令来创建/替换局部
变量。
 你可以使用global指令创建/替换全局(非局部)变量。
 include指令现在默认将传递的文件名视为是相对于包含的模板的路径。要指定绝对的模板路径,你现在必须在它们前面加一个斜线。
 指令现在可以使用捕获算法(和Zope系统很相似)来查找包含的模板。基本上来说说,如果一个模板没有在最开始查找的位置找到,那么就会在父目录查找。这不是默认的做法,相反,它是一个由新语法元素引起的。
 严格语法模式:允许你来生成任意的SGML(XML)而不用担心和FreeMarker指令的冲突。要获取更多信息请阅读:参考文档/废弃的FTL结构/老式FTL语法部分。
 简明的注释:你可以使用<#-- ... -->来替代<comment>...</comment>。
 你可以使用setting指令在模板中来改变本地化设置(和其它的配置)。
 明确地清空输出缓存的指令:flush
 通过变量root访问的顶级(根)哈希表变量现在是保留的名字了。
 混乱名称的function指令现在更名为macro。
 字符串文字支持各种新的转义序列(可以参考模板开发指南/模板/表达式/直接确定值部分获取详细内容,译者注),包括了UNICODE转义(\xCODE)。
 compress指令移除换行符时更加保守了。
 大写首字母的内建函数:cap_first
 生成即时解释模板的内建函数:interpret
 stop指令有一个可选的参数来描述终止的原因。
 更好的错误消息
 新的变量类型:日期。日期支持还在实验阶段。将来可能会大量改动。如果你要使用的话一定要记住这点。
Java部分的改变
 ObjectWrapper:你可以直接将非TemplateModel对象放入哈希表,序列和集合中,它们会自动被合适的TemplateModel实现类来包装。根据这点,对象的API已经改变了,它们在模板中(SimpleXXX)是暴露出来的,比如在SimpleHash中,老式的put(String key, TemplateModel value)现在已经是put(String key, Object object)了。而且,你可以给Template.process方法传递任意对象类型作为数据模型。基于ObjectWrapper的另外一种方法可以给设计者暴露出任意Java对象中的成员。更多信息可以阅读:程序开发指南/数据模型/对象包装器,程序开发指南/其它/Bean包装器和Jython包装器部分获取详细信息。
 Configuration对象被作为持有所有FreeMarker相关的全局设置的一个核心点引入了,还有通常想在任意模板中可用的变量。而且它封装了模板缓存,可以用于加载模板。更多信息请阅读程序开发指南/配置部分。
 TemplateLoader:插件式的模板加载器,从模板加载中分隔缓存。
 TemplateNumberModel不再控制它们的格式了。它们只是存数数据(也就是数字)。数字格式化由基于locale和number_format设置的FreeMarker核心来完成了。这个逻辑应用于新的实验阶段的日期类型
 引入了TemplateBooleanModel:仅实现了这个接口的对象可以在true/false条件中用作布尔值。更多信息可以阅读程序开发指南/数据模型/标量部分。
 引入了TemplateDateModel:实现了这个接口的对象被认为是日期,可以用本地化敏感的格式来格式化。日期支持在Freemarker 2.1版中还出于实验阶段。在将来可能会大幅度改变,所以如果你使用的话要注意这点。
 废弃了TemplateModelRoot接口:在FreeMarker 2.1版中,你可以仅仅使用TemplateHashModel实例作为替代。这就是因为一个重要的架构改变。在模板中设置或定义的变量存储在存在于呈现模板的分离的Environment对象中。因此,模板不能修改根哈希表。
 转换的变化
 完全重写了TemplateTransformModel接口。更加灵活,没有规定输出控制。更多信息可以阅读程序开发指南/数据模型/指令部分。
 transform指令现在可以接受一个可选的键/值对设置:<transform myTransform; key1=value1, key2=value2 ...>。更多信息可以参考transform指令部分。
 转换随着FreeMarker核心现在对所有模板默认都是可用的了。也就是说<transform html_escape>将会调用freemarker.template.utility.HtmlEscape转换。更多信息可以阅读程序开发指南/配置/共享变量部分。
 用户自定义TemplateModel对象现在可以使用Environment实例访问运行时环境(读取和设置变量,获取当前的本地化设置等),这个实例可以通过静态方法Environment.getCurrentEnvironment()来得到。因此TemplateScalarModel.getAsString就改变了:它没有本地化参数。
 在模板处理期间运行时错误发生时(比如访问一个不存在的变量),TemplateExceptionHandler使得用来定义你自己的规则处理是可能的。比如,你可以终止模板的处理(建议对大多数项目使用),或者略过出问题的区域,继续进行模板处理(这和老式的做法相似)。调试模式已经被去掉了,使用TemplateExceptionHandler.DEBUG_HANDLER或HTML_DEBUG_HANDLER来代替。
 日志:FreeMarker日志记录特定的事件(比如运行时错误)。要获取更多信息可以阅读程序开发指南/其它/日志部分。
 去除了SimpleIterator,但是提供一个TemplateCollectionModel实现:SimpleCollection。
 算术引擎是插件式的了(Configuration.setArithmeticEngine)。核心的发布包有两个引擎:ArithmeticEngine.BIGDECIMAL_ENGINE(默认的)会转换所有数字值到BigDecimal类型之后对它们执行操作,ArithmeticEngine.CONSERVATIVE_ENGINE使用(或多或少)Java语言的扩大转换,而不是把所有数值转换成BigDecimal类型。
 freemarker.ext.beans包的改变:Java Bean适配器层已经做出了很多重大变化。首先,BeansWrapper已经不再是静态的工具类了。你现在可以创建它的实例了,而且每个实例都可以有它自己的实例缓存策略和安全设置。这些安全设置也是新的,你现在可以创建Java Bean包装器在模板环境中来隐藏被认为是不安全或不合适的方法。默认情况下,你可以不用再从模板中(尽管你可以手动关闭这些保障措施)调用如System.exit()的方法。StaticModel和
StaticModels类已经移除了;现在,它们的功能可以用BeansWrapper.getStaticModels()方法来替换。
 freemarker.ext.jython包:FreeMarker现在可以直接使用使用了Jython包装器(可以参考程序开发指南/其它/Jython包装器部分获取详细内容,译者注)的Jython对象作为数据模型。
 freemarker.ext.jdom包的改变:这个包现在使用Jaxen包来代替它的前任者,werken.xpath包来计算XPath表达式。因为Jaxen是werken.xpath的继任者,这也可以认为是一个升级。因此,命名空间前缀现在可以在XPath表达式中被识别了,而且XPath的整体性能也更好了。
 更好的错误报告:如果模板的处理由TemplateException异常的抛出而终止,或者使用<#stop>指令,FreeMarker现在会使用关于模板源码的行和列的数字来输出执行轨迹。
 输出写入到简单的Writer中,而不再是PrintWriter了。这块的重新设计使得FreeMarker在模板处理期间不会再将IOException异常吞掉了。
 各个API的清理,主要是移除多余的构造方法和重载方法。
其它改变
 文档从头开始重写。
最终发行版和RC1的区别
 添加了对日期模型和本地化敏感的日期格式化的支持。日期支持在FreeMarker 2.1中是实验阶段的。在将来可能会大量改变。如果要使用的话请记住这点。
 添加内建函数default,它可以给未定义的表达式赋指定的默认值。
 SimpleIterator已经移除了,引入了SimpleCollection。
 算术引擎是插件式的了。现在的核心包含两个算术引擎:ArithmeticEngine.BIGDECIMAL_ENGINE和ArithmeticEngine.CONSERVATIVE_ENGINE。
 BeansWrapper支持新的暴露等级:EXPOSE_NOTHING。
 移除了Constants接口。常量..._WRAPPER从Constants移入到ObjectWrapper中了,常量EMPTY_STRING被移入到了TemplateScalarModel中,常量NOTHING被移入到了TemplateModel中,常量TRUE和FALSE被移入到了TemplateBooleanModel中。
 JAVABEANS_WRAPPER被重命名为BEANS_WRAPPER。
 Configuration.get,put和putAll被重命名为getSharedVariable,setSharedVariable和setAllSharedVariables。
 Configuration.getClassicCompatibility,setClassicCompatibility被重命名为isClassicCompatible,setClassicCompatible。
 用useReflection参数重载的Template.process方法被移除了。但是
现在我们在Configuration中有了setObjectWrapper方法,所以你可以在那里设置合适的根对象包装器。
 一些多余的重载方法被移除了;这些改变向后兼容了RC1版。
 很多小的JavaDoc和手册的改进。
 Bug修复:include指令错误地计算相对路径的基路径。
 Bug修复:我们偶尔使用J2SE 1.3版,但是FreeMarker 2.1必须能够运行在J2SE 1.2上。
2.01 版
主要的改进是错误报告。现在异常会更加信息化,因为它们是和完整的行号和列信息一同报出的。
2.0和2.01版的API改变仅仅是消除缓存监听器/缓存事件API。现在,如果更新模板文件失败,执行会抛出异常返回给调用者处理。如果你想记录当模板文件更新成功时发生的日志,你可以在FileTemplateCache中覆盖logFileUpdate()方法
2.0 版
FreeMarker 2.0最终版发布于2002-04-18。相对于以前版本的变化,2.0 RC3版是相当轻微的。
Bug修复
 处理null值的一些bug,Lazarus不能如FreeMarker经典版做同样的事情。FreeMarker在传统中,在适当的范围内null值被视作和空字符串是相等的。在这一点上,据我们所知,这方面和FreeMarker经典版有向后的兼容性。
 字符串文字现在可以包含换行符。这是修复的FreeMarker经典版向后兼容性的问题。
模板语言的改变
 你可以使用额外的内建函数myString?web_safe在转换字符串成“web安全”的相等形式,有可能出问题的字符,比如’<’会转换成&lt。
 在显示带有小数部分的数字,渲染器尊重模板本地化的小数分隔符,所以,比如在欧洲大陆,你会看到1,1而在美洲,会看到1.1。
API的改变
 在TemplateScalarModel接口中的getAsString()方法现在使用java.util.Locale作为一个参数。对于大多数情况,这是一个为以后备用
做的准备。默认实现情况下,SimpleScalar这个参数是不用的。如果你自己实现了这个接口,你的实现类可能会忽略这个参数。因此,最好使用特定的实现。
 FileTemplateCache的构造方法改变了。如果你正在使用文件系统上的绝对路径作为模板的位置,你需要传递一个java.io.File实例来告知位置。如果你使用了字符串参数的构造方法,这就意味着采用相对于类加载器的类路径。
集锦
Ant构建脚本build.xml现在包含了一个构建war文件的目标,这里面包含有Hello, World和留言板示例。它构建了fmexample.war文件。比如,如果你使用Tomcat作为开箱即用的配置,你可以在<TOMCAT_HOME>/webapps下放置它,之后你可以使用http://localhost:8080/fmexamples/servlet/hello和http://localhost:8080/fmexamples/servlet/guestbook来访问Hello, World和留言板示例。
2.0 RC3版
FreeMarker 2.0 RC3版发布于2002-04-11。这个发行主要是专注于修改RC2版报告出的bug。
Bug修复
 在<include …>中定义的变量在包含的页面中是不可见的。这个问题被修复了。
 JavaCC解析器没有被配置去处理正确的Unicode输入。现在,Unicode支持已经可以正常工作了。
 当比较数字和null值时有一个bug。应该返回false,但是会抛出异常。这个问题已经修复了。
模板语言的改变
 包含指令的语法已经改变了。要说明未解析的包含文件,你应该这样做:
你也可以这样来说明被包含文件的编码:
 内建函数myString?trim被加入来取出字符串头部和尾部的空白。
API的改变
 TemplateEventAdapter机制被取出了。它从来没有以有用的方式来创建,而且我们
<include "included.html" ; parsed="n" >
<include "included.html" ; encoding="ISO-8859-5">
预料2.1版本将会对日志事件有更完整的支持。
 模板缓存机制被精简了。
 FileTemplateCache现在可以被配置用来加载相对于类加载器的文件,调用Class.getResource()方法。这允许模板被绑定成jar文件或war文件,很容易用来部署基于Web的应用程序。
2.0 RC2版
FreeMarker 2.0 RC2版发布于2002-04-04。这里是对第一次发布的改变进行摘要性的总结。
模板语言的改变
 内建函数功能由一个新的操作符号’?’来提供。因此,myList?size提供一个列表中的元素数量。类似地,myString?length提供字符串的长度,myString?upper_case把字符串中的字符转换为大写形式,提供一个包含哈希表键的序列。可以阅读参考文档/内建函数参考文档部分来获取所有可用内建函数的信息。
 数字比较现在可以使用“自然”运算符<和>了,但是这里也有“Web-安全”的选择,比如和\lt和\gt,因为这些字符的使用会混淆HTML编辑器和解析器。注意rc1版和rc2版中的这些变化,现在它们以反斜线开头。这会有点不对称,因为如果你使用自然的大于或大于等于号(如>或>=),那么表达式必须在括号内。而对于其它运算符,括号是可选的。
 在迭代循环中,也就是foreach或list块,循环中的当前计数变量是可用的,就是特殊变量index_count。这里index是迭代变量的名字。一个称作index_has_next的布尔值变量也是定义用来指示本次迭代之后是否还有元素。注意索引是从零开始的,所以在实践中要加一来使用。
 <#break>指令现在可以被用来中断<#foreach...>或<list...>循环。(在此之前的版本,它只可以作用于switch-case块中)有一个叫做<#stop>的新的指令,当遇到它时,只是展厅模板的处理。这对于以调试为目的工作是很有用的。
 当调用暴露给页面的Java代码方法时,使用freemarker.ext.*包中的代码,有允许你指示想传递值的数值类型的内建函数。比如,如果你有两个方法,一个使用int类型的参数而另外一个使用long类型的,你想传递一个值,那么你就不得不确定要使用哪个方法,myMethod(1?int)或myMethod(1?long)。如果有一个方法是给定名字的这就不需要了。
 范围可以用来从列表中获取子列表,或从字符串中取子串。比如:myList[0..3]将会返回列表中的第0项到第3项。又比如,你可以通过myList[1..(myList?size-2)]获取到一个列表中除了第一个和最后一个元素的所有元素。
 我们可以通过myList[0..5]来获取到一个字符串的前6个字符。
 列表可以使用’+’运算符来连接。而以前,重载的’+’只能应用于字符串。
 现在试图将数字和字符串来比较是会抛出异常的,因为这是错误代码的指示。要注意有一个向后兼容的模式可以来设置(参考下面的叙述)放开它,来处理遗留的模
板。
API的改变
 现在,TemplateSequenceModel接口有一个size()方法来获取到序列中的元素个数。
 现在,TemplateModelIterator接口有一个hasNext()方法。
 默认的序列和哈希表实现,freemarker.template.SimpleSequence和freemarker.template.SimpleHash现在是不同步的了。如果你需要同步的方法,可以通过以上两个类的synchronizedWrapper()方法获取一个同步的包装器。
 移除了freemarker.utility.ExtendedList和freemarker.utility.ExtendedHash类,因为定义的所有的额外键现在都可以使用合适的’?’内建函数操作符,比如myHash?keys或myList?size或myList?last。
 在java.freemarker.Configuration中有一个名为setDebugMode()的方法,它允许你决定堆栈轨迹是简单输出到Web客户端(在开发模式下的最佳方案)或抛出给调用者来更好的控制(在生产环境中的最佳方案)。
 有一个可以开启处理模式的标识位,这对FreeMarker经典版有向后的兼容性。默认情况下它是关闭的,但是我们可以通过Template.setClassicCompatibility(true)方法来设置。这个方法所做的就是允许标量在list指令中被视作单独项的list。而且,它允许一些更松散的类型。在FreeMarker 1.x中,<#if x=="1">和<#if x==1>实际上是相等的。这就是说遗留的模板也许更倾向于松散。如果经典的兼容性没有被设置,那么试图去比较字符串”1”和数字1将会导致抛出异常。(要注意,让模板正常工作而不使用向后兼容的标志是最好的,因为通常情况下它会需要一点小的改变。然而,对于模板很多的用户来说,没有时间来检查它们,那么这个标识就很有用了。)
2.0 RC1版
FreeMarker 2.0的第一次公开发行是在2002-03-18。这里给出一个Lazarus发行包改变内容的总结,还有关于FreeMarker经典的最后一个稳定版本内容。
NOTA BENE:
除了以上列举的变化,Lazarus发行包基本是完全向后兼容FreeMarker经典版的了。我们详细大多数在FreeMarker经典版下工作的代码和模板将会在Lazarus下同样可行,或许会有很小的改变。在实践中,遗留模板代码中最常见的失效情况就是假设数字和字符串相同的地方。注意在FreeMarker 2中,2+2不会算作”22”。字符串”1”和数字1是完全不同的,因此如果基于布尔表达式(”1”==1)是true时,代码就会失效。通过Template.setClassCompatibility()方法可以设置经典的兼容模式,之后Lazarus可以模拟一些FreeMarker经典版的怪异行为。然而,任何依赖于上述FreeMarker经
典版特定的代码都应该重写。你不太可能碰到上面列出的其它不相容的内容。如果你碰到了其它的异常,请告诉我们详情。
支持包括算术和布尔值的数字运算符和数字范围
 标量现在可以是字符串或者数字。(在FreeMarker经典版中所有的标量都是字符串。)基本的运算符允许加法,减法,乘法,除法和求模运算,各自使用+,-,*,/和%操作符。整型和浮点型任意精度的计算都是提供的。尽管我们的目标是跟着最小差异的原则,对于向后兼容的特性,+操作符仍然可以用于字符串的连接。如果lhs + rhs的左边或右边不都是数字,我们就还原它为字符串连接。在FreeMarker 2中,2+2被算作是数字4,而”2”+2,2+”2”或”2”+”2”中的任意一个则被算作是字符串”22”。在FreeMarker经典版中,是相当尴尬的,上面所有的示例,包括2+2都会被算作是字符串”22”。试图使用其它除了+号的操作符对非数字操作都会抛出异常。
 输出一个数字表达式可以通过#{....}语法来明确。如果这个表达式使用花括号那就不会计算为数值,会抛出异常。老式的${…}语法可以算作数字或字符串。通常来讲,比如出于逻辑原因,输出必须是数字,那就最好使用#{…}语法,因为它会进行额外的合理的检查。要注意,有一些奇特之处,出现在模板中的字符序列”#{”,你需要使用一些方法来防止问题的发生。(<noparse>指令就是一种可能。)
 在这个发行包中,对于小数点之后显示的指定数位是有一些工具可用的。下面的代码就指定在小数点后显示至少3位,而不多余6位。这是可选的,这个选择仅在使用#{…}语法时可用。
(注意上面的只是权宜之计。后续的发布包将会支持完整的数字和货币格式的国际化和本地化)
 数值表达式可以通过比较运算符用在布尔表达式中:lt,gt,lte和gte。在Web中,大量使用FreeMarker的应用中,使用自然的如<和>操作符将会混淆面向HTML的编辑器。试图去比较使用了这些运算符的非数字表达式会导致抛出TemplateException异常。在某些巧合中,有命名为”lt”,”gt”,”lte”或”gte”的变量,那么就不得不去改名了,因为它们现在在语言中是关键字了。
 数字范围是支持的。
在..运算符左边和右边的必须是数字,要么就会抛出异常。它们不能是文字形式的数字,但是可以是计算成数值标量的复杂表达式。注意它也可能使用降序来编写一个范围:
#{foo + bar ; m3M6}
<#list 1990..2001 as year>
blah blah in the year ${year} blah
</#list>
<#list 2001..1990 as year>
blah blah in the year ${year} blah blah
</#list>
API的改变
 TemplateNumberModel接口和SimpleNumber实现加入了暴露数值的支持。
 在FreeMarker经典版中的TemplateListModel API有一些设计问题—特别是在支持线程安全代码的方面。在以下API中被废弃了:TemplateCollectionModel和TemplateSequenceModel。SimpleList类被重构来实现上面的接口(而且是自相矛盾的,没有实现TemplateModel接口。)。使用了废弃的TemplateListModel代码应该重构。
 由Attla Szegedi编写的暴露的包已经是FreeMarker发行包的一部分了,而且现在在freemarker.ext.*层下。这个包提供了代表任意Java对象作为模板对象,代表XML文档作为模板对象的高级模型,还有便于FreeMarker和Servlet,Ant整合的类。
 在FreeMarker经典版中,有一些如freemarker.template.utility.Addition等的工具类,对于缺少数字运算的FreeMarker来说是个解决方案。这些已经被删除了,可能不会被错过。
 在FreeMarker经典版中,SimpleScalar对象是易变的,它有一个setValue方法。这是一个相当明显的设计失误。任何依赖于它的代码都必须重构。注意在这个发行包中,SimpleScalar和新引入的SimpleNumber都是不可变的,是终极的。
语法集锦
 if-elseif-else语法被引入了。FreeMarker经典版仅仅有if-else。这个结构可能(本文档作者Revusky的意见)应该用于偏向switch-case形式,因为switch-case和落空结构对于普通人来说是非常容易出错的结构。
 现在你可以在<assign…>指令中使用多个定义。比如:<assign x=1, y=price*items, message=”foo”>
 标量不会在<list…>或<#foreach…>块中再被解释为一个元素的列表了。如果有基于这个特性的代码,那也有一个很容易的解决方法,因为你可以使用一个元素来定义一个文字列表。
<assign y=[x]>
and then...
<list y as item>...</list>
附录E 许可
FreeMarker 1.x版在LGPL许可协议下发布。之后,在社区中达成共识,我们将它转移到BSD下的许可协议。比如FreeMarker 2.2 预览版1,原作者Benjamin Geer,在Visigoth软件协会中放弃代表版权。当前的版权持有者是Visigoth软件协会。
版权所有 (c) 2003 Visigoth软件协会。保留所有权利。
重新发布,使用二进制形式的源码,是否进行修改,允许提供下列条件:
1. 源代码的重新发布必须保留上面的版权条款,此条件列表和以下免责声明。
2. 最终用户文档包括重新发布,必须包含下列须知:“本产品包含由Visigoth软件协会(http://www.visigoths.org)开发的软件。”如果需要出现这个第三方声明,这个须知也可以出现在软件本身之中。
3. “FreeMarker”或“Visigoth”的名字,或其它项目共享者的名字或许用来支持或推广这个软件产品,而不需要书面授权。对于书面授权,请联系visigoths@visigoths.org。
4. 来自本软件的产品可能不叫“FreeMarker”或“Visigoth”,“FreeMarker”或“Visigoth”出现在在Visigoth软件协会没有书面授权中的名称中。
本软件提供“AS IS”和任何明示或暗示的保证,包含但不仅限于,适销的默认保证或针对特定用途的实用性不承担责任。在任何情况下,VISIGOTH软件协会或它的贡献者都不对任何直接,间接,偶然,特殊,惩戒或间接损失(包括但不仅限于采购替代产品或服务;使用损失,数据,或利润;或业务中断)是如何造成的或是任何责任理论,是否是合同,严格责任,或侵权行为(包括疏忽或其它原因)以任何方式产生超出本软件的使用,即便是这种损害的可能性。
本软件包括志愿贡献者,许多代表Visigoth软件协会的个人。要获取更多关于Visigoth软件协会的信息,请参考http://www.visigoths.org
FreeMarker 子组件有不同的授权协议
FreeMarker包含一些由阿帕奇软件基金会授权的子组件,它们是基于阿帕奇2.0版协议的。你使用这些子组件时受制于阿帕奇2.0版协议的条款和条件。你可以在下述地址获得协议的拷贝:
http://www.apache.org/licenses/LICENSE-2.0
受制于该协议的子组件有以下文件,它们在freemarker.jar和源代码中都有:
freemarker/ext/jsp/web-app_2_2.dtd
freemarker/ext/jsp/web-app_2_3.dtd
freemarker/ext/jsp/web-app_2_4.xsd
freemarker/ext/jsp/web-app_2_5.xsd
freemarker/ext/jsp/web-jsptaglibrary_1_1.dtd
freemarker/ext/jsp/web-jsptaglibrary_1_2.dtd
freemarker/ext/jsp/web-jsptaglibrary_2_0.xsd
freemarker/ext/jsp/web-jsptaglibrary_2_1.xsd
词汇表
Attribute 属性
为了连接XML或HTML(或通用的SGML),属性是关联元素的命名的值。比如,在<body bgcolor=black text=green>...</body>中,属性是bgcolor=black和text=green。在符号=的左边是属性的名字,而在右边的是属性的值。注意在XML中,值必须是被引号引起来的(比如:<body bgcolor="black" text='green'>),而在HTML中它对某些值是可选的。
也可以参考本部分的Start-tag(开始标记)
Boolean 布尔值
这是一种变量类型。布尔值代表逻辑上的真或假(是或否)。比如,如果一个访问者是否登录了。布尔值只可能有两种:true和false。典型的用法是,在某些情况下,当你想展示基于文本时,和<#if ...>指令来使用布尔值,也就是说仅仅对登录的访问者展示页面的一个特定部分。
Character 字符
人们用于书写的符号。字符的示例:大写拉丁字母A(”A”),小写拉丁字母A(”a”),数字4(”4”),数字符号(“#”),冒号(”:”)
Charset 字符集
字符集是一种转换字符序(文本)列到比特序列(或在实际中,是字符序列)的规则(算法)。无论字符序列是存储在数字媒介上,或是通过数字线路(网络)发送的,必须要应用字符集。字符集的示例有ISO-8859-1,ISO-8859-6,Shift_JIS,UTF-8。
功能不同的字符集是不同的,也就是说,并不是所有字符集都能用户所有用于。比如ISO-8859-1不能代表阿拉伯字母,但是ISO-8859-6就可以,而它不能代表重音字母但ISO-8859-1就可以。很多字符集关于允许的字符都是有高度限制性的。UTF-8允许几乎所有可能的字符,但是很多编辑器还不能处理它(在2004年的时候)。
当不同的软件组件交换文本(比如HTTP服务器和浏览器,或者你用于保存FreeMarker加载模板的文本编辑器)时,它们对支持文本二进制编码使用的字符集是非常重要的。如果它们不支持,那么二进制数据就会被接收(加载器)组件所误解析,结果经常导致非英文字母的失真。
Collection 集合
可以分理处一系列变量的变量(结合list指令)。
Data-model 数据模型
当模板处理器组装输出(比如Web页面)时,持有模板要展示(或用于其它用途)的信息的对象。在FreeMarker中,它可以被视作是一棵树。
Directive 指令
在FTL模板中用来给FreeMarker进行说明的东西。它们由FTL标签调用。
也可以参考预定义指令,用户自定义指令。
Element 元素
元素是SGML文档最基本的构建块;一个SGML文档基本上是一棵元素树。比如在HTML中使用的元素:body,head,title,p,h1,h2。
End-tag 结束标记
标记,说明了其后面紧跟的内容就不在元素中了。比如:</body>。
也可以参考开始标记
Environment 环境
Environment对象存储单独模板处理任务运行时状态。那也就是,对于每个Template.process(...)调用,Environment实例都会被创建,之后当process返回时抛弃。这个对象存储由模板创建的临时变量的集合,模板设置的配置信息的值,指向数据模型根的引用等。需要填充模板运行时的所有东西。
Extensible Markup Language 可扩展标记语言(XML,译者注)
是SGML的子集(严格版本)。它没有SGML那么强大,但是它很容易掌握,也很容易用程序来处理。如果你是一名HTML开发人员。XML文档和HTML文档是很相似的,但是XML标准没有规定可用的元素。比起HTML,XML是一种更通用的标记语言。比如你可以使用XML来描述Web页面(就像HTML)或者描述非视觉的东西,比如电话簿数据库。
也可以参考标准通用标记语言。
FreeMarker Template Language FreeMarker模板语言
简单的编程语言,设计用来编写文本文件模板,特别是HTML模板。
FTL
参考FreeMarker模板语言。
FTL tag FTL标记
标记,就像文本片段,用来在FTL模板中调用FreeMarker指令。乍一看,这和HTML或XML标记很相似。最突出的不同是标记名称以#或@开头。另外一个重要的不同是FTL标记不使用属性,而是大量不同的语法来指定参数。FTL标记的示例:<#if newUser>,</#if>,<@menuitem title="Projects" link="projects.html"/>。
Full-qualified name 完全限定名
对节点(XML节点或其它FTL节点变量)来说:节点的完全限定名指定的不仅仅是节点名称(node?node_name),而且有节点的命名空间(node?node_namespace),这种方式不是含糊其辞地标识确定的节点类型。完全限定名的格式是:nodeName或prefix:nodeName。前缀是标识节点命名空间(节点命名空间通常是由很长的URI来指定的)的速记形式。在FTL中,前缀是用ftl指令的ns_prefixes参数和节点命名空间相关联的。在XML文件中,前缀是和节点命名空间用xmlns:prefix属性关联的。如果默认的节点命名空间定义的话,没有前缀,说明节点使用了默认的节点命名空间;否则就是说节点不属于任何节点命名空间。在FTL中定义的默认的节点命名空间是用注册的保留前缀D和ftl指令的ns_prefixes参数。在XML文件中,使用xmlns属性来定义。
对于Java类来说:Java类的完全限定名包含类名和类所属的包名。这种方法不会含糊
的指定类,而不考虑上下文。类名的完全限定名示例:java.util.Map(相对于Map)。
Hash 哈希表
担当容器的一种变量,用来存储子变量,可以通过字符串来查找名称检索值。
也可以参考序列
Line break 换行符
换行符是一种特殊的字符(或者是特殊字符的序列),当看到的文本是普通文本时(也就是说,是可以使用Windows的记事本来阅读的文本时)它会引起换行。典型的做法是,通过点击ENTER(回车)或RETURN(返回)键来输入这个字符。在不同的平台(会引起不兼容和混乱)上,换行符代表不同的字符。在UNIX-es平台上是“line feed”,在Macintosh平台上是“carriage return”,在Windows和DOS平台上,是“carriage return”+“line feed”(两种字符!)。要注意在浏览器中查看时,HTML中换行符是没有视觉效果的;你必须使用如<BR>这样的标记来处理。本文档中的“换行”从来不表示<BR>。(原文档是HTML格式的,而中文译本是PDF格式的,译者注)
Macro definition body 宏定义体
在<#macro ...>和</#macro ...>之间的模板片段。当调用宏(比如<@myMacro/>)时,模板片段将会执行。
Method 方法
基于给定的参数计算一些东西的变量,之后会返回结果。
MVC pattern MVC设计模式
MVC代表了Model View Controller(模型-视图-控制器,译者注)。它是一种设计模式,在20世纪70年代由框架设计师Trygve Reenskaug为Smalltalk语言设计时开始的,之后被主要用于UI(user interface,用户界面,译者注)。MVC中有三种角色:
 模型:模型代表了非视觉展示方式的应用程序(域)特定的信息。比如,在计算机内存中的产品对象数组就是模型的一部分。
 视图:视图展示了模型和提供的UI。比如,视图组件的任务是呈现产品对象数组给HTML页面。
 控制器:控制器处理用户输入,修改模型,并保证视图在需要时更新。比如,控制器的任务是处理接收到的HTTP请求,解析收到的参数(表单),分发请求到合适的业务逻辑对象中,选择正确的模板做出HTTP响应。
当为Web应用程序应用MVC模式是最重要的事情是将视图从其它二者中分离出来。这就允许将页面设计者(HTML编写者)从程序员中分离出来。页面设计者专心处理视觉呈现方面的问题,而程序员专心处理应用程序逻辑和其它技术问题。每个人都专注于他们擅长的一面。页面设计者和程序员不会相互依赖。页面设计者可以修改呈现效果而程序员不需要去重新编译程序。
莫获取更多信息建议阅读J2EE平台蓝本的4.4章节(设计企业级应用程序)。
Output encoding 输出编码
也就是输出字符集。在Java中,属于“encoding”(编码)通常是代表“charset”(字符集)。
Predefined directive 预定义指令
由FreeMarker定义的指令,那么一般情况下都是可用的。预定义指令的示例:if,list,include。
也可以参考用户自定义指令
Regular expression 正则表达式
正则表达式是指定一组字符串匹配它的字符串。比如,正则表达式"fo*"匹配"f", ,,"fo","foo"等。正则表达式应用于多种语言和工具中。在FreeMarker中,它们的使用是“专家级用户”的选择。所以如果你之前没有使用过正则表达式,也不用担心你不熟悉它们。如果你对正则表达式感兴趣,你可以查找有关它们的网页资料和书籍。FreeMarker使用的正则表达式的变化的描述在:
http://java.sun.com/j2se/1.4.1/docs/api/java/util/regex/Pattern.html
Scalar 标量
标量变量存储单独的值。标量可以是字符串,数字,日期/事件或布尔值。
Sequence 序列
序列是包含子变量序列的变量。序列的子变量可以通过数字索引来访问,而第一个对象的索引通常是0,第二个对象的索引是1,第三个对象的索引是2等。
也可以参考哈希表
SGML
参考标准通用标记语言
Standard Generalized Markup Language 标准通用标记语言
这是国际化标准(ISO 8879),它用来指定创建独立平台标记语言的规则。HTML是一种由SGML创建的标记语言。XML是SGML的一个子集(严格的版本)。
也可以参考可扩展标记语言。
Start-tag 开始标记
标记,表示着后续内容是在元素中的,直到结束标记。开始标记可能为元素指定属性。开始标记的示例:<body bgcolor=black>。
String 字符串
字符的序列,比如“m”,“o”,“u”,“s”,“e”。
Tag 标记
表示SGML中元素使用的文本段。标记的示例:<body bgcolor=black>,</body>。
也可以参考开始标记,结束标记
Template 模板
模板是包含了一些嵌入其中的特殊字符序列文本文件。模板处理器(比如FreeMarker)将会解释特殊的字符序列然后从原文本文件中输出不同的文本,这些不同通常是基于数据模
型的。因此,原文本担当了模板的可能输出。
Template encoding 模板编码方式
表示模板的字符集。在Java中术语“编码”是和“字符集”可以通用的。
Template processing job 模板处理工作
模板处理工作担当FreeMarker合并模板和数据模型为访问者生成输出时的任务。注意这可能包含多个模板文件的执行,因为用于Web页面的模板文件可能使用了include和import指令调用了其它模板。每个模板处理工作是分离的体系,它们仅存在很短的事件,而给定的页面呈现给访问者,之后它就和所有创建在模板中(比如使用assign,macro或global指令创建的变量)的变量一同消失了。
Thread-safe 线程安全
如果对象从多线程中,甚至是并行程序中可以安全调用它的方法,那么这个对象就是线程安全的。(也就是说多线程同时执行对象的方法)。线程不安全的对象可能在这种情况下表现出不可预知的行为,进而生成错误的结果,破坏内部数据结构等。线程安全在Java中有两种典型的实现:使用synchronized表达式(或synchronized方法),或者是使用不变的封装数据(也就是你创建对象后不能修改其中的字段)。
Transform 变换
这个数据指的是用户自定义指令实现了当前已经过时的TemplateTransformModel Java接口。这个特性故名,最初是用于实现输出过滤器的。
UCS
这是国际化的标准(ISO-10646),它定义了字符的巨大集合,而且还为每个字符定义了一些特殊的数字(“!”是33,…,“A”是61,“B”是62,…,阿拉伯字母hamza是1569等)。这个字符的集合(不是字符集)包含了几乎所有当前使用的字符(拉丁字母,西里尔字母,中文字符等)。UCS背后的思想是我们可以使用唯一的数字来指定字符,而不管使用的平台或语言是什么。
也可以参考Unicode
Unicode
由Unicode组织开发的事实上的标准。它处理了UCS(什么是字母,什么是数字,什么是大写,什么是小写等)字符集的分类,和其它处理UCS字符时的文本问题(比如正规化)。
User-defined directive 用户自定义指令
不是由FreeMarker本身定义的指令,而是用户定义的。这些是典型应用程序域指定的指令,比如下拉菜单生成指令,HTML表单处理指令。
也可以参考预定义指令
White-space 空白
完全透明的字符,但是对文本的视觉呈现会有作用。空白字符的示例:空格,制表符(水平和垂直),换行符(CR和LF),换页。
也可以参考换行符。
XML
参考可扩展标记语言。

0 0
原创粉丝点击