Thymeleaf模板引擎+Spring整合使用方式的介绍

Posted 陈咬金

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Thymeleaf模板引擎+Spring整合使用方式的介绍相关的知识,希望对你有一定的参考价值。

尊重原创,原文地址为:https://www.cnblogs.com/jiangchao226/p/5937458.html

前言

这个教程介绍了Thymeleaf与Spring框架的集成,特别是SpringMvc框架。

注意Thymeleaf支持同Spring框架的3.和4.版本的集成,但是这两个版本的支持是封装在thymeleaf-spring3和thymeleaf-spring4这两个独立的库中,项目中需要根据实际情况分别引用。

样例代码针对的是spring4.,但一般情况下,spring3.也可以无缝使用,所需要的仅仅是改变一下引用库。

1 Thymeleaf同Spring的整合

Thymeleaf与Spring进行整合后,可以在SpringMVC应用中完全替代JSP文件。

集成后你将:

注意,在使用本教程之前,您应该充分了解Thymeleaf的标准方言。

2 Spring标准方言

为了更加方便,更快捷的集成,Thymeleaf提供了一套能够与Spring正确工作的特有方言。

这套方言基于Thymeleaf标准方言实现,它在类org.thymeleaf.spring.dialect.SpringStandardDialect中,事实上,他继承于org.thymeleaf.standard.StandardDialect中。

除了已经出现在标准方言中的所有功能,Spring中还有以下特点:

  • 不适用OGNL,而是SpringEL做完变量表达式,因此,所有的${...}和*{...}表达式将用Spring的表达式引擎进行处理。
  • 访问应用context中的beans可以使用SpringEL语法:${@myBean.doSomething()}
  • 基于表格处理的新属性:th:field,th:errors和th:errorclass,除此还有一个th:object的新实现,允许它使用表单命令选择器(??)。
  • 一个新的表达式:#themes.code(...),相当于jsp自定义标签中的spring:theme。
  • 在spring4.0集成中的一个新的表达式:#mvc.uri(...),相当于jsp自定义标签中的spring:mvcUrl(...)

注意,上述这些方言特性是不能再普通的TemplateEngine对象中使用的,应该配置一个org.thymeleaf.spring4.SpringTemplateEngine来执行。

一个配置的简单例子:

<bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
    <property name="prefix" value="/WEB-INF/templates/" />
    <property name="suffix" value=".html" />
</bean>

<bean id="templateEngine"   class="org.thymeleaf.spring4.SpringTemplateEngine">
    <property name="templateResolver" ref="templateResolver" />
</bean>

视图和视图解释器

SpringMvc中的视图和视图解释器

Spring有两个符合其模板系统核心的接口:

  • org.springframework.web.servlet.View
  • org.springframework.web.servlet.ViewResolver

视图模型页面在应用中,让我修改和预定义他的行为的页面,可将其作为Bean来定义,视图是负责渲染实际的HTML,通常由一些模板引擎来负责,如JSP和Thymeleaf。

ViewResolvers是一个获取特定操作和语言的的视图对象的对象。通常,controller会向ViewResolvers要求转发到一个特定的视图(视图名为控制器返回的字符串)。然后在顺序执行应用中所有的视图解析器,直到有一个能够解析这个视图。在这种情况下,视图对象返回并控制传递给他的一个html渲染相。

注意,在一个应用中,并不是所有的页面都被定义为视图,但是只有那些行为我们希望以特定的方式进行非标准方式操作或者进行特定配置,例如,一些特殊的bean。如果一个ViewResolver请求一个view但没有响应的bean(这是一个常见的情况),一个新的视图对象将被临时创建并返回。

一个SpringMVC中Jsp+JSTL视图解释器的典型配置如下:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
  <property name="prefix" value="/WEB-INF/jsps/" />
  <property name="suffix" value=".jsp" />
  <property name="order" value="2" />
  <property name="viewNames" value="*jsp" />
</bean>

