1. 引言

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

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

让我们开始吧!

2. 快速概览

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

  • ${...}在模板中,生成的输出将被替换为大括号内表达式的实际值 - 我们称之为插值 - 几个示例是 ${1 + 2} 和 ${variableName}
  • FTL标签类似于HTML标签(但包含#@),自由标记解释它们,例如<#if...></#if>
  • 自由标记中的注释以 <#– 开头,以 –>

3. 包含标记

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

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

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

在我们的 HTML 页面上,让我们包括创建的菜单.ftl

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

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

4. 处理价值存在

FTL 会将任何值视为缺失值。因此,我们需要格外小心,并在模板中添加逻辑来处理 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>

而且,如果学生为空,我们将看到:

<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. 如果-否则标签

控制结构存在于自由标记中,传统的如果-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>

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

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

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

请务必记住,自由标记将>=和>视为FTL标签的结束字符。 解决方案是将其用法括在括号中,或者改用 gte 或 gt

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

<#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. 子变量的容器

在自由标记中,我们有三种类型的子变量容器:

  • 哈希是一系列键值对 - 键在哈希中必须是唯一的,我们没有排序
  • 序列是列表,其中我们有一个与每个值关联的索引 - 一个值得注意的事实是,子变量可以是不同的类型
  • 集合是序列的一种特殊情况,我们无法通过索引访问大小或检索值 - 我们仍然可以使用列表标记迭代它们!

6.1. 迭代项目

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

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

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

<#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>

表示循环变量的名称,但我们可以将其重命名为所需的名称。其他分支是可选的。

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

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

当我们的容器是[“200 OK”,“404未找到”,“500内部服务器错误”]时,这将返回以下HTML:

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

6.2. 物品处理

哈希允许我们使用两个简单的函数:仅检索包含的键,以及检索值的值。

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

  • 分块接以获取子序列或组合两个序列
  • 反转、排序排序用于修改元素顺序的依据
  • 第一个最后一个将分别检索第一个或最后一个元素
  • 大小表示序列中的元素数
  • seqContains、 seqIndexOf 或 seqLastIndexOf 以查找元素

7. 类型处理

自由标记具有各种各样的功能(内置功能)可用于处理对象。让我们看看一些常用的函数。

7.1. 字符串处理

  • 网址网址路径将对字符串进行网址转义,但网址路径不会转义斜杠 /
  • jstringjsStringjsonString将分别应用Java,Javascript和JSON的转义规则
  • capFirstuncapFirst大写小写大写对于更改字符串的大小写很有用,正如它们的名称所暗示的那样
  • 布尔值日期时间日期时间和数字是从字符串转换为其他类型的函数

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

<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 使用本地格式,例如在可用于日期对象的字符串函数中。

7.2. 数字处理

  • 圆形地板天花板可以帮助舍入数字
  • abs 将返回数字的绝对值
  • 字符串会将数字转换为字符串。我们还可以传递四种预定义的数字格式:计算机货币数字百分比,或者定义我们自己的格式,如[ “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 表示当前日期时间
  • 日期时间和日期时间可以返回日期时间对象的日期和时间部分
  • string 会将日期时间转换为字符串 – 我们还可以传递所需的格式或使用预定义的格式

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

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

生成的 HTML 将为:

<p>15:39</p>

8. 异常处理

我们将看到两种处理自由标记模板异常的方法。

第一种方法是使用尝试恢复标记来定义我们应该尝试执行的内容以及在发生错误时应执行的代码块。

语法为:

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

尝试恢复标记都是必需的。如果发生错误,它会回滚尝试的块,并且只会执行恢复部分中的代码

牢记此语法,让我们将模板定义为:

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

缺少属性与可能的值时,我们将看到:

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

属性存在可能值时,输出为:

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

第二种方法是配置自由标记在发生异常时应该发生的情况。

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

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

9. 调用方法

有时我们想从我们的自由标记模板调用Java方法。现在我们将了解如何执行此操作。

9.1. 静态成员

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

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

访问静态元素非常简单。

首先,我们使用赋值标记导入类的静态元素,然后确定名称,最后确定 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>

生成的上下文为:

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

9.2. 豆类成员

Bean会员非常容易访问:使用点(.),就是这样!

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

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

在我们的自由标记模板中,让我们生成一个随机数:

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

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

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

9.3. 自定义方法

添加自定义方法的第一步是有一个类,该类实现自由标记的模板方法ModelEx接口,并在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>

10. 结论

在本文中,我们已经了解了如何在我们的项目中使用自由标记模板引擎。我们重点介绍了常见操作、如何操作不同的对象以及一些更高级的主题。