https://freemarker.apache.org/

http://freemarker.foofun.cn/

https://search.bilibili.com/all?keyword=freemarker

FreeMarker概述

FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。

这种方式通常被称为 MVC (模型 视图 控制器) 模式,对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。

而FreeMarker最初的设计,是被用来在MVC模式的Web开发框架中生成HTML页面的,它没有被绑定到 Servlet或HTML或任意Web相关的东西上。它也可以用于非Web应用环境中。

FreeMarker 是 免费的, 基于Apache许可证2.0版本发布。

FreeMarker特性

通用目标

强大的模板语言

通用数据模型

为web准备

智能的国际化和本地化

强大的XML处理能力

入门

模板+数据模型=输出

模板:UI视图的基本结构,FTL模板。

数据模型:数据在面向对象设计中的职称。

输入:HTML。

数据模型一览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
(root)
|
+- animals
| |
| +- (1st)
| | |
| | +- name = "mouse"
| | |
| | +- size = "small"
| | |
| | +- price = 50
| |
| +- (2nd)
| | |
| | +- name = "elephant"
| | |
| | +- size = "large"
| | |
| | +- price = 5000
| |
| +- (3rd)
| |
| +- name = "python"
| |
| +- size = "medium"
| |
| +- price = 4999
|
+- misc
|
+- fruits
|
+- (1st) = "orange"
|
+- (2nd) = "banana"

模板一览

最简单的模板通常是普通的HTML文件(或者是其他任何文本文件; FreeMarker本身不属于HTML)。

**${…}**: FreeMarker将会输出真实的值来替换大括号内的表达式,这样的表达式被称为 interpolation(插值,译者注)。

FTL 标签(FreeMarker模板的语言标签): FTL标签和HTML标签有一些相似之处,但是它们是FreeMarker的指令,是不会在输出中打印的。 这些标签的名字以 # 开头。**(用户自定义的FTL标签则需要使用 @ 来代替 #)**

注释注释和HTML的注释也很相似, 但是它们使用 <#– and –> 来标识。

其他任何不是FTL标签,插值或注释的内容将被视为静态文本, 这些东西不会被FreeMarker所解析;会被按照原样输出出来。

FTL标签也被称为 指令。 这些指令在HTML的标签 (比如:
) 和HTML元素 (比如: table 元素) 中的关系是相同的。(如果现在还没有感觉到它们的不同, 那么把“FTL标签”和“指令”看做是同义词即可。)

指令

http://freemarker.foofun.cn/ref_directives.html

assign自定义变量指令

使用assign指令你可以创建一个新的变量,或者替换一个已经存在的变量。

1
2
3
4
5
6
7
8
9
10
<#--
assign自定义变量指令
语法:
<#assign 变量名=值>
<#assign 变量名=值 变量名=值> (定义多个变量)
-->
<#assign str="abc">
${str} <br>
<#assign num=1 names=["zhangsan","lisi","wangwu"]>
${num}--${name?join(",")}

if指令

if条件渲染,<#if condition> 和 </#if> ,condition如果为true,渲染,反之,不渲染。

1
2
3
4
5
6
7
8
9
10
11
12
<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>

<#else>标签:如同java中if{...}else{...}

1
2
3
4
5
<#if condition>
...condition为true。
<#else>
... condition为false。
</#if>

<#elseif>标签:如同java中if{...}else if{...}else{...}

1
2
3
4
5
6
7
<#if condition1>
...condition1为true。
<#elseif condition2>
...condition1为false,condition2为true。
<#else>
...condition1、condition2皆为false。
</#if>

list指令

当需要列表显示内容时,list指令是必须的。

1
2
3
4
5
6
<p>We have these animals:
<table border=1>
<#list animals as animal>
<tr><td>${animal.name}<td>${animal.price} Euros
</#list>
</table>

那么输出结果将会是这样的:

1
2
3
4
5
6
<p>We have these animals:
<table border=1>
<tr><td>mouse<td>50 Euros
<tr><td>elephant<td>5000 Euros
<tr><td>python<td>4999 Euros
</table>

list 指令的一般格式为: <#list sequence as loopVariable>repeatThis</#list>。repeatThis 部分将会在给定的 sequence 遍历时在每一项中重复, 从第一项开始,一个接着一个。在所有的重复中, loopVariable 将持有当前遍历项的值。 这个变量仅存在于 <#list …> 和 </#list> 标签内。sequence 可以是任意表达式, 比如我们可以列表显示示例数据模型中的水果,就像这样:

1
2
3
4
5
<ul>
<#list misc.fruits as fruit>
<li>${fruit}
</#list>
</ul>

上面示例中的一个问题是如果我们有0个水果,它仍然会输出一个空的 ,而不是什么都没有。 要避免这样的情况,可以这么来使用 list:

1
2
3
4
5
6
7
<#list misc.fruits>
<ul>
<#items as fruit>
<li>${fruit}
</#items>
</ul>
</#list>

此时, list 指令将列表视为一个整体, 在 items 指令中的部分才会为每个水果重复。 如果我们有0个水果,那么在 list 中的所有东西都被略过了, 因此就不会有 ul 标签了。

<#sep>标签:

列表相关的常见任务是:使用一些分隔符来列出水果,比如逗号:

1
<p>Fruits: <#list misc.fruits as fruit>${fruit}<#sep>, </#list>

输出:

1
<p>Fruits: orange, banana