根据他的属性就足够知道他是怎么配置的了:

  • viewClass:建立视图实例的类,在JSP解析的时候所必须的,但是现在我们使用Thymeleaf,所以它是不需要的。
  • prefix和suffix,和Thymeleaf的TemplateResolver对象的方式一直,设置前缀和后缀属性。
  • order:设置在视图解析器查询链中的顺序
  • viewNames:允许定义视图名称(可通过通配符),定义内的视图由视图解析器解析。

Thymeleaf中的视图和视图解析器

Thymeleaf和Spring类似,同样是对应两个接口:

  • org.thymeleaf.spring4.view.ThymeleafView
  • org.thymeleaf.spring4.view.ThymeleafViewResolver

这两个类将用于处理控制器返回Thymeleaf执行的结果。

Thymeleaf视图解析器的配置同样和JSP是非常相似的:

<bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
  <property name="templateEngine" ref="templateEngine" />
  <property name="order" value="1" />
  <property name="viewNames" value="*.html,*.xhtml" />
</bean>

它的templateEngin的值当然是前一章定义的SpringTemplateEngin对象,另外两个参数都是可选的,并且也之前的JSP 视图解析器配置的时候参数含义相同

需要注意一点,我们并不需要配置前缀和后缀,因为这些已经在模板解析器中指定,并会依次传递到模板引擎中。

如果我们想定义一个View的bean并设置一些静态变量该如何做呢?很简单:

<bean name="main" class="org.thymeleaf.spring4.view.ThymeleafView">
<property name="staticVariables">
    <map>
    <entry key="footer" value="foot信息" />
    </map>
 </property>
</bean>

模板配置

Spring基础配置

在与Spring配合使用的时候,Thymeleaf提供了ITemplateResolver和与之相关联的IResourceResolver的与Spring资源处理器相结合的实现,这些是:

  • org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver用于解析模板.
  • org.thymeleaf.spring4.resourceresolver.SpringResourceResourceResolver主要供内部使用.

这个模板解析器允许应用使用标准Spring资源解析语法来解析模板程序,它可以这样配置:

<bean id="templateResolver"
  class="org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver">
    <property name="suffix" value=".html" />
    <property name="templateMode" value="HTML5" />
</bean>

然后就可以像这样使用视图:

@RequestMapping("/doit")
public String doIt() {
    ...
    return "classpath:resources/templates/doit";
}

注意Spring基础的资源解析器不会被默认使用,它只是一个除了Thymeleaf核心所提供的模板资源解析器之外的模板资源解析器。

麝香生长管理系统

示例代码可以从此处下载下载

简介

有很多人都喜欢麝香,每年春天我们都会在小花盆里放上优良的土壤,还有麝香的种子,将它们放在阳光下,耐心的等待它们的生长。

但是今年我们受够了靠贴标签来知道每个花盆里种的是什么,所以我们决定使用Spring+Thymeleaf来制作一个应用,用于管理我们的一个培育目录,这个应用叫:春叶培育管理员系统。

同Thymeleaf教程中的古泰虚拟商店一样,这个春叶培育管理系统将会设计到Spring+Thymeleaf的最重要的部分。

业务层

我们将为我们的应用配置一个简单的业务层,首先看看数据模型:

用几个简单的服务类提供所需的业务方法:

@Service
public class SeedStarterService {

    @Autowired
    private SeedStarterRepository seedstarterRepository; 

    public List<SeedStarter> findAll() {
        return this.seedstarterRepository.findAll();
    }

    public void add(final SeedStarter seedStarter) {
        this.seedstarterRepository.add(seedStarter);
    }

}

和:

@Service
public class VarietyService {

    @Autowired
    private VarietyRepository varietyRepository; 

    public List<Variety> findAll() {
        return this.varietyRepository.findAll();
    }

    public Variety findById(final Integer id) {
        return this.varietyRepository.findById(id);
    }

}

Spring MVC配置

