Spring Boot 学习之路 -- Thymeleaf 模板引擎

前言

  1. 最近因为业务需要,被拉去研究后端的项目,代码框架基于 Spring Boot,后端对我来说完全小白,需要重新学习研究...
  2. 出于个人习惯,会以 Blog 文章的方式做一些记录,文章内容基本来源于「 Spring Boot 从入门到精通(明日科技) 」一书,做了一些整理,更易于个人理解和回顾查找,所以大家如果希望更系统性的学习,可以阅读此书。

系列 文章
Spring Boot 学习之路 📑 基础认知
Spring Boot 学习之路 📑 项目配置
Spring Boot 学习之路 📑 处理 HTTP 请求
Spring Boot 学习之路 📑 Service 层
Spring Boot 学习之路 📑 Thymeleaf 模板引擎

一、概述

Spring Boot 遵循"前后端分离"的设计理念。所谓的"前后端分离"中的前端指网页端、客户端,后端指服务器端。后端只提供服务接口,前端只有通过访问后端接口才能获取到数据。此外,前端还要完成页面的布局、渲染等工作。如果想让前端获取的数据可以根据用户的操作而发生变化,就需要使用动态网页技术。为此,Spring Boot 采用了 Web 模板引擎技术。Thymeleaf 是 Spring Boot 官方推荐使用的模板引擎。

Thymeleaf 是一个 Java 模板引擎,适用于 Web 开发和独立环境的服务器端。那么,什么是模板引擎呢?模板引擎是为了使用户界面与业务数据分离而生成的特定文本格式的文档,常用的文本格式有 HTML、XML 等。

Thymeleaf 的主要目标是提供一种可以被浏览器正确显示的、格式良好的模板创建方式。在实际开发中,程序开发人员可以使用 Thymeleaf 创建 XML 和 HTML 模板。所谓 XML 和 HTML 模板,指的是格式良好的 .html 文件。也就是说,Thymeleaf 把 .html 文件作为模板。与编写逻辑代码相比,程序开发人员只需要把标签属性添加到 .html 文件中,即可执行预先制定好的逻辑。

Thymeleaf 具有如下两个特点:

  • Thymeleaf 在有网络、无网络的环境下都可以运行。Thymeleaf 可以直接在浏览器中打开并查看静态页面。Thymeleaf 可以通过向 HTML 标签中添加其他属性实现数据渲染。
  • Thymeleaf 具有"开箱即用"的特性。Thymeleaf 直接以 .html 的格式予以显示。Thymeleaf 可以使前后端很好地分离。

二、添加 Thymeleaf

Thymeleaf 需要手动添加到 Spring Boot 项目中。添加 Thymeleaf 的方式有两种:

  1. 第一种是在创建项目的添加依赖界面中选择 Thymeleaf:
  1. 第二种是在已创建好的项目的 pom.xml 文件中添加以下依赖:
kotlin 复制代码
<dependency>
​​​​     <groupId>org.springframework.boot</groupId>
​​​​     <artifactId>spring-boot-starter-thymeleaf</artifactId>
​​​​</dependency>​

三、跳转至 .html 文件

在前面文章的实例中,控制器都是直接返回字符串,或者是跳转至其他 URL 地址。如果想让控制器跳转至项目中的某个 .html 文件,就需要使用 Thymeleaf 了。

3.1 明确 .html 文件的存储位置

Spring Boot 项目中所有页面文件都要放在 src/main/resources 目录的 templates 文件夹下。页面可能需要加载一些静态文件,例如图片、JS 文件等,静态文件需要放在与 templates 同级的 static 文件夹下。

3.2 跳转至指定的 .html 文件

