Spring Boot 整合视图层技术 Thymeleaf

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

Thymeleaf 简介

Thymeleaf 是新一代 Java 模板引擎,它类似于 Velocity、FreeMarker 等传统 Java 模板引擎,但是与传统 Java 模板引擎不同的是,Thymeleaf 支持 HTML 原型。它既可以让前端工程师在浏览器中直接打开查看样式,也可以让后端工程师结合真实数据查看显示效果,同时,SpringBoot 提供了 Thymeleaf 自动化配置解决方案,因此在 SpringBoot 中使用 Thymeleaf 非常方便。

事实上, Thymeleaf 除了展示基本的 HTML ,进行页面渲染之外,也可以作为一个 HTML 片段进行渲染,例如我们在做邮件发送时,可以使用 Thymeleaf 作为邮件发送模板。另外,由于 Thymeleaf 模板后缀为 .html,可以直接被浏览器打开,因此,预览时非常方便。

整合 Spring Boot

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Thymeleaf 不仅仅能在 Spring Boot 中使用,也可以使用在其他地方,只不过 Spring Boot 针对 Thymeleaf 提供了一整套的自动化配置方案,这一套配置类的属性在 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties 中,部分源码如下:

@ConfigurationProperties(
    prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING;
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";
    private boolean checkTemplate = true;
    private boolean checkTemplateLocation = true;
    private String prefix = "classpath:/templates/";
    private String suffix = ".html";
    private String mode = "HTML";
    private Charset encoding;
    private boolean cache;
    private Integer templateResolverOrder;
    private String[] viewNames;
    private String[] excludedViewNames;
    private boolean enableSpringElCompiler;
    private boolean renderHiddenMarkersBeforeCheckboxes;
    private boolean enabled;
    private final ThymeleafProperties.Servlet servlet;
    private final ThymeleafProperties.Reactive reactive;

    public ThymeleafProperties() {
        this.encoding = DEFAULT_ENCODING;
        this.cache = true;
        this.renderHiddenMarkersBeforeCheckboxes = false;
        this.enabled = true;
        this.servlet = new ThymeleafProperties.Servlet();
        this.reactive = new ThymeleafProperties.Reactive();
    }
    // ...
}

1)首先通过 @ConfigurationProperties 注解,将 application.properties 前缀为 spring.thymeleaf 的配置和这个类中的属性绑定。这些配置,如果开发者不自己提供,则使用默认的。

2)前三个 static 变量定义了默认的编码格式、视图解析器的前缀、后缀等。

3)从前三行配置中,可以看出来,Thymeleaf 模板的默认位置在 resources/templates 目录下,默认的后缀是 html

而我们刚刚提到的,Spring Boot 为 Thymeleaf 提供的自动化配置类,则是 org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration ,部分源码如下:

@AutoConfiguration(
    after = {WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class}
)
@EnableConfigurationProperties({ThymeleafProperties.class})
@ConditionalOnClass({TemplateMode.class, SpringTemplateEngine.class})
@Import({ReactiveTemplateEngineConfiguration.class, DefaultTemplateEngineConfiguration.class})
public class ThymeleafAutoConfiguration {}

可以看到,在这个自动化配置类中,首先导入 ThymeleafProperties ,然后 @ConditionalOnClass 注解表示当在系统中存在 TemplateModeSpringTemplateEngine 类时,当前的自动化配置类才会生效,即只要项目中引入了 Thymeleaf 相关的依赖,这个配置就会生效。

这些默认的配置我们几乎不需要做任何更改就可以直接使用了。如果开发者有特殊需求,则可以在 application.properties 中配置以 spring.thymeleaf 开头的属性即可。

示例-基本用法

创建类和接口

public class User {
    private Integer id;
    private String name;
    private String address;
    // setter, getter
}
@Controller
public class UserController {
    @GetMapping("/index")
    public String index(Model model, HttpSession session) {
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setId(i);
            user.setName("jackson - " + i);
            user.setAddress("Hangzhou - " + i);
            userList.add(user);
        }
        model.addAttribute("users", userList);
        return "index";
    }
}

IndexController 中返回逻辑视图名+数据,逻辑视图名为 index ,意思我们需要在 resources/templates 目录下提供一个名为 index.html 的 Thymeleaf 模板文件。