接下来我们需要在应用中建立MVC配置文件,它将不仅包括SpringMvc的资源处理和注解扫描,还创建了模板引擎和视图解释器的实例。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/mvc
                           http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
                           http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    
    
  <!-- **************************************************************** -->
  <!--  RESOURCE FOLDERS CONFIGURATION                                  -->
  <!--  Dispatcher configuration for serving static resources           -->
  <!-- **************************************************************** -->
  <mvc:resources location="/images/" mapping="/images/**" />
  <mvc:resources location="/css/" mapping="/css/**" />
    

  <!-- **************************************************************** -->
  <!--  SPRING ANNOTATION PROCESSING                                    -->
  <!-- **************************************************************** -->
  <mvc:annotation-driven conversion-service="conversionService" />
  <context:component-scan base-package="thymeleafexamples.stsm" />


  <!-- **************************************************************** -->
  <!--  MESSAGE EXTERNALIZATION/INTERNATIONALIZATION                    -->
  <!--  Standard Spring MessageSource implementation                    -->
  <!-- **************************************************************** -->
  <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basename" value="Messages" />
  </bean>


  <!-- **************************************************************** -->
  <!--  CONVERSION SERVICE                                              -->
  <!--  Standard Spring formatting-enabled implementation               -->
  <!-- **************************************************************** -->
  <bean id="conversionService" 
        class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
      <set>
        <bean class="thymeleafexamples.stsm.web.conversion.VarietyFormatter" />
        <bean class="thymeleafexamples.stsm.web.conversion.DateFormatter" />
      </set>
    </property>
  </bean>


  <!-- **************************************************************** -->
  <!--  THYMELEAF-SPECIFIC ARTIFACTS                                    -->
  <!--  TemplateResolver <- TemplateEngine <- ViewResolver              -->
  <!-- **************************************************************** -->

  <bean id="templateResolver"
        class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
    <property name="prefix" value="/WEB-INF/templates/" />
    <property name="suffix" value=".html" />
    <property name="templateMode" value="HTML5" />
  </bean>
    
  <bean id="templateEngine"
        class="org.thymeleaf.spring4.SpringTemplateEngine">
    <property name="templateResolver" ref="templateResolver" />
  </bean>
   
  <bean class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
    <property name="templateEngine" ref="templateEngine" />
  </bean>    

    
</beans>

注意:这里选择了HTML5作为模板模式。

控制器

当然,这个应用程序中还需要一个控制器,由于这个应用只有一个页面,用户种子的生长的查看和添加,所以只需要一个控制器就可以了:

@Controller
public class SeedStarterMngController {

    @Autowired
    private VarietyService varietyService;
    
    @Autowired
    private SeedStarterService seedStarterService;

    ...

}

现在看看在这个控制器中可以添加什么?

模型属性(ModelAttribute注解)

@ModelAttribute("allTypes")
public List<Type> populateTypes() {
    return Arrays.asList(Type.ALL);
}
    
@ModelAttribute("allFeatures")
public List<Feature> populateFeatures() {
    return Arrays.asList(Feature.ALL);
}
    
@ModelAttribute("allVarieties")
public List<Variety> populateVarieties() {
    return this.varietyService.findAll();
}
    
@ModelAttribute("allSeedStarters")
public List<SeedStarter> populateSeedStarters() {
    return this.seedStarterService.findAll();
}

方法映射

接下来是控制器最重要的一部分了,那就是方法映射(RequestMapping),一个表单页和一个新的种子对象添加页。

@RequestMapping({"/","/seedstartermng"})
public String showSeedstarters(final SeedStarter seedStarter) {
    seedStarter.setDatePlanted(Calendar.getInstance().getTime());
    return "seedstartermng";
}

@RequestMapping(value="/seedstartermng", params={"save"})
public String saveSeedstarter(
        final SeedStarter seedStarter, final BindingResult bindingResult, final ModelMap model) {
    if (bindingResult.hasErrors()) {
        return "seedstartermng";
    }
    this.seedStarterService.add(seedStarter);
    model.clear();
    return "redirect:/seedstartermng";
}

配置转换服务

为了在模板视图中更加方便的使用日期和我们自己定义的各种对象,我们注册的了一个转换服务在上下文中:

<?xml version="1.0" encoding="UTF-8"?>
<beans ...>
  ...    
  <mvc:annotation-driven conversion-service="conversionService" />
  ...
  <!-- **************************************************************** -->
  <!--  CONVERSION SERVICE                                              -->
  <!--  Standard Spring formatting-enabled implementation               -->
  <!-- **************************************************************** -->
  <bean id="conversionService"
        class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
      <set>
        <bean class="thymeleafexamples.stsm.web.conversion.VarietyFormatter" />
        <bean class="thymeleafexamples.stsm.web.conversion.DateFormatter" />
      </set>
    </property>
  </bean>
  ...
</beans>

转换服务允许我们注册两个org.springframework.format.Formatter接口的实现,关于Spring转换的更多信息,请查验文档

首先看一下DateFormatter,它的日期格式定义的字符串定义在Message.properties文件中,并且以date.format作为key.

public class DateFormatter implements Formatter<Date> {

    @Autowired
    private MessageSource messageSource;


    public DateFormatter() {
        super();
    }

    public Date parse(final String text, final Locale locale) throws ParseException {
        final SimpleDateFormat dateFormat = createDateFormat(locale);
        return dateFormat.parse(text);
    }

    public String print(final Date object, final Locale locale) {
        final SimpleDateFormat dateFormat = createDateFormat(locale);
        return dateFormat.format(object);
    }

    private SimpleDateFormat createDateFormat(final Locale locale) {
        final String format = this.messageSource.getMessage("date.format", null, locale);
        final SimpleDateFormat dateFormat = new SimpleDateFormat(format);
        dateFormat.setLenient(false);
        return dateFormat;
    }

}

VarietyFormatter可以自动转换我们的各种实体,将他们用在表单上(基本通过id)

public class VarietyFormatter implements Formatter<Variety> {

    @Autowired
    private VarietyService varietyService;


    public VarietyFormatter() {
        super();
    }

    public Variety parse(final String text, final Locale locale) throws ParseException {
        final Integer varietyId = Integer.valueOf(text);
        return this.varietyService.findById(varietyId);
    }


    public String print(final Variety object, final Locale locale) {
        return (object != null ? object.getId().toString() : "");
    }
}

在之后的内容,我们会学习更多的关于formatter的内容。

种子生长列表

首先,在/WEB-INF/templatesseedstartermng.html页将显示一个当前的已培育种子的列表,为此我们需要一些额外的信息,和通过表达式执行一些模型属性:

<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">

  <h2 th:text="#{title.list}">List of Seed Starters</h2>
  
  <table>
    <thead>
      <tr>
        <th th:text="#{seedstarter.datePlanted}">Date Planted</th>
        <th th:text="#{seedstarter.covered}">Covered</th>
        <th th:text="#{seedstarter.type}">Type</th>
        <th th:text="#{seedstarter.features}">Features</th>
        <th th:text="#{seedstarter.rows}">Rows</th>
      </tr>
    </thead>
    <tbody>
      <tr th:each="sb : ${allSeedStarters}">
        <td th:text="${{sb.datePlanted}}">13/01/2011</td>
        <td th:text="${sb.covered}? #{bool.true} : #{bool.false}">yes</td>
        <td th:text="#{${\'seedstarter.type.\' + sb.type}}">Wireframe</td>
        <td th:text="${#strings.arrayJoin(
                           #messages.arrayMsg(
                               #strings.arrayPrepend(sb.features,\'seedstarter.feature.\')),
                           \', \')}">Electric Heating, Turf</td>
        <td>
          <table>
            <tbody>
              <tr th:each="row,rowStat : ${sb.rows}">
                <td th:text="${rowStat.count}">1</td>
                <td th:text="${row.variety.name}">Thymus Thymi</td>
                <td th:text="${row.seedsPerCell}">12</td>
              </tr>
            </tbody>
          </table>
        </td>
      </tr>
    </tbody>
  </table>
</div>

这里几乎是全部代码,现在分别查看每一个片段。

首先,这一部分将只在有种子在培育的时候显示,我们将使用th:unless属性来通过#lists.iEmpty(...)方法来实现这个目标。