前面曾介绍了两种控制器注解:@Controller 和 @RestController。@Controller 中的方法如果返回字符串,则默认访问返回值对应的地址。如果项目添加了 Thymeleaf 依赖则会改变此处跳转的逻辑,Thymeleaf 会根据返回的字符串值,寻找 templates 文件夹下同名的网页文件,并跳转至该网页文件。例如,下图所示,如果方法的返回值为"login",Thymeleaf 在 templates 文件夹下发现了 login.html 文件,则会让其请求跳转至该文件。如果方法返回值没有对应的 .html 文件,则会抛出 TemplateInputException 异常。

说明:

想要实现此功能的控制器,必须用 @Controller 标注,不能使用 @RestController。

templates 文件夹下的 .html 文件无法通过 URL 地址直接访问,只能通过 Controller 类跳转。

html 文件可以放在 static 文件夹下,这样 .html 文件就是静态页面,可以直接通过 URL 地址访问,但无法获得动态数据。

3.3 跳转至 Thymeleaf 的默认页面

在不指定项目主页和错误页跳转规则的前提下,Thymeleaf 模板会默认将 index.html 当作项目的默认主页,将 error.html 当作项目默认错误页。如果发生的异常没有被捕捉,就自动跳转至 error.html。

注意:

默认的 index.html 和 error.html 必须在 templates 文件夹根目录下。

  1. 为项目添加默认首页

在 templates 文件夹下创建 index.html,代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>这是 Thymeleaf 默认的首页</h1>
</body>
</html>

创建 IndexController 控制器类,如果用户访问 "/login" 地址,则必须传入 name 参数。IndexController 类的代码如下:

kotlin 复制代码
public class IndexController {
    @RequestMapping("/login")
    public String login(@RequestParam String name) {
        return "您输入的用户名为:" + name;
    }
}

启动项目后,打开浏览器访问 http://127.0.0.1:8080 地址,可以看到下图的默认首页。

访问地址 http://127.0.0.1:8080/login?name=David 可以看到控制器返回如下界面:


四、常用表达式和标签

Thymeleaf 提供了许多独有的标签,程序开发人员可以利用这些标签让页面显示动态的内容。Thymeleaf 也提供了几个表达式用来为标签赋值。本节将介绍一些常用的表达式和标签。

4.1 表达式

Thymeleaf 有 4 种常用的表达式,分别用于不同场景,下面分别介绍。

1. 读取属性值

后端向前端发送的数据都会放在 Model 对象中,存放格式类似键值结构,就是"属性名:属性值"的结构。在页面中可以利用 *{} 表达式通过属性名获得 Model 中属性值。表达式语法如下:

kotlin 复制代码
​​​​*{属性名}​​

例如,获取属性名为 name 的值:

kotlin 复制代码
​​​​*{name}​​

2. 读取对象

如果后端向前端发送的不是一个具体值,而是一个对象(例如日期对象、集合对象等),想要调用该对象中的属性或方法,必须使用 ${} 表达式。表达式语法如下:

kotlin 复制代码
​​​​${对象}
​​​​${对象.属性}
​​​​${对象.方法()}​​

{对象}获得的是对象,而不是一个具体值,所以需要配合遍历、定义变量等标签一起使用。{对象.方法()} 获得的是该对象方法的返回值。

3. 封装地址

如果想要在 Thymeleaf 标签中赋值具体的 URL 地址,需要用到 @{} 表达式。表达式语法如下:

kotlin 复制代码
​​​​@{/URL地址}​​

使用该表达式可以为标签定义跳转地址。

4. 插入片段

插入片段表达式的功能类似 JSP 中的 jsp:include 标签,允许程序开发人员将 A 页面中的代码插入到 B 页面中。表达式语法如下:

kotlin 复制代码
​​​​~{创建片段的文件名::片段名}​​

该表达式必须配合 th:fragment 标签,在定义完代码片段之后使用。注意该表达式的写法比较特殊,"创建片段的文件名"是代码片段所在文件的抽象名称,例如代码片段定义在 src/main/resources/templates/top/head.html 页面文件中,文件名应该写为"top/head",不包含根目录名和后缀名。"片段名"为 th:fragment 标签定义的名称。表达式中间有两个冒号而不是一个。

