SpringBoot2.x 集成 FreeMarker

本文主要对SpringBoot2.x集成FreeMarker及其常用语法进行简单总结,其中SpringBoot使用的2.4.5版本。

一、FreeMarker简介

Apache FreeMarker™是一款模板引擎:一个Java库,用于根据模板和变化的数据生成文本输出(HTML网页、电子邮件、配置文件、源代码等)。模板是用FreeMarker模板语言(FTL)编写的,它是一种简单的、专门的语言(不是像PHP那样的全面的编程语言)。通常,一个通用的编程语言(如Java)被用来准备数据。然后,Apache FreeMarker使用模板显示这些准备好的数据。在模板中,关注的是如何呈现数据,而在模板之外,关注的是要呈现什么数据。

二、集成FreeMarker

通过Maven新建一个名为springboot-freemarker的项目。

1.引入依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-freemarkerartifactId>
dependency>

<dependency>
    <groupId>org.projectlombokgroupId>
    <artifactId>lombokartifactId>
    <version>1.18.8version>
dependency>

2.编写配置文件

spring:
  freemarker:
    # 模板的加载路径,多个以,分隔,默认为classpath:/templates/
    template-loader-path: classpath:/templates/
    # 在构建URL时附加到视图名称的前缀,注意不包含路径,默认为空字符串
    prefix: fm-
    # 在构建URL时附加到视图名称的后缀,默认为.ftlh
    suffix: .ftl
    # 模板文件编码,默认为UTF-8
    charset: UTF-8
    # 是否启用模板缓存,默认为true,表示启用,false不启用
    cache: false
    # 是否检查模板位置存在与否,默认为true
    check-template-location: true
    # Content-Type的值,默认为text/html
    content-type: text/html
    # 在与模板合并之前,是否将所有请求属性添加到模型中,默认为false
    expose-request-attributes: false
    # 在与模板合并之前,是否将所有HttpSession属性添加到模型中,默认为false
    expose-session-attributes: false
    # 是否以springMacroRequestContext的形式暴露一个RequestContext,供Spring的macro库使用,默认为true
    expose-spring-macro-helpers: true
    # 是否允许HttpServletRequest属性覆盖(隐藏)控制器生成的同名模型属性,默认为false
    allow-request-override: false
    # 是否允许HttpSession属性覆盖(隐藏)控制器生成的同名模型属性,默认为false
    allow-session-override: false
    # 是否优先从文件系统加载模板,以实现模板变化的热检测
    # 当模板路径被检测到是一个目录时,模板只从该目录加载,其他匹配的类路径位置将不被考虑
    prefer-file-system-access: true

3.准备模板

首先按照配置文件中配置的模板的加载路径在resources下创建一个templates目录,用于存放模板。然后创建如下名为fm-hello.html的模板:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello FreeMarkertitle>
head>
<body>
    <div>
        <#-- ${}:FreeMarker会输出真实的值来替换{}内的表达式,这样的表达式被称为插值 -->
        <span>${hello}span>
    div>
body>
html>

4.Controller层

创建Controller并将模板中要获取的变量设置到Model对象中,如果Controller类上使用的是@Controller注解,则可以返回跟模板名称相同的字符串(不包括前缀和后缀),视图解析器会解析出视图具体地址并生成视图,然后返回给前端控制器进行渲染:

package com.rtxtitanv.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author rtxtitanv
 * @version 1.0.0
 * @name com.rtxtitanv.controller.FreeMarkerController
 * @description FreeMarkerController
 * @date 2021/7/4 13:52
 */
@RequestMapping("/fm")
@Controller
public class FreeMarkerController {

    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("hello", "

Hello FreeMarker

"
); return "hello"; } }

运行项目,浏览器访问http://localhost:8080/fm/hello,发现数据成功渲染到模板:
1
如果Controller类上使用的是@RestController注解,则需要将视图添加到ModelAndView对象中并返回:

package com.rtxtitanv.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

/**
 * @author rtxtitanv
 * @version 1.0.0
 * @name com.rtxtitanv.controller.TestController
 * @description TestController
 * @date 2021/7/4 13:54
 */
@RequestMapping("/test")
@RestController
public class TestController {

    @GetMapping("/hello")
    public ModelAndView hello() {
        ModelAndView modelAndView = new ModelAndView("hello");
        modelAndView.addObject("hello", "

hello freemarker

"
); return modelAndView; } }

运行项目,浏览器访问http://localhost:8080/test/hello,发现数据成功渲染到模板:
2

三、FreeMarker常用语法

1.数据模型

模板 + 数据模型 = 输出。模板和静态HTML是相同的,只是它会包含一些FreeMarker将它们变成动态内容的指令。数据模型是为模板准备 的整体数据。数据模型的基本结构是树状的。

创建数据模型类:

package com.rtxtitanv.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.util.List;

/**
 * @author rtxtitanv
 * @version 1.0.0
 * @name com.rtxtitanv.model.Account
 * @description Account
 * @date 2021/7/4 13:53
 */
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Account {
    private Long accountId;
    private String accountName;
    private String accountPassword;
    private User user;
    private List<Order> orders;
}
package com.rtxtitanv.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

/**
 * @author rtxtitanv
 * @version 1.0.0
 * @name com.rtxtitanv.model.Order
 * @description Order
 * @date 2021/7/4 13:53
 */
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Order {
    private Long orderId;
    private String orderNumber;
    private String orderDescription;
    private Account account;
}
package com.rtxtitanv.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author rtxtitanv
 * @version 1.0.0
 * @name com.rtxtitanv.model.User
 * @description User
 * @date 2021/7/4 13:53
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
    private Long id;
    private String username;
    private String password;
}

模板fm-model1.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>数据模型title>
head>
<body>
    <p>Accountp>
    <div>
        <ul>
            <li><p>accountId = ${account.accountId}p>li>
            <li><p>accountName = "${account.accountName}"p>li>
            <li><p>accountPassword = "${account.accountPassword}"p>li>
            <li><p>userp>li>
            <ul>
                <li><p>id = ${account.user.id}p>li>
                <li><p>username = "${account.user.username}"p>li>
                <li><p>password = "${account.user.password}"p>li>
            ul>
            <li><p>ordersp>li>
            <ul>
                <#-- list指令:<#list sequence as loopVariable>repeatThis#list> -->
                <#list account.orders as order>
                    <li>
                        <p>(${order?index})p>
                        <ul>
                            <li><p>orderId = ${order.orderId}p>li>
                            <li><p>orderNumber = "${order.orderNumber}"p>li>
                            <li><p>orderDescription = "${order.orderDescription}"p>li>
                            <li><p>accountp>li>
                            <ul>
                                <li><p>accountId = ${order.account.accountId}p>li>
                                <li><p>accountName = "${order.account.accountName}"p>li>
                                <li><p>accountPassword = "${order.account.accountPassword}"p>li>
                                <li><p>userp>li>
                                <ul>
                                    <li><p>id = ${order.account.user.id}p>li>
                                    <li><p>username = "${order.account.user.username}"p>li>
                                    <li><p>password = "${order.account.user.password}"p>li>
                                ul>
                            ul>
                        ul>
                    li>
                #list>
            ul>
        ul>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/model/test1")
public String dataModel1(Model model) {
    List<Order> orders = new ArrayList<>();
    Account account = new Account().setAccountId(1L).setAccountName("MyAccount").setAccountPassword("123456")
        .setUser(new User(1L, "RtxTitanV", "654321"));
    orders.add(new Order().setOrderId(1L).setOrderNumber("785698232657798568").setOrderDescription("二两麻辣牛肉面")
        .setAccount(account));
    orders.add(new Order().setOrderId(2L).setOrderNumber("785938232669132551").setOrderDescription("三两三鲜米线")
        .setAccount(account));
    orders.add(new Order().setOrderId(3L).setOrderNumber("793382623157348612").setOrderDescription("二两老麻抄手")
        .setAccount(account));
    account.setOrders(orders);
    model.addAttribute("account", account);
    return "model1";
}

效果:

模板fm-model2.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>数据模型title>
head>
<body>
    <p>(root)p>
    <div>
        <ul>
            <li><p>accountId = ${accountId}p>li>
            <li><p>accountName = "${accountName}"p>li>
            <li><p>accountPassword = "${accountPassword}"p>li>
            <li><p>userp>li>
            <ul>
                <li><p>id = ${user.id}p>li>
                <li><p>username = "${user.username}"p>li>
                <li><p>password = "${user.password}"p>li>
            ul>
            <li><p>ordersp>li>
            <ul>
                <#-- list指令:<#list sequence as loopVariable>repeatThis#list> -->
                <#list orders as order>
                    <li>
                        <p>(${order?index})p>
                        <ul>
                            <li><p>orderId = ${order.orderId}p>li>
                            <li><p>orderNumber = "${order.orderNumber}"p>li>
                            <li><p>orderDescription = "${order.orderDescription}"p>li>
                            <li><p>accountp>li>
                            <ul>
                                <li><p>accountId = ${order.account.accountId}p>li>
                                <li><p>accountName = "${order.account.accountName}"p>li>
                                <li><p>accountPassword = "${order.account.accountPassword}"p>li>
                                <li><p>userp>li>
                                <ul>
                                    <li><p>id = ${order.account.user.id}p>li>
                                    <li><p>username = "${order.account.user.username}"p>li>
                                    <li><p>password = "${order.account.user.password}"p>li>
                                ul>
                            ul>
                        ul>
                    li>
                #list>
            ul>
        ul>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/model/test2")
public String dataModel2(Map<String, Object> map) {
    List<Order> orders = new ArrayList<>();
    Account account = new Account().setAccountId(1L).setAccountName("MyAccount").setAccountPassword("123456")
        .setUser(new User(1L, "RtxTitanV", "654321"));
    orders.add(new Order().setOrderId(1L).setOrderNumber("785698232657798568").setOrderDescription("二两麻辣牛肉面")
        .setAccount(account));
    orders.add(new Order().setOrderId(2L).setOrderNumber("785938232669132551").setOrderDescription("三两三鲜米线")
        .setAccount(account));
    orders.add(new Order().setOrderId(3L).setOrderNumber("793382623157348612").setOrderDescription("二两老麻抄手")
        .setAccount(account));
    map.put("accountId", account.getAccountId());
    map.put("accountName", account.getAccountName());
    map.put("accountPassword", account.getAccountPassword());
    map.put("user", account.getUser());
    map.put("orders", orders);
    return "model2";
}

效果:

模板fm-model3.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>数据模型title>
head>
<body>
    <p>(root)p>
    <div>
        <ul>
            <li><p>accountp>li>
            <ul>
                <li><p>accountId = ${account.accountId}p>li>
                <li><p>accountName = "${account.accountName}"p>li>
                <li><p>accountPassword = "${account.accountPassword}"p>li>
                <li><p>userp>li>
                <ul>
                    <li><p>id = ${account.user.id}p>li>
                    <li><p>username = "${account.user.username}"p>li>
                    <li><p>password = "${account.user.password}"p>li>
                ul>
                <li><p>ordersp>li>
                <ul>
                    <#-- list指令:<#list sequence as loopVariable>repeatThis#list> -->
                    <#list account.orders as order>
                        <li>
                            <p>(${order?index})p>
                            <ul>
                                <li><p>orderId = ${order.orderId}p>li>
                                <li><p>orderNumber = "${order.orderNumber}"p>li>
                                <li><p>orderDescription = "${order.orderDescription}"p>li>
                                <li><p>accountp>li>
                                <ul>
                                    <li><p>accountId = ${order.account.accountId}p>li>
                                    <li><p>accountName = "${order.account.accountName}"p>li>
                                    <li><p>accountPassword = "${order.account.accountPassword}"p>li>
                                    <li><p>userp>li>
                                    <ul>
                                        <li><p>id = ${order.account.user.id}p>li>
                                        <li><p>username = "${order.account.user.username}"p>li>
                                        <li><p>password = "${order.account.user.password}"p>li>
                                    ul>
                                ul>
                            ul>
                        li>
                    #list>
                ul>
            ul>
        ul>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/model/test3")
public String dataModel3(Map<String, Object> map) {
    List<Order> orders = new ArrayList<>();
    Account account = new Account().setAccountId(1L).setAccountName("MyAccount").setAccountPassword("123456")
        .setUser(new User(1L, "RtxTitanV", "654321"));
    orders.add(new Order().setOrderId(1L).setOrderNumber("785698232657798568").setOrderDescription("二两麻辣牛肉面")
        .setAccount(account));
    orders.add(new Order().setOrderId(2L).setOrderNumber("785938232669132551").setOrderDescription("三两三鲜米线")
        .setAccount(account));
    orders.add(new Order().setOrderId(3L).setOrderNumber("793382623157348612").setOrderDescription("二两老麻抄手")
        .setAccount(account));
    account.setOrders(orders);
    map.put("account", account);
    return "model3";
}

效果:

数据模型中一种存储变量及其相关且有唯一标识名称的容器(root、account、user)被称为哈希表。存储单个值的变量(accountId、accountName、accountPassword等)被称为标量。存储有序变量的容器(orders),被称为序列,存储的变量可以通过数字索引(通常从0开始)来检索。

标量分类如下:

  • 字符串:就是文本,也就是任意的字符序列。
  • 数字:这是数值类型。
  • 日期/时间:可以是date-time格式,或date(没有time)格式,或time(没有date)格式。
  • 布尔值:true,false。

2.总体结构