index.html 页面中渲染数据

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<table border="1">
    <tr>
        <td>编号</td>
        <td>姓名</td>
        <td>地址</td>
    </tr>
    <tr th:each="user : ${users}">
        <td th:text="${user.id}"></td>
        <td th:text="${user.name}"></td>
        <td th:text="${user.address}"></td>
    </tr>
</table>
</body>
</html>

在 Thymeleaf 中,通过 th:each 指令来遍历一个集合,数据的展示通过 th:text 指令来实现。

注意 index.html 最上面引入 thymeleaf 名称空间(最新版并无强制要求)

显示效果如下

示例-手动渲染

上面我们说的是返回一个 Thymeleaf 模板,我们也可以手动渲染 Thymeleaf 模板,这个一般在邮件发送时候有用,例如我在 resources/templates 目录下新建一个邮件模板 mail.html,如下:

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <p>hello 欢迎 <span th:text="${username}"></span>加入 XXX 集团,您的入职信息如下:</p>
    <table border="1">
        <tr>
            <td>职位</td>
            <td th:text="${position}"></td>
        </tr>
        <tr>
            <td>薪水</td>
            <td th:text="${salary}"></td>
        </tr>
    </table>
</body>
</html>

这一个 HTML 模板中,有几个变量,我们要将这个 HTML 模板渲染成一个 String 字符串,再把这个字符串通过邮件发送出去,那么如何手动渲染呢?

@SpringBootTest
class ThymeleafApplicationTests {
    @Autowired
    TemplateEngine templateEngine;
    @Test
    void contextLoads() {
        Context context = new Context();
        context.setVariable("username", "jackson");
        context.setVariable("position", "后端开发");
        context.setVariable("salary", "35000");
        String mail = templateEngine.process("mail", context);
        System.out.println(mail);
        // 省略邮件发送
    }
}

1、渲染时,我们需要首先注入一个 TemplateEngine 对象,这个对象就是在 Thymeleaf 的自动化配置类中配置的(即当我们引入 Thymeleaf 的依赖之后,这个实例就有了)。

2、然后,构造一个 Context 对象用来存放变量。

3、调用 process 方法进行渲染,该方法的返回值就是渲染后的 HTML 字符串,然后我们将这个字符串发送出去。

Thymeleaf 使用细节

标准表达式

定义接口

@Controller
public class UserController {
    @GetMapping("/index")
    public String index(Model model, HttpSession session) {
        session.setAttribute("name", "张三丰");
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setId(i);
            user.setName("jackson - " + i);
            user.setAddress("Hangzhou - " + i);
            userList.add(user);
        }
        model.addAttribute("users", userList);
        User instance = new User();
        instance.setId(99);
        instance.setName("向往的今越");
        instance.setAddress("杭州");
        model.addAttribute("user", instance);
        return "index";
    }
}

简单表达式

1)${…}

直接使用 th:xx = "${}" 获取对象属性。

<td th:text="${user.id}"></td>
<td th:text="${user.name}"></td>
<td th:text="${user.address}"></td>
2)*{…}

可以像 ${...} 一样使用,也可以通过 th:object 获取对象,然后使用 th:xx = "*{}" 获取对象属性,这种简写风格极为清爽,推荐大家在实际项目中使用。

<td th:text="*{user.id}"></td>
<td th:text="*{user.name}"></td>
<td th:text="*{user.address}"></td>
<hr/>
<div th:object="${user}">
    <div th:text="*{address}"></div>
    <div th:text="*{name}"></div>
    <div th:text="*{id}"></div>
</div>
3)#{…}

通常的国际化属性:#{...} 用于获取国际化语言翻译值。

在 resources 目录下新建两个文件:messages.propertiesmessages_zh_CN.properties,内容如下:

messages.properties

message = hello jackson!

messages_zh_CN.properties

message = 你好,少年!

然后在 thymeleaf 中引用 message,系统会根据浏览器的语言环境显示不同的值:

<div th:text="#{message}"></div>
4)@{…}

引用绝对 URL

<script type="text/javascript" th:src="@{http://localhost:8080/hello.js}"></script>

等价于

<script type="text/javascript" src="http://localhost:8080/hello.js"></script>

上下文相关的 URL

首先在 application.properties 中配置 Spring Boot 的上下文,以便于测试:

server.servlet.context-path=/myapp

引用路径

<!--无论上下文名称是什么,都能正常解析-->
<script type="text/javascript" th:src="@{/hello.js}"></script>

