Spring Boot 集成 Thymeleaf 搭建 Web 应用

230 阅读5分钟

大家好,我是磊磊落落,目前我在技术上主要关注:Java、Golang、架构设计、云原生和自动化测试。欢迎来我的博客(leileiluoluo.com)获取我的最近更新!

Thymeleaf 是一个流行的 Java 模板引擎,具有处理 HTML、XML、JavaScript、CSS 和纯文本的能力。Thymeleaf 可以和 Spring Boot 进行无缝集成,且可以非常容易地对 Java Model 类及其字段进行访问,从而对模板内容进行动态渲染。并且,Thymeleaf 还提供了一组简单有力的表达式来支持循环、条件判断、静态工具类及 Spring Bean 访问等能力。此外,Thymeleaf 还对自定义扩展以及表单提供了很好的支持。

本文将在 Spring Boot 中集成 Thymeleaf 来搭建一个简单的 Web 应用程序,以对 Thymeleaf 相关的知识进行梳理和运用。本文搭建的 Web 应用程序为一个简单的博客收集网站,拥有首页、博客列表、博客详情、博客提交 5 个页面。最后的效果如下:

博客收集应用程序

(上面动图依次为进入首页、查看博客列表、查看博客详情、提交博客,然后跳转到列表页)

本文所使用的 JDK、Maven、Spring Boot 与 Thymeleaf 的版本如下:

JDK:BellSoft Liberica 17.0.7
Maven:3.9.2
Spring Boot:3.3.4
Thymeleaf:3.1.2.RELEASE

接下来即分析一下该应用程序的项目结构和关键代码。

1 项目结构

该 Web 项目使用 Maven 管理,其项目结构如下:

spring-boot-thymeleaf-demo
├─ src/main
│  ├─ java
│  │  └─ com.example.demo
│  │     ├─ controller
│  │     │  ├─ BlogController.java
│  │     │  └─ HomeController.java
│  │     ├─ service
│  │     │  └─ BlogService.java
│  │     │  └─ impl
│  │     │     └─ BlogServiceImpl.java
│  │     ├─ model
│  │     │  └─ Blog.java
│  │     ├─ util
│  │     │  ├─ DateUtil.java
│  │     │  └─ IdGenerator.java
│  └─ resources
│     ├─ static
│     │  └─ css
│     │     └─ styles.css
│     └─ templates
│        ├─ blogs
│        │  ├─ add.html
│        │  ├─ blog.html
│        │  └─ blogs.html
│        ├─ error
│        │  └─ 404.html
│        ├─ home
│        │  └─ index.html
│        └─ layout.html
└─ pom.xml

可以看到,根目录下的 pom.xml 为 Maven 项目描述文件;Java 代码位于 src/main/java 文件夹下的 com.example.demo 包下;src/main/resources 文件夹下的 templates 子文件夹用于放置 Thymeleaf 模板文件,static 子文件夹用于放置静态资源(本文仅在这里放置了一个 CSS 文件)。

这里仅列出 pom.xml 文件里用到的依赖,并对其进行说明。其它一些关键的模板代码和 Java 代码将在下个部分进行说明。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>nz.net.ultraq.thymeleaf</groupId>
        <artifactId>thymeleaf-layout-dialect</artifactId>
        <version>3.3.0</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.34</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

可以看到,如上依赖中,除了 spring-boot-starter-web 为编写 Spring Boot Web 应用程序必备的依赖之外,在 Spring Boot 中集成 Thymeleaf 主要需要引入一个 spring-boot-starter-thymeleaf 依赖;而 thymeleaf-layout-dialect 依赖是为了方便重用模板代码,作布局管理的;lombok 依赖则是为了在编写 Java Model 类时可以省去 SettersGetters 的编写。

2 关键代码分析

2.1 模板布局管理

考虑到全站页面有一些页头、页脚是公有的,若每个页面单独去写的话,会产生较多的代码冗余。所以我们使用 thymeleaf-layout-dialect 做一个统一的布局出来,供其它页面去引用是很有必要的。

统一布局 src/main/resources/templates/layout.html 的代码如下:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
    <title>博客聚合</title>
    <link rel="stylesheet" th:href="@{/css/styles.css}">
</head>
<body>
<header layout:fragment="header">
    <nav>
        <a href="/">首页</a>
        <a href="/blogs/add-form">提交博客</a>
        <a href="/blogs">博客列表</a>
    </nav>
</header>

<main class="container" layout:fragment="content"></main>

<footer layout:fragment="footer">
    <p th:text="@{'© {year} 博客聚合'(year=${T(com.example.demo.util.DateUtil).getCurrentYear()})}"></p>
</footer>
</body>
</html>

可以看到,该公共布局模板在 <head> 标签内引用了公共样式文件 styles.css;在 <body> 内指定了公共 <header 和公共 <footer>,各个页面只需替换 <main> 部分即可。

需要注意,这里引用本地样式文件时使用了 th:href="@{/css/styles.css}",这就是 Thymeleaf 引用资源文件的语法;此外还需注意,在 <footer> 部分,使用 ${T(com.example.demo.util.DateUtil).getCurrentYear()} 表达式调用了 Java 静态工具类 DateUtilgetCurrentYear() 方法。

2.2 模板动态渲染与表单提交

上面介绍了公共布局模板,下面主要看一下博客列表页和提交博客页,从而关注模板的动态渲染和表单提交。