FreeMarker模板实际上是用FTL(FreeMarker Template Language,这是为编写模板设计的非常简单的编程语言。)的语言编写的程序。模板(FTL编程)是由以下部分组成:

  • 文本:文本会原样输出。
  • 插值:在输出中会被替换成计算值。插值是由${}(或#{},不推荐)来分隔。
  • FTL标签:FTL标签有点类似于HTML标签,但它们是对FreeMarker的指示,不会被打印输出。
  • 注释:注释和HTML的注释也很类似,但它们是由<#---->来分隔。注释会被FreeMarker直接忽略, 更不会在输出内容中显示。

注意:

  • FTL区分大小写。
  • FTL标签不可以在其他FTL标签和插值中使用。
  • 注释可以放在FTL标签和插值中。
  • 尽管文本是原样输出,但由于FreeMarker的“空白剥离”特性, 它会自动去除一些多余的空格,制表符和换行符。

3.常用指令

指令通过使用FTL标签来调用,FTL标签也被称为指令。FTL标签分为开始标签(<#directivename parameters>parameters的格式由directivename决定。)和结束标签()两种。如果标签没有嵌套内容(在开始标签和结束标签之间的内容),则可以只使用开始标签。

事实上指令分为预定义指令和用户自定义指令两种类型。对于用户自定义指令,使用@来代替#,更深的区别在于如果指令没有嵌套内容,则必须这么使用<@mydirective parameters />

注意:

  • FTL标签必须正确嵌套。即被嵌套的指令的开始和结束标签都要在嵌套指令内部。
  • FreeMarker只关心FTL标签的嵌套,不关心HTML标签的嵌套。它只会把HTML看做是文本,不会解释HTML。
  • FreeMarker会忽略FTL标签中多余的空白符,但是也不能在<和指令名中间加空白符。

(1)assign

语法:

<#-- name:变量的名称,它不是表达式,可以写成一个字符串字面量  -->
<#-- =:赋值运算符,也可以是以下简写的运算符之一,++、--、+=、-=、*=、/=、%= -->
<#-- value:存储的值,是表达式 -->
<#assign name1=value1 name2=value2 ... nameN=valueN><#-- namespacehash:(通过import)为一个命名空间创建的hash,是表达式 -->
<#assign same as above... in namespacehash><#assign name>
    capture this
#assign><#assign name in namespacehash>
    capture this
#assign>

assign指令可以创建一个新的变量,或替换一个已经存在的变量,注意只有顶级变量可以被创建或替换。

模板fm-directive-assign.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>assigntitle>
head>
<body>
    <div>
        <#-- 使用该指令可以创建一个新的变量,或替换一个已经存在的变量,注意只有顶级变量可以被创建或替换 -->
        <p><#assign username = "username: ${user.username}" password = "password: ${user.password}">${username}, ${password}p>
        <hr/>
        <#-- 赋值运算符,也可以是以下简写的运算符之一:++、--、+=、-=、*=、/=、%= -->
        <p><#assign x = user.id x++ y = 3 y += x>x: ${x}, y: ${y}p>
        <hr/>
        <p>orders:p>
        <ul>
            <#-- 变量seq存储一个序列 -->
            <#assign seq = orders>
            <#list seq as order>
                <li>
                    <p>index: ${order_index}p>
                    <ul>
                        <li><p>orderId: ${order.orderId}p>li>
                        <li><p>orderNumber: ${order.orderNumber}p>li>
                        <li><p>orderDescription: ${order.orderDescription}p>li>
                    ul>
                li>
            #list>
        ul>
        <hr/>
        <#-- 导入一个库并创建新的命名空间,可用在导入的模板定义使导入模板可用的宏(macro)、函数和其他变量的集合
             import指令创建了一个hash变量my来访问它所创建的命名空间
             该变量位于导入模板所用的命名空间中,并作为进入导入库的命名空间的窗口 -->
        <#import "/libs/mylib.ftl" as my>
        <#-- 使用hash变量来访问新创建的命名空间 -->
        <p>/libs/mylib.ftl的命名空间的变量mail: ${my.mail}p>
        <#-- 创建或替换指定命名空间的变量,这里替换了用于/libs/mylib.ftl的命名空间的变量mail -->
        <#assign mail = "[email protected]" in my>
        <p>/libs/mylib.ftl的命名空间的变量mail: ${my.mail}p>
        <#-- 在当前命名空间(和标签所在模板关联的命名空间)创建了变量mail -->
        <#assign mail = "[email protected]">
        <p>在当前命名空间的变量mail: ${mail}p>
        <hr/>
        <#-- assign指令的极端用法:捕捉在其起始标签和结束标签之间产生的输出
             标签之间的内容不会显示在页面上,但会存储在变量中 -->
        <#assign out>
            <#list orders as order>
                <p>orderId: ${order.orderId}, orderNumber: ${order.orderNumber}, orderDescription: ${order.orderDescription}p>
            #list>
        #assign>
        <#-- 内建函数word_list包含字符串中所有单词的序列,内建函数size为序列中子变量(相当于Java中的数组元素)的个数 -->
        <p>单词数量:${out?word_list?size}p>
        <p>${out}<br/>p>
    div>
body>
html>

/libs/mylib.ftl

<#-- 数据模型中的变量在任何位置都是可见的,所以这里依然能访问到user -->
<#assign mail = "${user.username}@xxx.com">

FreeMarkerController中新增以下方法:

@GetMapping("/directive/assign")
public String assignDirective(Model model) {
    User user = new User(1L, "RtxTitanV", "654321");
    ArrayList<Order> orders = new ArrayList<>();
    orders.add(new Order().setOrderId(1L).setOrderNumber("785698232657798568").setOrderDescription("二两麻辣牛肉面"));
    orders.add(new Order().setOrderId(2L).setOrderNumber("785938232669132551").setOrderDescription("三两砂锅三鲜米线"));
    orders.add(new Order().setOrderId(3L).setOrderNumber("793382623157348612").setOrderDescription("二两老麻抄手"));
    model.addAttribute("user", user);
    model.addAttribute("orders", orders);
    return "directive-assign";
}

效果:
SpringBoot2.x 集成 FreeMarker_第1张图片
7

(2)attempt、recover

语法:

<#attempt>
    <#-- attempt block:任意内容的模板块,总是会被执行,但是如果期间发生错误,该块的输出将会回滚,并且recover block会被执行 -->
    attempt block
<#-- recover是强制的。attempt和recover可以嵌套在其他attempt block或recover block中 -->
<#recover>
    <#-- recover block:任意内容的模板块,只在attempt block执行期间发生错误时被执行,可以在这里打印错误信息或进行其他操作 -->
    recover block
#attempt>

使用attemptrecover指令,即使页面某一部分输出失败,也可以让页面成功输出。

模板fm-directive-attempt-recover.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>attempt、recovertitle>
head>
<body>
    <div>
        <#-- 使用attempt和recover指令,即使页面某一部分输出失败,也可以让页面成功输出 -->
        <#attempt>
            <#-- attempt块总是会被执行,但是如果期间发生错误,该块的输出将会回滚,并且recover块会被执行 -->
            <p>username: ${user.username}, password: ${user.password}p>
        <#recover>
            <#-- recover块只在attempt块执行期间发生错误时被执行,可以在这里打印错误信息或进行其他操作 -->
            <p>出现错误啦!p>
        #attempt>
    div>
body>
html>

attempt块有一个全有或全无的语义:要么attempt块的全部内容被输出(当没有错误时),要么attempt块的执行根本没有输出(当有错误时)。

attemptrecover可以处理块执行期间发生的各种类型的错误,目的是包含更大的模板片段。在一些场景,对于某些特定的错误,不需要终止模板的执行,而是将错误信息打印输出后继续执行,使用attempt指令不会将这些抑制的错误视为错误。

recover块中,错误信息存在特殊变量error中。注意需要以点开始引用特殊变量(比如:${.error})。

FreeMarkerController中新增以下方法:

@GetMapping("/directive/attempt-recover")
public String attemptAndRecoverDirective(Model model) {
    User user = new User(1L, "RtxTitanV", "654321");
    return "directive-attempt-recover";
}

效果:
8
将变量user设置到Model中:

model.addAttribute("user", user)

效果:
9

(3)compress

语法:

<#compress>
    ...
#compress>

当使用对空白符不敏感的格式(如HTML或XML)时,compress指令对去除多余的空白很有用。它捕捉在指令体(即开始标签和结束标签之间)中生成的内容,然后缩小所有不间断的空白序列到一个单独的空白字符。如果被替换的序列包含换行符,插入的字符将是一个换行符,否则就是一个空格。开头和结尾没有间断的空白序列将被完全删除。

模板fm-directive-compress.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>compresstitle>
head>
<body>
    <#assign x = "    moo  \n\n   ">
    (<#compress>
        1 2  3   4    5  <br/>
        ${x}                  <br/>
        test only   <br/>

        I said, test only

    #compress>)
body>
html>

效果:
10

(4)escape、noescape

语法:

<#escape identifier as expression>
    ...
    <#noescape>...#noescape>
    ...
#escape>

escape指令中出现的插值会和转义表达式自动结合,noescape指令关闭转义。

模板fm-directive-escape-noescape.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>escape、noescapetitle>
head>
<body>
    <div>
        <#-- escape指令中出现的插值会和转义表达式自动结合 -->
        <#escape esc as esc?html>
            <p>开启转义:${hello}p>
            <#-- noescape指令关闭转义 -->
            <#noescape>
                <p>关闭转义:${hello}p>
            #noescape>
        #escape>
    div>
    <hr/>
    <div>
        <#-- 与escape指令等价 -->
        <p>开启转义:${hello?html}p>
        <#-- 与noescape指令等价 -->
        <p>关闭转义:${hello}p>
    div>
    <hr/>
    <div>
        <#-- 在调用宏(macro)或include指令时,转义只对模板文本中escape开始标签和结束标签之间的插值起作用
             也就是说,它不会转义文本中escape开始标签之前或escape结束标签之后的任何内容
             即使该部分是在escape内部调用的 -->
        <#-- 定义宏 -->
        <#macro m1>
            <p>m1: ${hello}p>
        #macro>
        <#escape esc as esc?html>
            <#macro m2>
                <p>m2: ${hello}p>
            #macro>
            <p>${hello}p>
            <#-- 调用宏 -->
            <@m1/>
        #escape>
        <p>${hello}p>
        <@m2/>
    div>
body>
html>

escape指令的效果是在模板解析时而不是模板处理时应用的。 这意味着在escape中调用一个宏(macro)或include另一个模板, 它不会影响在宏或被包含模板中的插值,因为宏调用和模板包含被算在模板处理时。 另外一方面,如果在escape中包围一个或多个宏的声明,这些宏中的插值将会和转义表达式合并。

FreeMarkerController中新增以下方法:

@GetMapping("/directive/escape-noescape")
public String escapeAndNoescapeDirective(Model model) {
    model.addAttribute("hello", "

Hello FreeMarker

"
); return "directive-escape-noescape"; }

效果:
SpringBoot2.x 集成 FreeMarker_第2张图片
从2.3.24开始,这些指令被基于输出格式的auto-escaping所废弃。此外,在使用auto-escaping的地方不允许使用escape指令。在使用auto-escaping的地方使用escape会出现如下错误:
SpringBoot2.x 集成 FreeMarker_第3张图片

(5)autoesc、noautoesc

语法:

<#autoesc>
    ...
#autoesc>
<#noautoesc>
    ...
#noautoesc>

可以在ftl头中为整个模板禁用auto-escaping。然后用autoesc指令在某个部分重新启用它。noautoesc指令可以嵌套在autoesc指令中使用,以重新禁用auto-escaping功能。?no_esc也可以在autoesc指令中使用,不对单个插值进行转义。在auto-escaping(自动转义)被禁用的情况下,只转义单个插值时可以使用${expression?esc}代替autoesc指令。

模板fm-directive-autoesc-noautoesc.ftl如下:

<#ftl output_format="HTML" auto_esc=false>
DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>autoesc、noautoesctitle>
head>
<body>
    <div>
        <#autoesc>
            <p>启用auto-escaping:${hello}p>
            <#-- noautoesc指令可以嵌套在autoesc指令中使用,以重新禁用auto-escaping功能 -->
            <#noautoesc>
                <p>重新禁用auto-escaping:${hello}p>
            #noautoesc>
            <p>?no_esc也可以在autoesc指令中使用,不对单个插值进行转义:${hello?no_esc}p>
        #autoesc>
        <#-- 在auto-escaping(自动转义)被禁用的情况下,只转义单个插值时可以使用${expression?esc}代替autoesc指令 -->
        <p>只转义单个插值的情况下代替autoesc指令的语法:${hello?esc}p>
        <p>由于禁用了auto-escaping,不使用autoesc指令或?esc则不会转义:${hello}p>
    div>
body>
html>

注意:

  • autoesc不能用于当前输出格式为非标记输出格式的情况;而noautoescautoesc不同,不管当前输出格式是什么,都可以被使用。
  • autoesc可以用在已经启用了auto-escaping的地方,甚至在另一个autoesc内。虽然这是多余的,但也是允许的;noautoesc可以用在已经禁用了auto-escaping的地方,甚至在另一个noautoesc内,虽然这是多余的,但也是允许的。

FreeMarkerController中新增以下方法:

@GetMapping("/directive/autoesc-noautoesc")
public String autoescDirective(Model model) {
    model.addAttribute("hello", "

Hello FreeMarker

"
); return "directive-autoesc-noautoesc"; }

效果:
SpringBoot2.x 集成 FreeMarker_第4张图片
修改模板:

<#ftl output_format="HTML">
DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>autoesc、noautoesctitle>
head>
<body>
    <div>
        <#noautoesc>
            <p>禁用auto-escaping:${hello}p>
            <#-- autoesc指令可以嵌套在noautoesc指令中使用,以重新启用auto-escaping功能 -->
            <#autoesc>
                <p>重新启用auto-escaping:${hello}p>
            #autoesc>
            <p>?esc也可以在noautoesc指令中使用,转义单个插值:${hello?esc}p>
        #noautoesc>
        <#-- 如果只是不想对单个插值进行转义,可以使用${expression?no_esc}代替noautoesc指令 -->
        <p>只是不想对单个插值进行转义情况下代替noautoesc指令的语法:${hello?no_esc}p>
        <p>默认启用了auto-escaping,不使用noautoesc指令或?no_esc则会转义:${hello}p>
    div>
body>
html>

在任何情况下,一个模板的输出格式都可以在ftl头中强制执行。如果在添加上述ftl头之后,转义没有发生,可以将ftl标签修改为<#ftl output_format="XML" auto_esc=true>

效果:
SpringBoot2.x 集成 FreeMarker_第5张图片

(6)ftl

<#-- param1、param2等:参数的名称,不是表达式 -->
<#-- value1、value2等:参数的值,必须是一个常量表达式,不能用变量 -->
<#ftl param1=value1 param2=value2 ... paramN=valueN>

告诉FreeMarker和其他工具关于模板的信息,而且帮助程序自动检测一个文本文件是否是FTL文件。该指令如果存在,则必须是模板的第一句。该指令前的任何空白符将被忽略。一些设置(如编码方式,空白剥离等)在这里给定的话就有最高的优先级,也就是说,它们直接作用于模板而不管其他任何FreeMarker的配置设置。

这里总结一下ftl中的常用参数:

  • attributes:关联模板任意属性(名-值对)的哈希表。属性值可以是任意类型。FreeMarker不会去理解属性的含义,它是由封装FreeMarker的应用决定的。
  • auto_esc:开启或关闭auto-escaping。这取决于FreeMarker配置中的auto_escaping_policy,但是如果当前的输出格式默认使用auto-escaping,通常auto-escaping会默认为打开。所以主要设置为false,用它来禁用auto-escaping。当当前输出格式为非标记输出格式时,设置为true将导致解析时错误。注意,可以用autoescnoautoesc指令只为模板的一部分打开或关闭auto-escaping。
  • encoding:在模板文件本身中为模板指定编码方式(字符集)。这是新创建的Templateencoding设置, 而且Configuration.getTemplate中的encoding参数不能覆盖它。
  • output_format:指定模板的输出格式。这必须是一个字符串字面量,它指的是输出格式的名称。预定义的输出格式有HTMLXHTMLXMLRTF等。
  • strict_syntax:开启或关闭"strict syntax",这是FreeMarker 2.1之后的标准语法。合法值为布尔常量truefalse(向下兼容字符串"yes""no""true""false")。默认值(不使用该参数时)取决于FreeMarker的配置,对新项目应该为true
  • strip_text:开启它,则模板被解析时模板中所有顶级文本被移除。这不会影响macro、指令或插值中的文本。合法值为布尔常量truefalse(向下兼容字符串"yes""no""true""false")。默认值(不使用该参数时)为false
  • strip_whitespace:开启或关闭空白剥离(white-space stripping)。合法值为布尔常量truefalse(向下兼容字符串"yes""no""true""false")。默认值(不使用该参数时)取决于FreeMarker的配置,对新项目应该为true

(7)function、return

语法:

<#-- name:方法变量的名称(不是表达式) -->
<#-- param1、param2等:存储参数的值(不是表达式)的局部变量名称,=后面的默认值(是表达式)可选 -->
<#-- paramN:最后一个参数,可以可选的包含一个尾部省略号(...),表示接受可变数量的参数,paramN将是额外参数的序列 -->
<#-- 没有默认值的参数必须在有默认值的参数之前 -->
<#-- return指令可以在function开始标签和function结束标签之间的任意位置使用并且可以使用任意次数 -->
<#function name param1 param2 ... paramN>
  ...
  <#-- returnValue:计算方法调用值的表达式 -->
  <#return returnValue>
  ...
#function>

再当前命名空间创建一个方法变量,return指令必须有一个参数来指定方法的返回值,而且视图写入输出的将会被忽略。如果到达 (也就是说没有return returnValue),那么方法的返回值就是未定义变量。

模板fm-directive-function-return.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>function、returntitle>
head>
<body>
    <#-- 计算两个数的平均值的方法 -->
    <#-- func为方法变量的名称,x、y为存储参数的值(不是表达式)的局部变量名称,=后面的默认值(是表达式)可选 -->
    <#-- 没有默认值的参数必须在有默认值的参数之前 -->
    <#function func x y = 20>
        <#return (x + y) / 2>
    #function>

    <#-- 计算多个数的平均值的方法 -->
    <#-- 最后一个参数可以可选的包含一个尾部省略号(...),表示接受可变数量的参数,nums是额外参数的序列 -->
    <#function avg nums...>
        <#-- 和assign指令类似,但是它创建或替换局部变量,它只能在macro和function内部定义才起作用 -->
        <#local sum = 0>
        <#list nums as num>
            <#local sum += num>
        #list>
        <#if nums?size != 0>
            <#return sum / nums?size>
        #if>
    #function>

    <#assign reslut = 1>
    <#-- 计算一个自然数的阶乘的方法 -->
    <#function factorial num>
        <#if num lt 0>
            <#return "num不能小于0">
        #if>
        <#if num gt 1>
            <#assign reslut *= num>
            <#-- 递归调用方法计算阶乘 -->
            ${factorial(num - 1)}
        #if>
        <#return reslut>
    #function>

    <div>
        <p>计算10和20的平均值:${func(10)}p>
        <p>计算20和30的平均值:${func(20, 30)}p>
        <p>计算10、20、30的平均值:${avg(10, 20, 30)}p>
        <p>计算10、20、30、40的平均值:${avg(10, 20, 30, 40)}p>
        <#-- 如果到达#function>(即没有return返回值),那么方法的返回值就是未定义变量 -->
        <p>计算平均值,没有传参数:${avg()!"N/A"}p>
        <p>${num}的阶乘为:${factorial(num)}p>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/directive/function-return")
public String functionAndReturnDirective(Model model) {
    model.addAttribute("num", 5);
    return "directive-function-return";
}

效果:
SpringBoot2.x 集成 FreeMarker_第6张图片
num变量分别设置为0和-1时:
16

(8)global

语法:

<#-- name:变量的名称,它不是表达式,可以写成一个字符串字面量 -->
<#-- =:赋值运算符,也可以是以下简写的运算符之一,++、--、+=、-=、*=、/=、%= -->
<#-- value:存储的值,是表达式 -->
<#global name=value><#global name1=value1 name2=value2 ... nameN=valueN><#global name>
    capture this
#global>

该指令和assign相似,但是被创建的变量在所有命名空间中都可见,但又不在任何命名空间中。准确地说就如同创建(或替换)一个数据模型变量。因此,这个变量是全局的。使用该指令创建的变量会隐藏数据模型中的同名变量。在当前命名空间中一个相同名称的变量存在会隐藏global指令创建的变量。

模板fm-directive-global.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>globaltitle>
head>
<body>
    <div>
        <p>数据模型中的username: ${username}p>
        <#-- assign指令创建的变量会隐藏数据模型中的同名变量 -->
        <#assign username = "GuanYu">
        <p>assign指令创建的变量username隐藏了数据模型中的username: ${username}p>
        <#-- 使用特殊变量globals可以访问数据模型中的变量 -->
        <p>可以使用特殊变量globals访问数据模型中的变量: ${.globals.username}p>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/directive/global")
public String globalDirective(Model model) {
    model.addAttribute("username", "ZhaoYun");
    return "directive-global";
}

效果:
17
修改模板:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>globaltitle>
head>
<body>
    <div>
        <p>数据模型中的username: ${username}p>
        <#-- global指令创建的变量(全局变量)会隐藏数据模型中的同名变量 -->
        <#global username = "MaChao">
        <p>global指令创建的变量username会隐藏数据模型中的username: ${username}p>
        <p>此时使用特殊变量globals访问的是全局变量username: ${.globals.username}p>
        <#-- 仍然可以使用特殊变量data_model访问数据模型中的变量 -->
        <p>可以使用特殊变量data_model访问数据模型中的变量username: ${.data_model.username}p>
        <#-- 在当前命名空间,相同名称的变量存在会隐藏global指令创建的变量 -->
        <#assign username = "ZhangFei">
        <p>assign指令在当前命名空间创建的变量username隐藏了global指令创建的变量username: ${username}p>
        <#-- 使用特殊变量globals不但可以访问数据模型中的变量,还可以访问global指令创建的变量 -->
        <p>可以使用特殊变量globals访问全局变量username: ${.globals.username}p>
    div>
body>
html>

效果:
SpringBoot2.x 集成 FreeMarker_第7张图片

(9)if、else、elseif

语法:

<#-- ondition、condition2等:将被计算成布尔值的表达式 -->
<#-- elseif和else是可选的 -->
<#if condition>
    ...
<#elseif condition2>
    ...
<#elseif condition3>
    ...
    ...
<#else>
    ...
#if>

可以使用ifelseifelse指令通过条件判断是否跳过模板的某个部分。条件必须计算成布尔值,否则会出错终止模板处理。elseifelse必须出现在if内部(即在if开始标签和结束标签之间)。if中可以包含任意数量的elseif(包括0个)并且结束时的else可选。

模板fm-directive-if.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>if、else、elseiftitle>
head>
<body>
    <h1>只有if,没有elseif和elseh1>
    <div>
        <p>
            成绩是否是正常数据:
            <#-- 注意使用比较运算符>、>=时,FreeMarker解释>时可以把它当做ftl标签的结束符
                 为了避免该问题,可以用小括号括起来或用lt代替<、lte代替<=、gt代替>、gte代替>= -->
            <#if (grade >= 0 && grade <=100)>
                <span>成绩数据正常span>
            #if>
        p>
    div>
    <h1>只有if和else,没有elseifh1>
    <div>
        <p>
            成绩是否及格:
            <#if grade gte 60>
                <span>及格span>
            <#else>
                <span>不及格span>
            #if>
        p>
    div>
    <h1>只有if和elseif,没有elseh1>
    <div>
        <p>
            成绩对应的级别:
            <#if (grade < 0 || grade > 100)>
                <span>无效数据span>
            <#elseif (grade < 60)>
                <span>不及格span>
            <#elseif (grade < 70)>
                <span>span>
            <#elseif (grade < 90)>
                <span>span>
            <#elseif (grade <= 100)>
                <span>span>
            #if>
        p>
    div>
    <h1>if、else、elseif都有h1>
    <div>
        <p>
            成绩对应的级别:
            <#if grade lt 0 || grade gt 100>
                <span>无效数据span>
            <#elseif grade gte 90>
                <span>span>
            <#elseif grade gte 70>
                <span>span>
            <#elseif grade gte 60>
                <span>span>
            <#else>
                <span>不及格span>
            #if>
        p>
    div>
    <h1>嵌套if指令h1>
    <div>
        <p>
            成绩对应的级别:
            <#if grade gte 0 && grade lte 100>
                <#if grade gte 60>
                    <#if grade gte 70>
                        <#if grade gte 90>
                            <span>span>
                        <#else>
                            <span>span>
                        #if>
                    <#else>
                        <span>span>
                    #if>
                <#else>
                    <span>不及格span>
                #if>
            <#else>
                <span>无效数据span>
            #if>
        p>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/directive/if")
public String ifDirective(Model model) {
    model.addAttribute("grade", 85);
    return "directive-if";
}

效果:
SpringBoot2.x 集成 FreeMarker_第8张图片
变量grade设置为-1时:
SpringBoot2.x 集成 FreeMarker_第9张图片

(10)import

语法:

<#-- path:模板的路径,是一个计算为字符串的表达式 -->
<#-- hash:可以通过它来访问命名空间的hash变量的不带引号的名称。不是一个表达式 -->
<#import path as hash>

import首先创建一个新的空命名空间,然后在该命名空间内执行带有path参数的模板,因此模板用变量(macro、function等)填充该命名空间。然后,命名空间被分配给用hash参数指定的变量,可以通过它访问其内容。命名空间是一个hash值,因此点运算符起了作用。赋值就像assign指令一样,也就是说,它将变量设置在当前的名字空间中。除了,如果导入发生在主(最顶层)模板的命名空间中,hash变量也会在全局命名空间中创建。

模板fm-directive-import.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>importtitle>
head>
<body>
    <div>
        <#-- 导入一个库并创建新的命名空间,可用在导入的模板定义使导入模板可用的宏(macro)、函数和其他变量的集合
             import指令创建了一个hash变量my来访问它所创建的命名空间
             该变量位于导入模板所用的命名空间中,并作为进入导入库的命名空间的窗口 -->
        <#import "/libs/common.ftl" as com>
        <#-- 在当前命名空间(和标签所在模板关联的命名空间)创建了变量mail -->
        <#assign mail="[email protected]">
        <#-- 调用宏 -->
        <@com.copyright date = "2020-2021"/>
        <hr/>
        <#-- 使用hash变量来访问新创建的命名空间 -->
        <p>/libs/common.ftl的命名空间的变量mail: ${com.mail}p>
        <#-- 创建或替换指定命名空间的变量,这里替换了用于/libs/common.ftl的命名空间的变量mail -->
        <#assign mail = "[email protected]" in com>
        <p>/libs/common.ftl的命名空间的变量mail: ${com.mail}p>
        <p>在当前命名空间的变量mail: ${mail}p>
    div>
    <div>
        <#-- 用同一path多次import,只在第一次调用import时创建命名空间并运行模板
             后面的调用将只是返回模板第一次被导入时创建和初始化的命名空间
             然后将相同的命名空间分配给指定的hash变量,通过该变量来访问命名空间 -->
        <#import "/libs/common.ftl" as co>
        <#import "/libs/common.ftl" as cm>
        <#import "/libs/common.ftl" as om>
        <#-- 三个hash变量访问的都是同一个命名空间 -->
        <p>${com.mail}, ${co.mail}, ${cm.mail}, ${om.mail}p>
        <#assign mail = "[email protected]" in co>
        <p>${com.mail}, ${co.mail}, ${cm.mail}, ${om.mail}p>
    div>
body>
html>

如果用同一path多次调用import,它将创建命名空间,并只在第一次调用import时运行模板。后面的调用将只是返回模板第一次被导入时创建和初始化的命名空间,而不会执行导入的模板。

路径参数可以是相对路径,如"foo.ftl"".../foo.ftl",或绝对路径,如"/foo.ftl"。相对路径是相对于使用import指令的模板目录而言的。绝对路径是相对于在配置FreeMarker时定义的基础路径(通常称为"模板的根目录")。

注意,命名空间是不分层次的,它们之间相互独立;当import创建另一个命名空间时,处于哪个命名空间中并不重要。例如,当在命名空间N1中import命名空间N2时,N2不会在N1中。N1只是得到与在主命名空间中导入N2时一样的N2。

/libs/common.ftl

<#macro copyright date>
  <#-- 数据模型中的变量在任何位置都是可见的,所以这里依然能访问到user -->
  <p>© ${date} ${user.username}. All rights reserved.p>
  <p>email: ${mail}p>
#macro>
<#assign mail = "${user.username}@xxx.com">

FreeMarkerController中新增以下方法:

@GetMapping("/directive/import")
public String importDirective(Model model) {
    model.addAttribute("user", new User(1L, "RtxTitanV", "654321"));
    return "directive-import";
}

效果:
SpringBoot2.x 集成 FreeMarker_第10张图片

(11)include

语法:

<#-- path:要包含的文件的路径,一个计算为字符串的表达式 -->
<#include path><#-- options:选项 -->
<#include path options>

在模板中插入另一个FreeMarker模板文件(由path参数指定),被包含的文件和包含它的模板共享变量。被包含模板的输出格式是在include标签出现的位置插入的。include指令还支持以下三个选项:

  • encoding:包含的模板的编码方式(字符集)。
  • parse:如果为true,则包含的文件将被解析为FTL,否则整个文件将被视为简单的文本。缺省默认为true。
  • ignore_missing:当为true时,模板引用为空时压制错误,而<#include ...>不会输出任何东西。否则,如果模板不存在,模板处理就会发生错误并停止。缺省默认为false。

模板fm-directive-include.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>includetitle>
head>
<body>
    <div>
        <#assign date = "2020-2021">
        <#assign mail = "[email protected]">
        <#-- 在模板中插入另一个FreeMarker模板文件,被包含的文件和包含它的模板共享变量 -->
        <#include "/libs/copyright.ftl">
        <hr/>
        <#include "/libs/common.ftl">
        <#-- 如果宏(macro)定义是用include指令插入的,那么在FreeMarker执行include指令之前,它们将无法使用
             所以调用include指令插入的宏需在include之后进行 -->
        <@copyright date = "2021-2022"/>
    div>
body>
html>

/libs/copyright.ftl

<p>© ${date} ${user.username}. All rights reserved.p>
<p>email: ${mail}p>

FreeMarkerController中新增以下方法:

@GetMapping("/directive/include")
public String includeDirective(Model model) {
    model.addAttribute("user", new User(1L, "RtxTitanV", "654321"));
    return "directive-include";
}

效果:
SpringBoot2.x 集成 FreeMarker_第11张图片

(12)list、else、items、sep、break、continue

语法:

序列的通用形式1:
<#-- sequence:计算为一个序列或要迭代的项的集合的表达式 -->
<#-- item:循环变量的名称(不是表达式) -->
<#list sequence as item>
    Part repeated for each item
<#-- else部分是可选的 -->
<#else>
    Part executed when there are 0 items
#list>

序列的通用形式2:
<#list sequence>
    Part executed once if we have more than 0 items
    <#items as item>
        Part repeated for each item
    #items>
    Part executed once if we have more than 0 items
<#else>
    Part executed when there are 0 items
#list>

哈希表的通用形式1:
<#list hash as key, value>
    Part repeated for each key-value pair
<#else>
    Part executed when there are 0 key-value pair
#list>

哈希表的通用形式2:
<#list hash>
    Part executed once if we have more than 0 key-value pair
    <#items as key, value>
        Part repeated for each key-value pair
    #items>
    Part executed once if we have more than 0 key-value pair
<#else>
    Part executed when there are 0 key-value pair
#list>

list指令对其第一个参数指定的序列(或集合)中的每个值执行list开始标签和list结束标签之间的代码。对于每次迭代,循环变量将存储当前项的值。循环变量(list中指定的)只存在于list标签体内。而且,从循环内调用的宏(macro)或函数不会看到它。注意,循环变量不存在于else标签和list结束标签之间,因为这部分不是循环的一部分。

模板fm-directive-list.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>list、else、items、sep、break、continuetitle>
head>
<body>
    <div>
        <table>
            <thead>
                <tr>
                    <th>idth>
                    <th>usernameth>
                    <th>passwordth>
                    <th>indexth>
                    <th>counterth>
                    <th>is_even_itemth>
                    <th>is_odd_itemth>
                    <th>item_parityth>
                    <th>item_parity_capth>
                    <th>item_cycleth>
                    <th>is_firstth>
                    <th>is_lastth>
                    <th>has_nextth>
                tr>
            thead>
            <tbody>
                <#-- users为要迭代的序列或集合,user为循环变量 -->
                <#list users as user>
                    <tr>
                        <td>${user.id}td>
                        <td>${user.username}td>
                        <td>${user.password}td>
                        <#-- 通过循环变量内建函数访问当前迭代状态
                             注意,这些内建函数只能用于list和items指令的循环变量(已废弃的foreach指令也可以)
                             list指令没有指定循环变量时,这些内建函数就作用于items指令的循环变量
                             循环变量内建函数只使用循环变量的名称,这样它们可以识别相关的正在进行的迭代
                             它们并不读取循环变量的值 -->
                        <#-- 返回当前迭代基于0的索引 -->
                        <td>${user?index}td>
                        <#-- 返回当前迭代基于1的索引 -->
                        <td>${user?counter}td>
                        <#-- 判断当前迭代项是否有一个基于1的偶数索引
                             c为布尔值内建函数,将布尔值转换为字符串 -->
                        <td>${user?is_even_item?c}td>
                        <#-- 判断当前迭代项是否有一个基于1的奇数索引 -->
                        <td>${user?is_odd_item?c}td>
                        <#-- 根据迭代当前所在的基于1的索引的奇偶性,返回字符串"odd"和"even" 通常用于表格中行间的颜色变换 -->
                        <td>${user?item_parity}td>
                        <#-- 根据迭代当前所在的基于1的索引的奇偶性,返回字符串"Odd"和"Even" -->
                        <td>${user?item_parity_cap}td>
                        <#-- 这是item_parity更为通用的版本,可以指定值来代替"odd"和"even",允许多于两个值
                             参数个数至少为1,无上限,类型任意 -->
                        <td>${user?item_cycle("□", "△", "○")}td>
                        <#-- 判断当前迭代项是否是第一项 -->
                        <td>${user?is_first?c}td>
                        <#-- 判断当前迭代项是否是最后一项 -->
                        <td>${user?is_last?c}td>
                        <#-- 判断当前迭代项是否不是最后一项 -->
                        <td>${user?has_next?c}td>
                    tr>
                <#-- else部分是可选的 -->
                <#else>
                    <p>No usersp>
                #list>
            tbody>
        table>
    div>
body>
html>

FreeMarkerController中新增以下方法:

    @GetMapping("/directive/list")
    public String listDirective(Model model) {
        List<User> users = new ArrayList<>();
        users.add(new User(1L, "刘备", "123132"));
        users.add(new User(2L, "关羽", "321231"));
        users.add(new User(3L, "张飞", "213312"));
        users.add(new User(4L, "赵云", "132213"));
        users.add(new User(5L, "马超", "312123"));
        model.addAttribute("users", users);
        return "directive-list";
    }

效果:
23
else指令用于没有迭代项时,需要输出一些特殊内容而不是什么也不输出的情况。将users设置为空之后会输出else中的内容:
24
如果必须在第一个列表项之前或最后一个列表项之后打印一些内容,只要有至少一项,就可以使用items指令。当list指令没有指定循环变量时,如果至少有一项,它的主体就会被执行一次,否则就完全不执行。内嵌的items指令会执行每一项,因此items指令来定义循环变量而不是list指令。

在模板中新增以下代码:

<div>
    <#-- 如果必须在第一个列表项之前或最后一个列表项之后打印一些内容,只要有至少一项,就可以使用items指令
         当list指令没有指定循环变量时,如果至少有一项,它的主体就会被执行一次,否则就完全不执行
         内嵌的items指令会执行每一项,因此items指令来定义循环变量而不是list指令 -->
    <#list users>
        <p>如果至少有一项,此处会被执行一次,否则就完全不执行p>
        <table>
                <thead>
                    <tr>
                        <th>idth>
                        <th>usernameth>
                        <th>passwordth>
                        <th>indexth>
                        <th>counterth>
                        <th>is_even_itemth>
                        <th>is_odd_itemth>
                        <th>item_parityth>
                        <th>item_parity_capth>
                        <th>item_cycleth>
                        <th>is_firstth>
                        <th>is_lastth>
                        <th>has_nextth>
                    tr>
                thead>
                <tbody>
                    <#-- 不能将items指令移出到宏或包含的模板中,items指令不能嵌入else指令
                         list可以有多个items指令,但只有其中一个被允许执行,所以多个items可以用于不同的if-else分支,但不能迭代两次
                         items指令不能有自己的嵌套else指令,只有包含它的list指令可以有
                         循环变量user仅存在于items指令体内部 -->
                    <#items as user>
                        <tr>
                            <td>${user.id}td>
                            <td>${user.username}td>
                            <td>${user.password}td>
                            <#-- 通过循环变量内建函数访问当前迭代状态
                                 注意,这些内建函数只能用于list和items指令的循环变量(已废弃的foreach指令也可以)
                                 list指令没有指定循环变量时,这些内建函数就作用于items指令的循环变量
                                 循环变量内建函数只使用循环变量的名称,这样它们可以识别相关的正在进行的迭代
                                 它们并不读取循环变量的值 -->
                            <#-- 返回当前迭代基于0的索引 -->
                            <td>${user?index}td>
                            <#-- 返回当前迭代基于1的索引 -->
                            <td>${user?counter}td>
                            <#-- 判断当前迭代项是否有一个基于1的偶数索引
                                 c为布尔值内建函数,将布尔值转换为字符串 -->
                            <td>${user?is_even_item?c}td>
                            <#-- 判断当前迭代项是否有一个基于1的奇数索引 -->
                            <td>${user?is_odd_item?c}td>
                            <#-- 根据迭代当前所在的基于1的索引的奇偶性,返回字符串"odd"和"even",通常用于表格中行间的颜色变换 -->
                            <td>${user?item_parity}td>
                            <#-- 根据迭代当前所在的基于1的索引的奇偶性,返回字符串"Odd"和"Even" -->
                            <td>${user?item_parity_cap}td>
                            <#-- 这是item_parity更为通用的版本,可以指定值来代替"odd"和"even",允许多于两个值
                                 参数个数至少为1,无上限,类型任意 -->
                            <td>${user?item_cycle("□", "△", "○")}td>
                            <#-- 判断当前迭代项是否是第一项 -->
                            <td>${user?is_first?c}td>
                            <#-- 判断当前迭代项是否是最后一项 -->
                            <td>${user?is_last?c}td>
                            <#-- 判断循当前迭代项是否不是最后一项 -->
                            <td>${user?has_next?c}td>
                        tr>
                    #items>
                tbody>
        table>
        <p>如果至少有一项,此处会被执行一次,否则就完全不执行p>
    <#-- else部分是可选的 -->
    <#else>
        <p>No usersp>
    #list>
div>

效果:
25
users设置为空之后会输出else中的内容:
26
通过list指令还可以迭代哈希表。在模板中新增以下代码:

<div>
    <table>
        <thead>
        <tr>
            <th>idth>
            <th>usernameth>
            <th>passwordth>
            <th>keyth>
            <th>indexth>
            <th>counterth>
            <th>is_even_itemth>
            <th>is_odd_itemth>
            <th>item_parityth>
            <th>item_parity_capth>
            <th>item_cycleth>
            <th>is_firstth>
            <th>is_lastth>
            <th>has_nextth>
        tr>
        thead>
        <tbody>
        <#-- userMap为要迭代的Map,key为键,user为值 -->
        <#list userMap as key, user>
            <tr>
                <td>${user.id}td>
                <td>${user.username}td>
                <td>${user.password}td>
                <td>${key}td>
                <#-- 通过循环变量内建函数访问当前迭代状态
                     注意,这些内建函数只能用于list和items指令的循环变量(已废弃的foreach指令也可以)
                     list指令没有指定循环变量时,这些内建函数就作用于items指令的循环变量
                     循环变量内建函数只使用循环变量的名称,这样它们可以识别相关的正在进行的迭代
                     它们并不读取循环变量的值 -->
                <#-- 返回当前迭代基于0的索引 -->
                <td>${user?index}td>
                <#-- 返回当前迭代基于1的索引 -->
                <td>${user?counter}td>
                <#-- 判断当前迭代项是否有一个基于1的偶数索引
                     c为布尔值内建函数,将布尔值转换为字符串 -->
                <td>${user?is_even_item?c}td>
                <#-- 判断当前迭代项是否有一个基于1的奇数索引 -->
                <td>${user?is_odd_item?c}td>
                <#-- 根据迭代当前所在的基于1的索引的奇偶性,返回字符串"odd"和"even" 通常用于表格中行间的颜色变换 -->
                <td>${user?item_parity}td>
                <#-- 根据迭代当前所在的基于1的索引的奇偶性,返回字符串"Odd"和"Even" -->
                <td>${user?item_parity_cap}td>
                <#-- 这是item_parity更为通用的版本,可以指定值来代替"odd"和"even",允许多于两个值
                     参数个数至少为1,无上限,类型任意 -->
                <td>${user?item_cycle("□", "△", "○")}td>
                <#-- 判断当前迭代项是否是第一项 -->
                <td>${user?is_first?c}td>
                <#-- 判断当前迭代项是否是最后一项 -->
                <td>${user?is_last?c}td>
                <#-- 判断当前迭代项是否不是最后一项 -->
                <td>${user?has_next?c}td>
            tr>
        <#-- else部分是可选的 -->
        <#else>
            <p>No userMapp>
        #list>
        tbody>
    table>
div>

userMap设置到Model中:

Map<String, Object> map = new HashMap<>(16);
map.put("user1", new User(1L, "刘备", "123132"));
map.put("user2", new User(2L, "关羽", "321231"));
map.put("user3", new User(3L, "张飞", "213312"));
map.put("user4", new User(4L, "赵云", "132213"));
map.put("user5", new User(5L, "马超", "312123"));
model.addAttribute("userMap", map);

效果:
27
当必须在每一项之间显示一些东西时(但不是在第一项之前或最后一项之后),可以使用sep指令。

在模板中新增以下代码:

<div>
    <p>
        username:
        <#-- 当必须在每一项之间显示一些东西时(但不是在第一项之前或最后一项之后),可以使用sep指令 -->
        <#list users as user>
            <#-- 如果将sep指令放到包含的指令关闭的位置,则可以省略sep结束标签 -->
            ${user.username}<#sep>,
        #list>
    p>
    <p>
        password:
        <#list users as user>
            <span>
                <#-- 由于sep指令没有放在包含的指令(这里是list指令)关闭的位置,所以这里的sep结束标签不能省略 -->
                ${user.password}<#sep>#sep>
            span>
        #list>
    p>
div>

sep<#if item?has_next>...的简便形式。因此,它可以用在任何有listitems循环变量的地方,可以出现多次,而且可以有任意嵌套的内容。

效果:
28
list指令可以嵌套,break指令可以在迭代的任意点退出。在模板中新增以下代码,通过list嵌套结合break输出99乘法表:

<div>
    <table>
        <#-- list指令可以嵌套 -->
        <#list 1..100 as i>
            <#if i gt 9>
                <#-- break指令可以退出迭代 -->
                <#break>
            #if>
            <tr>
                <#list 1..100 as j>
                    <#if j gt i>
                        <#break>
                    #if>
                    <td>${i} * ${j} = ${i * j}td>
                #list>
            tr>
        #list>
    table>
div>

如果breakitems里面,它将只从items退出,而不是从list退出。一般来说,break只会从为每个迭代项调用的指令主体中退出,并且只能放在该指令中。例如,不能在listelse部分使用break,除非list被嵌套到其他可break的指令中。

效果:
29
使用continue指令可以跳过迭代体的剩余部分(直到标签的部分),然后继续迭代下一项。

在模板中新增以下代码:

<div>
    <table>
        <#list 1..9 as i>
            <tr>
                <#if i % 2 == 0>
                    <#-- continue指令跳过当前迭代的剩余部分,继续下一次迭代 -->
                    <#continue>
                #if>
                <#list 1..9 as j>
                    <#if j gt i>
                        <#break>
                    #if>
                    <#if j % 3 == 0>
                        <#continue>
                    #if>
                    <td>${i} * ${j} = ${i * j}td>
                #list>
            tr>
        #list>
    table>
div>

只要list指令指定了循环变量(as item),breakcontinue指令可以放在list指令内的任何地方,否则它可以放在items指令内的任何地方。强烈建议把它放在迭代过程中的所有其他事情之前或之后。否则,很容易在输出中出现未封闭的元素,或者使模板更难理解。特别是要避免从自定义指令的嵌套内容中脱离出来(例如<#list ...>...<@foo>...<#break>......)。

breakcontinueelseitemssep一样,只能在指令体内部使用,而不能移出到宏或被包含的模板中。

效果:
30
breakcontinue在大多数情况下被弃用,因为它与<#sep>item?has_nextitem?counteritem?indexitem?item_parity等不兼容。

如果需要跳过list中的某些元素,使用if指令不是一个好主意,因为这样<#sep>item?has_nextitem?counteritem?indexitem?item_parity等就不可用了,因为FreeMarker不知道哪些项曾经和将要被显示。相反,应该从要迭代的序列中删除不需要的项,然后再进行迭代。下面通过内建函数take_whilefilter来代替ifbreakcontinue的组合。

在模板中新增以下代码:

<div>
    <table>
        <#-- 使用序列的内建函数来代替if和break的组合 -->
        <#assign seq = 1..100>
        <#-- 内建函数take_while返回只包含输入序列中第一个不符合参数条件的元素之前的元素的序列 -->
        <#list seq?take_while(i -> i lte 9) as i>
            <tr>
                <#list seq?take_while(j -> j lte i) as j>
                    <td>${i} * ${j} = ${i * j}td>
                #list>
            tr>
        #list>
    table>
div>
<hr/>
<div>
    <table>
        <#-- 使用序列的内建函数来代替if和break或continue的组合 -->
        <#assign seq = 1..9>
        <#-- 内建函数filter返回只包含参数条件返回真值的元素的序列 -->
        <#list seq?filter(i -> i % 2 != 0) as i>
            <tr>
                <#list seq?take_while(j -> j lte i)?filter(j -> j % 3 != 0) as j>
                    <td>${i} * ${j} = ${i * j}td>
                #list>
            tr>
        #list>
    table>
div>
<hr/>
<div>
    <#assign lines = ["Stop listing", "when a", "certain element", "", "is found"]>
    <p>
        if、break和sep指令一起使用:
        <#list lines as line>
            <#if line == ''>
                <#break>
            #if>
            ${line}<#sep>,
        #list>
    p>
    <p>
        使用内建函数take_while代替if和break的组合:
        <#list lines?take_while(line -> line != '') as line>
            ${line}<#sep>,
        #list>
    p>
div>

效果:
SpringBoot2.x 集成 FreeMarker_第12张图片
局部变量会隐藏同名的普通变量,循环变量也会隐藏同名的局部变量和普通变量。在模板中新增以下代码:

<div>
    <#-- 局部变量会隐藏同名的普通变量,循环变量也会隐藏同名的局部变量和普通变量 -->
    <#assign x = "plain1">
    <p>1: ${x}p>
    <@test/>
    <p>6: ${x}p>
    <#list ["loop2"] as x>
        <p>7: ${x}p>
        <#assign x = "plain2">
        <p>8: ${x}p>
    #list>
    <p>9: ${x}p>
    <#macro test>
        <p>2: ${x}p>
        <#local x = "local">
        <p>3: ${x}p>
        <#list ["loop1"] as x>
            <p>4: ${x}p>
        #list>
        <p>5: ${x}p>
    #macro>
div>

效果:
SpringBoot2.x 集成 FreeMarker_第13张图片
内部循环变量会隐藏外部循环变量。在模板中新增以下代码:

<div>
    <#-- 内部循环变量会隐藏外部循环变量 -->
    <#list ["loop1"] as x>
        <p>1: ${x}p>
        <#list ["loop2"] as x>
            <p>2: ${x}p>
            <#list ["loop3"] as x>
                <p>3: ${x}p>
            #list>
            <p>4: ${x}p>
        #list>
        <p>5: ${x}p>
    #list>
div>

效果:
33

(13)macro、nested、return

语法:

<#-- name:宏(macro)变量的名称,它不是表达式,和顶层变量语法相同,也可以写成一个字符串字面量 -->
<#-- param1、param2等:存储参数的值(不是表达式)的局部变量名称,=后面的默认值(是表达式)可选,默认值也可以是另外一个参数
     参数名称和顶层变量语法相同,所以有相同的特性和限制 -->
<#-- paramN: 最后一个参数,可能会有三个点(...),表示宏接受可变的数量的参数,不匹配其他参数的参数将被收集到最后一个参数中
     当用命名参数调用宏时,paramN将是包含所有传递给宏的未声明的键值对的哈希表
     当用位置参数调用宏时,paramN将是额外参数值的序列 -->
<#-- 没有默认值的参数必须在有默认值的参数之前 -->
<#-- nested和return指令是可选的,可以在macro开始标签和macro结束标签之间的任意位置使用并且可以使用任意次数 --> 
<#macro name param1 param2 ... paramN>
  ...
  <#-- loopvar1、loopvar2等:是nested指令想为嵌套内容创建的循环变量的值,这些是表达式,是可选的 -->
  <#nested loopvar1, loopvar2, ..., loopvarN>
  ...
  <#return>
  ...
#macro>

macro指令在当前命名空间中创建一个宏(macro)变量,宏变量存储模板片段,可以被用作自定义指令。该变量还存储了允许用户定义指令的参数名称。当将该变量作为指令时, 必须给所有参数赋值,除了有默认值的参数。默认值当且仅当调用宏而不给参数赋值时起作用。

模板fm-directive-macro.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>macro、nested、returntitle>
head>
<body>
    <div>
        <#-- 变量会在模板开始时被创建,而不管macro指令放置在模板的什么位置,所以可以将调用宏放在宏定义之前 -->
        <@copy/>
        <#-- 在当前命名空间中创建一个宏(macro)变量,宏变量存储模板片段,可以被用作自定义指令 -->
        <#macro copy>
            <p>© 2021 RtxTitanV . All rights reserved.p>
        #macro>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/directive/macro")
public String macroDirective(Model model) {
    model.addAttribute("num", 5);
    return "directive-macro";
}

效果:
34
宏可以带参数,参数可以设置默认值。在模板中新增以下代码:

<div>
    <#-- 宏可以带有参数,当将宏变量作为指令时,必须给所有参数赋值,除了有默认值的参数
         默认值当且仅当调用宏而不给参数赋值时起作用 -->
    <#macro mac x y = 20>
        <p>计算${x}和${y}的平均值:${(x + y) / 2}p>
    #macro>
    <@mac 10/>
    <@mac x = 20 y = 30/>
div>

效果:
35
宏支持可变数量的位置参数,不管它是使用位置参数还是命名参数传递。在模板中新增以下代码:

<div>
    <#-- 支持可变数量的位置参数的宏,不管它是使用位置参数还是命名参数传递 -->
    <#macro avg nums...>
        <#local sum = 0>
        <p>
            <#if nums?is_sequence>
                <#list nums>
                    计算
                    <#items as num>
                        <#local sum += num>
                        ${num}<#sep>#items>
                    的平均值:
                #list>
                <#if nums?size != 0>
                    ${sum / nums?size}
                #if>
            <#else>
                <#list nums>
                    计算
                    <#items as key, num>
                        <#local sum += num>
                        ${key}(${num})<#sep>#items>
                    的平均值:
                #list>
                <#if nums?size != 0>
                    ${sum / nums?size}
                #if>
            #if>
        p>
    #macro>
    <@avg/>
    <@avg 10 20 30/>
    <@avg 10 20 30 40/>
    <@avg a = 20 b = 30 c = 40/>
    <@avg a = 20 b = 30 c = 40 d = 50/>
div>

效果:
36
宏支持命名参数和可变数量的参数混合使用,不管它是使用位置参数还是命名参数传递。在模板中新增以下代码:

<div>
    <#-- 支持命名参数和可变数量的参数混合使用的宏,不管它是使用位置参数还是命名参数传递 -->
    <#macro m a b ext...>
        <ul>
            <li><p>a = ${a}p>li>
            <li><p>b = ${b}p>li>
            <ul>
                <#if ext?is_sequence>
                    <#list ext as e>
                        <li><p>ext[${e?index}] = ${e}p>li>
                    #list>
                <#else>
                    <#list ext as k, v>
                        <li><p>${k} = ${v}p>li>
                    #list>
                #if>
            ul>
        ul>
    #macro>
    <@m 1 3 5 7 9/>
    <@m a = 2 b = 4 c = 6 d = 8 e = 10/>
div>

效果:
SpringBoot2.x 集成 FreeMarker_第14张图片
宏也可以递归调用。在模板中新增以下代码,通过递归调用宏来计算自然数的阶乘:

<div>
    <#assign reslut = 1>
    <#-- 用于计算阶乘的宏 -->
    <#macro factorial num>
        <#if num lt 0>
            <#assign reslut = "num不能小于0">
        #if>
        <#if num gt 1>
            <#assign reslut *= num>
            <#-- 递归调用宏计算阶乘 -->
            <@factorial num = num - 1/>
        #if>
    #macro>
    <@factorial num/>
    <p>${num}的阶乘为:${reslut}p>
div>

效果:
38
num变量分别设置为0和-1时:
39
nested指令执行自定义指令开始和结束标签中间的模板片段。如果没有调用nested指令,自定义指令开始和结束标签中的部分将会被忽略。nested指令可以对嵌套内容创建循环变量。

在模板中新增以下代码,使用macronested输出99乘法表:

<div>
    <#macro nested_test count>
        <table>
            <#list 1..count as i>
                <tr>
                    <#list 1..count as j>
                        <#if j lte i>
                            <#-- nested指令执行自定义指令开始和结束标签中间的模板片段
                                 嵌套的片段可以包含模板中任意合法的内容,在宏调用的地方被执行
                                 如果没有调用nested指令,自定义指令开始和结束标签中的部分将会被忽略
                                 nested指令可以对嵌套内容创建循环变量 -->
                            <#nested i, j, i * j>
                        #if>
                    #list>
                tr>
            #list>
        table>
    #macro>
    <@nested_test 9; i, j>
        <td>${i} * ${j} = ${i * j}td>
    @nested_test>
div>

效果:
40
使用return指令可以将宏或函数定义体停留在任何位置。在模板中新增以下代码:

<div>
    <#macro return_test>
        <p>© 2021 RtxTitanV . All rights reserved.p>
        <#-- return指令可以将宏或函数定义体停留在任何位置 -->
        <#return>
        <p>email: [email protected]p>
    #macro>
    <@return_test/>
div>

效果:
41

(14)switch、case、default、break

语法:

<#-- value、refValue1等:表达式将会计算成相同类型的标量 -->
<#-- break和default是可选的 -->
<#switch value>
    <#case refValue1>
        ...
        <#break>
    <#case refValue2>
        ...
        <#break>
        ...
    <#case refValueN>
        ...
        <#break>
    <#default>
        ...
#switch>

switch指令会选择一个匹配的case指令并继续处理模板,遇到break指令会退出switch。如果没有匹配的case指令,存在default指令,那么它会继续处理default指令,否则就继续处理switch结束标签后的内容。

switch指令选择一个case指令后还会继续处理直到遇到break指令也就是它在遇到break指令前遇到另一个case指令或<#default>标签时也不会自动退出switch指令,这就是向下通行。

模板fm-directive-switch.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>switch、case、default、breaktitle>
head>
<body>
    <div>
        <p>
            段位:
            <#-- switch指令会选择一个匹配的case指令并继续处理模板,遇到break指令会退出switch
                 如果没有匹配的case指令,存在default指令,那么它会继续处理default指令
                 否则就继续处理switch结束标签后的内容 -->
            <#switch rank>
                <#case 1>
                    <span>青铜span>
                    <#break>
                <#case 2>
                    <span>白银span>
                    <#break>
                <#case 3>
                    <span>黄金span>
                    <#break>
                <#case 4>
                    <span>铂金span>
                    <#break>
                <#case 5>
                    <span>钻石span>
                    <#break>
                <#case 6>
                    <span>王者span>
                    <#break>
                <#default>
                    <span>无段位span>
            #switch>
        p>
    div>
    <hr/>
    <div>
        <p>
            <#-- switch指令选择一个case指令后还会继续处理直到遇到break指令
                 也就是它在遇到break指令前遇到另一个case指令或<#default>标签时也不会自动退出switch指令
                 这就是向下通行 -->
            <#switch rank>
                <#case 1>
                    <span>LV1span>
                <#case 2>
                    <span>LV2span>
                <#case 3>
                    <span>LV3span>
                <#case 4>
                    <span>LV4span>
                <#case 5>
                    <span>LV5span>
                <#case 6>
                    <span>LV6span>
                <#default>
                    <span>LV0span>
            #switch>
        p>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/directive/switch")
public String switchDirective(Model model) {
    model.addAttribute("rank", 3);
    return "directive-switch";
}

效果:
42
变量rank设置为0时:
43

4.表达式

(1)直接指定值

可以直接指定以下类型的值:

  • 字符串:通过给文本加上双引号或单引号可以直接指定字符串值。如果文本本身包含用于引号的字符("')或反斜杠,则必须在前面加上反斜杠\转义。转义允许直接在文本中输入包括换行符在内的任何字符。
  • 数字:输入不带引号的数字就直接指定了一个数值。必须使用点作为小数的分隔符而不能是其他的分组分隔符。可以使用-+来表明符号(+是多余的)。科学记数法暂不支持,也不能在小数点之前不写0。
  • 布尔值:直接写true和false就指定了一个布尔值,无需使用引号。
  • 序列:列出由逗号分隔的子变量并将整个列表放入方括号内,就指定了一个字面序列。列表中的每一项都可以是表达式。
  • 值域:值域也是序列,但它们由指定包含的数字范围所创建, 而不需指定序列中每一项。值域主要用来迭代一定范围内的数字,以及字符串切分和序列切分。值域表达式的通用形式是(startend可以是任意的结果为数字表达式):
    • start..end:包含结尾的值域。例如,1..4就是[1, 2, 3, 4]4..1就是[4, 3 , 2, 1]。包含结尾的值域不会是一个空序列,所以0..length-1是 错误的,因为当长度是0时,序列就成了[0, -1]
    • start..start..!end:不包含结尾的值域。例如1..<4就是[1, 2 , 3]4..<1就是[4, 3, 2]1..<1[]
    • start..*length:限定长度的值域。例如5..*3就是[5, 6, 7]5..*-3就是[5, 4, 3]5..*0[]
    • start..:无右边界值域,长度是无限的。例如,1..就是[1, 2, 3, 4, 5, 6, ... ]
  • 哈希表:列出用逗号分隔的键值对,键值对中的键和值用冒号分开,并将该列表放入大括号中,就指定了一个哈希表。注意,名称和值都是表达式。键必须是字符串。值可以是任意类型。

模板fm-expression-specify-value.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>直接指定值title>
head>
<body>
    <div>
        <ul>
            <li><p>字符串:p>li>
            <ul>
                <li><p>${"It's double quote: \"\", this is a backslash: \\"}p>li>
                <li><p>${'It\'s double quote: "", this is a backslash: \\'}p>li>
                <#-- 直接在模板中输入本文 -->
                <li><p>It's double quote: "", this is a backslash: \p>li>
                <#-- 通过转义的方式打印具有特殊含义的${} -->
                <li><p>${"$\{user}"}p>li>
                <#-- 通过原生字符串(引号前加r)打印具有特殊含义的${}以及\,原生字符串是一种特殊的字符串,${}和\在其中没有特殊含义,被视为普通字符串 -->
                <li><p>${r"${user}, C:\Windows"}p>li>
            ul>
            <li><p>数字:${1}、${+1}、${001}、${1.00}、${-1}、${0.11}p>li>
            <#-- c为布尔值内建函数,将布尔值转换为字符串 -->
            <li><p>布尔值:${true?c}、${false?c}p>li>
            <li>
                <p>
                    序列:[
                    <#list ["刘备", "关羽", "张飞"] as name>
                        "${name}"<#sep>,
                    #list>
                    ]
                p>
            li>
            <li><p>值域:p>li>
            <ul>
                <li>
                    <#-- 1..9: [1, 2, 3, 4, 5, 6, 7, 8, 9] -->
                    <p>
                        1..9: [
                        <#list 1..9 as i>
                            ${i}<#sep>,
                        #list>
                        ]
                    p>
                li>
                <li>
                    <#-- 1..<9: [1, 2, 3, 4, 5, 6, 7, 8] -->
                    <p>
                        1..<9: [
                        <#list 1..<9 as i>
                            ${i}<#sep>,
                        #list>
                        ]
                    p>
                li>
                <li>
                    <#-- 5..*6: [5, 6, 7, 8, 9, 10] -->
                    <p>
                        5..*6: [
                        <#list 5..*6 as i>
                            ${i}<#sep>,
                        #list>
                        ]
                    p>
                li>
            ul>
            <li>
                <p>
                    哈希表:{
                    <#list {"id": 1, "username": "admin", "password": "123456"} as k, v>
                        <#if k == "id">
                            "${k}": ${v}<#sep>,
                        <#else>
                            "${k}": "${v}"<#sep>,
                        #if>
                    #list>
                    }
                p>
            li>
        ul>
    div>
body>
html>

值域的一些注意事项:

  • 值域表达式本身没有方括号。
  • ..两侧写算术表达式无需小括号。
  • ....<..!..*是运算符, 所以它们中间不能有空格。
  • 值域不存储它们包含的数字。

效果:
SpringBoot2.x 集成 FreeMarker_第15张图片

(2)检索变量

  • 顶层变量:访问顶层变量,可以简单使用变量名。例如${user}用表达式user就可以在根(root)上获取以user为名存储的变量值。然后打印出存储在里面的内容。
  • 从哈希表中检索数据:如果有一个表达式的结果是哈希表, 那么可以使用点和子变量的名字得到它的值。 如果有一个模型为{root: {book: {title: "java核心技术", author: {name: "xxx", info: "java基础"}}, test: "title"}}。那么可以通过book.title来读取titlebook.author.name来读取authorname。还可以通过book["title"]来读取title,通过book["author"].namebook.author["name"]book["author"]["name"]来读取authorname
  • 从序列中检索数据:和从哈希表中检索是相同的,但是只能使用方括号语法形式来进行, 而且方括号内的表达式最终必须是一个数字而不是字符串。例如users[0].id
  • 特殊变量:特殊变量是由FreeMarker引擎本身定义的。可以按照.variable_name的语法形式使用它们。例如之前使用的globalsdata_model就是特殊变量。

注意:在顶层变量的变量名表达式中,变量名只可以包含字母(也可以是非拉丁文),数字(也可以是非拉丁数字),下划线_, 美元符号$,at符号@。 此外,第一个字符不可以是ASCII码数字(0-9)。从FreeMarker 2.3.22版本开始,变量名在任何位置也可以包含负号-,点.和冒号:,但这些必须使用前置的反斜杠\来转义,否则它们将被解释成操作符。(注意,这些转义仅在标识符中起作用,而不是字符串中。)当使用方括号语法时,因为名称可以是任意表达式的结果,则没有这样的限制。

(3)字符串操作

字符串操作有:

  • 插值和连接:可以在字符串字面量中使用${}在字符串中插入表达式的值。${}在字符串中的作用与在文本区相同。通过+进行字符串连接也可以达到和插值类似的效果。
  • 获取字符:给定索引值时可以获取字符串中的单个字符,这和序列类似,如user[0],执行的结果是一个长度为1的字符串,FTL并没有独立的字符类型。和序列中的子变量一样,这个索引也必须是数字,范围是从0到字符串长度,否则模板的执行将会发生错误并终止。
  • 字符串切分:类似于序列切分,只是用字符来代替序列的项。不同的是降序值域不允许进行字符串切分;如果变量的值既是字符串又是序列(多类型值),那么切分将会对序列进行,而不是字符串;还有一个遗留bug,值域包含结尾时,结尾小于开始索引并且是非负数时, 会返回空字符串而不是错误,现在这个bug已经向后兼容,但是不应该使用它。

警告:插值只在文本区(如

Hello ${name}!>)和字符串字面量(如<#include "/footer/${company}.html">)中起作用。形如<#if ${big}>...这样的写法是一个典型的语法错误,简单写为<#if big>...即可,而且<#if "${big}">...也是错误的, 因为它将参数值转换为字符串,但是if指令只接受布尔值,这将导致运行时错误。

模板fm-expression-string-operations.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>字符串操作title>
head>
<body>
    <p>插值和连接p>
    <div>
        <ul>
            <li>
                <#-- 可以在字符串字面量中使用${}在字符串中插入表达式的值 -->
                <#assign message = "Hello ${user.username} !">
                <p>message: ${message}p>
            li>
            <li>
                <#-- 也可以使用+号将表达式的值连接到字符串中,达到类似于插值的效果 -->
                <#assign message = "Hello " + user.username + " !">
                <p>message: ${message}p>
            li>
        ul>
    div>
    <p>获取字符p>
    <div>
        <ul>
            <li><p>username索引为0的字符:${user.username[0]}p>li>
            <li><p>username索引为3的字符:${user.username[3]}p>li>
            <li><p>username索引为8的字符:${user.username[8]}p>li>
        ul>
    div>
    <p>字符串切分(子串)p>
    <div>
        <ul>
            <li><p>username从0到2(包括2)的子串:${user.username[0..2]}p>li>
            <li><p>username从0到2(不包括2)的子串:${user.username[0..<2]}p>li>
            <li><p>username从3开始长度为5的子串:${user.username[3..*5]}p>li>
            <li><p>username从3开始长度为100的子串(切分长度已经超过了从开始索引到被切分字符串末尾的长度,所以这里只截取到字符串末尾):${user.username[3..*100]}p>li>
            <li><p>username从3开始一直到末尾的子串:${user.username[3..]}p>li>
        ul>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/expression/string-operations")
public String stringOperations(Model model) {
    model.addAttribute("user", new User(1L, "RtxTitanV", "654321"));
    return "expression-string-operations";
}

效果:
SpringBoot2.x 集成 FreeMarker_第16张图片

(4)序列操作

序列操作有:

  • 序列连接:可以使用+号来连接序列。
  • 序列切分:使用seq[range]seq是序列,range是值域),可以从序列中切分出一个片段。结果序列将包含原始序列中索引在值域中的项。此外,切片后序列的项将与值域的顺序相同。值域中的数字必须是序列可使用的合法索引,否则模板的处理将会终止并报错。

模板fm-expression-sequence-operations.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>序列操作title>
head>
<body>
    <h1>序列连接h1>
    <div>
        <table>
            <thead>
                <th>idth>
                <th>usernameth>
                <th>passwordth>
            thead>
            <tbody>
                <#list users1 + users2 as user>
                    <tr>
                        <td>${user.id}td>
                        <td>${user.username}td>
                        <td>${user.password}td>
                    tr>
                #list>
            tbody>
        table>
    div>
    <h1>序列切分h1>
    <div>
        <p>将序列切分为从索引1到3(包括3)的子序列p>
        <table>
            <thead>
                <th>idth>
                <th>usernameth>
                <th>passwordth>
            thead>
            <tbody>
                <#list (users1 + users2)[1..3] as user>
                    <tr>
                        <td>${user.id}td>
                        <td>${user.username}td>
                        <td>${user.password}td>
                    tr>
                #list>
            tbody>
        table>
        <p>将序列切分为从索引1到3(不包括3)的子序列p>
        <table>
            <thead>
                <th>idth>
                <th>usernameth>
                <th>passwordth>
            thead>
            <tbody>
                <#list (users1 + users2)[1..<3] as user>
                    <tr>
                        <td>${user.id}td>
                        <td>${user.username}td>
                        <td>${user.password}td>
                    tr>
                #list>
            tbody>
        table>
        <p>将序列切分为从索引2开始长度为2的子序列p>
        <table>
            <thead>
                <th>idth>
                <th>usernameth>
                <th>passwordth>
            thead>
            <tbody>
                <#list (users1 + users2)[2..*2] as user>
                    <tr>
                        <td>${user.id}td>
                        <td>${user.username}td>
                        <td>${user.password}td>
                    tr>
                #list>
            tbody>
        table>
        <p>将序列切分为从索引2开始长度为5的子序列(切分长度已经超过了从开始索引到被切分序列末尾的长度,所以这里只截取到序列末尾)p>
        <table>
            <thead>
                <th>idth>
                <th>usernameth>
                <th>passwordth>
            thead>
            <tbody>
                <#list (users1 + users2)[2..*5] as user>
                    <tr>
                        <td>${user.id}td>
                        <td>${user.username}td>
                        <td>${user.password}td>
                    tr>
                #list>
            tbody>
        table>
        <#-- 有限长度切分允许开始索引超过最后项索引一个数(但不能再多了) -->
        <p>将序列切分为从索引5开始长度为2的子序列(开始索引已经超出了序列的索引范围,所以这里切分结果为空)p>
        <table>
            <thead>
                <th>idth>
                <th>usernameth>
                <th>passwordth>
            thead>
            <tbody>
                <#list (users1 + users2)[5..*2] as user>
                    <tr>
                        <td>${user.id}td>
                        <td>${user.username}td>
                        <td>${user.password}td>
                    tr>
                #list>
            tbody>
        table>
        <p>将序列切分为从索引1开始的子序列p>
        <table>
            <thead>
            <th>idth>
            <th>usernameth>
            <th>passwordth>
            thead>
            <tbody>
            <#list (users1 + users2)[1..] as user>
                <tr>
                    <td>${user.id}td>
                    <td>${user.username}td>
                    <td>${user.password}td>
                tr>
            #list>
            tbody>
        table>
        <#-- 无右边界切分允许开始索引超过最后项索引一个数(但不能再多了) -->
        <p>将序列切分为从索引5开始的子序列(开始索引已经超出了序列的索引范围,所以这里切分结果为空)p>
        <table>
            <thead>
            <th>idth>
            <th>usernameth>
            <th>passwordth>
            thead>
            <tbody>
            <#list (users1 + users2)[5..] as user>
                <tr>
                    <td>${user.id}td>
                    <td>${user.username}td>
                    <td>${user.password}td>
                tr>
            #list>
            tbody>
        table>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/expression/sequence-operations")
public String sequenceOperations(Model model) {
    List<User> users1 = new ArrayList<>();
    users1.add(new User(1L, "刘备", "123132"));
    users1.add(new User(2L, "关羽", "321231"));
    users1.add(new User(3L, "张飞", "213312"));
    User[] users2 = {new User(4L, "赵云", "132213"), new User(5L, "马超", "312123")};
    model.addAttribute("users1", users1);
    model.addAttribute("users2", users2);
    return "expression-sequence-operations";
}

效果:

(5)哈希表操作

可以使用+号来连接哈希表。如果哈希表都包含相同的键,在+号右侧的哈希值优先。

模板fm-expression-hash-operations.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>哈希表操作title>
head>
<body>
    <h1>哈希表连接h1>
    <div>
        <table>
            <thead>
                <th>idth>
                <th>usernameth>
                <th>passwordth>
                <th>keyth>
            thead>
            <tbody>
                <#list map1 + map2 as k, v>
                    <tr>
                        <td>${v.id}td>
                        <td>${v.username}td>
                        <td>${v.password}td>
                        <td>${k}td>
                    tr>
                #list>
            tbody>
        table>
    div>
body>
html>

FreeMarkerController中新增以下方法:

    @GetMapping("/expression/hash-operations")
    public String hashOperations(Model model) {
        Map<String, Object> map1 = new HashMap<>(16);
        map1.put("user1", new User(1L, "刘备", "123132"));
        map1.put("user2", new User(2L, "关羽", "321231"));
        Map<String, Object> map2 = new HashMap<>(16);
        map2.put("user1", new User(3L, "张飞", "213312"));
        map2.put("user4", new User(4L, "赵云", "132213"));
        map2.put("user5", new User(5L, "马超", "312123"));
        model.addAttribute("map1", map1);
        model.addAttribute("map2", map2);
        return "expression-hash-operations";
    }

效果:
SpringBoot2.x 集成 FreeMarker_第17张图片

(6)运算符

运算符主要分为:

  • 算术运算符:加法(+)、减法(-)、乘法(*)、除法(/)、求模(%)。
  • 比较运算符:等于(==)、不等于(!=)、大于(>gt)、小于(<lt)、大于等于(>=gte)、小于等于(<=lte)。
  • 逻辑运算符:逻辑与(&&)、逻辑或(||)、逻辑非(!)。

模板fm-expression-operation.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>运算符title>
head>
<body>
    <p>x=${x}, y=${y}p>
    <#-- 需要保证算术运算符两边都是结果为数字的表达式
         +号例外,如果+一边是数字,另一边是字符串,那么数字会转换成字符串,然后进行字符串连接 -->
    <p>算术运算符:+、-、*、/、%p>
    <ul>
        <li><p>(y + x % 3) * (y - x / 2) :${(y + x % 3) * (y - x / 2)}p>li>
    ul>
    <#-- ==或!=两边的表达式的结果都必须是标量,而且两个标量都必须是相同类型
         由于FreeMarker是精确比较,所以在比较字符串时要注意大小写和空格
         <、<=、>=、>不能作用于字符串 -->
    <p>比较运算符:==、!=、>(gt)、<(lt)、>=(gte)、<=(lte)p>
    <ul>
        <li>
            <p>
                x == y:
                <#if x == y>
                    x等于y
                <#else>
                    x不等于y
                #if>
            p>
        li>
        <li>
            <p>
                x != y:
                <#if x != y>
                    x不等于y
                <#else>
                    x等于y
                #if>
            p>
        li>
        <li>
            <p>
                x > 5:
                <#-- 注意使用比较运算符>、>=时,FreeMarker解释>时可以把它当做ftl标签的结束符
                     为了避免该问题,可以用小括号括起来或用lt代替<、lte代替<=、gt代替>、gte代替>= -->
                <#if (x > 5)>
                    x大于5
                <#else>
                    x不大于5
                #if>
            p>
        li>
        <li>
            <p>
                y lte 2:
                <#if y lte 2>
                    y小于等于2
                <#else>
                    y大于2
                #if>
            p>
        li>
    ul>
    <#-- 逻辑运算符只对布尔值起作用,否则,将出现错误导致模板处理中止 -->
    <p>逻辑运算符:&&、||、!p>
    <ul>
        <li>
            <p>
                y lt x && -x gt -y:
                <#if y lt x && -x gt -y>
                    true
                <#else>
                    false
                #if>
            p>
        li>
        <li>
            <p>
                -x <= -y || y >= x:
                <#if (-x <= -y || y >= x)>
                    true
                <#else>
                    false
                #if>
            p>
        li>
        <li>
            <p>
                !(-x < -y || y gte x):
                <#if !(-x < -y || y gte x)>
                    true
                <#else>
                    false
                #if>
            p>
        li>
    ul>
body>
html>

FTL忽略表达式中的多余的空格。小括号可以用来给任意表达式分组,注意方法调用使用的小括号与给表达式分组的小括号含义完全不同。

FreeMarkerController中新增以下方法:

@GetMapping("/expression/operation")
public String operation(Model model) {
    model.addAttribute("x", 10);
    model.addAttribute("y", 3);
    return "expression-operation";
}

效果:
SpringBoot2.x 集成 FreeMarker_第18张图片

(7)处理不存在的值

当试图访问一个不存在的变量时,就会发生错误并中止模板处理。然而,有两个特殊的操作符可以抑制这个错误,并处理这种问题。被处理的变量可以是顶级变量,也可以是哈希表或者序列的子变量。此外,这些操作符还可以处理方法调用没有返回值的情况。(从Java的角度来看:返回null或者返回类型为void),所以说这些操作符一般处理不存在的值,而不仅仅是不存在的变量更为正确。

这两个特殊的操作符:

  • 默认值操作符:允许为可能不存在的变量指定一个默认值。默认值可以是任何类型的表达式。
    • 使用形式:unsafe_expr!default_exprunsafe_expr!(unsafe_expr)!default_expr(unsafe_expr)!
  • 不存在值检测操作符:检测一个值是否存在,结果是true或false。
    • 使用形式:unsafe_expr??(unsafe_expr)??

模板fm-expression-missing.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>处理不存在的值title>
head>
<body>
    <p>默认值操作符p>
    <div>
        <ul>
            <li><p>name不存在时为默认值:${name!"No name"}p>li>
            <#assign name = "RtxTitanV">
            <li><p>name存在时为name的值:${name!"No name"}p>li>
            <#-- 默认值可以是任何类型的表达式 -->
            <li><p>num不存在时为默认值:${num!0}p>li>
            <li>
                <p>
                    users不存在时为默认值:[
                    <#list users!["刘备", "关羽", "张飞"] as user>
                        "${user}"<#sep>,
                    #list>
                    ]
                p>
            li>
            <#-- 如果默认值被省略了,那么它将同时是空字符串和空序列以及空哈希表 -->
            <li><p>message不存在时为默认值:${message!}p>li>
            <#assign message = "Hello World">
            <li><p>message存在时为message的值:${message!}p>li>
            <#-- 对于非顶层变量,默认值有两种使用方式 -->
            <li><p>第一种方式,user必须存在,否则会报错,user存在但username不存在才会使用默认值:${user.username!"No username"}p>li>
            <#-- 当用小括号包围时,允许表达式的任意部分可以未定义,而没有小括号时,只允许表达式的最后部分未定义 -->
            <li><p>第二种方式,user不存在或user存在但username不存在都会使用默认值而不会报错:${(user.username)!"No username"}p>li>
            <#assign seq = ["A", "B", "C"]>
            <#-- 默认值操作符也可以作用于序列子变量,序列索引为负数时会出错 -->
            <li><p>默认值操作符也可以作用于序列子变量:[${seq[0]!"?"}, ${seq[1]!"?"}, ${seq[2]!"?"}, ${seq[3]!"?"}, ${seq[4]!"?"}]p>li>
        ul>
    div>
    <p>不存在值检测操作符p>
    <ul>
        <li>
            <p>
                password是否存在:
                <#-- 不存在值检测操作符可以检测一个值是否存在,结果为true和false,对于非顶层变量,规则与默认值操作符相同 -->
                <#if (user.password)??>
                    存在
                <#else>
                    不存在
                #if>
            p>
        li>
    ul>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/expression/missing")
public String missing(Model model) {
    model.addAttribute("user", new User(1L, null, null));
    return "expression-missing";
}

效果:
SpringBoot2.x 集成 FreeMarker_第19张图片
不设置变量user到Model中时:
SpringBoot2.x 集成 FreeMarker_第20张图片
如上图所示,发现${user.username!"No username"}报错,说明对于非顶层变量的默认值使用,没有用小括号包围时user不存在,模板就会报错,只有user存在但username不存在才会使用默认值。去掉${user.username!"No username"}这行代码再次访问模板:
SpringBoot2.x 集成 FreeMarker_第21张图片
根据上面的测试结果,可以说明${(user.username)!"No username"},即对于非顶层变量的默认值使用,用小括号包围后在user不存在时或user存在但username不存在时都会使用默认值。

5.常用内建函数

内建函数就像FreeMarker添加到对象中的方法。为了防止与实际的方法和其他子变量的名称冲突,用问号(?)来代替点(.),将它们与父对象分开。FreeMarker中有很多内建函数,这里结合实例总结一些常用的内建函数。

(1)字符串内建函数

模板fm-built-ins-strings.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>字符串内建函数title>
head>
<body>
    <div>
        <#-- 字符串字符串必须是true或false(大小写敏感),或必须是由boolean_format设置的特定格式
             否则访问该内建函数时,会发生错误终止模板 -->
        <p>
            字符串转为布尔值(boolean):
            <#if "true"?boolean>
                true
            #if>
            <#if !"false"?boolean>
                false
            #if>
        p>
        <#-- 可以识别科学记数法,从FreeMarker 2.3.21版本开始,可以识别所有XML Schema数字格式,如NaN、INF、-INF,以及Java本地格式Infinity和-Infinity -->
        <p>
            字符串转换为数字(number):
            ${"8"?number}、${"0.88"?number}、${"8.88E8"?number}、
            ${"NaN"?number}、${"INF"?number}、${"-INF"?number}、${"Infinity"?number}、${"-Infinity"?number}
        p>
        <p>字符串中首单词的首字母大写(cap_first):${str1?cap_first}p>
        <p>字符串中首单词的首字母小写(uncap_first):${str2?uncap_first}p>
        <p>字符串转换为小写形式(lower_case):${user.username?lower_case}p>
        <p>字符串转换为大写形式(upper_case):${user.username?upper_case}p>
        <p>
            检查字符串是否包含指定子串(contains):
            <#if user.username?contains("FreeMarker")>
                包含Freemarker
            <#else>
                不包含FreeMarker
            #if>
        p>
        <p>
            检查字符串是否以指定子串开头(ends_with):
            <#if user.username?starts_with("SpringBoot")>
                字符串以SpringBoot开头
            <#else>
                字符串没有以SpringBoot开头
            #if>
        p>
        <p>
            检查字符串是否以指定子串结尾(ends_with):
            <#if user.username?ends_with("Test")>
                字符串以Test结尾
            <#else>
                字符串没有以Test结尾
            #if>
        p>
        <p>如果字符串没有以第一个参数指定的子串开头,就会将它加到字符串前面,否则返回原字符串(ensure_starts_with):${user.username?ensure_starts_with("用户名是")}、${user.username?ensure_starts_with("Spring")}p>
        <p>如果字符串没有以第一个参数指定的子串结尾,就会将它加到字符串后面,否则返回原字符串(ensure_ends_with):${user.username?ensure_ends_with("是用户名")}、${user.username?ensure_ends_with("Test")}p>
        <#-- 如果第一个参数作为子串没有在该字符串中出现时(如果使用了第二个参数,那么就从给定的索引开始),返回-1 -->
        <p>返回字符串中第一次出现子串时的索引(index_of):${user.username?index_of("in")}p>
        <#-- 第二个参数的数值没有限制,如果是负数,效果和0一样,如果比字符串的长度大,和是字符串长度那个数效果一样 -->
        <p>返回字符串中第一次出现子串时的索引,指定开始搜索的索引位置(index_of):${user.username?index_of("in", 5)}p>
        <p>返回字符串中最后一次出现子串时的索引(last_index_of):${user.username?last_index_of("in")}p>
        <p>返回字符串中第一次出现子串时的索引,指定开始搜索的索引位置(last_index_of):${user.username?last_index_of("in", 5)}p>
        <p>字符串中字符数量(length):${user.username?length}p>
        <p>去除字符串首尾空格(trim):${user.password?trim}p>
        <#-- 如果没有找到参数字符串,会返回空串;如果参数是长度为0的字符串,会返回未改变的源字符串 -->
        <p>保留字符串中给定子串第一次出现之后的部分(keep_after):${user.username?keep_after("-")}p>
        <p>保留字符串中给定子串最后一次出现之后的部分(keep_after_last):${user.username?keep_after_last("-")}p>
        <p>保留字符串中给定子串第一次出现之前的部分(keep_before):${user.username?keep_before("-")}p>
        <p>保留字符串中给定子串最后一次出现之前的部分(keep_before_last):${user.username?keep_before_last("-")}p>
        <p>从字符串开头移除给定子串,如果字符串不以给定子串开头,会返回源字符串(remove_beginning):${user.username?remove_beginning("Spring")}、${user.username?remove_beginning("spring")}p>
        <p>从字符串结尾移除给定子串,如果字符串不以给定子串结尾,会返回源字符串(remove_ending):${user.username?remove_ending("-Test")}、${user.username?remove_ending("-test")}p>
        <#-- 字符串截断至给定长度并附加一个终止符字符串(默认为[...],可以另外指定),如果字符串的长度不超过给定长度,则原样返回
             结束符字符串的长度也算在给定长度内,当给定长度比结束符字符串的长度短时,结果长度也可以比给定长度长,这种情况下,结束符字符串仍然原样返回
             还有一个额外的参数可以指定终止符字符串的假定长度,否则使用实际长度 -->
        <p>
            字符串截断至给定长度并附加一个终止符字符串(truncate):
            <ul>
                <li>
                    <#-- 如果截断发生在词的边界,那么在词尾和终止符串之间就有一个空格,否则它们之间就没有空格 -->
                    <p>倾向于在词的边界处截断,但如果结果长度小于指定长度的75%,还是会在词的中间截断(truncate):${str1?truncate(18)}、${str1?truncate(22)}、${str1?truncate(34)}、${str1?truncate(3)}、${str1?truncate(18, "...")}、${str1?truncate(18, "...", 1)}p>
                li>
                <li>
                    <p>总是在词的边界处截断(truncate_w):${str1?truncate_w(18)}、${str1?truncate_w(22)}、${str1?truncate_w(18, "...")}、${str1?truncate_w(18, "...", 1)}p>
                li>
                <li>
                    <p>任何字符处截断(truncate_c):${str1?truncate_c(18)}、${str1?truncate_c(22)}、${str1?truncate_c(18, "...")}、${str1?truncate_c(18, "...", 1)}p>
                li>
            ul>
        p>
        <#-- 字符串替换不处理单词边界,从左向右执行,若第一个参数为空串,那么所有空字符串都将被替换 -->
        <p>字符串替换(replace):${user.username?replace("-", "_")}、${"ABABA"?replace("ABA", "BAB")}、${user.username?replace("", "_")}p>
        <p>
            沿着另一字符串的出现处将字符串拆分为序列(split):
            <#list user.username?split("-")>
                [
                <#items as s>
                    ${s}<#sep>,
                #items>
                ]
            #list>
        p>
        <p>
            包含字符串中所有单词的序列,顺序为出现在字符串中的顺序(word_list):
            <#list user.password?word_list as word>
                [${word}]<#sep>,
            #list>
        p>
        <#-- 如果只有1个参数,则用空格补齐,直到整个字符串长度达到参数指定的长度
             如果有两个参数,则用第二个参数指定的字符串补齐到指定长度,如果参数2字符串长度大于1,则该字符串会周期性插入 -->
        <p>字符串从左侧补齐至指定长度(left_pad):p>
        <p>
            [${""?left_pad(5)}]、[${"A"?left_pad(5)}]、[${"AB"?left_pad(5)}]、
            [${"ABC"?left_pad(5)}]、[${"ABCDEF"?left_pad(5)}]、[${""?left_pad(5, "#")}]、
            [${"A"?left_pad(5, "#")}]、[${"AB"?left_pad(5, "#")}]、[${"ABC"?left_pad(5, "#")}]、
            [${"ABCDEF"?left_pad(5, "#")}]、[${""?left_pad(10, "^_^")}]、[${"A"?left_pad(10, "^_^")}]、
            [${"ABC"?left_pad(10, "^_^")}]、[${"ABCDE"?left_pad(10, "^_^")}]、[${"ABCDEFG"?left_pad(10, "^_^")}]
        p>
        <p>字符串从右侧补齐至指定长度(right_pad):p>
        <p>
            [${""?right_pad(5)}]、[${"A"?right_pad(5)}]、[${"AB"?right_pad(5)}]、
            [${"ABC"?right_pad(5)}]、[${"ABCDEF"?right_pad(5)}]、[${""?right_pad(5, "#")}]、
            [${"A"?right_pad(5, "#")}]、[${"AB"?right_pad(5, "#")}]、[${"ABC"?right_pad(5, "#")}]、
            [${"ABCDEF"?right_pad(5, "#")}]、[${""?right_pad(10, "^_^")}]、[${"A"?right_pad(10, "^_^")}]、
            [${"ABC"?right_pad(10, "^_^")}]、[${"ABCDE"?right_pad(10, "^_^")}]、[${"ABCDEFG"?right_pad(10, "^_^")}]
        p>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/built-ins/strings")
public String builtInsStrings(Model model) {
    model.addAttribute("user",
        new User(1L, "SpringBoot-FreeMarker-Strings-Test", "   SpringBoot _Free-Marker  . Strings,  @Test ? !  "));
    model.addAttribute("str1", "springboot freemarker strings test");
    model.addAttribute("str2", "SpringBoot FreeMarker Strings Test");
    return "built-ins-strings";
}

效果:
SpringBoot2.x 集成 FreeMarker_第22张图片
SpringBoot2.x 集成 FreeMarker_第23张图片

(2)数字内建函数

模板fm-built-ins-numbers.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>数字内建函数title>
head>
<body>
    <div>
        <p>求给定数字的绝对值(abs):${(-5)?abs}p>
        <p>数字转换为字符串(c):${num?c}、${num2?c}p>
        <#-- 1, 2, 3等转换为字符串"a", "b", "c"等,到达"z"会继续转换为"aa", "ab"等,数字最小为1,无上限 -->
        <p>
            数字转换为小写字母(lower_abc):
            <#list 1..30 as i>
                ${i?lower_abc}<#sep>,
            #list>
        p>
        <p>
            数字转换为大写字母(upper_abc):
            <#list 1..30 as i>
                ${i?upper_abc}<#sep>,
            #list>
        p>
        <#-- 向正无穷方向进位 -->
        <p>返回最近的整数,以.5结尾将进位(round):${0.25?round}、${0.5?round}、${1.75?round}、${(-0.25)?round}、${(-0.5)?round}、${(-1.75)?round}p>
        <#-- 向负无穷方向舍弃 -->
        <p>返回舍弃小数后的整数(floor):${0.25?floor}、${0.5?floor}、${1.75?floor}、${(-0.25)?floor}、${(-0.5)?floor}、${(-1.75)?floor}p>
        <#-- 向正无穷方向进位 -->
        <p>返回小数进位后的整数(ceiling):${0.25?ceiling}、${0.5?ceiling}、${1.75?ceiling}、${(-0.25)?ceiling}、${(-0.5)?ceiling}、${(-1.75)?ceiling}p>
        <p>
            数字转换成字符串(string):
            <ul>
                <#-- 四种预定义数字格式:number、currency、percent、computer -->
                <li><P>${num?string}、${num?string.number}、${num?string.currency}、${num2?string.percent}、${num?string.computer}P>li>
                <li><p>最小整数位为1,保留1位小数:${num2?string["0.0"]}p>li>
                <li><p>最小整数位为3,保留2位小数:${num2?string["000.00"]}p>li>
                <li><p>最小整数位为1,保留4位小数:${num2?string["0.####"]}p>li>
                <li><p>最小整数位为4,保留5位小数:${num2?string["0000.#####"]}p>li>
                <li><p>科学计数法:${num?string["0.##E0"]}p>li>
            ul>
        p>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/built-ins/numbers")
public String builtInsNumbers(Model model) {
    model.addAttribute("num", 888888);
    model.addAttribute("num2", 66.6658932);
    return "built-ins-numbers";
}

效果:
SpringBoot2.x 集成 FreeMarker_第24张图片

(3)日期内建函数

模板fm-built-ins-date.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>日期内建函数title>
head>
<body>
    <div>
        <#-- 可以用来指定日期变量的哪部分在使用 -->
        <p>日期时间(datetime):${date?datetime}p>
        <p>日期(date):${date?date}p>
        <p>时间(time):${date?time}p>
        <p>以指定的格式将日期类型转换为字符串类型(string):p>
        <p>
            <ul>
                <#-- 由于FreeMarker不能决定数据模型中的日期变量是日期,时间还是日期-时间,在这种情况下,
                     ${date?string.short}这种写法没有指定要显示的确定字段,FreeMarker不指定如何显示该值,最终模板会中止执行并报错
                     为了防止这种情况,可以用?date、?time和?datetime来确定要显示的部分 -->
                <li><p>short: ${date?datetime?string.short}p>li>
                <li><p>medium: ${date?datetime?string.medium}p>li>
                <li><p>long: ${date?datetime?string.long}p>li>
                <li><p>full: ${date?datetime?string.full}p>li>
                <li><p>iso: ${date?datetime?string.iso}p>li>
                <li><p>iso_m_u: ${date?datetime?string.iso_m_u}p>li>
                <li><p>xs: ${date?datetime?string.xs}p>li>
                <li><p>xs_ms_nz: ${date?datetime?string.xs_ms_nz}p>li>
                <#-- 指定了格式之后可以不用?date、?time和?datetime,格式可以是变量或任意表达式 -->
                <li><p>${date?string["yyyy/MM/dd EEEE a hh:mm:ss"]}p>li>
                <li><p>${date?date?string["yyyy"]}年${date?date?string["MM"]}月${date?date?string["dd"]}日p>li>
                <li><p>${date?time?string["hh"]}时${date?time?string["mm"]}分${date?time?string["ss"]}秒p>li>
            ul>
        p>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/built-ins/date")
public String builtInsDate(Model model) {
    model.addAttribute("date", new Date());
    return "built-ins-date";
}

效果:
SpringBoot2.x 集成 FreeMarker_第25张图片

(4)布尔值内建函数

模板fm-built-ins-booleans.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>布尔值内建函数title>
head>
<body>
    <div>
        <#-- 不管boolean_format设置是什么,结果是"true"或"false" -->
        <p>布尔值转换为字符串(c):${(grade > 60)?c}、${(grade <= 60)?c}p>
    div>

    <div>
        <p>布尔值转换为字符串(string):p>
        <ul>
            <#-- 布尔值为true,则返回第一个参数,否则返回第二个参数,返回值总是字符串 -->
            <li><p>成绩是否及格:${(grade > 60)?string("及格", "不及格")}p>li>
        ul>
    div>
    <div>
        <#-- booleanExp为true,则返回第一个参数,否则返回第二个参数,参数表达式可以是任意类型,也可以是不同类型 -->
        <p>使用booleanExp?then(whenTrue, whenFalse),类似于三元运算符(then):p>
        <ul>
            <li>
                <p>
                    成绩对应级别:${(grade gte 0 && grade lte 100)?then((grade gte 60)?then((grade gte 70)?then((grade gte 90)?then("优", "良"), "差"), "不及格"), "无效数据")}
                p>
            li>
        ul>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/built-ins/booleans")
public String builtInsBooleans(Model model) {
    model.addAttribute("grade", 85);
    return "built-ins-booleans";
}

效果:
SpringBoot2.x 集成 FreeMarker_第26张图片

(5)序列内建函数

模板fm-built-ins-sequences.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>序列内建函数title>
head>
<body>
    <div>
        <p>序列子变量的数量(size):${users?size}p>
        <p>序列的第一个子变量(first):${users?first}p>
        <p>序列的最后一个子变量(last):${users?last}p>
        <#-- 序列中不是字符串的项会被转换为字符串,参数1为给定分隔符,参数2为代替空序列的值,参数3为列表结尾
             来自Java的序列中的null值会被该内建函数忽略 -->
        <p>使用给定分隔符将序列的所有项串联成一个字符串(join):${users?join("-", "[]", ".")}、${[]?join("-", "[]", ".")}p>
        <p>判断序列中是否包含指定值(seq_contains):${colors?seq_contains("green")?then("yes", "no")}、${colors?seq_contains("black")?then("yes", "no")}p>
        <#-- 如果第一个参数指定的值没有在该序列中出现时(如果使用了第二个参数,那么就从给定的索引开始),返回-1 -->
        <p>返回序列中第一次出现指定值时的索引(seq_index_of):${colors?seq_index_of("green")}p>
        <#-- 第二个参数的数值没有限制,如果是负数,效果和0一样,如果比序列的长度大,和是序列长度那个数效果一样 -->
        <p>返回序列中第一次出现指定值时的索引,指定开始搜索的索引位置(seq_index_of):${colors?seq_index_of("green", 3)}p>
        <p>返回序列中最后一次出现指定值时的索引(seq_last_index_of):${colors?seq_last_index_of("green")}p>
        <p>返回序列中最后一次出现指定值时的索引,指定开始搜索的索引位置(seq_last_index_of):${colors?seq_last_index_of("green", 3)}p>
    div>
    <div>
        <#-- 参数1指定拆分后子序列的大小(必须是数字,至少为1),参数2(以是任意类型的值)用来填充最后一个序列,以达到给定大小 -->
        <p>将序列拆分成给定大小的多个序列(chunk):p>
        <table>
            <#list colors?chunk(3, "?") as row>
                <tr>
                    <#list row as color><td>${color}td>#list>
                tr>
            #list>
        table>
    div>
    <div>
        <p>序列的反序形式(reverse):p>
        <table>
            <thead>
                <th>idth>
                <th>usernameth>
                <th>passwordth>
            thead>
            <tbody>
                <#list users?reverse as user>
                    <tr>
                        <td>${user.id}td>
                        <td>${user.username}td>
                        <td>${user.password}td>
                    tr>
                #list>
            tbody>
        table>
    div>
    <div>
        <#-- 只在子变量都是字符串,或都是数字,或都是日期值,或都是布尔值时起作用,子变量都是字符串时的排序通常是大小写不敏感的 -->
        <p>
            以升序的方式返回序列(sort):
            <#list colors?sort>
                [
                <#items as color>
                    "${color}"<#sep>,
                #items>
                ]
            #list>
        p>
    div>
    <div>
        <#-- 如果要降序排列,在使用该内建函数后还要使用reverse -->
        <p>返回按给定哈希表子变量升序排序的哈希序列(sort_by):p>
        <table>
            <thead>
            <th>idth>
            <th>usernameth>
            <th>passwordth>
            thead>
            <tbody>
            <#list users?sort_by("username") as user>
                <tr>
                    <td>${user.id}td>
                    <td>${user.username}td>
                    <td>${user.password}td>
                tr>
            #list>
            tbody>
        table>
    div>
    <div>
        <p>返回包含输入序列中从第一个不符合参数条件的元素开始的元素的序列(drop_while):<#list colors?drop_while(color -> color?length <= 5) as color>"${color}" #list>p>
        <p>返回只包含输入序列中第一个不符合参数条件的元素之前的元素的序列(take_while):<#list colors?take_while(color -> color?length <= 5) as color>"${color}" #list>p>
        <p>返回只包含参数条件返回真值的元素的序列(filter):<#list colors?filter(color -> color?length <= 5) as color>"${color}" #list>p>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/built-ins/sequences")
public String builtInsSequences(Model model) {
    List<User> users = new ArrayList<>();
    users.add(new User(1L, "刘备", "123132"));
    users.add(new User(2L, "关羽", "321231"));
    users.add(new User(3L, "张飞", "213312"));
    users.add(new User(4L, "赵云", "132213"));
    users.add(new User(5L, "马超", "312123"));
    String[] colors = {"red", "blue", "green", "yellow", "blue", "green", "white", "purple", "cyan", "orange"};
    model.addAttribute("users", users);
    model.addAttribute("colors", colors);
    return "built-ins-sequences";
}

效果:
SpringBoot2.x 集成 FreeMarker_第27张图片
SpringBoot2.x 集成 FreeMarker_第28张图片

(6)哈希表内建函数

模板fm-built-ins-hashes.ftl如下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>哈希表内建函数title>
head>
<body>
    <div>
        <#-- 可以使用<#list attrs as key, value>...<#list>同时列出键和值 -->
        <#-- 因为哈希表通常没有定义子变量的顺序,所以键的返回顺序是任意的 -->
        <p>包含哈希中所有查询到的键的序列(keys):p>
        <table>
            <thead>
                <th>idth>
                <th>usernameth>
                <th>passwordth>
                <th>keyth>
            thead>
            <tbody>
                <#list userMap?keys as key>
                    <tr>
                        <td>${userMap[key].id}td>
                        <td>${userMap[key].username}td>
                        <td>${userMap[key].password}td>
                        <td>${key}td>
                    tr>
                #list>
            tbody>
        table>
    div>
    <div>
        <p>包含哈希中所有值的序列(values):p>
        <table>
            <thead>
                <th>idth>
                <th>usernameth>
                <th>passwordth>
            thead>
            <tbody>
                <#list userMap?values as value>
                    <tr>
                        <td>${value.id}td>
                        <td>${value.username}td>
                        <td>${value.password}td>
                    tr>
                #list>
            tbody>
        table>
    div>
body>
html>

FreeMarkerController中新增以下方法:

@GetMapping("/built-ins/hashes")
public String builtInsHashes(Model model) {
    Map<String, Object> map = new HashMap<>(16);
    map.put("user1", new User(1L, "刘备", "123132"));
    map.put("user2", new User(2L, "关羽", "321231"));
    map.put("user3", new User(3L, "张飞", "213312"));
    map.put("user4", new User(4L, "赵云", "132213"));
    map.put("user5", new User(5L, "马超", "312123"));
    model.addAttribute("userMap", map);
    return "built-ins-hashes";
}

效果:
SpringBoot2.x 集成 FreeMarker_第29张图片

代码示例

  • Github:https://github.com/RtxTitanV/springboot-learning/tree/master/springboot2.x-learning/springboot-freemarker
  • Gitee:https://gitee.com/RtxTitanV/springboot-learning/tree/master/springboot2.x-learning/springboot-freemarker

你可能感兴趣的:(SpringBoot,spring,boot,freemarker,freemarker语法,freemarker指令,freemarker内建函数)