<div class="seedstarterlist" th:unless="${#lists.isEmpty(allSeedStarters)}">

objects的工具类,比如#lists是SpringEL表达式,他就像在OGNL表达式中同样的方式使用。

接下来是一些国际化的文本:

<h2 th:text="#{title.list}">List of Seed Starters</h2>

<table>
  <thead>
    <tr>
      <th th:text="#{seedstarter.datePlanted}">Date Planted</th>
      <th th:text="#{seedstarter.covered}">Covered</th>
      <th th:text="#{seedstarter.type}">Type</th>
      <th th:text="#{seedstarter.features}">Features</th>
      <th th:text="#{seedstarter.rows}">Rows</th>
      ...

在这个SpringMVC应用程序中,我们通过一个bean定义了一个MessageSource在我们的spring的XML配置文件中:

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
  <property name="basename" value="Messages" />
</bean>

basename表示我们将使用message打头的资源文件,如Message_en.properties或者Message_ZH_cn.properties,比如英文版如下:

title.list=Lista de semilleros

date.format=dd/MM/yyyy
bool.true=sí
bool.false=no

seedstarter.datePlanted=Fecha de plantación
seedstarter.covered=Cubierto
seedstarter.type=Tipo
seedstarter.features=Características
seedstarter.rows=Filas

seedstarter.type.WOOD=Madera
seedstarter.type.PLASTIC=Plástico

seedstarter.feature.SEEDSTARTER_SPECIFIC_SUBSTRATE=Sustrato específico para semilleros
seedstarter.feature.FERTILIZER=Fertilizante
seedstarter.feature.PH_CORRECTOR=Corrector de PH

在表格的第一列,将显示种子的培育开始时间,我们将通过定义的DateFormatter将它自动格式化显示,为了做到这一点,将使用${{}}语法,这个语法将自动应用Spring的转换服务。

<td th:text="${{sb.datePlanted}}">13/01/2011</td>

下面将显示花盆中是否有种子,通过改变bean的布尔值属性将布尔值转换为国际化的是和否。:

<td th:text="${sb.covered}? #{bool.true} : #{bool.false}">yes</td>

下一步将展示花盆的类型,它的类型是有两个值的枚举型(值分别为木制和塑料),这也是我为什么在配置文件中定义了seedstarter.type.WOOD和seedstarter.type.PLAStIC两个属性的原因。

但为了获取国际化之后的值,我们需要给实际值增加seedstarter.type的前缀,来生成Message 属性的key返回所需的值:

<td th:text="#{${\'seedstarter.type.\' + sb.type}}">Wireframe</td>

列表中最困难的部分就是功能列,因为在这里需要显示左右的功能,如"电加热,草皮",这里讲采用逗号分隔原有枚举数组的方式。

注意这样也是有些困难的,因为这些枚举需要根据他的类型进行具体化,需要:

  • 给特征数组的所有元素规划响应的前缀,
  • 获得从步骤1相对应的外部信息
  • 把所有从步骤2获取的信息,用逗号分隔

为了实现这一点,我们创建了如下的代码:

<td th:text="${#strings.arrayJoin(
               #messages.arrayMsg(
                   #strings.arrayPrepend(sb.features,\'seedstarter.feature.\')),
               \', \')}">Electric Heating, Turf</td>

列表的最有一列很简单,事实上,它有一个嵌套表,用于显示每一行的内容。

<td>
  <table>
    <tbody>
      <tr th:each="row,rowStat : ${sb.rows}">
        <td th:text="${rowStat.count}">1</td>
        <td th:text="${row.variety.name}">Thymus Thymi</td>
        <td th:text="${row.seedsPerCell}">12</td>
      </tr>
    </tbody>
  </table>
</td>

创建表单

处理命令对象

SpringMVC的表单支持bean就是命令对象,这个对象通过对象领域模型的方式提供get和set方法,在浏览器建立获取用户输入值的输入框架。

Thymeleaf需要你显示的在form标签内通过th:object属性指定命令对象:

<form action="#" th:action="@{/seedstartermng}" th:object="${seedStarter}" method="post">
    ...
</form>

这个th:object与其他的的地方用途是一直的,但是事实上在这种特定情况下,为了与SpringMVC框架的正确整合增加了一些特定的限制:

  • 在form标签中的th:object的值必须是变量表达式(...),使...),只能指定属性模型属性的名字,而不能使用属性导航,这意味着,表达式{seedStarter}是正确的,而${seedStarter.data}则不是。
  • 一个form标签内只能指定一个th:object属性,这与html中form标签不能嵌套的特性相一致。

input

下面是如何将一个input插入到表单中

<input type="text" th:field="*{datePlanted}" />

正象上边的代码所示,新增了一个th:field的属性,这是SpringMVC集成的一个重要特征,它帮你完成了表单bean和输入框之间的繁重的绑定工作。可以看出他在from中的路径属性和SpringMVC的jsp标签库一样。

th:field属性的不同行为取决于它所附加的不同标签,包括<input>,<select><textarea>(还包括标签的不同type属性类型),在这种情况下,时间上上面哪行代码会是这样的:

<input type="text" id="datePlanted" name="datePlanted" th:value="*{datePlanted}" />

事实上,可能比上边的代码还要多一些东西,因为th:fild还可能会注册一个Spring的转换服务,包括之前我们看到的DateFormatter(甚至这个表达式中没使用双大括号),因此,这个日期也将被正确的格式化。

th:field的值必须使用选择表达式,这样将在这个环境中使用表单bean,而不是上下文变量或SpringMVC的模型属性。

相反对于th:object这类,它的表达式可以使用属性导航(事实上在JSP的<form:input标签中,可以使用任何的路径属性表达式)

注意th:field属性也可以在HTML5的的新增类型中使用,如<input type="datetime"><input type="color">等,有效的增加了对SpringMVC对HTML5支持的完整性。

复选框

th:field也可以用在checkbox中,比如如下代码:

<div>
  <label th:for="${#ids.next(\'covered\')}" th:text="#{seedstarter.covered}">已种植</label>
  <input type="checkbox" th:field="*{covered}" />
</div>

注意这里有一些除了复选框之外的好东西,比如外部label和它使用的#ids.next("covered")方法,用于当改id的复选框执行的时候获取它的id值。

那么为什么我们需要这个字段的id属性动态生成呢?因为复选框可能是多值的,因此它会给id值添加一个序列号后缀(内部使用#ids.seq(...)函数)来保证同一属性的复选框有不同的id值。

我们可以看看多值的复选框:

<ul>
  <li th:each="feat : ${allFeatures}">
    <input type="checkbox" th:field="*{features}" th:value="${feat}" />
    <label th:for="${#ids.prev(\'features\')}" 
           th:text="#{${\'seedstarter.feature.\' + feat}}">Heating</label>
  </li>
</ul>

注意这次我们增加了一个th:value属性,因为这次的特征属性不是一个布尔值,而是一个数组。

一般情况下,它的输出为:

<ul>
  <li>
    <input id="features1" name="features" type="checkbox" value="SEEDSTARTER_SPECIFIC_SUBSTRATE" />
    <input name="_features" type="hidden" value="on" />
    <label for="features1">Seed starter-specific substrate</label>
  </li>
  <li>
    <input id="features2" name="features" type="checkbox" value="FERTILIZER" />
    <input name="_features" type="hidden" value="on" />
    <label for="features2">Fertilizer used</label>
  </li>
  <li>
    <input id="features3" name="features" type="chec

以上是关于Thymeleaf模板引擎+Spring整合使用方式的介绍的主要内容,如果未能解决你的问题,请参考以下文章

关于spring boot整合thymeleaf模板引擎

Spring Boot2 系列教程 | 整合 thymeleaf

spring mvc 整合jsp和thymeleaf两个模板引擎

Spring Boot入门系列六( SpringBoot 整合thymeleaf)

SpringbootSpringBoot基础知识及整合Thymeleaf模板引擎

springboot整合thymleaf模板引擎