等价于

<script type="text/javascript" src="/myapp/hello.js"></script>

相对 URL

这个相对是指相对于服务器的 URL

<script type="text/javascript" th:src="@{~/hello.js}"></script>

等价于

<script type="text/javascript" src="/hello.js"></script>

应用程序的上下文 /myapp 将被忽略。

协议相对 URL

<script type="text/javascript" th:src="@{//localhost:8080/hello.js}"></script>

等价于

<script type="text/javascript" src="//localhost:8080/hello.js"></script>

带参数的 URL

<script type="text/javascript" th:src="@{//localhost:8080/hello.js(name='jackson',age=18)}"></script>

等价于

<script type="text/javascript" src="//localhost:8080/hello.js?name=jackson&age=18"></script>
5)~{…}

片段表达式是 Thymeleaf 的特色之一,细粒度可以达到标签级别,这是 JSP 无法做到的。片段表达式拥有三种语法:

  • ~{ viewName } : 表示引入完整页面
  • ~{ viewName ::selector} : 表示在指定页面寻找片段,其中 selector 可为片段名、jquery 选择器等
  • ~{ ::selector} : 表示在当前页寻找

举例

在 resources/templates 目录下新建 my_fragment.html 文件,内容如下

<div th:fragment="github_link">
  <a href="https://github.com">github</a>
</div>
<div th:fragment="alibaba_link">
  <a href="https://alibaba.com">alibaba</a>
</div>

这里有两个 div,通过 th:fragment 来定义片段,两个 div 分别具有不同的名字。

然后在另外一个页面中引用该片段:

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <p>hello 欢迎 <span th:text="${username}"></span>加入 XXX 集团,您的入职信息如下:</p>
    <table border="1" th:fragment="aaa">
        <tr>
            <td>职位</td>
            <td th:text="${position}"></td>
        </tr>
        <tr>
            <td>薪水</td>
            <td th:text="${salary}"></td>
        </tr>
    </table>
    <hr/>
    <div th:replace="my_fragment.html"></div>
    <hr/>
    <div th:replace="~{my_fragment.html::github_link}"></div>
    <hr/>
    <div th:replace="~{::aaa}"></div>
</body>
</html>

通过 th:replace 来引用片段。第一个表示引用完整的 my_fragment.html 页面;第二个表示引用 my_fragment.html 中的名为 github_link 的片段;第三个表示引用当前页面名为 aaa 的片段,也就是上面那个 table。

字面量

这些是一些可以直接写在表达式中的字符,主要有如下几种:

  • 文本字面量:‘one text’, ‘Another one!’,…
  • 数字字面量:0, 34, 3.0, 12.3,…
  • 布尔字面量:true, false
  • Null字面量:null
  • 字面量标记:one, sometext, main,…

举例

<div th:text="'你好 thymeleaf动态模版引擎'"></div>
<div th:text="9"></div>
<div th:text="true"></div>
<!--没有写单引号,不能使用空格,逗号或者中文-->
<div th:text="jackson"></div>

如果文本是英文,并且不包含空格、逗号等字符,可以不用加单引号。

文本运算

文本可以使用 + 进行拼接

<div th:text="'hello ' + 'jackson'"></div>
<div th:text="'hello' + ${user.name}"></div>

如果字符串中包含变量,也可以使用另一种简单的方式,叫做字面量置换,用 | 代替 '...' + '...',如下:

<div th:text="|hello ${user.name}|"></div>
<div th:text="'hello' + ' ' + 'jackson and ' + |hello ${user.name}|"></div>

算术运算

算术运算有:+, -, *, /%

<div th:with="age=(99*99/99+99-1)">
    <div th:text="${age}"></div>
</div>

th:with 定义了一个局部变量 age,在其所在的 div 中可以使用该局部变量。

逻辑运算

二元运算符:and, or

布尔非(一元运算符):!, not

<div th:with="age=(99*99/99+99-1)">
    <div th:text="9 eq 9 and 8 ne 8"></div>
    <div th:text="9 eq 9 or 8 ne 8"></div>
    <div th:text="!(9 eq 9 or 8 ne 8)"></div>
    <div th:text="not(9 eq 9 or 8 ne 8)"></div>
</div>

比较运算

表达式里的值可以使用 >, <, >=<= 符号比较。==!= 运算符用于检查相等(或者不相等)。

