集成Thymeleaf模板引擎

Thymeleaf是一个用于Java Web开发的模板引擎,主要用于在服务端将数据渲染成HTML页面。Thymeleaf能够和Spring生态无缝集成,因此搭配SpringMVC/SpringBoot使用是非常流畅和推荐使用的。

官方网站:https://www.thymeleaf.org/

同类技术对比

在早期的SpringMVC(Spring 3.x及之前)时代,JSP几乎是标配的视图层方案,不过JSP本身有很多问题,包括允许耦合Java代码、语法比较偏上个时代的写法不够优雅、不利于前端人员使用等缺点,SpringBoot后来移除了JSP的直接支持。后来专用模板引擎逐渐开始流行,包括Velocity和FreeMarker,其中Velocity早期是比较火的,但它更新缓慢,社区也逐渐冷清了,SpringBoot后来也移除了Velocity的直接支持,FreeMarker则一直被保留到了现在。不过至今为止,SpringBoot中使用最多的视图层方案其实是Thymeleaf模板引擎。

Thymeleaf模板语法比较像现在流行的前端框架(比如Angular或Vue),它通过在HTML中定义一个th命名空间,引入th:系列的XML标签属性,模板的显示逻辑如th:ifth:each都是放在真正的HTML标签中的属性,这样做的好处就是不用启动应用服务器就能够正确的让浏览器直接解析预览我们的模板,利于程序员和设计人员合作。

当然,Thymeleaf也有缺点,比如语法不如JSP、Velocity等灵活,实际开发任务中经常碰到过于绕弯、难以实现的问题。

引入Maven依赖

SpringBoot工程中,使用Thymeleaf需要引入对应的起步依赖。

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

引入后,Thymeleaf相关的ViewResolver等配置默认已经具备了,我们可以直接使用。

控制器和模板代码的编写

下面我们写一个最简单的例子,从控制器中传一个字符串到Thymeleaf模板。

MainController.java

package com.gacfox.demo.controller;

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

@Controller
public class MainController {
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String getMainPage(Model model) {
        model.addAttribute("msg", "hello, world!!");
        return "main";
    }
}

main.html

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>测试</title>
</head>
<body>
<h1 th:text="${msg}"></h1>
</body>
</html>

代码比较简单,对于SpringMVC中使用@Controller标注的控制器,如果控制器方法返回字符串,那么它默认将以模板视图的方式处理,返回的字符串就是模板视图名。

Model是一个类似Map的对象,可以用来存储基本类型、字符串、JavaBean等,如果我们的控制器方法声明了该参数,Spring框架会自动注入其实例,我们在Model中保存的数据可以在模板中取用。

Thymeleaf语法

Thymeleaf是一个为web设计的现代模板引擎,使用起来比较简便,这里我们简要介绍一些Thymeleaf模板的语法。

引入XML命名空间

使用Thymeleaf模板前,我们需要先引入XML命名空间,否则标签中无法使用th:属性。

<html lang="zh" xmlns:th="http://www.thymeleaf.org">

显示控制器传来的变量

基本类型和字符串变量可以直接通过th:text属性渲染,对于传递过来的变量需要用${}包裹填入模板。

<!--显示字符串-->
<div th:text="${msg}">defaultMsg</div>

显示JavaBean的属性

JavaBean变量可以通过.操作符直接访问其中的对应getter方法或public属性。

<!--显示Java Bean-->
<table>
    <tr>
        <th>用户名</th>
        <th>密码</th>
        <th>出生日期</th>
    </tr>
    <tr>
        <td th:text="${user.username}">defaultUsername</td>
        <td th:text="${user.password}">defaultPassword</td>
        <td th:text="${user.birthday}">defaultBirthday</td>
    </tr>
</table>

迭代显示List数据

th:each用于迭代显示数据,下面是两个例子。

<!--迭代显示List-->
<ul>
    <li th:each="s : ${strings}" th:text="${s}">defaultItem</li>
