1 Thymeleaf
1.1 基本介绍
(1)官方文档:Tutorial: Using Thymeleaf
(2)Thymeleaf 是什么
- Thymeleaf 是一个跟 Velocity、FreeMarker 类似的模板引擎,可完全替代 JSP
- Thymeleaf 是一个 java 类库,它是一个 xml/xhtml/html5 的模板引擎,可以作为mvc的web应用的view层
(3)Thymeleaf的优点
- 实现 JSTL、OGNL 表达式效果,语法类似,上手快
- Thymeleaf 模板页面无需服务器渲染,也可以被浏览器运行,页面简洁
- SpringBoot支持 Thymeleaf、Velocity、FreeMarker
(4)Thymeleaf的缺点
- 并不是一个高性能的引擎,适用于单体应用
- 如果要做一个高并发的应用,选择前后端分离更好,但是作为SpringBoot推荐的模板引擎,还是需要了解一下
1.2 Thymeleaf机制说明
(1)Thymeleaf 是服务器渲染技术,页面数据是在服务器端进行渲染的
(2)比如:manage.html 中一段 Thymeleaf 代码,是在客户请求该页面时,由 Thymeleaf 模板引擎完成处理的(在服务器端完成),并将结果返回。因此使用了Thymeleaf,并不是前后端分离
<tr bgcolor="pink" th:each="user:${users}">
<td th:text="${user.id}">a</td>
<td th:text="${user.name}">b</td>
<td th:text="${user.age}">c</td>
<td th:text="${user.eamil}">d</td>
<td th:text="${user.password}">e</td>
</tr>
1.3 Thymeleaf语法
3.3.1 表达式
3.3.1.1 表达式一览
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${...} | 获取请求域、session域、对象等值 |
选择变量 | *{...} | 获取上下文对象值 |
消息 | #{...} | 获取国际化等值 |
链接 | @{...} | 生成链接 |
片段表达式 | ~{...} | jsp:include 作用,引入公共页面片段 |
1.3.1.2 字面量
文本值:'hello'
数字:10,7,36.8
布尔值:true,false
空值:null
变量:name,age
注意:变量不能有空格
1.3.1.3 文本操作
字符串拼接:+
变量替换:|age= ${age}|
1.3.2 运算符
(1)数学运算
运算符:+,-,*,/,%
(2)布尔运算
运算符:and,or
一元运算符:!,not
(3)比较运算
比较:>,<,>=,<=( gt,lt,ge,le)
等式:==,!=(eq,ne)
(4)条件运算
If-then:(if) ? (then)
If-then-else:(if)?(then):(else)
Default:(value)?:(defaultvalue)
1.3.3 th属性
html有的属性,Thymeleaf基本都有,而常用的属性大概有七八个。其中th属性执行的优先级从1到8,数字越低,优先级越高
- th:text:设置当前元素的文本内容,相同功能的还有th:utext,两者的区别在于前者不会转义html标签,后者会。优先级不高,order=7
- th:value:设置当前元素的value值。类似修改指定属性的还有th:src,th:href。优先级不高,order=6
- th:each:遍历循环元素,和th:text或th:value一起使用。注意该属性修饰的标签位置,详细往后看。优先级很高,order=2
<tr th:each="prod : ${prods}">
# ${prod.name}的值会替换Onions
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStar : ${prods}" th:class="${iterStar.odd}?'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
- th:if:条件判断,类似的还有th:unless,th:switch,th:case。优先级较高,order=3
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manage</p>
<p th:case="*">User is some other thing</p>
</div>
- th:insert:代码块引入,类似的还有th:replace,th:include,三者的区别较大,若使用不恰当会破坏html结构,常用于公共代码块提取的场景。优先级最高,order=1
- th:fragment:定义代码块,方便被th:insert引用。优先级最低,order=8
- th:object:声明变量,一般和*{}一起配合使用,达到偷懒的效果。优先级一般,order=4
- th:attr:修改任意属性,实际开发中用的较少,因为有丰富的其他th属性帮忙,类似的还有th:attrappend,th:attrprepend。优先级一般,order=5
1.3.4 使用Thymeleaf的th属性需要注意的点
- 若要使用Thymeleaf语法,首先要声明名称变量:xmlns:th="http://www.thymeleaf.org"
- 设置文本内容 th:text,设置 input 的值 th:value,循环输出 th:each,条件判断 th:if,插入代码块 th:insert,定义代码块 th:fragment,声明变量 th:object
- th:each 的用法需要格外注意:如果你要循环一个div中的标签,则th:each属性必须放在p标签上。若将th:each属性放在div上,则循环的是整个div
- 变量表达式中提供了很多内置方法,该内置方法是用#开头,请不要与 #{} 消息表达式弄混
1.4 Thymeleaf综合案例
需求说明:使用SpringBoot + Thymeleaf 完成简单的用户登录-列表功能
代码实现:
(1)创建maven项目,项目名使用 springboot-usersys
(2)要支持Thymeleaf,需要引入starter-Thymeleaf,在 pom.xml 配置(其他依赖是springboot项目开发需要)
<!-- 导入 springboot 父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
</parent>
<dependencies>
<!--引入Thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<!-- 导入 web 项目场景启动器,会自动导入和 web 开发相关依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 引入配置处理器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
</dependencies>
Thymeleaf 默认在类路径下的templates目录下去找对应的html文件,查看 ThymeleafProperties 源码即可知
(3)在resources目录下(maven项目的类路径就是resources)新建 templates 目录,在 templates 目录下创建 login.html 和 manage.html(这里使用了 Thymeleaf)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>login</title>
<style>
* {
margin: 0;
padding: 0;
}
html {
height: 100%;
}
body {
height: 100%;
}
.container {
height: 100%;
background-image: linear-gradient(to right, #999999, #330867);
}
.login-wrapper {
background-color: bisque;
width: 358px;
height: 588px;
border-radius: 15px;
padding: 0 50px;
position: relative;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
.header {
font-size: 38px;
font-weight: bold;
text-align: center;
line-height: 200px;
}
.input-item {
display: block;
width: 100%;
margin-bottom: 20px;
border: 0;
padding: 10px;
border-bottom: 1px solid rgb(128,125,125);
font-size: 15px;
outline: none;
}
.input-item::placeholder {
text-transform: uppercase;
}
.btn {
text-align: center;
padding: 10px;
width: 100%;
margin-top: 40px;
background-image: linear-gradient(to right,#a6c1ee, #fbc2eb);
color: #fff;
}
.msg {
text-align: center;
line-height: 88px;
}
a {
text-decoration-line: none;
color: #abc1ee;
}
</style>
</head>
<body>
<div class="container">
<div class="login-wrapper">
<div class="header">Login</div>
<form class="form-wrapper" action="#" th:action="@{/login}" method="post">
<label style="color: red" th:text="${msg}"></label>
<input type="text" name="name" placeholder="username" class="input-item">
<input type="password" name="password" placeholder="password" class="input-item" />
<button type="submit" class="btn" >Login</button>
<button type="reset" class="btn">Login</button>
</form>
<div class="msg">
Don't have account?
<a href="#">Sign up</a>
</div>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>管理后台</title>
</head>
<body bgcolor="#CED3FE">
<a href="#">返回管理页面</a>
<a href="#" th:href="@{/}">安全退出</a> 欢迎您:[[${session.adminLogin.name}]]
<hr/>
<div style="text-align: center">
<h1>管理雇员</h1>
<table border="1px" cellspacing="0" bordercolor="green" style="">
<tr bgcolor="#ffc0cb">
<td>id</td>
<td>name</td>
<td>pwd</td>
<td>email</td>
<td>job</td>
</tr>
<tr bgcolor="#ffc0cb" th:each="user:${users}">
<td th:text="${user.id}">a</td>
<td th:text="${user.name}">b</td>
<td th:text="${user.password}">c</td>
<td th:text="${user.email}">d</td>
<td th:text="${user.age}">e</td>
</tr>
</table>
</div>
</body>
</html>
(4)新建软件包bean,在该包下创建两个实体类 User.java 和 Admin.java
package org.wwj.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
private Integer id;
private String name;
private String password;
private Integer age;
private String email;
}
package org.wwj.bean;
import lombok.Data;
@Data
public class Admin {
private String name;
private String password;
}
(5)新建软件包 controller,在该包下创建 IndexController.java 和 AdminController,java
IndexController.java 用于请求转发到登录页面,实现功能:浏览器输入 localhost:8080/ 和localhost:8080/login 均可以访问到登录页面
package org.wwj.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
// 编写方法,转发到登录页
@GetMapping(value = {"/", "/login"})
public String login() {
// 因为引入了 Thymeleaf,Thymeleaf里面包含了视图解析
// 这里就会直接使用视图解析器解析到templates下的模板文件login.html
return "login";
}
}
AdminController,java 用于对登录的校验,以及登录成功页面的跳转。实现功能:用户没有登录直接访问登录成功页面 以及 登录账号/密码 返回错误信息,并跳转到登录页面;如果校验通过就转发到 manage.html 页面显示用户信息
package org.wwj.controller;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.wwj.bean.Admin;
import org.wwj.bean.User;
import java.util.ArrayList;
@Controller
public class AdminController {
//响应用户的登录请求
@PostMapping("/login")
public String login(Admin admin, HttpSession session, Model model) {
// 验证用户是否合法
if (StringUtils.hasText(admin.getName()) && "666".equals(admin.getPassword())){
session.setAttribute("adminLogin", admin);
// 合法,重定向到 manage.html,重定向的请求方式为 get
// 不能使用请求转发,防止刷新页面后表单重复提交
// return "redirect: /manage.html"; 表示要去找映射路径为manage.html的controller方法
return "redirect:/manage.html";
}else {
// 不合法,重新登录
model.addAttribute("msg","账号/密码错误");
return "login";
}
}
// 处理用户的请求到 manage.html
@GetMapping("/manage.html")
public String mainPage(Model model, HttpSession session) {
if (session.getAttribute("adminLogin") != null) {// 这里可以使用集合模拟用户数据,放入 request 域中
ArrayList<User> users = new ArrayList<>();
users.add(new User(1, "关羽~", "666666", 20, "gr@sohu.com"));
users.add(new User(2, "张飞", "666666", 30, "zi@sohu.com"));
users.add(new User(3, "赵云", "666666", 22, "zy@sohu.com"));
users.add(new User(4, "马超", "666666", 28, "me@sohu.com"));
users.add(new User(5, "黄忠", "666666", 50, "hz@sohu.com"));
// 将数据放入到request域中
model.addAttribute("users", users);
// 因为引入了 Thymeleaf,Thymeleaf里面包含了视图解析
// 这里的manage才会使用视图解析器转发到templates目录下相应的页面
return "manage";
}else {
// 返回登录页,并给出提示
model.addAttribute("msg", "你没有登录/请登录");
return "login";
}
}
}
(6)编写并启动主程序
package org.wwj;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext ioc = SpringApplication.run(Application.class, args);
}
}
(7)浏览器输入 localhost:8080/ 进行测试
2 拦截器
2.1 基本介绍
(1)在Spring Boot项目中,拦截器是开发中常用手段,用来做登录验证、性能验证、日志记录等。
(2)使用拦截器基本步骤
- 编写一个拦截器实现 HandlerInterceptor接口
- 将拦截器注册到配置类中(实现WebMvcConfigurer的addInterceptors)
- 指定拦截规则
(3) Spring Boot实现拦截器的方式和springmvc差不多,区别是配置方式不同
2.2 拦截器应用实例
需求说明:使用拦截器防止用户非法访问,例如用户直接在浏览器输入 localhost:8080/manage.html,如果用户没有登录,则返回登录页面,并给出提示信息
代码实现:
在上面Thymeleaf案例的基础上进行拦截
(1)创建软件包 interceptor 用来存放拦截器,在该软件包下创建 LoginInterceptor.java 作为拦截器,该拦截器会对拦截的请求进行登录验证,只有用户是登录状态时才放行,非登录状态转发到登录页面
package org.wwj.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 为了看到访问的URL
String requestURI = request.getRequestURI();
log.info("preHandle拦截到的请求的URL={}",requestURI);
// 进行登录验证
HttpSession session = request.getSession();
if (session.getAttribute("adminLogin") != null) return true;
else {
// 拦截,重新返回到登录页面
request.setAttribute("msg", "你没有登录");
request.getRequestDispatcher("/login").forward(request, response);
return false;
}
}
}
(2)在配置类中注册拦截器,新建软件包 config,在该软件包下创建 WebConfig.java 作为配置类。这里注册拦截器的方式有两种
第一种
package org.wwj.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.wwj.interceptor.LoginInterceptor;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //拦截所有请求
.excludePathPatterns("/", "/login");// 放行登录请求
}
}
第二种
package org.wwj.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.wwj.interceptor.LoginInterceptor;
@Configuration
public class WebConfig{
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/", "/login");
}
};
}
}
2.3 注意事项和细节
(1)URI 和 URL 的区别
URI = Universal Resource Identifier
URL = Universal Resource Locator
Identifier:标识符,Locator:定位器。从字面上来看,URI可以唯一标识一个资源,URL可以提供找到该资源的路径
举例:
URI=/manage.html
URL=http://localhost:8080/manage.html
3 文件上传应用实例
需求说明:演示Spring-Boot 通过表单注册用户,并支持上传图片(简化了 Spring MVC文件上传的方式)
代码实现:
(1)创建 templates/upload.html,要求头像只能选择一个,而宠物可以上传多个图片
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="text-align: center">
<h1>注册用户~</h1>
<form action="#" th:action="@{/upload}" method="post" enctype="multipart/form-data">
用户名:<input type="text" style="width: 150px" name="name"><br/><br/>
电 邮:<input type="text" style="width: 150px" name="email"><br/><br/>
年 龄:<input type="text" style="width: 150px" name="age"><br/><br/>
职 位:<input type="text" style="width: 150px" name="job"><br/><br/>
头 像:<input type="file" style="width: 150px" name="header"><br/><br/>
<!--在该标签尾部加个 multiple 即可选择多张图片-->
宠 物:<input type="file" style="width: 150px" name="photos" multiple><br/><br/>
<input type="submit" value="注册"/>
<input type="reset" value="重新填写"/>
</form>
</div>
<hr/>
</body>
</html>
(2)创建 UploadController.java,对注册的请求进行处理
package org.wwj.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.util.ResourceUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
@Controller
@Slf4j
@SuppressWarnings("all")
public class UploadController {
// 处理转发到用户注册-可以完成文件上传页面
@GetMapping("/upload.html")
public String uploadPage(){
// Thymeleaf 进行视图解析,转发到templates/upload.html
return "upload";
}
// 处理用户注册请求,包含处理文件上传
@PostMapping("/upload")
@ResponseBody
public String upload(String name,
String email,
Integer age,
String job,
MultipartFile header,
MultipartFile[] photos) throws IOException {
// 输出获取到的信息
log.info("上传的信息 name={} email={} age={} job={} header={} photos={}",
name,email,age,job,header.getSize(),photos.length);
// 成功获取到信息,将文件保存到当前项目类路径下的static/images/upload目录下
// 得到类路径
String path = ResourceUtils.getURL("classpath:").getPath();
log.info("path = {}",path);
// 拼接目录
// String fullPath = path+"static/images/upload/";
File file = new File(path + "static/images/upload/");
if (!file.exists()){//如果目录不存在,就创建
file.mkdirs();
}
// 处理头像
if (!header.isEmpty()){
String headerName = header.getOriginalFilename();
header.transferTo(new File(file, headerName));
}
// 处理宠物头像
for (MultipartFile multipartFile : photos) {
String fileName = multipartFile.getOriginalFilename();
multipartFile.transferTo(new File(file,fileName));
}
return "注册成功/文件上传成功";
}
}
(3)在注册拦截器的配置类中放行注册请求
package org.wwj.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.wwj.interceptor.LoginInterceptor;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //拦截所有请求
.excludePathPatterns("/", "/login","/upload.html","/upload");// 放行登录注册请求
}
}
(4)浏览器输入 localhost:8080/upload.html 进行测试
注意事项:
(1)默认单个文件最大1MB,一次上传多文件最大10MB,可以通过yaml配置文件修改该默认参数,在resouces目录下创建application.yml
spring:
servlet:
multipart:
max-file-size: 2MB
max-request-size: 20MB
(2)如果文件名相同,会出现覆盖问题,解决办法如下
对上传的文件名进行处理,前面加一个前缀,保证前缀是是唯一即可
// 处理头像
if (!header.isEmpty()){
String headerName = UUID.randomUUID().toString()+"_"+System.currentTimeMillis() + "_"
+ header.getOriginalFilename();
header.transferTo(new File(file, headerName));
}
// 处理宠物头像
for (MultipartFile multipartFile : photos) {
String fileName = UUID.randomUUID().toString()+"_"+System.currentTimeMillis() + "_"
+ multipartFile.getOriginalFilename();
multipartFile.transferTo(new File(file,fileName));
}
效果如下
(3)解决文件分目录存放问题,如果将文件都上传到一个目录下,当上传文件很多时,会造成访问文件速度变慢,因此可以将文件上传到不同目录,比如一天上传的文件,统一放到一个文件夹 年/月/日,比如2022/11/11目录
编写一个工具类用来动态生成当前日期的目录
package org.wwj.utils;
import java.text.SimpleDateFormat;
import java.util.Date;
public class WebUtils {
// 定义文件上传的路径
public static String UPLOAD_FILE_DIRECTORY = "static/images/upload/";
// 编写方法,生成一个当前日期的目录 年/月/日
public static String getUploadFileDirectory(){
return UPLOAD_FILE_DIRECTORY +
new SimpleDateFormat("yyyy/MM/dd").format(new Date());
}
}
修改 UploadController.java,将固定文件上传目录修改为由工具类方法动态生成
测试效果如下