Thymeleaf 基础入门笔记(SpringBoot页面展示)

一、Thymeleaf 概述

1、概述
  • 开发传统Java WEB工程时,我们可以使用JSP页面模板语言,但是在SpringBoot中已经不推荐使用JSP了。
  • Thymeleaf 是一个页面展示的模板引擎,跟 Velocity、FreeMarker 类似
  • Thymeleaf 是SpringBoot官方所推荐使用的
2、Thymeleaf 的特点
  • 动静结合:Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外的属性来达到模板+数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。
  • 开箱即用:它提供标准和spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、该jstl、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。
  • 多方言支持:Thymeleaf 提供spring标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。
  • 与SpringBoot完美整合:SpringBoot提供了Thymeleaf的默认配置,并且为Thymeleaf设置了视图解析器,我们可以像以前操作jsp一样来操作Thymeleaf。代码几乎没有任何区别,就是在模板语法上有区别。

二、Thymeleaf 案例

Thymeleaf 基础入门笔记(SpringBoot页面展示)_第1张图片

第一步:引入 SpringBoot 相关构建
  • spring-boot-starter-parent Spring Boot的父级依赖,表示当前的项目就是Spring Boot项目
  • spring-boot-starter-web 导入web场景的所有依赖
  • druid-spring-boot-starter 具有Druid支持的Spring Boot,可帮助您简化Spring Boot中的Druid配置
  • spring-boot-starter-jdbc 通过HikariCP连接池使用JDBC的入门
  • mysql-connector-java 用于MySQL的JDBC 驱动程序
  • mybatis-spring-boot-starter SpringBoot 整合 MyBatis
  • spring-boot-starter-test 导入junit 测试的所有依赖
  • mapper-spring-boot-starter 通用Mapper启动器
第二步:编写 Spring Boot 启动类 Application.java
package cn.lemon;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan("cn.lemon.dao")/*@MapperScan("dao所在的包"),自动搜索包中的接口,产生dao的代理对象*/
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
第三步:在 resource 中新建 application.properties 配置文件
#修改端口为 ;1000,系统默认端口为:8080
server.port=1000

#如果想在控制台打印日志是需要配置的,因为我们记录的log级别是debug,默认是显示info以上
#SpringBoot通过`logging.level.*=debug`来配置日志级别,*填写包名
logging.level.cn.lemon=debug

#数据库连接配置
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/springboot
jdbc.username=root
jdbc.password=lemon

# mybatis 别名扫描
mybatis.type-aliases-package=cn.lemon.domain
# mapper.xml文件位置,如果没有映射文件,请注释掉
mybatis.mapper-locations=classpath:mappers/*.xml
第四步:编写 JdbcConfig.java ,用于读取属性文件 application.properties(默认读取)
package cn.lemon.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class JdbcConfig {
    @Bean
    @ConfigurationProperties(prefix = "jdbc")
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }
}
第五步:新建实体类 User.java,对应数据库表
package cn.lemon.domain;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

@Table(name = "tb_user")
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")/*可以省略*/
    private Integer id;
    @Column(name = "user_name")
    private String userName;
    private String password;
    private String name;
    private Integer age;
    private Integer sex;
    private Date birthday;
    private Date created;
    private Date updated;
    private String note;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

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

    public Integer getSex() {
        return sex;
    }

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

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public Date getCreated() {
        return created;
    }

    public void setCreated(Date created) {
        this.created = created;
    }

    public Date getUpdated() {
        return updated;
    }

    public void setUpdated(Date updated) {
        this.updated = updated;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}
第六步:新建数据访问层dao 中的接口 UserDao.java
package cn.lemon.dao;

import cn.lemon.domain.User;
import tk.mybatis.mapper.common.Mapper;

public interface UserDao extends Mapper<User> {

}
第七步:编写测试类,测试一下 UserDao.java
package cn.lemon.dao;

import cn.lemon.domain.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserDaoTest {
    @Autowired
    private UserDao userDao;

    @Test
    public void testFindAll() {
        List<User> userList = userDao.selectAll();
        for (User user : userList) {
            System.out.println(user.getId() + "\t" + user.getUserName() + "\t" + user.getPassword());
        }
    }
}
第八步:添加拦截器(也可以不添加)

