写在前面

尽管在目前企业级的应用开发中,前后端分离是趋势,但是视图层技术还是占用一席之地。SpringBoot对视图层技术也提供了很好的支持,官方推荐使用的模板引擎是Thymeleaf,但是FreeMaker也支持,当然你可以像SSM中使用JSP等,但是非常不推荐使用。

FreeMarker

FreeMarker简介

FreeMarker是一个非常古老的Java模板引擎,可以用在Web或者非Web环境中。与Thymeleaf不同的是FreeMarker需要经过解析才能够在浏览器中展示出来。FreeMarker不仅可以用来配置HTML页面模板,也可以作为电子邮箱模板、配置文件模板以及源码模板等。正是由于它可以适应不同的应用场景,因此它虽然古老但是依旧还是有人愿意使用它。

下面是一张摘自FreeMarker官网的图片:

可以看到FreeMarker可以将模板(Template)和数据(Java Objects)进行渲染为HTML页面。

FreeMarker模板文件及存放位置

查看一下这个spring-boot-autoconfigure依赖的META-INF文件夹下面的spring.factories文件,里面有如下配置信息:

1
2
3
# Template availability providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\

然后进入查看一下这个类,源码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class FreeMarkerTemplateAvailabilityProvider extends PathBasedTemplateAvailabilityProvider {
public FreeMarkerTemplateAvailabilityProvider() {
super("freemarker.template.Configuration", FreeMarkerTemplateAvailabilityProvider.FreeMarkerTemplateAvailabilityProperties.class, "spring.freemarker");
}

protected static final class FreeMarkerTemplateAvailabilityProperties extends TemplateAvailabilityProperties {
private List<String> templateLoaderPath = new ArrayList(Arrays.asList("classpath:/templates/"));

FreeMarkerTemplateAvailabilityProperties() {
super("", ".ftlh");
}

protected List<String> getLoaderPath() {
return this.templateLoaderPath;
}

public List<String> getTemplateLoaderPath() {
return this.templateLoaderPath;
}

public void setTemplateLoaderPath(List<String> templateLoaderPath) {
this.templateLoaderPath = templateLoaderPath;
}
}
}

可以看到FreeMarker模板后缀为ftlh(FreeMarker Template Language)。FTL是一种简单专用的语言,不是编程语言,因此在模板中开发者只需专注于如何展现数据, 而数据的获取则在模板之外确定。FreeMarker模板文件的存放路径位于classpath:/templates/路径下,这一点需要引起注意。

小试牛刀

第一步,新建一个名为learn-freemarker的SpringBoot项目,然后在POM文件中新增FreeMarker和Web依赖:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

接着继续查看这个spring-boot-autoconfigure依赖的META-INF文件夹下面的spring.factories文件中关于FreeMarker的自动配置类FreeMarkerAutoConfiguration

1
2
3
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\

查看一下这个FreeMarkerAutoConfiguration自动配置类的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({freemarker.template.Configuration.class, FreeMarkerConfigurationFactory.class})
@EnableConfigurationProperties({FreeMarkerProperties.class})
@Import({FreeMarkerServletWebConfiguration.class, FreeMarkerReactiveWebConfiguration.class, FreeMarkerNonWebConfiguration.class})
public class FreeMarkerAutoConfiguration {
private static final Log logger = LogFactory.getLog(FreeMarkerAutoConfiguration.class);
private final ApplicationContext applicationContext;
private final FreeMarkerProperties properties;

//有参构造方法注入ApplicationContext、FreeMarkerProperties对象
public FreeMarkerAutoConfiguration(ApplicationContext applicationContext, FreeMarkerProperties properties) {
this.applicationContext = applicationContext;
this.properties = properties;
this.checkTemplateLocationExists();
}

//检查模板存放位置是否存在
public void checkTemplateLocationExists() {
if (logger.isWarnEnabled() && this.properties.isCheckTemplateLocation()) {
List<TemplateLocation> locations = this.getLocations();
if (locations.stream().noneMatch(this::locationExists)) {
logger.warn("Cannot find template location(s): " + locations + " (please add some templates, check your FreeMarker configuration, or set spring.freemarker.checkTemplateLocation=false)");
}
}

}

//获取模板的位置
private List<TemplateLocation> getLocations() {
List<TemplateLocation> locations = new ArrayList();
String[] var2 = this.properties.getTemplateLoaderPath();
int var3 = var2.length;

for(int var4 = 0; var4 < var3; ++var4) {
String templateLoaderPath = var2[var4];
TemplateLocation location = new TemplateLocation(templateLoaderPath);
locations.add(location);
}

return locations;
}

//是否存在模板位置
private boolean locationExists(TemplateLocation location) {
return location.exists(this.applicationContext);
}
}