4.2 标签

很多表达式都需要配合标签一起使用,Thymeleaf 提供的标签非常多,基本满足了所有动态页面的需求。下图表中列出了一些常用的标签,想要使用这些标签,就必须先在页面顶部导入标签,代码如下:

html 复制代码
​​​​<html xmlns:th="http://www.thymeleaf.org">​​

导入之后就可以把 Thymeleaf 的标签以标签属性的形式写在 HTML 各元素之中。


五、向前端页面传值

Thymeleaf 从后端向前端页面传值的语法比 JSP 技术简洁许多。本节将介绍使用 Thymeleaf 向前端页面传值的两步操作。

5.1 把要传的值添加到 Model 对象中

Model 是 org.springframework.ui 包下的接口,用法类似 Map 键值对。Model 接口提供的接口如下:

程序开发人员只需为 Controller 的跳转方法添加 Model 参数,然后把要传给前端的值保存成 Model 的属性,Thymeleaf 可以自动读取 Model 里的属性值,并将其写入前端页面中。例如,把用户名 "张三" 传输给前端,可以参照如下代码:

kotlin 复制代码
​​​​@RequestMapping("/index")
​​​​public String show(Model model) {
​​​​    model.addAttribute("name", "张三");
​​​​    return "index";
​​​​}​​

5.2 在前端页面中获取 Model 的属性值

前端读取 Model 的属性值时需要用到 *{} 或 ${} 表达式。如果读取基本数据类型或字符串,就用*{},例如 *{name} 即可读取 Model 中名为 name 的属性值。

比如,我们做个测试:在前端页面显示用户的 IP 地址信息。

  1. 创建 ParameterController 控制器类,为映射 "/index" 的方法添加 Model 参数和 HttpServletRequest 参数。获取发送请求的 IP 地址、请求类型,以及请求头中的浏览器类型,将这些数据都保存在 Model 的属性中,最后跳转至 main.html。

ParameterController 类的代码如下:

kotlin 复制代码
@Controller
public class ParameterController {
    @RequestMapping("/index")
    public String index(Model model, HttpServletRequest request) {
        model.addAttribute("ip", request.getRemoteAddr());                 // 记录请求 IP 地址
        model.addAttribute("method", request.getMethod());				   // 记录请求类型
        String brow = "未知";
        String userAgent = request.getHeader("User-Agent").toLowerCase();  // 读取请求头
        if (userAgent.contains("Chrome")) {								   // 如果包含谷歌浏览器名称
            brow = "谷歌浏览器";
        } else if (userAgent.contains("Firefox")) {						   // 如果包含火狐浏览器名称
            brow = "火狐浏览器";
        }
        model.addAttribute("brow", brow);								   // 记录浏览器识别结果
        return "main";
    }
}
  1. 在 main.html 中获取 Model 中的 IP 地址、请求类型和浏览器类型的值,展示在页面中。
html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <p th:text=" '您的 IP 地址:' + ${ip}"></p>
    <p th:text=" '您提供的方式:' + ${method}"></p>
    <p th:text=" '您使用的浏览器:' + ${brow}"></p>
</body>
</html>

启动项目后,访问 http://127.0.0.1:8080/index 地址。


六、内置对象

除了之前介绍的表达式和标签,Thymeleaf 还提供了一些内置对象,程序开发人员可以直接调用这些对象的方法。Thymeleaf 提供的内置对象如下表所示。

对象 说明
#request 可直接替代 HttpServletRequest 对象
#session 可直接替代 HttpSession 对象
#aggregates 聚合操作工具类
#arrays 数组工具类
#bools 布尔类型工具类
#calenders 日历工具类
#dates 日期工具类
#lists list 工具类
#maps map 工具类
#numbers 数字工具类
#objects 一般对象工具类
#sets set 工具类
#strings 字符串工具类