首先我们定义一个拦截器:

package cn.lemon.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginInterceptor implements HandlerInterceptor {
    private Logger logger = LoggerFactory.getLogger(LoginInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.debug("处理器执行之前执行");
        return true;/*若返回 false,处理器不会跳转*/
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.debug("处理器执行之后执行");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.debug("完成页面跳转后执行");
    }
}

然后定义配置类,注册拦截器:

package cn.lemon.config;

import cn.lemon.interceptor.LoginInterceptor;
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;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    /**
     * 通过@Bean注解,将我们定义的拦截器注册到Spring容器
     */
    @Bean
    public LoginInterceptor loginInterceptor() {
        return new LoginInterceptor();
    }

    /**
     * 重写接口中的addInterceptors方法,添加自定义拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 通过registry来注册拦截器,通过addPathPatterns来添加拦截路径
        registry.addInterceptor(this.loginInterceptor()).addPathPatterns("/**");
    }
}
第九步:编写控制器controller/UserController.java
package cn.lemon.controller;

import cn.lemon.dao.UserDao;
import cn.lemon.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
public class UserController {
    @Autowired
    private UserDao userDao;

    @GetMapping("/all")
    public String findUserAll(Model model) {
        List<User> userList = userDao.selectAll();//查询所有用户
        model.addAttribute("users", userList);
        return "users";
    }
}
第十步:引入启动器

spring-boot-starter-thymeleaf
SpringBoot会自动为Thymeleaf注册一个视图解析器,与解析JSP的InternalViewResolver类似,Thymeleaf也会根据前缀和后缀来确定模板文件的位置:
Thymeleaf 基础入门笔记(SpringBoot页面展示)_第2张图片

  • 默认前缀:classpath:/templates/
  • 默认后缀:.html

所以如果我们返回视图:users,会指向到 classpath:/templates/users.html

第十一步:编写 users.html 静态页面
  • 根据上面的代码,模板默认放在classpath下的templates文件夹中
  • 注意:把html 的名称空间,改成:xmlns:th="http://www.thymeleaf.org"

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页title>
    <style type="text/css">
        table {border-collapse: collapse; font-size: 14px; width: 80%; margin: auto}
        table, th, td {border: 1px solid darkslategray;padding: 10px}
    style>
head>
<body>
<div style="text-align: center">
    <span style="color: darkslategray; font-size: 30px">欢迎光临!span>
    <hr/>
    <table class="list">
        <tr>
            <th>编号th>
            <th>姓名th>
            <th>用户名th>
            <th>年龄th>
            <th>性别th>
            <th>生日th>
            <th>备注th>
        tr>
        
        <tr th:each="user : ${users}">
            <td th:text="${user.id}">1td>
            <td th:text="${user.name}">张三td>
            <td th:text="${user.userName}">zhangsantd>
            <td th:text="${user.age}">20td>
            <td th:text="${user.sex} == 1 ? '': ''">td>
            <td th:text="${#dates.format(user.birthday, 'yyyy-MM-dd')}">1980-02-30td>
            <td th:text="${user.note}">1td>
        tr>
    table>
div>
body>
html>
第十二步:运行,测试

Thymeleaf 基础入门笔记(SpringBoot页面展示)_第3张图片

三、Thymeleaf 模板缓存

Thymeleaf会在第一次对模板解析之后进行缓存,极大的提高了并发处理能力。但是这给我们开发带来了不便,修改页面后并不会立刻看到效果,我们开发阶段可以关掉缓存使用:

#开发阶段关闭thymeleaf的模板缓存
spring.thymeleaf.cache=false

注意

  • 在IDEA中,我们需要在修改页面后按快捷键:Ctrl + Shift + F9 对项目进行rebuild才可以。

四、Thymeleaf 详解

1、标准表达式语法——变量表达式

  • 变量表达式即OGNL表达式或Spring EL表达式(在Spring术语中也叫model attributes)。如:${session.user.name}
  • 它们将以HTML标签的一个属性来表示:
<span th:text="${book.author.name}">  
<li th:each="book : ${books}"> 

2、标准表达式语法——选择或星号表达式

  • 选择表达式很像变量表达式,不过它们用一个预先选择的对象来代替上下文变量容器(map)来执行,如:*{customer.name}
  • 被指定的object由th:object属性定义:
<div th:object="${book}">  
      ...  
      <span th:text="*{title}">...span>  
      ...  
div> 

3、标准表达式语法——URL表达式

  • URL表达式指的是把一个有用的上下文或回话信息添加到URL,这个过程经常被叫做URL重写: @{/order/list}
  • URL还可以设置参数: @{/order/details(id=${orderId})}
  • 相对路径:@{../documents/report}

让我们看这些表达式:

<form th:action="@{/createOrder}">  
<a href="main.html" th:href="@{/main}">

4、变量表达式和星号表达的什么区别

  • 如果不考虑上下文的情况下,两者没有区别;星号语法评估在选定对象上表达,而不是整个上下文,什么是选定对象?就是父标签的值,如下:
<div th:object="${session.user}">
    <p>Name: <span th:text="*{firstName}">Sebastianspan>.p>
    <p>Surname: <span th:text="*{lastName}">Pepperspan>.p>
    <p>Nationality: <span th:text="*{nationality}">Saturnspan>.p>
div>

这是完全等价于:

<div th:object="${session.user}">
    <p>Name: <span th:text="${session.user.firstName}">Sebastianspan>.p>
    <p>Surname: <span th:text="${session.user.lastName}">Pepperspan>.p>
    <p>Nationality: <span th:text="${session.user.nationality}">Saturnspan>.p>
div>

当然,美元符号和星号语法可以混合使用:

<div th:object="${session.user}">
    <p>Name: <span th:text="*{firstName}">Sebastianspan>.p>
    <p>Surname: <span th:text="${session.user.lastName}">Pepperspan>.p>
    <p>Nationality: <span th:text="*{nationality}">Saturnspan>.p>
div>

5、表达式支持的语法

5-1、字面(Literals)
  • 文本文字(Text literals): 'one text', 'Another one!',…
  • 数字文本(Number literals): 0, 34, 3.0, 12.3,…
  • 布尔文本(Boolean literals): true, false
  • 空(Null literal): null
  • 文字标记(Literal tokens): one, sometext, main,…
5-2、文本操作(Text operations)
  • 字符串连接(String concatenation): +
  • 文本替换(Literal substitutions): |The name is ${name}|
5-3、算术运算(Arithmetic operations)
  • 二元运算符(Binary operators): +, -, *, /, %
  • 减号(单目运算符)Minus sign (unary operator): -
5-4、布尔操作(Boolean operations)
  • 二元运算符(Binary operators):and, or
  • 布尔否定(一元运算符)Boolean negation (unary operator):!, not
5-5、比较和等价(Comparisons and equality)
  • 比较(Comparators): >, <, >=, <= (gt, lt, ge, le)
  • 等值运算符(Equality operators):==, != (eq, ne)
5-6、条件运算符(Conditional operators)
  • If-then: (if) ? (then)
  • If-then-else: (if) ? (then) : (else)
  • Default: (value) ?: (defaultvalue)

所有这些特征可以被组合并嵌套:

'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))

五、常用的 th 标签

Thymeleaf 基础入门笔记(SpringBoot页面展示)_第4张图片
还有非常多的标签,这里只列出最常用的几个,由于一个标签内可以包含多个th:x属性,其生效的优先级顺序为:include,each,if/unless/switch/case,with,attr/attrprepend/attrappend,value/href,src ,etc,text/utext,fragment,remove

六、常用的使用方法

1、赋值、字符串拼接

 <p  th:text="${collect.description}">descriptionp>
 <span th:text="'Welcome to our application, ' + ${user.name} + '!'">

字符串拼接还有另外一种简洁的写法:

<span th:text="|Welcome to our application, ${user.name}!|">

2、条件判断 If/Unless

Thymeleaf中使用th:if和th:unless属性进行条件判断,下面的例子中,标签只有在th:if中条件成立时才显示:

<a th:if="${myself=='yes'}" > i> a>
Logina>

th:unless于th:if恰好相反,只有表达式中的条件不成立,才会显示其内容。
也可以使用 (if) ? (then) : (else) 这种语法来判断显示的内容

3、for 循环

  <tr  th:each="collect,iterStat : ${collects}"> 
     <th scope="row" th:text="${collect.id}">1th>
     <td >
        <img th:src="${collect.webLogo}"/>
     td>
     <td th:text="${collect.url}">Marktd>
     <td th:text="${collect.title}">Ottotd>
     <td th:text="${collect.description}">@mdotd>
     <td th:text="${terStat.index}">indextd>
 tr>

iterStat称作状态变量,属性有:

  • index:当前迭代对象的index(从0开始计算)
  • count: 当前迭代对象的index(从1开始计算)
  • size:被迭代对象的大小
  • current:当前迭代变量
  • even/odd:布尔值,当前循环是否是偶数/奇数(从0开始计算)
  • first:布尔值,当前循环是否是第一个
  • last:布尔值,当前循环是否是最后一个

4、URL

URL在Web应用模板中占据着十分重要的地位,需要特别注意的是Thymeleaf对于URL的处理是通过语法@{…}来处理的。 如果需要Thymeleaf对URL进行渲染,那么务必使用th:hrefth:src等属性,下面是一个例子:


 <a  th:href="@{/standard/{type}(type=${type})}">viewa>
 

<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">viewa>

设置背景

<div th:style="'background:url(' + @{/} + ');'">div>

根据属性值改变背景:

<div class="media-object resource-card-image"  th:style="'background:url(' + @{(${collect.webLogo}=='' ? 'img/favicon.png' : ${collect.webLogo})} + ')'" >div>

说明:

  • 上例中URL最后的(orderId=${o.id}) 表示将括号内的内容作为URL参数处理,该语法避免使用字符串拼接,大大提高了可读性
  • @{...}表达式中可以通过{orderId}访问Context中的orderId变量
  • @{/order}是Context相关的相对路径,在渲染时会自动添加上当前Web应用的Context名字,假设context名字为app,那么结果应该是/app/order

5、内联js

内联文本:[[…]]内联文本的表示方式,使用时,必须先用th:inline=”text/javascript/none”激活,th:inline可以在父级标签内使用,甚至作为body的标签。内联文本尽管比th:text的代码少,不利于原型显示。
js附加代码:

/*[+
var msg = 'This is a working application';
+]*/