被 sep 覆盖的部分(我们也可以这么来写: …<#sep>, </#sep></#list>) 只有当还有下一项时才会被执行。 因此最后一个水果后面不会有逗号。

再次回到这个话题,如果我们有0个水果,会怎么样?只是打印 “Fruits:” 也没有什么不方便。 list 指令,也像 if 指令那样,可以有 else 部分,如果列表中有0个元素时就会被执行:

1
<p>Fruits: <#list misc.fruits as fruit>${fruit}<#sep>, <#else>None</#list>

事实上,这个过于简单的示例可以这么来写, 但是它使用了本主题中没有介绍的语言特性:<p>Fruits: ${fruits?join(", ", "None")}

所有的这些指令(list, items, sep, else)可以联合起来使用:

1
2
3
4
5
6
7
8
9
10
<#list misc.fruits>
<p>Fruits:
<ul>
<#items as fruit>
<li>${fruit}<#sep> and</#sep>
</#items>
</ul>
<#else>
<p>We have no fruits.
</#list>

macro自定义指令(宏)

可以使用macro指令来定义一些自定义指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<#--
macro 自定义指令:
1. 基本使用
格式:
<#macro 指令名>
指令内容
</#macro>
使用:
<@指令名></@指令名>
2. 有参数的自定义指令
格式:
<#macro 指令名 参数名1 参数名2>
指令内容
</#macro>
使用:
<@指令名 参数名1=参数值1 参数名2=参数值2></@指令名>

注:
1. 指令可以被多次使用
2. 自定义指令中可以包含字符串,也可以包含内置指令。
-->
<#macro name>
....content
</#macro>

<@name></name>

<#macro name2 query1 query2>
....content2 ${query1} ${query2}
</#macro>

<@name2 query1="abc" query2="def"></@name>

nested占位(嵌套)指令

nested指令执行自定义指令开始和结束标签中间的模板片段。嵌套的片段可以包含模板中任意合法的内容。

1
2
3
4
5
6
7
8
9
10
<#--
nested相当于占位符,一般结合macro指令一起使用。可以将自定义指令中的内容通过nested指令占位,当使用自定义指令时,会将占位内容显示。
-->

<#macro test>
这是一段文本!
<#nested>
<#nested>
</#macro>
<@test><h4>这是文本后面的内容</h4></@test>

import导入指令

import指令可以导入一个库。也就是说,它创建一个新的命名空间,然后在那个命名空间中执行给定路径的模板。可以使用引入的空间中的指令。

commons.ftl

1
2
3
<#macro macroName>
。。。
</#macro>

在其他ftl页面中通过import导入commons.ftl的命名空间,使用该命名空间中的指令。

test.ftl

1
2
3
4
<#-- 导入命名空间-->
<#import "commons.ftl" as common>
<#-- 使用命名空间中的指令 -->
<@common.macroName></@common.macroName>

include 指令

使用 include 指令, 我们可以在模板中插入其他文件的内容。

假设要在一些页面中显示版权声明的信息。那么可以创建一个文件来单独包含这些版权声明, 之后在需要它的地方插入即可。比方说,我们可以将版权信息单独存放在页面文件 copyright_footer.html 中:

1
2
3
4
5
6
<hr>
<i>
Copyright (c) 2000 <a href="http://www.acmee.com">Acmee Inc</a>,
<br>
All Rights Reserved.
</i>

当需要用到这个文件时,可以使用 include 指令来插入:

1
2
3
4
5
6
7
8
9
10
<html>
<head>
<title>Test page</title>
</head>
<body>
<h1>Test page</h1>
<p>Blah blah...
<#include "/copyright_footer.html">
</body>
</html>

此时,输出的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>

当修改了 copyright_footer.html 文件, 那么访问者在所有页面都会看到版权声明的新内容。

使用内建函数

内建函数很像子变量(如果了解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”。在给不同行着色时非常有用,比如在 中。

一些内建函数需要参数来指定行为,比如:

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转义。(这就像可以链式使用 .(点)一样)

可以阅读 全部内建函数参考

处理不存在的变量

数据模型中经常会有可选的变量(也就是说有时并不存在)。 除了一些典型的人为原因导致失误外,FreeMarker 绝不能容忍引用不存在的变量, 除非明确地告诉它当变量不存在时如何处理。这里来介绍两种典型的处理方法。

这部分对程序员而言: 一个不存在的变量和一个是 null 值的变量, 对于FreeMarker来说是一样的,所以这里所指的”丢失”包含这两种情况。

不论在哪里引用变量,都可以指定一个默认值来避免变量丢失这种情况, 通过在变量名后面跟着一个 !(叹号,译者注)和默认值。 就像下面的这个例子,当 user 不存在于数据模型时, 模板将会将 user 的值表示为字符串 “visitor”。(当 user 存在时, 模板就会表现出 ${user} 的值):

1
<h1>Welcome ${user!"visitor"}!</h1>

也可以在变量名后面通过放置 ?? 来询问一个变量是否存在。将它和 if 指令合并, 那么如果 user 变量不存在的话将会忽略整个问候的代码段:

1
<#if user??><h1>Welcome ${user}!</h1></#if>

关于多级访问的变量,比如 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)??来看。

数据类型

布尔型:等价于java的Boolean类型,不同的是不能直接输出,可转换为字符串输出。

日期型:等价于java的Date类型,不同的是不能直接输出,可转换为字符串输出。

数值型:等价于Java中的int,float,double等数值类型。

有三种显示形式:数值型(默认)、货币型、百分比型。

字符型:等价于Java中的字符串,有很多内置函数。

sequence(序列)类型:等价于Java中的数组,list,set等集合类型。

hash类型:等价于java中Map类型。

布尔类型

变量flag是布尔类型,在freemarker页面中不能直接输出,需要转换为字符串输出。

方式一:?c。

方式二:?string或?string(‘1’,’2’)。

1
2
3
4
5
${flag} //这是错的
${flag?c}
${flag?string}
${flag?string('yes','no')}
${flag?string('喜欢','不喜欢')}

日期类型

变量myDate是日期类型。在freemarker中日期类型不能直接输出;如果输出要先转成日期型或字符串。

  • 年月日 ?date

  • 时分秒 ?time

  • 年月日时分秒 ?datetime

  • 指定格式 ?string(“自定义格式”)

    • y:年 M:月 d:日
    • H:时 m:分 s:秒
1
2
3
4
${myDate?date}
${myDate?time}
${myDate?datetime}
${myDate?string("yy年MM月dd日 HH时mm分ss秒")}

数值类型

数值类型:

在freemarker中数值类型可以直接输出:

  • 转字符串

    • 普通字符串 ?c
    • 货币型字符串 ?string.currency
    • 百分比型字符串 ?string.percent
  • 保留浮点型数值指定小数位(##表示小数位)

    • ?string[“0.##”]

字符串类型

在freemarker中字符串类型可以直接输出:

  • 截取字符串(左闭右开)?substring(start,end)
  • 首字母小写输出 ?uncap_first
  • 首字母大写输出 ?cap_first
  • 字母转小写输出 ?lower_case
  • 获取字符串长度 ?length
  • 是否以指定字符开头(boolean类型)?starts_with(“xx”)?string
  • 是否以指定字符结尾(boolean类型)?ends_with(“xx”)?string
  • 获取指定字符的索引 ?index_of(“xx”)
  • 去除字符串前后空格 ?trim
  • 替换指定字符串 ?replace(“xx”,”xx”)

sequence类型

  • 通过list指令输出序列

<#list 序列名 as 元素名>

${元素名}

</#list>

  • 获取序列的长度 ${序列名?size}
  • 获取序列元素的下标 ${元素名?index}
  • 获取第一个元素 ${序列名?first}
  • 获取最后一个元素 ${序列名?last}

倒序输出 序列名?reverse

升序输出 序列名?sort

降序输出 序列名?sort?reverse

指定字段名排序 序列名?sort_by(“字段名”)

hash类型

key遍历输出

<#list hashName?keys as key>

${key} – ${hash[key]}

</#list>

value遍历输出

<#list hash?values as value>

${value}

</#list>

空值情况处理

  • :指定缺失变量的默认值

    • ${value!}:如果value值为空,则默认值是空字符串。
    • ${value?”默认值”}:如果value值为空,则默认值是字符串“默认值”
  • ??:判断变量是否存在

    • 如果变量存在,返回true。否则返回false。${(value??)?string}

FreeMarker页面静态化

通过上述介绍可知 Freemarker是一种基于模板的、用来生成输出文本的通用工具,所以我们必须要定制符合自己业务的模板,然后生成自己的html页面。Freemarker是通过freemarker.template. Configuration这个对象对模板进行加载的(它也处理创建和缓存预解析模板的工作),然后我们通过 getTemplate方法获得你想要的模板,有一点要记住

freemarker.template.Configuration在你整个应用必须保证唯一实例。

对不需要频繁改变的数据进行静态化处理,减少服务器/数据库的访问压力。

FreeMarker运算符

算术运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--
算术运算
+、-、*、/、%
-->

<#assign al=8 a2=2>
${a1}+${a2}=${a1+a2}<br/>
${a1}-${a2}=${a1-a2}<br/>
${a1}*${a2}=s{a1*a2}<br/>
${a1}/${a2}=s{a1/a2}<br/>
${a1}%s{a2}=s{a1%a2}<br/>
<!-- 字符串运算 -->
${"hello"+","+"freemarker"}

逻辑运算符

1
2
3
4
<#--
逻辑运算符
&&、||、!
-->

比较运算符

1
2
3
4
5
6
7
8
9
<#--
比较运算符
>(gt):大于号,推荐使用gt
<(1t):小于号,推荐使用lt
>=(gte):大于等于,推荐是用gte
<=(lte):小于等于,推荐使用lte
==:等于
!=:不等于
-->

空值运算符

1
2
3
4
5
6
空值运算符
1. `??`:判断是否为空,返回布尔类型
如果不为空返回 false,如果为空返回true,不能直接输出${(name??)?string}。
2. `!`:设置默认值,如果为空,则设置默认值
1. 设置默认为空字符串${name!}
2. 设置指定默认值${name!'zhangsan'}