博客列表页 src/main/resources/templates/blogs/blogs.html 的代码如下:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout::layout}">
<head>
    <title>博客列表</title>
</head>
<body>
<main layout:fragment="content">
    <div class="blog-list">
        <ul>
            <li th:each="blog : ${blogs}" th:if="${!blog.deleted}">
                <a th:href="@{/blogs/{id}(id=${blog.id})}" th:text="${blog.name}"></a>
            </li>
        </ul>
    </div>
</main>
</body>
</html>

可以看到,博客列表页使用 layout:decorate="~{layout::layout}" 表达式引入了前面定义的公共布局;然后在 <head> 标签修改了本页的标题;然后对 <main> 部分进行了修改,使用 th:each="blog : ${blogs}" 表达式遍历了 blogs,并对每个 blog.deletedfalseblog 显示其名称且将其链接到博客详情页面 /blogs/{id}

对应博客列表与博客详情页面的 Controller 部分的代码如下:

package com.example.demo.controller;
// ...

@Controller
@RequestMapping("/blogs")
public class BlogController {

    @Autowired
    private BlogService blogService;

    @GetMapping("")
    public String listAllBlogs(Model model) {
        List<Blog> allBlogs = blogService.listAllBlogs();

        model.addAttribute("blogs", allBlogs);

        return "blogs/blogs";
    }

    @GetMapping("/{id}")
    public String getBlogById(@PathVariable("id") Integer id, Model model) {
        Optional<Blog> optional = blogService.getBlogById(id);
        if (optional.isEmpty()) {
            return "error/404";
        }

        model.addAttribute("blog", optional.get());

        return "blogs/blog";
    }
}

可以看到,要想将数据输出给模板,需要在 Controller 的方法携带一个 Model model 参数,并将对应的对象设置到其属性里(model.addAttribute("blogs", allBlogs)),这样即可在模板中使用属性名进行访问了(${blogs})。数据设置完成后,只需 returntemplates 下的页面即可,如 return "blogs/blogs" 表示寻找并渲染 templates/blogs/blogs.html 模板。

Thymeleaf 除了可以渲染用于显示的页面之外,还支持表单渲染、校验和提交。

下面看一下博客提交页面 src/main/resources/templates/blogs/add.html 的代码:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layout::layout}">
<head>
    <title>提交博客</title>
</head>
<body>
<main layout:fragment="content">
    <div class="form">
        <form th:action="@{/blogs}" th:object="${blog}" method="post">
            <div>
                <label>博客名称:</label>
                <span class="error" th:if="${#fields.hasErrors('name')}" th:errors="*{name}" />
            </div>
            <div>
                <input name="name" th:field="*{name}"/>
            </div>

            <div>
                <label>博客描述:</label>
                <span class="error" th:if="${#fields.hasErrors('description')}" th:errors="*{description}" />
            </div>
            <div>
                <textarea name="description" th:field="*{description}"/>
            </div>

            <div>
                <label>技术博客:</label>
            </div>
            <div>
                <select id="options" name="technical">
                    <option value="false">否</option>
                    <option value="true">是</option>
                </select>
            </div>

            <div>
                <button>提交</button>
            </div>
        </form>
    </div>
</main>
</body>
</html>

可以看到,该页面有一个 <form>,其中有一个 <input>、一个 <textarea 和一个 <select> 需要输入,最后是一个 <button>;该表单会将字段设置到 blog Model(th:object="${blog}"),并在点击提交按钮后使用 POST 方法将数据发送至路径 /blogs

该表单页面对应 Controller 中的代码如下:

package com.example.demo.controller;
// ...

@Controller
@RequestMapping("/blogs")
public class BlogController {

    @Autowired
    private BlogService blogService;

    @GetMapping("/add-form")
    public String addBlogForm(Blog blog) {
        return "blogs/add";
    }

    @PostMapping("")
    public String addBlog(Blog blog, Errors errors) {
        // validation
        if (StringUtils.isBlank(blog.getName())) {
            errors.rejectValue("name", "fields.invalid", "名称不能为空");
            return "blogs/add";
        }
        if (StringUtils.isBlank(blog.getDescription())) {
            errors.rejectValue("description", "fields.invalid", "描述不能为空");
            return "blogs/add";
        }

        // add blog
        blogService.addBlog(blog);

        return "redirect:/blogs";
    }
}

可以看到,addBlogForm() 方法用于显示表单页面;addBlog() 方法用于接收从表单提交过来的数据,且其可以使用一个 Errors errors 参数来将字段校验错误信息返回给表单,然后用于显示。由此可以看到,Thymeleaf 对于参数校验错误信息的处理非常便捷。

提交博客表单

这就是使用 Thymeleaf 处理普通页面和表单页面的方法。由此衍生出多个页面,做一个功能丰富的应用程序是不无可能的。

3 小结

综上,本文以使用 Spring Boot 和 Thymeleaf 来搭建一个博客收集 Web 应用程序为目标,演示了 Thymeleaf 与 Spring Boot 的集成,以及 Thymeleaf 的基础功能。总的来说,在 Spring Boot 中集成 Thymeleaf 来编写一个前后端一体的 Web 应用程序是非常便捷的,是一个值得考虑的解决方案。

本文完整示例工程已提交至 GitHub,欢迎关注或 Fork。

参考资料

[1] Tutorial: Using Thymeleaf - www.thymeleaf.org/doc/tutoria…