大家好!我是今越。简单记录一下在 Spring Boot 框架中如何整合 Freemarker 及使用。
FreeMarker 简介
FreeMarker 是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本( HTML 网页,电子邮件,配置文件,源代码等)的通用工具。它不是面向最终用户的,而是一个 Java 类库,是一款程序员可以嵌入他们所开发产品的组件。
模板编写语言为 FreeMarker Template Language,它是简单的,专用的语言,不是像 PHP 那样成熟的编程语言。那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算,之后模板显示已经准备好的数据在模板中,你可以专注于如何展现数据,而在模板之外可以专注于要展示什么数据。
这种方式通常被称为 MVC(模型 视图 控制器)模式,对于动态网页来说,是一种特别流行的模式。它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML 设计师)。设计师无需面对模板中的复杂逻辑,在没有程序员来修改或重新编译代码时,也可以修改页面的样式。而 FreeMarker 最初的设计,是被用来在 MVC 模式的 Web 开发框架中生成 HTML 页面的,它没有被绑定到 Servlet 或 HTML 或任意 Web 相关的东西上。它也可以用于非 Web 应用环境中。
FreeMarker 是免费的,基于 Apache 许可证 2.0 版本发布。
整合 Spring Boot
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
在 org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration
类中,可以看到自动化的配置:
@AutoConfiguration
@ConditionalOnClass({Configuration.class, FreeMarkerConfigurationFactory.class})
@EnableConfigurationProperties({FreeMarkerProperties.class})
@Import({FreeMarkerServletWebConfiguration.class, FreeMarkerReactiveWebConfiguration.class, FreeMarkerNonWebConfiguration.class})
public class FreeMarkerAutoConfiguration {}
从这里可以看出,当 classpath 下存在 Configuration
以及 FreeMarkerConfigurationFactory
时,配置才会生效,也就是说当我们引入了 Freemarker 依赖之后,配置就会生效。但是这里的自动化配置只做了模板位置检查,其他配置则是在导入的 FreeMarkerServletWebConfiguration
配置中完成的。那么我们再来看看 FreeMarkerServletWebConfiguration
类,部分源码如下:
package org.springframework.boot.autoconfigure.freemarker;
import javax.servlet.DispatcherType;
import javax.servlet.Servlet;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfig;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
@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;
}
}
我们来简单看下这段源码:
-
@ConditionalOnWebApplication 表示当前配置在 web 环境下才会生效。
-
ConditionalOnClass 表示当前配置在存在 Servlet 和 FreeMarkerConfigurer 时才会生效。
-
@AutoConfigureAfter 表示当前自动化配置在 WebMvcAutoConfiguration 之后完成。
-
代码中,主要提供了 FreeMarkerConfigurer 和 FreeMarkerViewResolver。
-
FreeMarkerConfigurer 是 Freemarker 的一些基本配置,例如 templateLoaderPath、defaultEncoding 等
-
FreeMarkerViewResolver 则是视图解析器的基本配置,包含了 viewClass、suffix、allowRequestOverride、allowSessionOverride 等属性。
另外还有一点,在这个类的构造方法中,注入了 FreeMarkerProperties:
@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;
}
FreeMarkerProperties 中则配置了 Freemarker 的基本信息,例如模板位置在 classpath:/templates/
,再例如模板后缀为 .ftlh
,那么这些配置我们以后都可以在 application.properties
中进行修改。
spring.freemarker.allow-request-override=false
spring.freemarker.allow-session-override=false
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=false
spring.freemarker.expose-session-attributes=false
spring.freemarker.suffix=.ftl
spring.freemarker.template-loader-path=classpath:/templates/
配置文件按照顺序依次解释如下:
- HttpServletRequest 的属性是否可以覆盖 controller 中 model 的同名项
- HttpSession 的属性是否可以覆盖 controller 中 model 的同名项
- 是否开启缓存
- 模板文件编码
- 是否检查模板位置
- Content-Type 的值
- 是否将 HttpServletRequest 中的属性添加到 Model 中
- 是否将 HttpSession 中的属性添加到 Model 中
- 模板文件后缀
- 模板文件位置
示例
创建类和接口
public class User {
private Long id;
private String username;
private String address;
// setter, getter
}
@Controller
public class UserController {
@GetMapping("/hello")
public String hello(Model model) {
List<User> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setId((long) i);
user.setUsername("jackson>>>" + i);
user.setAddress("Hangzhou>>>" + i);
list.add(user);
}
model.addAttribute("users", list);
return "hello";
}
}
在 hello.ftlh
页面中渲染数据
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<table border="1">
<tr>
<td>用户编号</td>
<td>用户名称</td>
<td>用户地址</td>
</tr>
<#list users as u>
<tr>
<td>${u.id}</td>
<td>${u.username}</td>
<td>${u.address}</td>
</tr>
</#list>
</table>
</body>
</html>
显示效果如下
FreeMarker 使用细节
插值与表达式
直接输出值
1)字符串
<div>${"hello,我是直接输出的字符串"}</div>
<div>${"我的文件保存在C:\\盘"}</div>
注意:\ 需要转义
在目标字符串的引号前增加 r 标记,在 r 标记后的文本内容将会直接输出,例如
<div>${r"我的文件保存在C:\\盘"}</div>
2)数字
在 FreeMarker 中使用数值需要注意
a.数值不能省略小数点前面的 0,所以 “.5” 是错误的写法;
b.数值 8 , +8 , 8.00 都是相同的;
数字的其它用法:
将数字以钱的形式,以百分数的形式展示
<#assign price=99>
<div>${price?string.currency}</div>
<div>${price?string.percent}</div>
3)布尔
布尔类型可以直接定义,不需要引号,例如
<#assign flag=true>
<div>${flag?string("yes","no")}</div>
<!--如果 flag 为 true,则输出 yes,否则输出 no-->
4)集合
集合也可以现场定义现场输出,例如
<#list [2+2,"anson","jackson"] as x>
<div>${x}</div>
</#list>
<#list 5..1 as x>
<div>${x}</div>
</#list>
<#list 1..5 as x>
<div>${x}</div>
</#list>
其中,x 代表集合中的每一个元素。
也可以定义 Map 集合,Map 集合用一个 {}
来描述,例如
<#assign userinfo={"name":"jackson","address":"hanghzou西湖"}>
<#list userinfo?keys as key>
<div>${key}--${userinfo[key]}</div>
</#list>
<hr/>
<#list userinfo?values as value>
<div>${value}</div>
</#list>
<hr/>
<div>${userinfo.name}</div>
<div>${userinfo['address']}</div>
上面两个循环分别表示遍历 Map 中的 key 和 value。
输出变量
@Controller
public class UserController {
@GetMapping("/hello")
public String hello(Model model) {
List<User> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
User user = new User();
user.setId((long) i);
user.setUsername("jackson>>>" + i);
user.setAddress("Hangzhou>>>" + i);
list.add(user);
}
model.addAttribute("users", list);
Map<String, Object> info = new HashMap<>();
info.put("name", "向往的今越");
info.put("age", 18);
info.put("address", "市中心");
model.addAttribute("info", info);
model.addAttribute("name", "shengsheng");
model.addAttribute("birthday", new Date());
return "hello";
}
}
1)普通变量
普通变量的展示,例如
<div>${name}</div>
2)集合
直接遍历:
<div>
<table border="1">
<#list users as u>
<tr>
<td>${u.id}</td>
<td>${u.username}</td>
<td>${u.address}</td>
</tr>
</#list>
</table>
</div>
输出集合中索引为 3 的元素:
<div>${users[3].address}</div>
输出子集合:
<div>
<table border="1">
<#list users[3..5] as u>
<tr>
<td>${u.id}</td>
<td>${u.username}</td>
<td>${u.address}</td>
<td>${u_index}</td>
<td>${u_has_next?string("yes","no")}</td>
</tr>
</#list>
</table>
</div>
遍历时,可以通过 变量名_index
获取遍历的下标,变量名_has_next
判断是否有后继元素。
3)Map
直接获取 Map 中的值有不同的写法,例如
<div>${info.name}</div>
<div>${info['age']}</div>
<div>${info['address']}</div>
获取 Map 中的所有 key,并根据 key 获取 value
<div>
<#list info?keys as key>
<div>${key}--${info[key]}</div>
</#list>
</div>
获取 Map 中的所有 value
<div>
<#list info?values as value>
<div>${value }</div>
</#list>
</div>
字符串操作
字符串的拼接有两种方式
<div>${"hello ${name}"}</div>
<div>${"hello " + name}</div>
也可以从字符串中截取子串
<div>${name[0]}${name[1]}</div>
<div>${name[1..3]}</div>
集合操作
集合相加
<div>
<#list [1,2,3] + [4,5,6] as x>
${x},
</#list>
</div>
Map 相加
<div>
<#list (info+{"weather":"sunny"})?keys as key>
${key},
</#list>
</div>
算术运算
+
、—
、*
、/
、%
运算都是支持的。
<div>
<#assign age=99>
<div>${age*99/99+99-1}</div>
</div>
比较运算
比较运算和 Thymeleaf 比较类似:
-
=
或者==
判断两个值是否相等 -
!=
判断两个值是否不等 -
>
或者gt
判断左边值是否大于右边值 -
>=
或者gte
判断左边值是否大于等于右边值 -
<
或者lt
判断左边值是否小于右边值 -
<=
或者lte
判断左边值是否小于等于右边值
<div>
<#assign age=99>
<#if age=99>age=99</#if><br/>
<#if age gt 99>age gt 99</#if><br/>
<#if (age > 99)>age > 99</#if><br/>
<#if age gte 99>age gte 99</#if><br/>
<#if age lt 99>age lt 99</#if><br/>
<#if age lte 99>age lte 99</#if><br/>
<#if age!=99>age!=99</#if><br/>
<#if age==99>age==99</#if><br/>
</div>
提示:带 <
或者 >
的符号,也都有别名,建议使用别名。
逻辑运算
逻辑运算符有三个:
- 逻辑与
&&
- 逻辑或
||
- 逻辑非
!
<div>
<#assign age=99>
<#if age=99 && 1==1>age=99 && 1==1</#if>
<#if age=99 || 1==0>age=99 || 1==0</#if>
<#if !(age gt 99)>!(age gt 99)</#if>
</div>
注意:逻辑运算符只能作用于布尔值,否则将产生错误。
空值处理
为了处理缺失变量,Freemarker 提供了两个运算符:
-
!
:指定缺失变量的默认值 -
??
:判断某个变量是否存在
如果某个变量不存在,则设置其为 jackson,例如
<div>${aaa!"jackson"}</div>
如果某个变量不存在,则设置其为空字符串,例如
<div>${aaa!}</div>
即 !
后面的东西如果省略了,默认就是空字符串。
判断某个变量是否存在,例如
<div><#if aaa??>aaa</#if></div>
内建函数
内建函数可参考官网文档:http://freemarker.foofun.cn/ref_builtins.html
<div>
<#--cap_first 使字符串第一个字母大写-->
<div>${"hello"?cap_first}</div>
<#--lower_case 将字符串转换成小写-->
<div>${"HELLO"?lower_case}</div>
<#--upper_case 将字符串转换成大写-->
<div>${"hello"?upper_case}</div>
<#--trim 去掉字符串前后的空白字符-->
<div>${" hello "?trim}</div>
<#--size 获取序列中元素的个数-->
<div>${users?size}</div>
<#--int 取得数字的整数部分,结果带符号-->
<div>${-3.14?int}</div>
<#--日期格式化-->
<div>${birthday?string("yyyy-MM-dd")}</div>
</div>
常用指令
if/else
分支控制指令,作用类似于 Java 语言中的 if
<div>
<#assign age=23>
<#if (age>60)>老年人
<#elseif (age>40)>中年人
<#elseif (age>20)>青年人
<#else> 少年人
</#if>
</div>
比较符号中用了 ()
,因此不用转义。
switch
分支指令,类似于 Java 中的 switch
<div>
<#assign age=99>
<#switch age>
<#case 23>23<#break>
<#case 24>24<#break>
<#default>9999
</#switch>
</div>
<#break>
是提前退出,也可以用在 <#list>
中。
noparse
如果想在页面展示一些 Freemarker 语法而不被渲染,则可以使用 noparse 标签,如下:
<#noparse>
<div>hhh</div>
<#noparse>
include
include 包含外部页面进来。
<#include "./test.ftlh">
macro
macro 用来定义一个宏。例如定义一个名为 book 的宏,并引用它:
<#macro book>
三国演义
</#macro>
<@book/>
最终页面中会输出宏中所定义的内容。
在定义宏的时候,也可以传入参数,那么引用时,也需要传入参数:
<#macro book bs>
<table border="1">
<#list bs as b>
<tr>
<td>${b}</td>
</tr>
</#list>
</table>
</#macro>
<@book ["三国演义","水浒传"]/>
bs 就是需要传入的参数。可以通过传入多个参数,多个参数跟在 bs 后面即可,中间用空格隔开。
还可以使用 <#nested>
引入用户自定义指令的标签体,像下面这样:
<#macro book bs>
<table border="1">
<#list bs as b>
<tr>
<td>${b}</td>
</tr>
</#list>
</table>
<#nested>
</#macro>
<@book ["三国演义","水浒传"]>
<h1>hello javaboy!</h1>
</@book>
在宏定义的时候,<#nested>
相当于是一个占位符,在调用的时候,<@book>
标签中的内容会出现在 <#nested>
位置。
前面的案例中,宏都是定义在当前页面中,宏也可以定义在一个专门的页面中。新建 mymarcro.ftlh
页面,内容如下:
<#macro book bs title>
<table>
<#list bs as b>
<tr>
<td>${b}</td>
</tr>
</#list>
</table>
<#nested>
</#macro>
此时,需要先通过 <#import>
标签导入宏,然后才能调用,如下:
<#import "./mymacro.ftlh" as com>
<@com.book bs=["三国演义", "水浒传"] title="hhh">
<h1>hello jackson!</h1>
</@com.book>
唯有热爱可抵岁月漫长。我是今越,欢迎大家点赞、收藏和评论,感谢支持!