从里面可以看出,当classpath下面存在freemarker.template.ConfigurationFreeMarkerConfigurationFactory类时,这个自动配置类才会生效,实际上这两个类在我们引入freemarker场景启动器时就已经存在了。属性POJO类则是FreeMarkerProperties,到时候开发者需要重写配置项的时候可以从该类中进行查阅。

同时还导入了FreeMarkerServletWebConfigurationFreeMarkerReactiveWebConfigurationFreeMarkerNonWebConfiguration这三个类,由于我们使用的是普通的Web项目,因此真正用到的还是FreeMarkerServletWebConfiguration这个类。查看一下这个类的源码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, FreeMarkerConfigurer.class})
@AutoConfigureAfter({WebMvcAutoConfiguration.class})
class FreeMarkerServletWebConfiguration extends AbstractFreeMarkerConfiguration {
protected FreeMarkerServletWebConfiguration(FreeMarkerProperties properties) {
super(properties);
}

@Bean
@ConditionalOnMissingBean({FreeMarkerConfig.class})
FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
this.applyProperties(configurer);
return configurer;
}

@Bean
freemarker.template.Configuration freeMarkerConfiguration(FreeMarkerConfig configurer) {
return configurer.getConfiguration();
}

@Bean
@ConditionalOnMissingBean(
name = {"freeMarkerViewResolver"}
)
@ConditionalOnProperty(
name = {"spring.freemarker.enabled"},
matchIfMissing = true
)
FreeMarkerViewResolver freeMarkerViewResolver() {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
this.getProperties().applyToMvcViewResolver(resolver);
return resolver;
}

@Bean
@ConditionalOnEnabledResourceChain
@ConditionalOnMissingFilterBean({ResourceUrlEncodingFilter.class})
FilterRegistrationBean<ResourceUrlEncodingFilter> resourceUrlEncodingFilter() {
FilterRegistrationBean<ResourceUrlEncodingFilter> registration = new FilterRegistrationBean(new ResourceUrlEncodingFilter(), new ServletRegistrationBean[0]);
registration.setDispatcherTypes(DispatcherType.REQUEST, new DispatcherType[]{DispatcherType.ERROR});
return registration;
}
}

简单解释一下上述代码的含义:
(1)@Configuration注解表示当前类为一个配置类,可以被Spring扫描的到;
(2)@ConditionalOnWebApplication表示当前配置类在Web环境下才生效;
(3)@ConditionalOnClass表示当前环境中只有存在Servlet和FreeMarkerConfigurer时才会生效;
(4)@AutoConfigureAfter({WebMvcAutoConfiguration.class})表示当前自动化配置需要在WebMvcAutoConfiguration之后才会完成;
(5)之后这个FreeMarkerServletWebConfiguration提供了一个有参的构造方法,里面注入了FreeMarkerProperties所对应的属性POJO类;
(6)当我们没有提供FreeMarkerConfig实例时,系统会自动提供一个FreeMarkerConfigurer实例。FreeMarkerConfigurer是一个类,实现了FreeMarkerConfig接口,里面定义了Freemarker的基本配置;
(7)FreeMarkerViewResolver则是FreeMarker视图解析器,上面使用了@ConditionalOnMissingBean@ConditionalOnProperty注解表示只有当前环境中缺失FreeMarkerViewResolver对象和spring.freemarker.enabled属性值为true,系统会自动提供一个FreeMarkerViewResolver实例。