注意 XML 规定 <> 标签不能用于属性值,所以应当把它们转义为 &lt; 和 &gt;。

如果不想转义,也可以使用别名:gt (>);lt (<);ge (>=);le (<=);not (!)。还有 eq (==), neq/ne (!=)。

举例

<div th:with="age=(99*99/99+99-1)">
    <div th:text="${age} eq 197"></div>
    <div th:text="${age} ne 197"></div>
    <div th:text="${age} ge 197"></div>
    <div th:text="${age} gt 197"></div>
    <div th:text="${age} le 197"></div>
    <div th:text="${age} lt 197"></div>
</div>

条件运算

类似于我们 Java 中的三目运算符

<div th:with="age=(99*99/99+99-1)">
    <!--省略冒号后面的部分,当计算结果为false时,将返回null-->
    <div th:text="(${age} ne 197)?'yes':'no'"></div>
</div>

其中,: 后面的部分可以省略,如果省略了,又同时计算结果为 false 时,将返回 null。

内置对象

基本内置对象:

  • #ctx:上下文对象
  • #vars: 上下文变量
  • #locale:上下文区域设置
  • #request:(仅在 Web 上下文中)HttpServletRequest 对象
  • #response:(仅在 Web 上下文中)HttpServletResponse 对象
  • #session:(仅在 Web 上下文中)HttpSession 对象
  • #servletContext:(仅在 Web 上下文中)ServletContext 对象

在页面可以访问到上面这些内置对象,举例:

<div th:text="${#session.getAttribute('name')}"></div>

实用内置对象:

  • #execInfo:有关正在处理的模板的信息
  • #messages:在变量表达式中获取外部化消息的方法,与使用 #{…} 语法获得的方式相同。
  • #uris:转义URL / URI部分的方法
  • #conversions:执行配置的转换服务(如果有)的方法
  • #dates:java.util.Date 对象的方法:格式化,组件提取等
  • #calendars:类似于 #dates 但是 java.util.Calendar 对象
  • #numbers:用于格式化数字对象的方法
  • #strings:String 对象的方法:contains,startsWith,prepending / appending 等
  • #objects:一般对象的方法
  • #bools:布尔评估的方法
  • #arrays:数组方法
  • #lists:列表的方法
  • #sets:集合的方法
  • #maps:地图方法
  • #aggregates:在数组或集合上创建聚合的方法
  • #ids:处理可能重复的id属性的方法(例如,作为迭代的结果)

这是一些内置对象以及工具方法,使用方式也都比较容易,如果使用的是 IntelliJ IDEA,都会自动提示对象中的方法,很方便。

举例

<div th:text="${#lists.size(users)}"></div>
<div th:text="${#arrays.length(users)}"></div>
<div th:text="${#execInfo.getProcessedTemplateName()}"></div>

设置属性值

这个是给 HTML 元素设置属性值。可以一次设置多个,多个之间用 , 分隔开。

举例

<img th:attr="src=@{/1.png},title=${user.name},alt=${user.name}">

会被渲染成

<img src="/myapp/1.png" title="向往的今越" alt="向往的今越">

当然这种设置方法不太美观,可读性也不好。

Thymeleaf 还支持在每一个原生的 HTML 属性前加上 th: 前缀的方式来使用动态值,像下面这样:

<img th:src="@{/1.png}" th:alt="${user.name}" th:title="${user.name}">

这种写法看起来更清晰一些,渲染效果和前面一致。

上面案例中的 alt 和 title 则是两个特殊的属性,可以一次性设置,像下面这样:

<img th:src="@{/1.png}" th:alt-title="${user.name}">

遍历

数组/集合/Map/Enumeration/Iterator 等的遍历也算是一个非常常见的需求,Thymeleaf 中通过 th:each 来实现遍历。

<table border="1">
    <tr th:each="u : ${users}">
        <td th:text="${u.name}"></td>
        <td th:text="${u.address}"></td>
    </tr>
</table>

users 是要遍历的集合/数组,u 则是集合中的单个元素。

遍历的时候,我们可能需要获取遍历的状态,Thymeleaf 也对此提供了支持:

  • index:当前的遍历索引,从 0 开始
  • count:当前的遍历索引,从 1 开始
  • size:被遍历变量里的元素数量
  • current:每次遍历的遍历变量
  • even/odd:当前的遍历是偶数次还是奇数次
  • first:当前是否为首次遍历
  • last:当前是否为最后一次遍历