</ul>
<!--迭代显示Java Bean的List-->
<table>
    <tr>
        <th>用户名</th>
        <th>密码</th>
        <th>出生日期</th>
    </tr>
    <tr th:each="u : ${userList}">
        <td th:text="${u.username}"></td>
        <td th:text="${u.password}"></td>
        <td th:text="${u.birthday}"></td>
    </tr>
</table>

条件判断

th:if用于判断,Thymeleaf中没有完整的if-else,比较推荐的用法是写多个if逻辑。

<!--判断-->
<span th:if="${var} == 1">aa</span>
<span th:if="${var} == 2">bb</span>

通过session进行登录判断

模板中,我们可以直接用session对象访问Servlet规范中的HttpSession中的数据。

<!--登录判断-->
<span th:if="!${session.user}">请登录</span>
<span th:if="${session.user}">已登录</span>
</body>
</html>

使用工具类

Thymeleaf中提供了一系列工具类,我们可以使用#符号引用他们,下面例子我们引用日期工具类#dates,实现格式化输出Date对象。

<td th:text="${#dates.format(u.birthday, 'yyyy-MM-dd')}"></td>

使用URL

Thymeleaf中可以自动处理URL,不需要我们在前端代码中判断Context Path等信息,下面是一个例子。

<!--URL-->
<a th:href="@{/test2}">URL</a>

注:最开始的/代表项目的Context Path。

绑定表单字段并使用Bean Validation注解进行后端验证

对于表单操作,Thymeleaf做了一系列封装。下面例子中写了一个简单的登录表单示例,代码中使用Bean Validation注解进行了服务端的表单字段验证,并演示了如何将FormBean绑定到Thymeleaf模板的<form>标签上,以及如何将注解式验证的错误信息回显到Thymeleaf模板。

LoginFormBean.java

package com.gacfox.demo.model;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class LoginFormBean {
    @NotNull(message = "用户名能为空")
    @Size(min = 1, max = 255, message = "用户名必须在1-255个字符之间")
    @Pattern(regexp = "[a-zA-Z0-9]+", message = "用户名必须是字母和数字")
    private String username;
    @NotNull(message = "密码不能为空")
    @Size(min = 1, max = 20, message = "密码必须在1-20个字符之间")
    private String password;
}

LoginController.java

package com.gacfox.demo.controller;

import com.gacfox.demo.model.LoginFormBean;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/login")
public class LoginController {
    @GetMapping
    public String loginPage(Model model) {
        model.addAttribute("loginFormBean", new LoginFormBean());
        return "login";
    }

    @PostMapping
    public String doLogin(@Valid LoginFormBean loginFormBean, BindingResult bindingResult, Model model) {
        if (!bindingResult.hasErrors()) {
            System.out.println(loginFormBean);
            if ("root".equals(loginFormBean.getUsername()) && "123".equals(loginFormBean.getPassword())) {
                return "redirect:/";
            } else {
                model.addAttribute("err_msg", "用户名或密码错误");
                return "login";
            }
        } else {
            return "login";
        }
    }
}

login.html

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>测试</title>
</head>
<body>
<form th:action="@{/login}" method="post" th:object="${loginFormBean}">
    <label>
        <input type="text" name="username" th:value="${loginFormBean.username}">
    </label>
    <label>
        <input type="password" name="password" th:value="${loginFormBean.password}">
    </label>
    <input type="submit" value="提交">
    <span th:if="${err_msg != null}" th:text="${err_msg}"></span>
    <span th:errors="${loginFormBean.username}"></span>
    <span th:errors="${loginFormBean.password}"></span>
</form>
</body>
</html>

@Valid实现表单验证的原理在其它章节我们已经介绍过了,这里我们主要关注模板的编写:

  • th:action:值是一个URL语法,用于指定表单的提交地址,使用@{}而不是相对路径的好处是可以自动处理Context Path。
  • th:object:用于将FormBean绑定到HTML的form标签,FormBean需要我们首先实例化并通过控制器传入模板中。
  • th:errors:用户回显错误信息。
  • th:value:用于填充表单字段值。在上面例子中,这个标签用于在提交失败时回显上次的表单的数据,以免用户全部重填。
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。