接下来我们再来看一下这个FreeMarkerProperties属性POJO类的源码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@ConfigurationProperties(
prefix = "spring.freemarker"
)
public class FreeMarkerProperties extends AbstractTemplateViewResolverProperties {
public static final String DEFAULT_TEMPLATE_LOADER_PATH = "classpath:/templates/";
public static final String DEFAULT_PREFIX = "";
public static final String DEFAULT_SUFFIX = ".ftlh";
private Map<String, String> settings = new HashMap();
private String[] templateLoaderPath = new String[]{"classpath:/templates/"};
private boolean preferFileSystemAccess;

public FreeMarkerProperties() {
super("", ".ftlh");
}

public Map<String, String> getSettings() {
return this.settings;
}

public void setSettings(Map<String, String> settings) {
this.settings = settings;
}

public String[] getTemplateLoaderPath() {
return this.templateLoaderPath;
}

public boolean isPreferFileSystemAccess() {
return this.preferFileSystemAccess;
}

public void setPreferFileSystemAccess(boolean preferFileSystemAccess) {
this.preferFileSystemAccess = preferFileSystemAccess;
}

public void setTemplateLoaderPath(String... templateLoaderPaths) {
this.templateLoaderPath = templateLoaderPaths;
}
}

可以看到这个FreeMarkerProperties属性POJO类中配置了FreeMarker的一些基本信息,如模板默认存放地址为classpath:/templates/,模板后缀为.ftlh,这些后续开发者可以在项目配置文件中进行覆盖。

第二步,出于简单考虑,这里就不使用数据库了,直接定义一个实体类Book:

1
2
3
4
5
6
public class Book {
private int id;
private String name;
private int price;
//getter和setter方法,无参和全参构造方法
}

第三步,新建BookController类,提供一个访问数据的接口/books,注意它返回的是视图,因此不能使用@RestController注解,必须使用@Controller注解。

ModelAndView是模型数据和逻辑视图对象,它装载了模型的数据和逻辑视图,你可以通过modelAndView.addObject("key",ojbect);方式来添加模型数据,然后使用modelAndView.setViewName("视图名称");方式来设置逻辑视图。BookController类里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Controller
public class BookController {
@GetMapping(value = "/books")
public ModelAndView books(){
List<Book> books = Arrays.asList(
new Book(1, "三国演义", 168),
new Book(2, "红楼梦", 188),
new Book(2, "西游记", 128),
new Book(2, "水浒传", 108)
);
//实例化模型数据和逻辑视图对象
ModelAndView modelAndView = new ModelAndView();
//添加模型数据
modelAndView.addObject("books",books);
//设置逻辑视图
modelAndView.setViewName("index");
return modelAndView;
}
}

第四步,在resources目录下的templates文件夹中新建index.ftlh(请注意这里的index就是你在modelAndView.setViewName("index")中设置的逻辑视图名称,这个需要保持一致),里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>图书列表</title>
</head>
<body>
<h2>图书列表</h2>
<table border="1px">
<tr>
<td>图书编号</td>
<td>图书名称</td>
<td>图书价格</td>
</tr>
<#if books ?? && (books?size>0) >
<#list books as book>
<tr>
<td>${book.id}</td>
<td>${book.name}</td>
<td>${book.price}</td>
</tr>
</#list>
</#if>
</table>
</body>
</html>

请注意首先你需要判断model中的books不为空且books中有数据,然后才能进行遍历,接着通过遍历books集合,将集合中的数据通过表格展示出来。

第五步,启动项目。在浏览器地址栏中输入http://localhost:8080/books即可看到运行结果,如下图所示:

自定义属性

如果开发者需要覆盖FreeMarkerProperties属性POJO类中的默认值,那么可以在aplication.properties文件中进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# HttpServletRequest的属性是否可以覆盖Controller中的model的同名项
spring.freemarker.allow-request-override=false
# HttpSession的属性是否可以覆盖Controller中的model的同名项
spring.freemarker.allow-session-override=false
# 是否开启缓存,开发时可设置为false,默认为true
spring.freemarker.cache=false
# 检查模板位置是否存在,默认为true
spring.freemarker.check-template-location=true
# 模板文件编码
spring.freemarker.charset=UTF-8
# 模板文件位置
spring.freemarker.prefix=classpath:/templates/
# Content-Type配置
spring.freemarker.content-type=text/html
# 模板文件后缀
spring.freemarker.suffix=.ftlh
# 是否将HttpServletRequest中的属性添加到Model中
spring.freemarker.expose-request-attributes=false
# 是否将HttpSession中的属性添加到Model中
spring.freemarker.expose-session-attributes=false

FreeMarker详细使用

插值与表达式

直接输出值

(1)数字。在FreeMarker中使用数字有两个注意事项:(a)小数点之前的0不能省略,即0.1不能写成.1;(b)对于1、+1、和1.0而言,它们是一致的。