js移除代码:

/*[- */
var msg = 'This is a non-working template';
/* -]*/

6、内嵌变量

为了模板更加易用,Thymeleaf还提供了一系列Utility对象(内置于Context中),可以通过#直接访问:

  • dates : java.util.Date**的功能方法类。
  • calendars : 类似#dates,面向java.util.Calendar
  • numbers : 格式化数字的功能方法类
  • strings : 字符串对象的功能类,contains,startWiths,prepending/appending等等
  • objects: 对objects的功能类操作
  • bools: 对布尔值求值的功能方法
  • arrays:对数组的功能类方法
  • lists: 对lists功能类方法
  • sets
  • maps

下面用一段代码来举例一些常用的方法:
dates

/*
 * Format date with the specified pattern
 * Also works with arrays, lists or sets
 */
${#dates.format(date, 'dd/MMM/yyyy HH:mm')}
${#dates.arrayFormat(datesArray, 'dd/MMM/yyyy HH:mm')}
${#dates.listFormat(datesList, 'dd/MMM/yyyy HH:mm')}
${#dates.setFormat(datesSet, 'dd/MMM/yyyy HH:mm')}
 
/*
 * Create a date (java.util.Date) object for the current date and time
 */
${#dates.createNow()}
 
/*
 * Create a date (java.util.Date) object for the current date (time set to 00:00)
 */
${#dates.createToday()}

strings

/*
 * Check whether a String is empty (or null). Performs a trim() operation before check
 * Also works with arrays, lists or sets
 */
${#strings.isEmpty(name)}
${#strings.arrayIsEmpty(nameArr)}
${#strings.listIsEmpty(nameList)}
${#strings.setIsEmpty(nameSet)}
 
/*
 * Check whether a String starts or ends with a fragment
 * Also works with arrays, lists or sets
 */
${#strings.startsWith(name,'Don')}                  // also array*, list* and set*
${#strings.endsWith(name,endingFragment)}           // also array*, list* and set*
 
/*
 * Compute length
 * Also works with arrays, lists or sets
 */
${#strings.length(str)}
 
/*
 * Null-safe comparison and concatenation
 */
${#strings.equals(str)}
${#strings.equalsIgnoreCase(str)}
${#strings.concat(str)}
${#strings.concatReplaceNulls(str)}
 
/*
 * Random
 */
${#strings.randomAlphanumeric(count)}

七、使用thymeleaf布局

使用thymeleaf布局非常的方便
在/resources/templates/目录下创建footer.html,内容如下:


<html xmlns:th="http://www.thymeleaf.org">
<body>
    <div th:fragment="copy(title)">
        © 2011 The Good Thymes Virtual Grocery
        <span th:text="${title}">abcdefa234234span>
    div>
body>
html>

在页面任何地方引入:

<body> 
  <div th:include="footer:: copy('pbj44')">div>
  <div th:replace="footer:: copy('pbj44')">div>
body>

th:includeth:replace区别,include只是加载,replace是替换
返回的HTML如下:

<body> 
	<div> © 2016 div> 
	<footer>© 2016 footer> 
body>

下面是一个常用的后台页面布局,将整个页面分为头部,尾部、菜单栏、隐藏栏,点击菜单只改变content区域的页面

<body class="layout-fixed">
    <div th:fragment="navbar"  class="wrapper"  role="navigation">
    	<div th:replace="fragments/header:: header">Headerdiv>
    	<div th:replace="fragments/left:: left">leftdiv>
    	<div th:replace="fragments/sidebar:: sidebar">sidebardiv>
    	<div layout:fragment="content" id="content" >div>
    	<div th:replace="fragments/footer:: footer">footerdiv>
	div>
body>

八、单元测试(了解)

pom.xml中加载测试启动器

		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-testartifactId>
			<scope>testscope>
		dependency>

测试类:

@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloControllerTest {

    private MockMvc mvc;

    @Before
    public void setUp() throws Exception {
        mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
    }

    @Test
    public void getHello() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("hello spring boot2")));
    }

    @Test
    public void getHello2() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
    }

mockmv常用的用法:

  • mockMvc.perform执行一个请求;
  • MockMvcRequestBuilders.get("/user/1")构造一个请求
  • ResultActions.andExpect添加执行完成后的断言
  • ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情,比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
  • ResultActions.andReturn表示执行完成后返回相应的结果。

九、开发环境的调试

热启动在正常开发项目中已经很常见了吧,虽然平时开发web项目过程中,改动项目启重启总是报错;但springBoot对调试支持很好,修改之后可以实时生效,需要添加以下的配置:

 <dependencies>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-devtoolsartifactId>
        <optional>trueoptional>
    dependency>
dependencies>
 
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-maven-pluginartifactId>
            <configuration>
                <fork>truefork>
            configuration>
        plugin>
plugins>
build>

该模块在完整的打包环境下运行的时候会被禁用。如果你使用java -jar启动应用或者用一个特定的classloader启动,它会认为这是一个“生产环境”。
在application.yml文件加入spring:thymeleaf:cache: false 配置

如果你通过上面的步骤还没实现想要的热部署效果,可以继续做以下两个配置:

1、开启Java Compiler的自动build

Thymeleaf 基础入门笔记(SpringBoot页面展示)_第5张图片

2、按住Ctrl + Shift +Alt + / 选择Registry,按照下图标注配置

Thymeleaf 基础入门笔记(SpringBoot页面展示)_第6张图片
或者不进行热部署配置直接点Ctrl+F9

你可能感兴趣的:(Spring,Boot)