Spring Boot 整合视图层技术 FreeMarker

大家好!我是今越。简单记录一下在 Spring Boot 框架中如何整合 Freemarker 及使用。

FreeMarker 简介

FreeMarker 是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本( HTML 网页,电子邮件,配置文件,源代码等)的通用工具。它不是面向最终用户的,而是一个 Java 类库,是一款程序员可以嵌入他们所开发产品的组件。

模板编写语言为 FreeMarker Template Language,它是简单的,专用的语言,不是像 PHP 那样成熟的编程语言。那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算,之后模板显示已经准备好的数据在模板中,你可以专注于如何展现数据,而在模板之外可以专注于要展示什么数据。

FreeMarker官网插图

这种方式通常被称为 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/

配置文件按照顺序依次解释如下:

  1. HttpServletRequest 的属性是否可以覆盖 controller 中 model 的同名项
  2. HttpSession 的属性是否可以覆盖 controller 中 model 的同名项
  3. 是否开启缓存
  4. 模板文件编码
  5. 是否检查模板位置
  6. Content-Type 的值
  7. 是否将 HttpServletRequest 中的属性添加到 Model 中
  8. 是否将 HttpSession 中的属性添加到 Model 中
  9. 模板文件后缀
  10. 模板文件位置

示例

创建类和接口

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官网插图

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>

唯有热爱可抵岁月漫长。我是今越,欢迎大家点赞、收藏和评论,感谢支持!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值