查看一下如下的ftlh语法:

1
2
<#assign num=11>
<div>${num}</div>

<#assign num=11>表示定义一个名为num的变量,值为11,然后通过<div>${num}</div>将其进行输出:

如果开发者需要输出人民币,可以使用如下语法:

1
2
<#assign num=11>
<div>${num?string.currency}</div>

可以看到数字前面就多出了一个人民币符号:

如果需要展示百分数,可以使用如下语法:

1
2
<#assign num=0.66>
<div>${num?string.percent}</div>

可以看到数字就以百分数形式进行展示了:

(2)字符串。在FreeMarker中可以使用如下方式直接输出字符串:

1
<div>${"啃饼思录"}</div>

可以看到文字就直接显示了:

如果要输出一些包含特殊符号的字符串,如文件磁盘D:\测试,那么需要使用\进行转义:

1
<div>${"文件磁盘D:\\测试"}</div>

这样上述字符串才可以被显示:

不过转义这种太麻烦了,尤其是字符串中包含很多需要转义的字符,此时可以在目标字符串引号之前添加r字符,表示自然转义,那么该目标字符串就会被直接输出:

1
<div>${r"文件磁盘D:\测试"}</div>

这样上述字符串也是可以被显示的:

(3)布尔值。在FreeMarker中布尔值可以直接定义,不需要使用引号引起来:

1
2
<#assign flag=false>
<div>${flag?string("A","B")}</div>

<#assign flag=false>表示定义一个名为flag的变量,值为false,然后通过<div>${flag?string("A","B")}</div>将其进行输出,它判断flag值是否为true,如果是则输出A,否则输出B:

(4)集合。在FreeMarker中开发者可以直接定义一个集合,然后遍历该集合进行输出:

1
2
3
<#list ["春天", "夏天", "秋天", "冬天"] as season>
<p>${season}</p>
</#list>

season表示其中的每个季节,遍历输出结果如下所示:

当然了,集合中的元素也可以是一个表达式,可以是字符串,不要求这些元素的类型是一致的,如下面的例子:

1
2
3
<#list [1+1,8-1,2*4,6/3,"kenbingthoughts"] as v>
<p>${v}</p>
</#list>

此时的遍历输出结果如下所示:

如果你需要输出某些特殊的序列,如1-6这个序列,那么只需使用1..6来表示,而6..1则表示从6输出到1这个序列:

1
2
3
4
5
6
7
8
9
<p>顺序</p>
<#list 1..6 as v1>
<text>${v1}</text>
</#list>

<p>倒序</p>
<#list 6..1 as v2>
<text>${v2}</text>
</#list>

此时页面输出结果如下所示:

当然了这个集合中也是可以表示Map对象,Map对象是使用大括号括起来的,就像下面的:

1
2
3
4
5
6
7
8
9
<p>所有的key</p>
<#list {"name": "kenbingthoughts","nickName": "kenbing"}?keys as k>
<text>${k}</text>
</#list>

<p>所有的value</p>
<#list {"name": "kenbingthoughts","nickName": "kenbing"}?values as v>
<text>${v}</text>
</#list>

上面这两种语法分别表示遍历Map对象的所有key和value,页面输出结果如下所示:

输出变量

首先我们创建一个接口,然后准备一些数据,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@GetMapping(value = "/test")
public ModelAndView test(){
List<Book> books = Arrays.asList(
new Book(1, "三国演义", 168),
new Book(2, "红楼梦", 188),
new Book(2, "西游记", 128),
new Book(2, "水浒传", 108)
);
Map<String,String> userMap = new HashMap<>();
userMap.put("name","kenbingthoughts");
userMap.put("nickName","kenbing");
userMap.put("website","kenbingthoughts.top");
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("books",books);
modelAndView.addObject("userMap",userMap);
modelAndView.addObject("address","上海");
modelAndView.setViewName("index");
return modelAndView;
}

(1)普通变量。普通变量的展示很简单,直接通过变量名称就可以展示:

1
<div>${address}</div>

页面输出结果如下所示:

(2)集合变量。集合的展示有多种方式,如下所示,输出书籍信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<table border="1px">
<tr>
<td>图书编号</td>
<td>图书名称</td>
<td>图书价格</td>
</tr>
<#if books ?? && (books?size>0) >
<#list books as book>
<tr>
<td>${book.id}</td>
<td>${book.name}</td>
<td>${book.price}</td>
</tr>
</#list>
</#if>
</table>

页面输出结果如下所示:

如果你需要输出第三本书的名称(下标从0开始),只需将代码修改为如下所示:

1
2
3
<div>
${books[2].name}
</div>

页面输出结果如下所示:

如果要输出第二和三本书的名称,即子集合,此时只需将代码修改为如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<table border="1px">
<tr>
<td>图书编号</td>
<td>图书名称</td>
<td>图书价格</td>
</tr>
<#if books ?? && (books?size>0) >
<#list books[1..2] as book>
<tr>
<td>${book.id}</td>
<td>${book.name}</td>
<td>${book.price}</td>
</tr>
</#list>
</#if>
</table>

页面输出结果如下所示:

如果开发者想要输出当前变量的下标,此时只需使用变量名称_index就可以输出下标信息,此时将代码修改为如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<table border="1px">
<tr>
<td>图书下标</td>
<td>图书编号</td>
<td>图书名称</td>
<td>图书价格</td>
</tr>
<#if books ?? && (books?size>0) >
<#list books[1..2] as book>
<tr>
<td>${book_index}</td>
<td>${book.id}</td>
<td>${book.name}</td>
<td>${book.price}</td>
</tr>
</#list>
</#if>
</table>

页面输出结果如下所示:

(3)Map变量。如果开发者想直接输出Map中的值,可以使用如下方式:

1
2
3
4
<div>
<div>${userMap.name}</div>
<div>${userMap['name']}</div>
</div>

页面输出结果如下所示:

如果要获取Map中的所有key并输出对应value的值,可以使用如下方式:

1
2
3
4
5
<div>
<#list userMap?keys as k>
<div>${k}--${userMap[k]}</div>
</#list>
</div>

页面输出结果如下所示:

如果只是单纯的想输出所有value的值,可以使用如下方式:

1
2
3
4
5
<div>
<#list userMap?values as v>
<div>${v}</div>
</#list>
</div>

页面输出结果如下所示:

(4)字符串拼接。如果开发者想对字符串进行拼接,可以使用如下两种方式:

1
2
3
4
<div>
<div>${"My name is ${userMap.name}"}</div>
<div>${"My name is "+ userMap.name}</div>
</div>

页面输出结果如下所示:

(5)字符串截取。如果开发者想对字符串进行截取,可以使用如下方式:

1
2
3
<div>
<div>${userMap.name[0..6]}</div>
</div>

页面输出结果如下所示:

(6)集合相加。如果开发者想对集合相加,可以使用如下方式:

1
2
3
4
5
<div>
<#list ["春天", "夏天", "秋天", "冬天"] + ["昨天","今天","明天"] as time>
<p>${time}</p>
</#list>
</div>

页面输出结果如下所示:

(7)Map相加。如果开发者想对Map相加,可以使用如下方式:

1
2
3
4
5
<div>
<#list (userMap +{'sex': 'male'})?keys as key>
<p>${key}</p>
</#list>
</div>

页面输出结果如下所示:

算术运算符

FreeMarker支持一些常用的+-*/%等运算,举个例子:

1
2
3
4
<div>
<#assign num=1>
<div>${num*2/2%2+2-2}</div>
</div>

页面输出结果如下所示:

比较运算符

FreeMarker支持一些常用的比较运算操作,举个例子:
(1)= 或者 == 用于判断两个值是否相等;
(2)!=用于 判断两个值是否不等;
(3)> 或者 gt 用于判断左边值是否大于右边值;
(4)>= 或者 gte用于 判断左边值是否大于等于右边值;
(5)< 或者 lt 用于判断左边值是否小于右边值;
(6)<= 或者 lte 用于判断左边值是否小于等于右边值。
建议使用后面的英文符号,不要使用比较号,这样能避免带来一些不必要的错误。一些例子如下所示:

1
2
3
4
5
6
7
8
9
10
<div>
<#assign num=1>
<#if num=1> num=1</#if>
<#if num==1> num==1</#if>
<#if num!=1> num!=1</#if>
<#if num gt 1> num gt 1</#if>
<#if num gte 1> num gte 1</#if>
<#if num lt 1> num lt 1</#if>
<#if num lte 1> num lte 1</#if>
</div>