u 后面的 state 表示遍历状态,通过遍历状态可以引用上面的属性。

<table border="1">
    <tr th:each="u,state : ${users}">
        <td th:text="${u.name}"></td>
        <td th:text="${u.address}"></td>
        <td th:text="${state.index}"></td>
        <td th:text="${state.count}"></td>
        <td th:text="${state.size}"></td>
        <td th:text="${state.current}"></td>
        <td th:text="${state.even}"></td>
        <td th:text="${state.odd}"></td>
        <td th:text="${state.first}"></td>
        <td th:text="${state.last}"></td>
    </tr>
</table>

分支语句

只显示奇数次的遍历,可以使用 th:if,如下:

<table border="1">
    <tr th:each="u,state : ${users}" th:if="${state.odd}">
        <td th:text="${u.name}"></td>
        <td th:text="${u.address}"></td>
        <td th:text="${state.index}"></td>
        <td th:text="${state.count}"></td>
        <td th:text="${state.size}"></td>
        <td th:text="${state.current}"></td>
        <td th:text="${state.even}"></td>
        <td th:text="${state.odd}"></td>
        <td th:text="${state.first}"></td>
        <td th:text="${state.last}"></td>
    </tr>
</table>

th:if 不仅仅只接受布尔值,也接受其他类型的值,例如如下值都会判定为 true:

  • 如果值是布尔值,并且为 true

  • 如果值是数字,并且不为 0

  • 如果值是字符,并且不为 0

  • 如果值是字符串,并且不为 “false”, “off” 或者 “no”

  • 如果值不是布尔值,数字,字符或者字符串

但是,如果值为 null,th:if 会求值为 false


th:unless 的判定条件则与 th:if 完全相反。

<table border="1">
    <tr th:each="u,state : ${users}" th:unless="${state.odd}">
        <td th:text="${u.name}"></td>
        <td th:text="${u.address}"></td>
        <td th:text="${state.index}"></td>
        <td th:text="${state.count}"></td>
        <td th:text="${state.size}"></td>
        <td th:text="${state.current}"></td>
        <td th:text="${state.even}"></td>
        <td th:text="${state.odd}"></td>
        <td th:text="${state.first}"></td>
        <td th:text="${state.last}"></td>
    </tr>
</table>

这个显示效果则与上面的完全相反。


当可能性比较多的时候,也可以使用 switch:

<table border="1">
    <tr th:each="u,state : ${users}">
        <td th:text="${u.name}"></td>
        <td th:text="${u.address}"></td>
        <td th:text="${state.index}"></td>
        <td th:text="${state.count}"></td>
        <td th:text="${state.size}"></td>
        <td th:text="${state.current}"></td>
        <td th:text="${state.even}"></td>
        <td th:text="${state.odd}"></td>
        <td th:text="${state.first}"></td>
        <td th:text="${state.last}"></td>
        <td th:switch="${state.odd}">
            <span th:case="true">odd</span>
            <span th:case="*">even</span>
        </td>
    </tr>
</table>

th:case="*" 则表示默认选项。

本地变量

使用 th:with 可以定义一个本地变量,参考简单表达式部分。

內联

我们可以使用属性将数据放入页面模版中,但是很多时候,内联的方式看起来更加直观一些,像下面这样:

<!--[[...]]对应于th:text 结果会是转义的html-->
<div>hello [[${user.name}]]</div>
<!--[(...)]对应于th:utext 它不会执行任何的html转义-->
<div>hello [(${user.name})]</div>

用内联的方式去做拼接也显得更加自然。

举例

<div th:with="str='hello <strong>jackson</strong>'">
    <div>[[${str}]]</div>
    <div>[(${str})]</div>
</div>

显示效果如下

不过内联方式有一个问题。我们使用 Thymeleaf 的一大优势在于不用动态渲染就可以直接在浏览器中看到显示效果,当我们使用属性配置的时候确实是这样,但是如果我们使用内联的方式,各种表达式就会直接展示在静态网页中。

也可以在 js 或者 css 中使用内联,以 js 为例,使用方式如下:

<!--js 中需要通过 th:inline="javascript" 开启内联-->
<script th:inline="javascript">
    var n = [[${user.name}]];
    console.log(n);
</script>

官方文档

Thymeleaf 官网:https://www.thymeleaf.org

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值