注意:

每一个内置对象前都必须有 # 前缀,除了 #request 和 #session,其他对象名称末尾均有小写 s。

内置对象要在 ${} 表达式中使用。

【 示例:读取当前登录的用户名并写入要展示的消息 】

  1. 创建 IndexController 控制器类,在映射方法中添加 HttpServletRequest 和 HttpSession 参数,向 HttpServletRequest 写入要展示的消息,向 HttpSession 写入当前登录的用户名,代码如下:
java 复制代码
@Controller
public class IndexController {
    @RequestMapping("/index")
    public String index(HttpServletRequest request, HttpSession session) {
        request.setAttribute("message", "欢迎访问 XXX 网站");
        session.setAttribute("user", "David");
        return "index";
    }
}
  1. 在 index.html 中使用 #session 就可以直接从 HttpSession 中读取用户名,用 #request 直接从 HttpServletRequest 中读取消息,代码如下:
html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <p th:text="'您好,' + ${#session.getAttribute('user')}" />
    <p th:text="${#request.getAttribute('message')}" />
</body>
</html>
  1. 启动项目后,打开浏览器访问 http://127.0.0.1:8080/index 地址,可以看到下图结果,HttpServletRequest 和 HttpSession 中的数据可以正常读出。

注意:

Thymeleaf 3.0 之前,上面代码是可以运行的,但是 Thymeleaf 3.0 之后,运行会报错:

java.lang.IllegalArgumentException: The 'request','session','servletContext' and 'response' expression utility objects are no longer available by default for template expressions and their use is not recommended. In cases where they are really needed, they should be manually added as context variables.

从 Thymeleaf 3.0 开始,默认不再支持通过 #request 和 #session 来获取这些对象了。需要手动将这些对象添加到模板上下文中!

  1. 我们修改 IndexController:
java 复制代码
@Controller
public class IndexController {
    @RequestMapping("/index")
    public String index(HttpServletRequest request, HttpSession session, Model model) {
        // 使用模型传递数据
        model.addAttribute("message", "欢迎访问 XXX 网站");
        model.addAttribute("user",
                session.getAttribute("user") != null ? session.getAttribute("user") : "David");
        return "index";
    }
}
  1. 修改 index.html:
html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <p th:text="'您好,' + ${user}" />
    <p th:text="${message}" />
</body>
</html>
  1. 运行看效果:

七、条件语句

Java 的条件语句有两种:if 判断语句和 switch 分支语句,Thymeleaf 模板引擎也提供了这两种语句,可以显示或隐藏网页中的一些特殊内容。

th:if 是 Thymeleaf 的判断语句,支持下表所示的比较运算符。

例如,如果后端发送的 num 是 100,就显示"您充值的金额为 100",前端的代码如下:

html 复制代码
​​​​<div th:if="*{num} == 100">
​​​​     <p>您充值的金额为100</p>
​​​​</div>​​

上述代码也可以写成英文替代符号形式:

html 复制代码
​​​​<div th:if="*{num} eq 100">
​​​​     <p>您充值的金额为100</p>
​​​​</div>​​

如果后端发送的 num 不等于 100,则不会显示 th:if 标签内的任何内容。

如果 th:if 需要同时判断多个条件,可以使用下表所示的逻辑运算符:

例如,如果后端发送的 name是张三,并且 age 大于或等于 18,则显示"张三-成年人",代码如下:

html 复制代码
​​​​<div th:if="*{name} == 张三 and age >= 18 ">
​​​​     <p>张三-成年人</p>
​​​​</div>​​

逻辑运算符中没有取反运算,因为 Thymeleaf 使用 th:unless 标签来取 th:if 标签的反结果,相当于 Java 里 else 语句的效果。例如,后端发送的 age 如果大于或等于 18 则显示成年人,小于 18 则显示未成年人,代码如下:

html 复制代码
​​​​<div th:if="age >= 18 ">
​​​​     <p>成年人</p>
​​​​</div>
​​​​<div th:unless="age >= 18 ">
​​​​     <p>未成年人</p>
​​​​</div>​​

【 示例:判断某个人是否是成年人 】

  1. 创建 IndexController 控制器类,为映射"/index"的方法添加 Model 参数,分别将该参数的 name(姓名)属性赋值为 "Leon"、age(年龄)属性赋值为 17,最后跳转至 main.html 页面。
java 复制代码
@Controller
public class IndexController {
    @RequestMapping("/index")
    public String index(Model model) {
        // 使用模型传递数据
        model.addAttribute("name", "Leon");
        model.addAttribute("age", 17);
        return "main";
    }
}
  1. 在 main.html 中获取 Model 的 name 和 age 属性,如果姓名不为空,则判断年龄是否小于 18 岁,小于 18 岁则这个人是未成年人,否则这个人是成年人。代码如下:
html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
</head>
<body>
  <div th:if="*{name}!=null">
    <p th:text="*{name}" />
    <p th:if="*{age}<18">未成年人</p>
    <p th:unless="*{age}<18">成年人</p>
  </div>
</body>
</html>
  1. 看下效果:

除了判断语句,Thymeleaf 还支持 switch 分支语句,需要用到 th:switch 和 th:case 这两个标签,其语法如下:

html 复制代码
​​​​<div th:switch="*{属性名}">
​​​​    <div th:case="值1"> </div>
​​​​    <div th:case="值2"> </div>
	...
​​​​</div>​

如果 th:switch 读出的属性值与某个 th:case 的值相等,就会显示该 th:case 标签中的内容。


八、循环语句

虽然 Java 常用的循环语句有 while 循环语句和 for 循环语句,但是 Thymeleaf 中的循环语句既不是 while,也不是 for,而是 th:each。可以把 th:each 理解为遍历、迭代的意思。

th:each 的语法比较特殊,比较像 Java 语言中的 foreach 循环。th:each 只能读取后端发来的队列对象(常用 List 类型),然后遍历队列中的所有元素,每取出一个元素就会将其保存在一个临时的循环变量中,其语法如下:

html 复制代码
​​​​<div th:each="临时变量:${队列对象}">
​​​​    <div th:text="${临时变量.属性}"></div>
​​​​    <div th:text="${临时变量.方法())}"></div>
​​​​</div>​​

类似的 foreach 语法如下:

java 复制代码
​​​​List list = new ArrayList();
​​​​for (Object o : list) {
​​​​    o.getClass();
}

Thymeleaf 可以自动创建一个遍历状态变量,该变量名称为"临时变量名称+Stat",调用${临时变量Stat.index}可获得遍历的行索引,第一行的索引为 0。例如,遍历人员列表的行索引:

html 复制代码
<div th:each="people:${list}">
​​​​    <div th:text="'当前为第' + ${peopleStat.index} + '行'"></div>
​​​​</div>​​

【 示例:打印存储在队列里的人员的姓名、年龄和性别 】

  1. 首先创建人员实体类,类中包含姓名、年龄和性别3个属性,同时要包含构造方法和属性的 Getter/ Setter 方法。代码如下:
java 复制代码
public class People {
    private String name;
    private Integer age;
    private String sex;