页面输出结果如下所示:

逻辑运算符

FreeMarker中有三个逻辑运算符,分别是与(&&)或(||)非(),注意逻辑运算符只能作用于布尔值,其他类型无法使用,这个很好理解:

1
2
3
4
5
6
7
8
9
10
11
12
<div>
<#assign num=1>
<div>
<#if num=1 && 1 gt 2> num=1 && 1 gt 2 </#if>
</div>
<div>
<#if num=1 || 1 gt 2> num=1 || 1 gt 2</#if>
</div>
<div>
<#if !(num=1)> !(num=1)</#if>
</div>
</div>

页面输出结果如下所示:

空值处理

为了处理变量的空值,FreeMarker提供了两个运算符!??,其中!用于指定缺失变量的默认值;??则用于判断某个变量是否存在。

举个例子,判断hobby变量是否存在,如果不存在则设置值为reading:

1
2
3
<div>
${hobby!'reading'}
</div>

页面输出结果如下所示:

如果希望hobby变量不存在,则设置值为空字符串,那么可以有如下两种方式:

1
2
3
4
<div>
${hobby!}
${hobby!""}
</div>

也就是说如果!后面没有设置值,那么默认值就是空字符串。关于FreeMarker的更多资料可以点击 这里,或者 国内翻译站

内建函数

由于FreeMarker内置的函数有多个,因此这里只列举几个比较常用的进行学习,具体的可以点击 国内翻译站 进行阅读。

cap_first

cap_first可以让字符串的第一个字母变成大写,举个例子:

1
2
3
<div>
${"kenbing"?cap_first}
</div>

页面输出结果如下所示:

lower_case

lower_case可以让字符串转变成小写,举个例子:

1
2
3
<div>
${"KENBING"?lower_case}
</div>

页面输出结果如下所示:

upper_case

upper_case可以让字符串转变成大写,举个例子:

1
2
3
<div>
${"kenbing"?upper_case}
</div>

页面输出结果如下所示:

日期格式化

FreeMarker,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div>
<#assign someDate = "10/25/1995"?date("MM/dd/yyyy")>
<#assign someTime = "15:05:30"?time("HH:mm:ss")>
<#assign someDatetime = "1995-10-25 03:05 PM"?datetime("yyyy-MM-dd hh:mm")>
<div>
${someDate}
</div>
<div>
${someTime}
</div>
<div>
${someDatetime}
</div>
</div>

页面输出结果如下所示:

trim

trim可以去掉字符串前后的空白字符,举个例子:

1
2
3
4
<div>
<#assign str=" kenbing ">
<div>${str?trim}</div>
</div>

页面输出结果如下所示:

int

int可以获取数值的整数部分,举个例子:

1
<div>${3.1415926?int}</div>

页面输出结果如下所示:

size

size可以获取序列中元素的个数,举个例子:

1
<div>${userMap?size}</div>

页面输出结果如下所示:

常用指令

if, else, elseif

这是分支控制语句,格式如下:

1
2
3
4
5
6
7
8
9
10
<#if condition>
...
<#elseif condition2>
...
<#elseif condition3>
...
...
<#else>
...
</#if>

注意这其中的condition和condition2都将被计算成布尔值的表达式,同时elseif 和else是可选的。详细使用细节可以参考 这里

assign

开发者可以使用该指令来创建一个新变量或者替换一个已存在的变量。注意仅仅顶级变量可以被创建/替换 (也就是说你不能创建/替换some_hash.subvar, 除了some_hash),格式如下:

1
2
3
4
5
6
7
8
9
10
11
<#assign name1=value1 name2=value2 ... nameN=valueN>

<#assign same as above... in namespacehash>

<#assign name>
capture this
</#assign>

<#assign name in namespacehash>
capture this
</#assign>

关于变量的更多内容,请阅读:模板开发指南/其它/在模板中定义变量

import

该指令用于引入一个库,即创建一个新的空命名空间, 然后在该命名空间中执行给定 path 参数中的模板, 所以模板用变量(宏,函数等)填充命名空间,之后新创建的命名空间对哈希表的调用者可用。 这个哈希表变量将会在命名空间中,由 import (就像你可以用 assign 指令来创建一样。) 的调用者被创建成一个普通变量,名字就是hash参数给定的:

1
<#import path as hash>
switch, case, default, break

switch, case, default, break是分支指令,有点类似于Java中的switch,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<#switch value>
<#case refValue1>
...
<#break>
<#case refValue2>
...
<#break>
...
<#case refValueN>
...
<#break>
<#default>
...
</#switch>

注意这里的break和default是可选的,不过笔者不太推荐使用这个指令,因为向下通过的行为容易出错,开发者可以使用 elseif来代替, 除非想利用向下通过这种行为。

include

include可以包含一个外部页面,格式如下:

1
2
3
<#include path>

<#include path options>

举个例子,如下所示:

1
<#include "./kenbing.ftlh">
macro, nested, return

macro指令用于定义一个宏,由于宏在实际工作中用的比较多,所以这里建议还是看 官方文档 最为合适。举个例子,这里我们定义一个名为book的宏,然后引用它:

1
2
3
4
5
6
<div>
<#macro book>
西游记
</#macro>
<div><@book/></div>
</div>

页面输出结果如下所示:

当然了,在定义宏的时候也可以传入参数,那么此时在引入宏的时候,也需要传入对应的参数:

1
2
3
4
5
6
7
8
9
10
11
12
<div>
<#macro books bs>
<table>
<#list bs as b>
<tr>
<td>${b}</td>
</tr>
</#list>
</table>
</#macro>
<div><@books ["西游记","三国演义","水浒传","红楼梦"]/></div>
</div>

这里的bs其实就是定义的名为books的宏的参数,可以传入多个参数,所以我们在使用这个宏的时候通过集合也传入了参数。页面输出结果如下所示:

除此之外,开发者还可以使用nested指令还引入开发者自定义的标签体,这个有点类似于Vue中的slot插槽这个概念,即只是起到一个占位的作用。举个例子,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div>
<#macro books bs>
<table>
<#list bs as b>
<tr>
<td>${b}</td>
</tr>
</#list>
<#nested>
</table>
</#macro>
<@books ["西游记","三国演义","水浒传","红楼梦"]>
<p>啃饼思录</p>
</@books>
</div>

页面输出结果如下所示:

可以看到上面我们添加的p标签的内容就取代了books这一宏中的<#nested>标签所占据的位置。

在实际开发过程中,宏一般都是单独定义的,然后在使用的地方进行导入,定义一个名为kenbing.ftlh的文件,在里面定义一个名为books的宏:

1
2
3
4
5
6
7
8
9
10
11
12
<div>
<#macro books bs>
<table>
<#list bs as b>
<tr>
<td>${b}</td>
</tr>
</#list>
<#nested>
</table>
</#macro>
</div>

之后在index.ftlh文件中导入这个kenbing.ftlh文件并使用这个名为books的宏:

1
2
3
4
5
6
<div>
<#import "./kenbing.ftlh" as kb>
<@kb.books ["西游记","三国演义","水浒传","红楼梦"]>
<p>啃饼思录</p>
</@kb.books>
</div>

注意宏需要先导入才能使用,否则会报找不到文件的错误。页面输出结果如下所示:

noparse

noparse指令顾名思义就是不解析,因此如果开发者想直接在HTML页面上展示Freemarker语法,而不被页面渲染,那么就可以将Freemarker语法使用noparse指令包括起来:

1
2
3
4
5
6
7
8
<div>
<#noparse>
<#import "./kenbing.ftlh" as kb>
<@kb.books ["西游记","三国演义","水浒传","红楼梦"]>
<p>啃饼思录</p>
</@kb.books>
</#noparse>
</div>

页面输出结果如下所示:

function, return

Freemarker中也允许开发者自定义函数,且函数是可以有返回值的,格式如下所示:

1
2
3
4
5
<#function name param1 param2 ... paramN>
...
<#return returnValue>
...
</#function>

举个例子,下面的函数用于计算两个数值的平均值,如下所示:

1
2
3
4
5
6
7
8
<div>
<#function avg x y>
<#return (x+y)/2>
</#function>
<div>
${avg(12,18)}
</div>
</div>

页面输出结果如下所示:

总结

本篇对FreeMarker模板引擎有了一个较为清晰的介绍,了解和学习这些基本上能满足日常的开发需要,如果开发者需要实现更具定制化的功能,需要点击 这里 ,阅读文档进行深度学习。