1. Introduction
1.绪论
FreeMarker is a template engine, written in Java, and maintained by the Apache Foundation. We can use the FreeMarker Template Language, also known as FTL, to generate many text-based formats like web pages, email, or XML files.
FreeMarker是一个模板引擎,用Java编写,并由Apache基金会维护。我们可以使用FreeMarker模板语言,也被称为FTL,来生成许多基于文本的格式,如网页、电子邮件或XML文件。
In this tutorial, we’ll see what we can do out-of-the-box with FreeMarker, though note that it is quite configurable and even integrates nicely with Spring.
在本教程中,我们将看到FreeMarker开箱即用的功能,不过请注意,它是相当可配置的,甚至能很好地与Spring集成。
Let’s get started!
让我们开始吧!
2. Quick Overview
2.快速概述
To inject dynamic content in our pages, we need to use a syntax that FreeMarker understands:
为了在我们的页面中注入动态内容,我们需要使用FreeMarker理解的语法。
- ${…} in the template will be replaced in the generated output with the actual value of the expression inside the curly brackets – we call this interpolation – a couple of examples are ${1 + 2} and ${variableName}
- FTL tags are like HTML tags (but contain # or @) and FreeMarker interprets them, for example <#if…></#if>
- Comments in FreeMarker start with <#– and end with –>
3. The Include Tag
3.包含标签
The FTL include directive is a way for us to follow the DRY principle in our application. We will define the repetitive content in a file and reuse it across different FreeMarker templates with single include tag.
FTL include指令是我们在应用中遵循DRY原则的一种方式。我们将在一个文件中定义重复的内容,并通过单个include标签在不同的FreeMarker模板中重复使用。
One such use case is when we want to include the menu section inside many pages. First, we’ll define the menu section inside a file – we’ll call it menu.ftl – with the following content:
一个这样的用例是当我们想在许多页面中包含菜单部分。首先,我们将在一个文件中定义菜单部分–我们称之为menu.ftl–其内容如下。
<a href="#dashboard">Dashboard</a>
<a href="#newEndpoint">Add new endpoint</a>
And on our HTML page, let’s include the created menu.ftl:
而在我们的HTML页面上,让我们包括创建的menu.ftl。
<!DOCTYPE html>
<html>
<body>
<#include 'fragments/menu.ftl'>
<h6>Dashboard page</h6>
</body>
</html>
And we can also include FTL in our fragments, which is great.
而且我们还可以在我们的碎片中包括超光速,这很好。
4. Handling Value Existence
4.处理价值的存在
FTL will consider any null value as a missing value. Thus, we need to be extra careful and add logic to handle null inside our template.
FTL会将任何null值视为缺失值。因此,我们需要特别小心,在我们的模板中添加逻辑来处理null。
We can use the ?? operator to check if an attribute, or nested property, exists. The result is a boolean:
我们可以使用??操作符来检查一个属性,或嵌套属性是否存在。其结果是一个布尔值。
${attribute??}
So, we’ve tested the attribute for null, but that’s not always enough. Let’s now define a default value as a fallback for this missing value. To do this, we need the ! operator placed after the name of the variable:
所以,我们已经测试了该属性的null,但这并不总是足够。现在让我们定义一个默认值,作为这个缺失值的回退。要做到这一点,我们需要将!操作符放在变量名称的后面。
${attribute!'default value'}
Using round brackets, we can wrap many nested attributes.
使用圆括号,我们可以包住许多嵌套的属性。
For example, to check if the attribute exists and has a nested property with another nested property, we wrap everything:
例如,为了检查属性是否存在,并且有一个与另一个嵌套属性的嵌套属性,我们把一切都包起来。
${(attribute.nestedProperty.nestedProperty)??}
Finally, putting everything together, we can embed these among static content:
最后,把所有东西放在一起,我们可以在静态内容中嵌入这些内容。
<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>
And, if the student were null, we’d see:
而且,如果学生是null,我们会看到。
<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>
Please notice the additional ?c directive used after the ??. We did it to convert the boolean value to a human-readable string.
请注意在??之后使用了额外的?c指令。我们这样做是为了将布尔值转换为人类可读的字符串。
5. The If-Else Tag
5.If-Else标签
Control structures are present in FreeMarker, and the traditional if-else is probably familiar:
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>
While the elseif and else branches are optional, the conditions must resolve to a boolean value.
虽然elseif和else分支是可选的,但这些条件必须解析为一个布尔值。
To help us with our evaluations, we’ll likely use one of:
为了帮助我们进行评估,我们可能会使用以下方法之一。
- x == y to check is x is equal to y
- x != y to return true only if x differs from y
- x lt y means that x must be strictly smaller than y – we can also use < instead of lt
- x gt y evaluates to true only if x is strictly greater than y – we can use > instead of gt
- x lte y tests if x is less than or equal to y – the alternative to lte is <=
- x gte y tests if x is greater than or equal to y – the alternative of gte is >=
- x?? to check the existence of x
- sequence?seqContains(x) validates the existence of x inside a sequence
It’s very important to keep in mind that FreeMarker considers >= and > as closing characters for an FTL tag. The solution is to wrap their usage in parentheses or use gte or gt instead.
请记住,FreeMarker认为>=和>是FTL标签的结束字符,这一点非常重要。解决方案是用圆括号包住它们的用法,或者使用gte或gt代替。
Putting it together, for the following template:
把它放在一起,为以下模板。
<#if status??>
<p>${status.reason}</p>
<#else>
<p>Missing status!</p>
</#if>
We end up with the resulting HTML code:
我们最后得到了所产生的HTML代码。
<!-- When status attribute exists -->
<p>404 Not Found</p>
<!-- When status attribute is missing -->
<p>Missing status!</p>
6. Containers of Sub-Variables
6.子变量的容器
In FreeMarker, we have three types of containers for sub-variables:
在FreeMarker中,我们有三种类型的子变量的容器。
- Hashes are a sequence of key-value pairs – the key must be unique inside the hash and we don’t have an ordering
- Sequences are lists where we have an index associated with each value – a noteworthy fact is that sub-variables can be of different types
- Collections are a special case of sequences where we can’t access the size or retrieve values by index – we can still iterate them with the list tag though!
6.1. Iterating Items
6.1.迭代项目
We can iterate over a container in two basic ways. The first one is where we iterate over each value and have logic happening for each of them:
我们可以用两种基本方式来迭代一个容器。第一种是我们对每个值进行迭代,并对每个值都有逻辑发生。
<#list sequence as item>
<!-- do something with ${item} -->
</#list>
Or, when we want to iterate a Hash, accessing both the key and the value:
或者,当我们想遍历一个Hash时,同时访问键和值。
<#list hash as key, value>
<!-- do something with ${key} and ${value} -->
</#list>
The second form is more powerful because it also allows us to define the logic that should happen at various steps in the iteration:
第二种形式更强大,因为它还允许我们定义在迭代的各个步骤中应该发生的逻辑。
<#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>
The item represents the name of the looped variable, but we can rename it to what we want. The else branch is optional.
item代表循环变量的名称,但我们可以把它重命名为我们想要的东西。else分支是可选的。
For a hands-on example, well define a template where we list some statuses:
对于一个实践的例子,我们定义一个模板,在那里我们列出一些状态。
<#list statuses>
<ul>
<#items as status>
<li>${status}</li>
</#items>
</ul>
<#else>
<p>No statuses available</p>
</#list>
This will return us the following HTML when our container is [“200 OK”, “404 Not Found”, “500 Internal Server Error”]:
当我们的容器是[“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. Items Handling
6.2.项目处理
A hash allows us two simple functions: keys to retrieve only the keys contained, and values to retrieve only the values.
一个哈希允许我们使用两个简单的函数。keys只检索包含的键,values只检索值。
A sequence is more complex; we can group the most useful functions:
一个序列更为复杂;我们可以将最有用的功能分组。
- chunk and join to get a sub-sequence or combine two sequences
- reverse, sort, and sortBy for modifying the order of elements
- first and last will retrieve the first or last element, respectively
- size represents the number of elements in the sequence
- seqContains, seqIndexOf, or seqLastIndexOf to look for an element
7. Type Handling
7.类型处理
FreeMarker comes with a huge variety of functions (built-ins) available for working with objects. Let’s see some frequently used functions.
FreeMarker带有大量可用于处理对象的函数(内置)。让我们看看一些经常使用的函数。
7.1. String Handling
7.1.字符串处理
- url and urlPath will URL-escape the string, with the exception that urlPath will not escape slash /
- jString, jsString, and jsonString will apply the escaping rules for Java, Javascript and JSON, respectively
- capFirst, uncapFirst, upperCase, lowerCase and capitalize are useful for changing the case of our string, as implied by their names
- boolean, date, time, datetime and number are functions for converting from a string to other types
Let’s now use a few of those functions:
现在让我们使用其中的几个函数。
<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>
And the output for the template above will be:
而上述模板的输出将是。
<p>http%3A//myurl.com/%3Fsearch%3DHello%20World</p>
<p>MY VALUE</p>
<p>Using \" in text</p>
<p>12.01.2019</p>
When using the date function, we’ve also passed the pattern to use for parsing the String object. FreeMarker uses the local format unless specified otherwise, for example in the string function available for date objects.
当使用date函数时,我们也传递了用于解析String对象的模式。FreeMarker使用本地格式,除非另有指定,例如在可用于日期对象的string函数。
7.2. Number Handling
7.2 数字处理
- round, floor and ceiling can help with rounding numbers
- abs will return a number’s absolute value
- string will convert the number to a string. We can also pass four pre-defined number formats: computer, currency, number, or percent or define our own format, like [ “0.###” ]
Let’s do a chain of a few mathematical operations:
让我们做一连串的几个数学运算。
<p>${(7.3?round + 3.4?ceiling + 0.1234)?string('0.##')}</p>
<!-- (7 + 4 + 0.1234) with 2 decimals -->
And as expected, the resulting value is 11.12.
正如预期的那样,结果值是11.12.。
7.3. Date Handling
7.3 日期处理
- .now represents the current date-time
- date, time and datetime can return the date and time sections of the date-time object
- string will convert date-times to strings – we can also pass the desired format or use a pre-defined one
We’re going to now get the current time and format the output to a string containing only the hours and minutes:
我们现在要获取当前时间,并将输出格式化为只包含小时和分钟的字符串。
<p>${.now?time?string('HH:mm')}</p>
The resulting HTML will be:
由此产生的HTML将是。
<p>15:39</p>
8. Exception Handling
8.异常处理
We’ll see two ways to handle exceptions for a FreeMarker template.
我们将看到两种处理FreeMarker模板异常的方法。
The first way is to use attempt-recover tags to define what we should try to execute and a block of code that should execute in case of error.
第一种方法是使用attempt-recover标签来定义我们应该尝试执行的内容,以及在出现错误时应该执行的代码块。
The syntax is:
语法是。
<#attempt>
<!-- block to try -->
<#recover>
<!-- block to execute in case of exception -->
</#attempt>
Both attempt and recover tags are mandatory. In case of an error, it rolls back the attempted block and will execute only the code in the recover section.
attempt和recover标签都是强制性的。如果出现错误,它会回滚尝试块,并只执行recover部分的代码。
Keeping this syntax in mind, let’s define our template as:
牢记这个语法,让我们把我们的模板定义为。
<p>Preparing to evaluate</p>
<#attempt>
<p>Attribute is ${attributeWithPossibleValue??}</p>
<#recover>
<p>Attribute is missing</p>
</#attempt>
<p>Done with the evaluation</p>
When attributeWithPossibleValue is missing, we’ll see:
当attributeWithPossibleValue丢失时,我们会看到。
<p>Preparing to evaluate</p>
<p>Attribute is missing</p>
<p>Done with the evaluation</p>
And the output when attributeWithPossibleValue exists is:
而当attributeWithPossibleValue存在时的输出是。
<p>Preparing to evaluate</p>
<p>Attribute is 200 OK</p>
<p>Done with the evaluation</p>
The second way is to configure FreeMarker what should happen in case of exceptions.
第二种方法是配置FreeMarker在出现异常情况时应该如何处理。
With Spring Boot, we easily configure this via properties file; here are some available configurations:
有了Spring Boot,我们可以通过属性文件轻松地进行配置;这里有一些可用的配置。
- spring.freemarker.setting.template_exception_handler=rethrow re-throws the exception
- spring.freemarker.setting.template_exception_handler=debug outputs the stack trace information to the client and then re-throws the exception.
- spring.freemarker.setting.template_exception_handler=html_debug outputs the stack trace information to the client, formatting it so it will be usually well readable in the browser, and then re-throws the exception.
- spring.freemarker.setting.template_exception_handler=ignore skips the failing instructions, letting the template continue executing.
- spring.freemarker.setting.template_exception_handler=default
9. Calling Methods
9.调用方法
Sometimes we want to call Java methods from our FreeMarker templates. We’ll now see how to do it.
有时我们想从我们的FreeMarker模板中调用Java方法。我们现在来看看如何做到这一点。
9.1. Static Members
9.1.静态成员
To start accessing static members, we could either update our global FreeMarker configuration or add a StaticModels type attribute on the model, under the attribute name statics:
为了开始访问静态成员,我们可以更新我们的全局FreeMarker配置,或者在模型上添加一个StaticModels类型属性,属性名为statics。
model.addAttribute("statics", new DefaultObjectWrapperBuilder(new Version("2.3.28"))
.build().getStaticModels());
Accessing static elements is straight-forward.
访问静态元素是直截了当的。
First, we import the static elements of our class using the assign tag, then decide on a name and, finally, the Java classpath.
首先,我们使用assign标签导入我们的类的静态元素,然后决定一个名字,最后决定Java classpath。
Here’s how we’ll import Math class in our template, show the value of the static PI field, and use the static pow method:
下面是我们如何在模板中导入Math类,显示静态PI字段的值,并使用静态pow方法。
<#assign MathUtils=statics['java.lang.Math']>
<p>PI value: ${MathUtils.PI}</p>
<p>2*10 is: ${MathUtils.pow(2, 10)}</p>
The resulting HTML is:
由此产生的HTML是。
<p>PI value: 3.142</p>
<p>2*10 is: 1,024</p>
9.2. Bean Members
9.2.Bean成员
Bean members are very easy to access: use the dot (.) and that’s it!
Bean成员非常容易访问。使用点(.),就可以了!。
For our next example, we will add a Random object to our model:
对于我们的下一个例子,我们将添加一个Random对象到我们的模型。
model.addAttribute("random", new Random());
In our FreeMarker template, let’s generate a random number:
在我们的FreeMarker模板中,让我们生成一个随机数。
<p>Random value: ${random.nextInt()}</p>
This will cause output similar to:
这将导致类似的输出。
<p>Random value: 1,329,970,768</p>
9.3. Custom Methods
9.3.自定义方法
The first step for adding a custom method is to have a class that implements FreeMarker’s TemplateMethodModelEx interface and defines our logic inside the exec method:
添加自定义方法的第一步是要有一个实现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);
}
}
We’ll add an instance of our new class as an attribute on the model:
我们将添加我们新类的一个实例作为模型上的一个属性。
model.addAttribute("lastChar", new LastCharMethod());
The next step is to use our new method inside our template:
下一步是在模板中使用我们的新方法。
<p>Last char example: ${lastChar('mystring')}</p>
Finally, the resulting output is:
最后,得出的输出结果是。
<p>Last char example: g</p>
10. Conclusion
10.结语
In this article, we’ve seen how to use the FreeMarker template engine inside our project. We’ve focused on common operations, how to manipulate different objects, and a few more advanced topics.
在这篇文章中,我们已经看到如何在我们的项目中使用FreeMarker模板引擎。我们集中讨论了常见的操作,如何操作不同的对象,以及一些更高级的主题。
The implementation of all these snippets is available over on GitHub.
所有这些片段的实现都可以在GitHub上找到,。