freemarker详细教程从入门到精通(四)数据类型与方法函数
数据类型
字符串
在文本中确定字符串值的方法是看双引号,比如: "some text"
,或单引号,比如: 'some text'
。这两种形式是等同的。 如果文本自身包含用于字符引用的引号 ( "
或 '
)或反斜杠时, 应该在它们的前面再加一个反斜杠;这就是转义。 转义允许直接在文本中输入任何字符, 也包括换行。
${"It's \"quoted\" and this is a backslash: \\"} ${'It\'s "quoted" and this is a backslash: \\'}
将会输出:
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 水平制表符(又称为tab) (u0009) \b 退格 (u0008) \f 换页 (u000C) \l 小于号:< \g 大于号:> \a &符:& \xCode 字符的16进制 Unicode 码 (UCS 码)
原生字符串是一种特殊的字符串。在原生字符串中, 反斜杠和 ${
没有特殊含义, 它们被视为普通的字符。为了表明字符串是原生字符串, 在开始的引号或单引号之前放置字母r
,例如:
${r"${foo}"} ${r"C:\foo\bar"} 将会输出: ${foo} C:\foo\bar
数字
输入不带引号的数字就可以直接指定一个数字, 必须使用点作为小数的分隔符而不能是其他的分组分隔符。 可以使用 -
或 +
来表明符号 (+
是多余的)。 科学记数法暂不支持使用 (1E3
就是错误的), 而且也不能在小数点之前不写0(.5
也是错误的)。
下面的数字都是合法的:0.08
, -5.013
,8
, 008
,11
, +11
请注意,像 08
、 +8
、 8.00
和 8
这样的数值是完全等同的,它们都是数字8。 所以, ${08}
、${+8}
、 ${8.00}
和 ${8}
的输出都是一样的。
布尔值
直接写 true
或者 false
就表示一个布尔值了,不需使用引号。
序列
指定一个文字的序列,使用逗号来分隔其中的每个 子变量, 然后把整个列表放到方括号中。例如:
<#list ["foo", "bar", "baz"] as x> ${x} </#list> 将会输出: foo bar baz
列表中的项目是表达式,那么也可以这样做: [2 + 2, [1, 2, 3, 4], "foo"]
。 其中第一个子变量是数字4,第二个子变量是一个序列, 第三个子变量是字符串"foo"。
值域
值域也是序列,但它们由指定包含的数字范围所创建, 而不需指定序列中每一项。比如: 0..<m
,这里假定 m
变量的值是5,那么这个序列就包含 [0, 1, 2, 3, 4]
。值域的主要作用有:使用 <#list...>
来迭代一定范围内的数字,序列切分 和 字符串切分。
值域表达式的通用形式是( start
和 end
可以是任意的结果为数字表达式):
-
start..end
: 包含结尾的值域。比如1..4
就是[1, 2, 3, 4]
, 而4..1
就是[4, 3, 2, 1]
。当心一点, 包含结尾的值域不会是一个空序列,所以0..length-1
就是 错误的,因为当长度是0
时, 序列就成了[0, -1]
。 -
start..<end
或start..!end
: 不包含结尾的值域。比如1..<4
就是[1, 2, 3]
,4..<1
就是[4, 3, 2]
, 而1..<1
表示[]
。请注意最后一个示例; 结果可以是空序列,和..<
和..!
没有区别; 最后这种形式在应用程序中使用了<
字符而引发问题(如HTML编辑器等)。 -
start..*length
: 限定长度的值域,比如10..*4
就是[10, 11, 12, 13]
,10..*-4
就是[10, 9, 8, 7]
,而10..*0
表示[]
。当这些值域被用来切分时, 如果切分后的序列或者字符串结尾在指定值域长度之前,则切分不会有问题;请参考 序列切分 来获取更多信息。 start..
: 无右边界值域。这和限制长度的值域很像,只是长度是无限的。 比如1..
就是[1, 2, 3, 4, 5, 6, ... ]
,直到无穷大。 但是处理(比如列表显示)这种值域时要万分小心,处理所有项时, 会花费很长时间,直到内存溢出应用程序崩溃。 和限定长度的值域一样,当它们被切分时, 遇到切分后的序列或字符串结尾时,切分就结束了。-
值域的进一步注意事项:
-
值域表达式本身并没有方括号,比如这样编写代码
<#assign myRange = 0..<x>
, 而不是<#assign myRange = [0..<x]>
。 后者会创建一个包含值域的序列。方括号是切分语法的一部分,就像seq[myRange]
。 -
可以在
..
的两侧编写算术表达式而不需要圆括号, 就像n + 1 ..< m / 2 - 1
。 -
..
,..<
,..!
和..*
是运算符, 所以它们中间不能有空格。就像n .. <m
这样是错误的,但是n ..< m
这样就可以。 -
无右边界值域的定义大小是2147483647 (如果
incompatible_improvements
低于2.3.21版本,那么就是0), 这是由于技术上的限制(32位)。但当列表显示它们的时候,实际的长度是无穷大。 -
值域并不存储它们包含的数字,那么对于
0..1
和0..100000000
来说,创建速度都是一样的, 并且占用的内存也是一样的。
-
哈希表
在模板中指定一个哈希表,就可以遍历用逗号分隔开的"键/值"对, 把列表放到花括号内即可。键和值成对出现并以冒号分隔。比如: { "name": "green mouse", "price": 150 }
。 请注意名和值都是表达式,但是用来检索的名称就必须是字符串类型, 而值可以是任意类型。
检索变量
顶层变量
访问顶层的变量,可以简单地使用变量名。例如, 用表达式 user
就可以在根上获取以 "user" 为名存储的变量值。然后打印出存储在里面的内容:
${user}
如果没有顶层变量,那么 FreeMarker 在处理表达式时就会发生错误, 进而终止模板的执行(除非程序员事先配置了 FreeMarker)。
在这种表达式中,变量名只可以包含字母(也可以是非拉丁文), 数字(也可以是非拉丁数字),下划线 (_
), 美元符号 ($
),at符号 (@
)。 此外,第一个字符不可以是ASCII码数字(0
-9
)。 从 FreeMarker 2.3.22 版本开始,变量名在任何位置也可以包含负号 (-
),点(.
)和冒号(:
), 但这些必须使用前置的反斜杠(\
)来转义, 否则它们将被解释成操作符。比如,读取名为"data-id"的变量, 表达式为 data\-id
,因为 data-id
将被解释成 "data minus id"。 (请注意,这些转义仅在标识符中起作用,而不是字符串中。)
从哈希表中检索数据
可以使用表达式 book.author.name
来读取到auther的name。
从序列中检索数据
这和从哈希表中检索是相同的,但是只能使用方括号语法形式来进行, 而且方括号内的表达式最终必须是一个数字而不是字符串。比如animals[0].name
特殊变量
特殊变量是由FreeMarker引擎本身定义的。 使用它们,可以按照如下语法形式来进行: .variable_name
。.
通常情况下是不需使用特殊变量,而对专业用户来说可能用到。
字符串操作
插值 (或连接)
如果要在字符串中插入表达式的值,可以在字符串的文字中使用 ${...}
(已经废弃的 #{...}
)。 ${...}
在字符串中的作用和在 文本 区是相同的
使用 +
号来达到类似的效果
获取字符
在给定索引值时可以获取字符串中的一个字符,这和 序列的子变量是相似的, 比如 user[0]
。
字符串切分 (子串)
可以按照 序列切分 (请参看)的相同方式来切分字符串,这就是使用字符来代替序列。不同的是:
-
降序域不允许进行字符串切分。 (因为不像序列那样,很少情况下会想反转字符串。 如果真要这样做了,那就是疏忽。)
-
如果变量的值既是字符串又是序列(多类型值), 那么切分将会对序列进行,而不是字符串。当处理XML时, 这样的值就是普通的了。此时,可以使用
someXMLnode?string[range]
。 -
一个遗留的bug:值域 包含 结尾时, 结尾小于开始索引并且是是非负的(就像在
"abc"[1..0]
中), 会返回空字符串而不是错误。(在降序域中这应该是个错误。) 现在这个bug已经向后兼容,但是不应该使用它,否在就会埋下一个错误。
<#assign s = "ABCDEF"> ${s[2..3]} ${s[2..<4]} ${s[2..*3]} ${s[2..*100]} ${s[2..]} 将会输出: CD CD CDE CDEF CDEF
序列操作
连接
序列的连接可以按照字符串那样使用 +
号来进行,例如:
<#list ["Joe", "Fred"] + ["Julia", "Kate"] as user> - ${user} </#list>
请注意,不要在很多重复连接时使用序列连接操作, 比如在循环中往序列上追加项目,而这样的使用是可以的: <#list users + admins as person>
。 尽管序列连接的速度很快,而且速度是和被连接序列的大小相独立的, 但是最终的结果序列的读取却比原先的两个序列慢那么一点。 通过这种方式进行的许多重复连接最终产生的序列读取的速度会慢。
序列切分
使用 seq[range]
, 这里 range
是一个值域 , 就可以得到序列的一个切分。此外,切分后序列中的项会和值域的顺序相同。 值域中的数字必须是序列可使用的合法索引, 否则模板的处理将会终止并报错。
哈希表操作
连接
像连接字符串那样,也可以使用 +
号的方式来连接哈希表。如果两个哈希表含有键相同的项,那么在 +
号右侧的哈希表中的项优先。例如:
<#assign ages = {"Joe":23, "Fred":25} + {"Joe":30, "Julia":18}> - Joe is ${ages.Joe} - Fred is ${ages.Fred} - Julia is ${ages.Julia}
请注意,很多项连接时不要使用哈希表连接, 比如在循环时往哈希表中添加新项。这和序列连接 的情况是一致的。
算数运算
算数运算包含基本的四则运算和求模运算,运算符有:
- 加法:
+
- 减法:
-
- 乘法:
*
- 除法:
/
- 求模 (求余):
%
比较运算
有时我们需要知道两个值是否相等,或者哪个值更大一点。
为了演示具体的例子,我们在这里使用 if
指令。 if
指令的用法是: <#if expression>...</#if>
, 其中的表达式的值必须是布尔类型,否则将会出错,模板执行中断。 如果表达式的结果是 true
, 那么在开始和结束标记内的内容将会被执行,否则就会被跳过。
逻辑操作
常用的逻辑操作符:
- 逻辑 或:
||
- 逻辑 与:
&&
- 逻辑 非:
!
逻辑操作符仅仅在布尔值之间有效,若用在其他类型将会产生错误导致模板执行中止。
内建函数
在页面上也可以多次使用指令,而且指令间也可以很容易地相互嵌套。 比如,在 list
指令中嵌套 if
指令:
<#list animals as animal> <div<#if animal.protected>class="protected"</#if>> ${animal.name} for ${animal.price} Euros </div> </#list>
请注意,FreeMarker并不解析FTL标签以外的文本、插值和注释, 上面示例在HTML属性中使用FTL标签也不会有问题。
内建函数很像子变量(如果了解Java术语的话,也可以说像方法), 它们并不是数据模型中的东西,是 FreeMarker 在数值上添加的。 为了清晰子变量是哪部分,使用 ?
(问号)代替 .
(点)来访问它们。常用内建函数的示例:
-
user?html
给出user
的HTML转义版本, 比如&
会由&
来代替。 -
user?upper_case
给出user
值的大写版本 (比如 "JOHN DOE" 来替代 "John Doe") -
animal.name?cap_first
给出animal.name
的首字母大写版本(比如 "Mouse" 来替代 "mouse") -
user?length
给出user
值中 字符的数量(对于 "John Doe" 来说就是8) -
animals?size
给出animals
序列中 项目 的个数(我们示例数据模型中是3个) -
如果在
<#list animals as animal>
和对应的</#list>
标签中:-
animal?index
给出了在animals
中基于0开始的animal
的索引值 -
animal?counter
也像index
, 但是给出的是基于1的索引值 -
animal?item_parity
基于当前计数的奇偶性,给出字符串 "odd" 或 "even"。在给不同行着色时非常有用,比如在<td class="${animal?item_parity}Row">
中。
-
一些内建函数需要参数来指定行为,比如:
-
animal.protected?string("Y", "N")
基于animal.protected
的布尔值来返回字符串 "Y" 或 "N"。 -
animal?item_cycle('lightRow','darkRow')
是之前介绍的item_parity
更为常用的变体形式。 -
fruits?join(", ")
通过连接所有项,将列表转换为字符串, 在每个项之间插入参数分隔符(比如 "orange,banana") -
user?starts_with("J")
根据user
的首字母是否是 "J" 返回布尔值true或false。
内建函数应用可以链式操作,比如user?upper_case?html
会先转换用户名到大写形式,之后再进行HTML转义。(这就像可以链式使用 .
(点)一样)
方法调用
如果有一个方法,那么可以使用方法调用操作。 方法调用操作是使用逗号来分割在括号内的表达式而形成参数列表,这些值就是参数。 方法调用操作将这些值传递给方法,然后返回一个结果。 这个结果就是整个方法调用表达式的值。
处理不存在的值
默认值操作符
使用形式: unsafe_expr!default_expr
或 unsafe_expr!
or (unsafe_expr)!default_expr
或 (unsafe_expr)!
这个操作符允许你为可能不存在的变量指定一个默认值。
默认值可以是任何类型的表达式,也可以不必是字符串。 也可以这么写:hits!0
或 colors!["red", "green", "blue"]
。 默认值表达式的复杂程度没有严格限制,还可以这么来写: cargo.weight!(item.weight * itemCount + 10)
。
不存在值检测操作符
使用形式: unsafe_expr??
或 (unsafe_expr)??
这个操作符告诉我们一个值是否存在。基于这种情况, 结果是 true
或 false
。
赋值操作符
这些并不是表达式,只是复制指令语法的一部分,比如 assign
, local
和 global
。 照这样,它们不能任意被使用。
<#assign x += y>
是 <#assign x = x + y>
的简写,<#assign x *= y>
是 <#assign x = x * y>
的简写等等。。。
<#assign x++>
和 <#assign x += 1>
(或 <#assign x = x + 1>
)不同,它只做算术加法运算 (如果变量不是数字的话就会失败),而其它的是进行字符串,序列连接和哈希表连接的重载。 <#assign x-->
是 <#assign x -= 1>
的简写。
括号
括号可以用来给任意表达式分组。
表达式中的空格
FTL 忽略表达式中的多余的 空格。
操作符的优先级
下面的表格显示了已定义操作符的优先级。 表格中的运算符按照优先程度降序排列:上面的操作符优先级高于它下面的。 高优先级的运算符执行要先于优先级比它低的。表格同一行上的两个操作符优先级相同。 当有相同优先级的二元运算符(运算符有两个''参数'',比如 +
和-
)挨着出现时,它们按照从左到右的原则运算。
运算符组 运算符 最高优先级运算符 [subvarName] [subStringRange] . ? (methodParams) expr! expr?? 一元前缀运算符 +expr -expr !expr 乘除法,求模运算符 * / % 加减法运算符 + - 数字值域 .. ..< ..! ..* 关系运算符 < > <= >= (and equivalents: gt, lt, etc.) 相等,不等运算符 == != (and equivalents: =) 逻辑 "与" 运算符 && 逻辑 "或" 运算符 ||
子程序
方法和函数
当一个值是方法或函数的时候,那么它就可以计算其他值,结果取决于传递给它的参数。
这部分是对程序员来说的:方法/函数是一等类型值, 就像函数化的编程语言。也就是说函数/方法也可以是其他函数/方法的参数或者返回值, 并可以把它们定义成变量等。
假设程序员在数据模型中放置了一个方法变量 avg
, 该变量用来计算数字的平均值。如果给定3和5作为参数,访问 avg
时就能得到结果4。
方法的使用将会在 后续章节 中进行解释, 下面这个示例会帮助我们理解方法的使用:
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
那么方法和函数有什么区别呢?这是模板作者所关心的, 它们没有关系,但也不是一点关系都没有。 方法是来自于数据模型 (它们反射了Java对象的方法) 而函数是定义在模板内的 (使用 function
指令 -- 也是高级话题),但二者可以用同一种方式来使用。
用户自定义指令
这种类型的值可以作为用户自定义指令(换句话说,就是FreeMarker的标签) 用户自定义指令是一种子程序,一种可以复用的模板代码段。但这也是一个高级话题, 将会在 后续章节 中进行解释。
这部分是对程序员来说的: 用户自定义指令(比如宏)也是一等值类型,就像函数/方法一样。
这里仅仅对用户自定义指令有一个认识即可(如果现在还不能理解可以先忽略它)。 假设现在有一个变量 box
,它的值是用户自定义的指令, 用来打印一些特定的HTML信息,包含标题和一条信息。那么, box
变量就可以在模板中使用(示例如下):
<@box title="Attention!"> Too much copy-pasting may leads to maintenance headaches. </@box>