    public People(String name, int age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Integer getAge() {
        return age;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getSex() {
        return sex;
    }
}
  1. 然后创建 IndexController 控制器类,创建 4 个 People 对象并保存在 List 队列中,将队列保存在 Model中,跳转至 main.html 页面。IndexController 类的代码如下:
java 复制代码
@Controller
public class IndexController {
    @RequestMapping("/index")
    public String index(Model model) {
        List<People> list = new ArrayList<>();
        list.add(new People("David", 26, "Male"));
        list.add(new People("Leon", 17, "Male"));
        list.add(new People("Rose", 21, "Female"));
        list.add(new People("Steven", 34, "Male"));
        model.addAttribute("peoples", list);
        return "main";
    }
}
  1. 在 main.html 中使用 th:each 标签遍历人员队列,并将每个人员的数据保存在 people 变量中,在页面中打印每个人员的姓名、年龄和性别数据,代码如下:
html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
</head>
<body>
  <div th:each="people:${peoples}">
    <a th:text="${peopleStat.index + 1} + '号'"></a>
    <a  th:text="',姓名:' + ${people.name}"></a>
    <a  th:text="',年龄:' + ${people.age}"></a>
    <a  th:text="',性别:' + ${people.sex}"></a>
  </div>
</body>
</html>
  1. 运行看效果:

九、~{} 表达式

很多网站的页面会共用同一个页面内容。例如,网站头部的菜单、网站底部的声明,有些网站还会共用两侧的广告栏。这些被多个页面重复使用的页面板块通常会被单独保存成一个 .html 文件。为了能够把 .html 文件嵌入其他页面中,Thymeleaf 提供了 ~{} 表达式,被插入的片段必须通过 th:fragment 标签定义。

【 示例:在主页插入顶部的登录菜单和底部的声明页面 】

  1. 首先要创建 3 个 .html 文件,index.html 为主页,bottom 文件夹下的 foot.html 为所有网页共用的底部页面,top 文件夹下的 head.html 为所有网页共用的底部页面。3 个文件的位置如图所示:
  1. 在 head.html 文件中,创建"登录"和"注册"两个超链接,并使用 th:fragment 将最外层的 div 定义为 "login" 代码片段,这样其他页面通过嵌入 "login" 就可以展示此顶部页面。head.html 的代码如下:
html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
</head>
<div th:fragment="login">
  <div style="float: right">
    <a href="#">登录</a>&nbsp;&nbsp;<a href="#">注册</a>
  </div>
</div>
</html>
  1. 在 foot.html 文件中,模拟展示一行简易的声明文字,然后将最外层的 div 定义为 "foot" 代码片段,其他页面通过嵌入 "foot" 就可以展示此底部页面。foot.html 的代码如下:
html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
</head>
<div th:fragment="foot">
  <div style="width: 100%; position: fixed; bottom: 0px; text-align: center;">
    <p>联系我们 XXXX公司 公安备案XXXXXXXXXX</p>
  </div>
</div>
</html>
  1. 在 index.html 主页文件中,通过 th:include 标签插入刚才写好的顶部和底部。例如:~{top/head::login} 是插入顶部片段的表达式,其含义为:此处插入的代码片段来自 top 目录下的 head.html 文件,代码片段的名称为 login。~{bottom/foot::foot} 同理。th:fragment 标签定义的代码片段是什么,就会在 th:include 内显示什么。index.html 的代码如下:
html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <div th:include="~{top/head::login}"></div>
    <br><br>
    <div style="text-align: center">
        <h1>欢迎来到 XXX 网站</h1>
    </div>
    <br><br>
    <div th:include="~{bottom/foot::foot}"></div>
</body>
</html>
  1. 编写完所有 .html 文件后,创建一个简单 Controller 类以跳转至主页,代码如下:
java 复制代码
@Controller
public class IndexController {
    @RequestMapping("/index")
    public String index() {
        return "index";
    }
}
  1. 启动查看效果:
相关推荐
苹果酱05675 分钟前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
武昌库里写JAVA9 分钟前
【MySQL】7.0 入门学习(七)——MySQL基本指令:帮助、清除输入、查询等
spring boot·spring·毕业设计·layui·课程设计
刘大辉在路上3 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
追逐时光者5 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~5 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581365 小时前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳6 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾6 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
黑胡子大叔的小屋6 小时前
基于springboot的海洋知识服务平台的设计与实现
java·spring boot·毕业设计