Contents

FreeMarker 简介

1. 简介

FreeMarker 是一个模板引擎,用 Java 编写,由 Apache 基金会维护。我们可以使用 FreeMarker 模板语言(也称为 FTL)来生成许多基于文本的格式,例如网页、电子邮件或 XML 文件。

在本教程中,我们将看到我们可以使用 FreeMarker 开箱即用地做什么,但请注意它是非常可配置 的 ,甚至可以很好地与 Spring 集成

让我们开始吧!

2. 快速概览

为了在我们的页面中注入动态内容,我们需要使用 FreeMarker 理解的语法

  • 模板中的*${…}将在生成的输出中替换为大括号内表达式的实际值——我们称之为interpolation ——几个例子是${1 + 2* } 和*${variableName}*
  • FTL 标签类似于 HTML 标签(但包含*#@),FreeMarker 会解释它们,例如<#if…></#if>*
  • FreeMarker 中的注释以 <#– 开头并以 –> 结尾

3. 包含标签

FTL*include 指令是我们在应用程序中遵循 DRY 原则的一种方式。我们将在一个文件中定义重复的内容,并在具有单个include *标签的不同 FreeMarker 模板中重复使用它。

一个这样的用例是当我们想要在许多页面中包含菜单部分时。首先,我们将在一个文件中定义菜单部分——我们称之为menu.ftl——具有以下内容:

<a href="#dashboard">Dashboard</a>
<a href="#newEndpoint">Add new endpoint</a>

在我们的 HTML 页面上,让我们包含创建的menu.ftl

<!DOCTYPE html>
<html>
<body>
<#include 'fragments/menu.ftl'>
    <h6>Dashboard page</h6>
</body>
</html>

而且我们还可以在片段中包含 FTL,这很棒。

4.处理价值存在

FTL 会将任何null 值视为缺失值。因此,我们需要格外小心,并在模板中**添加处理null的逻辑。**

我们可以使用*??* 运算符来检查属性或嵌套属性是否存在。结果是一个布尔值:

${attribute??}

因此,我们已经测试了该属性是否为null,但这并不总是足够的。现在让我们定义一个默认值作为这个缺失值的后备。为此,我们需要 ! 运算符放在变量名之后:

${attribute!'default value'}

使用圆括号,我们可以包装许多嵌套属性。

例如,要检查属性是否存在并且是否有一个嵌套属性和另一个嵌套属性,我们将所有内容包装起来:

${(attribute.nestedProperty.nestedProperty)??}

最后,将所有内容放在一起,我们可以将它们嵌入到静态内容中:

<p>Testing is student property exists: ${student???c}</p>
<p>Using default value for missing student: ${student!'John Doe'}</p>
<p>Wrapping student nested properties: ${(student.address.street)???c}</p>

而且,如果studentnull,我们会看到:

<p>Testing is student property exists: false</p>
<p>Using default value for missing student: John Doe</p>
<p>Wrapping student nested properties: false</p>

请注意在*??之后使用的附加?c*指令。. 我们这样做是为了将布尔值转换为人类可读的字符串。

5. If-Else 标签

FreeMarker 中存在控制结构,传统的 if-else 可能很熟悉:

<#if condition>
    <!-- block to execute if condition is true -->
<#elseif condition2>
    <!-- block to execute if condition2 is the first true condition -->
<#elseif condition3>
    <!-- block to execute if condition3 is the first true condition -->
<#else>
    <!-- block to execute if no condition is true -->
</#if>

虽然elseifelse分支是可选的,但条件必须解析为布尔值。

为了帮助我们进行评估,我们可能会使用以下方法之一:

  • x == y检查x是否等于y
  • x != y仅当xy不同时才返回true
  • x lt y意味着x必须严格小于y——我们也可以使用<代替lt
  • x gt y只有当x严格大于y时才计算为——我们可以使用*>代替gt*
  • x lte y测试x是否小于或等于y - lte的替代方案是*<=*
  • x gte y测试x是否大于或等于y – gte的替代选项是 >=
  • X?? 检查x的存在
  • sequence?seqContains(x)验证序列中是否存在x

请务必记住,FreeMarker 将 >= 和 > 视为 FTL 标记的结束字符。解决方案是将它们的用法括在括号中或使用gtegt代替。

将其放在一起,用于以下模板:

<#if status?? >
    <p>${status.reason}</p>
<#else>
    <p>Missing status!</p>
</#if>

我们最终得到了 HTML 代码:

 <!-- When status attribute exists -->
<p>404 Not Found</p>
<!-- When status attribute is missing -->
<p>Missing status!</p>

6. 子变量的容器

在 FreeMarker 中,我们为子变量提供了三种类型的容器:

  • *Hashes *是键值对的序列——键在散列中必须是唯一的,并且我们没有排序
  • *Sequences *是我们有一个与每个值相关联的索引的列表——一个值得注意的事实是子变量可以是不同的类型
  • *Collections 是序列的一种特殊情况,我们无法通过索引访问大小或检索值——但我们仍然可以使用list *标签对其进行迭代!

6.1. 迭代项目

我们可以通过两种基本方式迭代容器。第一个是我们迭代每个值并为每个值发生逻辑的地方:

<#list sequence as item>
    <!-- do something with ${item} -->
</#list>

或者,当我们想要迭代Hash时,同时访问键和值:

<#list hash as key, value>
    <!-- do something with ${key} and ${value} -->
</#list>

第二种形式更强大,因为它还允许我们定义应该在迭代的各个步骤中发生的逻辑:

<#list sequence>
    <!-- one-time logic if the sequence is not empty -->
    <#items as item>
        <!-- logic repeated for every item in sequence -->
    </#items>
    <!-- one-time logic if the sequence is not empty -->
<#else>
    <!-- one-time logic if the sequence is empty -->
</#list>

item表示循环变量的名称,但我们可以将其重命名为我们想要的名称。else分支是可选的。

对于一个动手示例,请定义一个模板,我们在其中列出一些状态:

<#list statuses>
    <ul>
    <#items as status>
        <li>${status}</li>
    </#items>
    </ul>
<#else>
    <p>No statuses available</p>
</#list>

当我们的容器为*[“200 OK”, “404 Not Found”, “500 Internal Server Error”]*时,这将返回以下 HTML :

<ul>
<li>200 OK</li>
<li>404 Not Found</li>
<li>500 Internal Server Error</li>
</ul>

6.2. Item 处理

散列允许我们使用两个简单的功能:只检索包含*keys 的键和只检索values *的值。

序列更复杂;我们可以对最有用的功能进行分组:

  • chunkjoin得到一个子序列或组合两个序列
  • reversesortsortBy用于修改元素的顺序
  • firstlast将分别检索第一个或最后一个元素
  • size表示序列中的元素个数
  • seqContainsseqIndexOfseqLastIndexOf查找元素

7. 类型处理

FreeMarker 带有大量 可用于处理对象的函数(内置)。让我们看看一些常用的功能。

7.1.字符串处理

  • urlurlPath将对字符串进行 URL 转义,但urlPath不会转义斜杠 /
  • jStringjsStringjsonString将分别应用 Java、Javascript 和 JSON 的转义规则
  • capFirstuncapFirstupperCaselowerCasecapitalize可用于更改字符串的大小写,正如它们的名称所暗示的那样
  • booleandatetimedatetimenumber是用于将字符串转换为其他类型的函数

现在让我们使用其中的一些函数:

<p>${'http://myurl.com/?search=Hello World'?urlPath}</p>
<p>${'Using " in text'?jsString}</p>
<p>${'my value?upperCase}</p>
<p>${'2019-01-12'?date('yyyy-MM-dd')}</p>

上面模板的输出将是:

<p>http%3A//myurl.com/%3Fsearch%3DHello%20World</p>
<p>MY VALUE</p>
<p>Using \" in text</p>
<p>12.01.2019</p>

使用date函数时,我们还传递了用于解析 String 对象的模式。FreeMarker 使用本地格式,除非另有说明,例如在可用于日期对象的*string *函数中。

7.2. 号码处理

  • round,*floor ceiling *可以帮助四舍五入
  • abs将返回一个数字的绝对值
  • string将数字转换为字符串。我们还可以传递四种预定义的数字格式:computercurrencynumberpercent ,或者定义我们自己的格式,例如[“0.###”]

让我们做一些数学运算的链:

<p>${(7.3?round + 3.4?ceiling + 0.1234)?string('0.##')}</p>
<!-- (7 + 4 + 0.1234) with 2 decimals -->

正如预期的那样,结果值为11.12

7.3. 日期处理

  • .now表示当前日期时间
  • datetimedatetime可以返回日期时间对象的日期和时间部分
  • string将日期时间转换为字符串——我们也可以传递所需的格式或使用预定义的格式

我们现在要获取当前时间并将输出格式化为仅包含小时和分钟的字符串:

<p>${.now?time?string('HH:mm')}</p>

生成的 HTML 将是:

<p>15:39</p>

8. 异常处理

我们将看到两种处理 FreeMarker 模板异常的方法。

第一种方法是使用attempt-recover标签来定义我们应该尝试执行的内容以及在出现错误时应该执行的代码块。 语法是:

<#attempt>
    <!-- block to try -->
<#recover>
    <!-- block to execute in case of exception -->
</#attempt>

*attempt recover 标签都是强制性的。**如果出现错误,它会回滚尝试的块并仅执行recover *部分**中的代码。

记住这个语法,让我们将我们的模板定义为:

<p>Preparing to evaluate</p>
<#attempt>
    <p>Attribute is ${attributeWithPossibleValue??}</p>
<#recover>
    <p>Attribute is missing</p>
</#attempt>
<p>Done with the evaluation</p>

当缺少attributeWithPossibleValue时,我们将看到:

<p>Preparing to evaluate</p>
    <p>Attribute is missing</p>
<p>Done with the evaluation</p>

attributeWithPossibleValue存在时的输出是:

<p>Preparing to evaluate</p>
    <p>Attribute is 200 OK</p>
<p>Done with the evaluation</p>

第二种方法是配置 FreeMarker 在异常情况下应该发生什么。

使用 Spring Boot,我们可以通过属性文件轻松配置它;以下是一些可用的配置:

  • spring.freemarker.setting.template_exception_handler=rethrow 重新抛出异常
  • spring.freemarker.setting.template_exception_handler=debug 将堆栈跟踪信息输出到客户端,然后重新抛出异常。
  • spring.freemarker.setting.template_exception_handler=html_debug 将堆栈跟踪信息输出到客户端,对其进行格式化,使其通常在浏览器中可读,然后重新抛出异常。
  • spring.freemarker.setting.template_exception_handler=ignore 跳过失败的指令,让模板继续执行。
  • spring.freemarker.setting.template_exception_handler=default

9. 调用方法

有时我们想从 FreeMarker 模板中调用 Java 方法。我们现在将看到如何做到这一点。

9.1. 静态成员

要开始访问静态成员,我们可以更新我们的全局 FreeMarker 配置或在模型上添加一个 StaticModels类型属性,在属性名称statics下:

model.addAttribute("statics", new DefaultObjectWrapperBuilder(new Version("2.3.28"))
    .build().getStaticModels());

访问静态元素是直截了当的。

首先,我们使用 assign 标签导入类的静态元素,然后确定名称,最后确定 Java 类路径。

下面是我们如何在模板中导入Math类,显示静态PI字段的值,并使用静态pow方法:

<#assign MathUtils=statics['java.lang.Math']>
<p>PI value: ${MathUtils.PI}</p>
<p>2*10 is: ${MathUtils.pow(2, 10)}</p>

生成的 HTML 是:

<p>PI value: 3.142</p>
<p>2*10 is: 1,024</p>

9.2. Bean 成员

Bean 成员很容易访问:**使用点 (.)**就可以了!

对于我们的下一个示例,我们将在模型中添加一个Random对象:

model.addAttribute("random", new Random());

在我们的 FreeMarker 模板中,让我们生成一个随机数:

<p>Random value: ${random.nextInt()}</p>

这将导致类似于以下内容的输出:

<p>Random value: 1,329,970,768</p>

9.3. 自定义方法

添加自定义方法的第一步是拥有一个实现 FreeMarker 的TemplateMethodModelEx接口并在exec方法中定义我们的逻辑的类:

public class LastCharMethod implements TemplateMethodModelEx {
    public Object exec(List arguments) throws TemplateModelException {
        if (arguments.size() != 1 || StringUtils.isEmpty(arguments.get(0)))
            throw new TemplateModelException("Wrong arguments!");
        String argument = arguments.get(0).toString();
        return argument.charAt(argument.length() - 1);
    }
}

我们将添加新类的实例作为模型的属性:

model.addAttribute("lastChar", new LastCharMethod());

下一步是在模板中使用我们的新方法:

<p>Last char example: ${lastChar('mystring')}</p>

最后,得到的输出是:

<p>Last char example: g</p>