Thymeleaf

教程:使用 Thymeleaf

1 Thymeleaf 介绍

1.1 什么是 Thymeleaf?

Thymeleaf 是一个适用于 Web 和独立环境的现代服务器端 Java 模板引擎,能够处理 HTML、XML、JavaScript、CSS 甚至纯文本。

Thymeleaf 的主要目标是提供一种优雅且高度可维护的模板创建方式。为了实现这一目标,它以自然模板的概念为基础,将其逻辑注入模板文件,而不会影响模板作为设计原型的使用。这改善了设计沟通并弥合了设计团队和开发团队之间的差距。

Thymeleaf 从一开始就考虑到了 Web 标准(尤其是HTML5),允许您创建完全验证的模板(如果需要)。

1.2 Thymeleaf 可以处理哪些类型的模板?

开箱即用,Thymeleaf 允许您处理六种模板,每种模板称为一种模板模式

  • HTML
  • XML
  • 文本
  • JavaScript
  • CSS
  • 生的

有两种标记模板模式(HTMLXML),三种文本模板模式(TEXTJAVASCRIPTCSS)和一种无操作模板模式(RAW)。

模板HTML模式将允许任何类型的 HTML 输入,包括 HTML5、HTML 4 和 XHTML。不会执行任何验证或格式正确性检查,输出中将最大程度地尊重模板代码/结构。

模板XML模式将允许 XML 输入。在这种情况下,代码应格式正确 - 没有未关闭的标签、没有未加引号的属性等 - 如果发现格式错误,解析器将抛出异常。请注意,不会执行任何验证(针对 DTD 或 XML Schema)。

模板TEXT模式允许使用特殊语法来处理非标记性质的模板。此类模板的示例可能是文本电子邮件或模板化文档。请注意,HTML 或 XML 模板也可以作为 处理TEXT,在这种情况下,它们将不会被解析为标记,并且每个标签、DOCTYPE、注释等都将被视为纯文本。

模板JAVASCRIPT模式将允许在 Thymeleaf 应用程序中处理 JavaScript 文件。这意味着能够以与在 HTML 文件中相同的方式在 JavaScript 文件中使用模型数据,但具有 JavaScript 特定的集成,例如专门的转义或自然脚本JAVASCRIPT模板模式被视为文本模式,因此使用与模板模式相同的特殊语法TEXT

模板CSS模式将允许处理 Thymeleaf 应用程序中涉及的 CSS 文件。与该JAVASCRIPT模式类似,CSS模板模式也是一种文本模式,并使用来自模板模式的特殊处理语法TEXT

模板RAW模式根本不会处理模板。它旨在用于将未触及的资源(文件、URL 响应等)插入正在处理的模板中。例如,HTML 格式的外部、不受控制的资源可以包含在应用程序模板中,并且可以放心地知道这些资源可能包含的任何 Thymeleaf 代码都不会被执行。

1.3 方言:标准方言

Thymeleaf 是一个极具扩展性的模板引擎(事实上它可以被称为模板引擎框架),它允许您定义和自定义模板处理方式的精细程度。

将某种逻辑应用于标记工件(标签、文本、注释或模板不是标记时的占位符)的对象称为处理器方言通常由一组这样的处理器(可能还有一些额外的工件)组成。Thymeleaf 的核心库开箱即用地提供了一种称为标准方言的方言,这对大多数用户来说应该足够了。

请注意,方言实际上可以没有处理器,而完全由其他类型的工件组成,但处理器绝对是最常见的用例。

本教程涵盖了标准方言。您将在以下页面中了解的每个属性和语法特性都是由此方言定义的,即使没有明确提及。

当然,如果用户想定义自己的处理逻辑并利用该库的高级功能,他们可以创建自己的方言(甚至扩展标准方言)。Thymeleaf 还可以配置为同时使用多种方言。

官方的 thymeleaf-spring3 和 thymeleaf-spring4 集成包都定义了一种称为“SpringStandard Dialect”的方言,它与标准方言基本相同,但略有改动,以便更好地利用 Spring Framework 中的某些功能(例如,使用 Spring Expression Language 或 SpringEL 代替 OGNL)。因此,如果您是 Spring MVC 用户,那么您不会浪费时间,因为您在这里学到的几乎所有内容都将在您的 Spring 应用程序中派上用场。

标准方言的大多数处理器都是属性处理器。这使得浏览器即使在处理之前也能正确显示 HTML 模板文件,因为它们会忽略附加属性。例如,虽然使用标记库的 JSP 可能包含浏览器无法直接显示的代码片段,例如:

<form:inputText name="userName" value="${user.name}" />

...Thymeleaf 标准方言允许我们通过以下方式实现相同的功能:

<input type="text" name="userName" value="James Carrot" th:value="${user.name}" />

这不仅能被浏览器正确显示,而且这还允许我们(可选)在其中指定一个值属性(在本例中为“James Carrot”),当原型在浏览器中静态打开时将显示该值,并且该值将由在${user.name}处理模板期间评估的结果替换。

这可帮助您的设计师和开发人员处理同一个模板文件,并减少将静态原型转换为工作模板文件所需的工作量。实现此功能的功能称为“自然模板化”

2. The Good Thymes 虚拟杂货店

本指南中显示的示例以及后续章节的源代码可以在Good Thymes Virtual Grocery GitHub 存储库中找到。

2.1 一家杂货店的网站

为了更好地解释使用 Thymeleaf 处理模板所涉及的概念,本教程将使用一个演示应用程序,您可以从项目网站下载。

该应用程序是一个虚构的虚拟杂货店的网站,将为我们提供许多场景来展示 Thymeleaf 的众多功能。

首先,我们需要为我们的应用程序创建一组简单的模型实体:通过创建Products出售给。我们还将管理这些CustomersOrdersCommentsProducts

示例应用程序模型
示例应用程序模型

我们的应用程序还将有一个非常简单的服务层,由Service包含以下方法的对象组成:

public class ProductService {

    ...

    public List<Product> findAll() {
        return ProductRepository.getInstance().findAll();
    }

    public Product findById(Integer id) {
        return ProductRepository.getInstance().findById(id);
    }
    
}

在 Web 层,我们的应用程序将有一个过滤器,该过滤器将根据请求 URL 将执行委托给启用 Thymeleaf 的命令:

private boolean process(HttpServletRequest request, HttpServletResponse response)
        throws ServletException {
    
    try {

        // This prevents triggering engine executions for resource URLs
        if (request.getRequestURI().startsWith("/css") ||
                request.getRequestURI().startsWith("/images") ||
                request.getRequestURI().startsWith("/favicon")) {
            return false;
        }

        
        /*
         * Query controller/URL mapping and obtain the controller
         * that will process the request. If no controller is available,
         * return false and let other filters/servlets process the request.
         */
        IGTVGController controller = this.application.resolveControllerForRequest(request);
        if (controller == null) {
            return false;
        }

        /*
         * Obtain the TemplateEngine instance.
         */
        ITemplateEngine templateEngine = this.application.getTemplateEngine();

        /*
         * Write the response headers
         */
        response.setContentType("text/html;charset=UTF-8");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);

        /*
         * Execute the controller and process view template,
         * writing the results to the response writer. 
         */
        controller.process(
                request, response, this.servletContext, templateEngine);
        
        return true;
        
    } catch (Exception e) {
        try {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final IOException ignored) {
            // Just ignore this
        }
        throw new ServletException(e);
    }
    
}

这是我们的IGTVGController界面:

public interface IGTVGController {

    public void process(
            HttpServletRequest request, HttpServletResponse response,
            ServletContext servletContext, ITemplateEngine templateEngine);    
    
}

我们现在要做的就是创建IGTVGController接口的实现,从服务中检索数据并使用ITemplateEngine对象处理模板。

最后它看起来会像这样:

示例应用程序主页
示例应用程序主页

但首先让我们看看该模板引擎是如何初始化的。

2.2 创建并配置模板引擎

我们的过滤器中的process(…)方法包含以下行:

ITemplateEngine templateEngine = this.application.getTemplateEngine();

这意味着GTVGApplication类负责创建和配置 Thymeleaf 应用程序中最重要的对象之一:TemplateEngine实例(接口的实现ITemplateEngine)。

我们的org.thymeleaf.TemplateEngine对象初始化如下:

public class GTVGApplication {
  
    
    ...
    private final TemplateEngine templateEngine;
    ...
    
    
    public GTVGApplication(final ServletContext servletContext) {

        super();

        ServletContextTemplateResolver templateResolver = 
                new ServletContextTemplateResolver(servletContext);
        
        // HTML is the default mode, but we set it anyway for better understanding of code
        templateResolver.setTemplateMode(TemplateMode.HTML);
        // This will convert "home" to "/WEB-INF/templates/home.html"
        templateResolver.setPrefix("/WEB-INF/templates/");
        templateResolver.setSuffix(".html");
        // Template cache TTL=1h. If not set, entries would be cached until expelled
        templateResolver.setCacheTTLMs(Long.valueOf(3600000L));
        
        // Cache is set to true by default. Set to false if you want templates to
        // be automatically updated when modified.
        templateResolver.setCacheable(true);
        
        this.templateEngine = new TemplateEngine();
        this.templateEngine.setTemplateResolver(templateResolver);
        
        ...

    }

}

配置对象的方法有很多种TemplateEngine,但是现在这几行代码就足以让我们了解所需的步骤。

模板解析器

让我们从模板解析器开始:

ServletContextTemplateResolver templateResolver = 
        new ServletContextTemplateResolver(servletContext);

模板解析器是实现 Thymeleaf API 接口的对象org.thymeleaf.templateresolver.ITemplateResolver

public interface ITemplateResolver {

    ...
  
    /*
     * Templates are resolved by their name (or content) and also (optionally) their 
     * owner template in case we are trying to resolve a fragment for another template.
     * Will return null if template cannot be handled by this template resolver.
     */
    public TemplateResolution resolveTemplate(
            final IEngineConfiguration configuration,
            final String ownerTemplate, final String template,
            final Map<String, Object> templateResolutionAttributes);
}

这些对象负责确定如何访问我们的模板,在这个 GTVG 应用程序中,这org.thymeleaf.templateresolver.ServletContextTemplateResolver意味着我们将从Servlet 上下文中检索我们的模板文件作为资源:一个存在于每个 Java Web 应用程序中的应用程序范围的javax.servlet.ServletContext对象,并从 Web 应用程序根目录中解析资源。

但这并不是我们能说的关于模板解析器的全部,因为我们可以对其设置一些配置参数。首先是模板模式:

templateResolver.setTemplateMode(TemplateMode.HTML);

HTML 是 的默认模板模式ServletContextTemplateResolver,但无论如何建立它都是很好的做法,这样我们的代码就可以清楚地记录正在发生的事情。

templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");

前缀后缀修改我们将传递给引擎的模板名称,以获取要使用的真实资源名称

使用此配置,模板名称“product/list”将对应:

servletContext.getResourceAsStream("/WEB-INF/templates/product/list.html")

可选地,已解析模板可以在缓存中生存的时间量可以通过cacheTTLMs属性在模板解析器中配置:

templateResolver.setCacheTTLMs(3600000L);

如果达到最大缓存大小并且模板是当前缓存的最旧条目,则在达到该 TTL 之前仍可能将该模板从缓存中驱逐。

用户可以通过实现接口ICacheManager或者修改StandardCacheManager对象来管理默认缓存,从而定义缓存行为和大小。

关于模板解析器还有很多东西需要学习,但现在让我们看一下模板引擎对象的创建。

模板引擎

模板引擎对象是org.thymeleaf.ITemplateEngine接口的实现。Thymeleaf 核心提供了这些实现之一:org.thymeleaf.TemplateEngine,我们在这里创建它的一个实例:

templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(templateResolver);

相当简单,不是吗?我们需要做的就是创建一个实例并将模板解析器设置为它。

模板解析器是所需的唯一必需参数TemplateEngine,尽管还有许多其他参数将在后面介绍(消息解析器、缓存大小等)。目前,这就是我们所需要的全部。

我们的模板引擎现在已经准备好了,我们可以开始使用 Thymeleaf 创建我们的页面。

3 使用文本

3.1 多语言欢迎

我们的首要任务是为我们的杂货网站创建一个主页。

此页面的第一个版本非常简单:只有一个标题和一条欢迎信息。这是我们的/WEB-INF/templates/home.html文件:

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
  </head>

  <body>
  
    <p th:text="#{home.welcome}">Welcome to our grocery store!</p>
  
  </body>

</html>

您首先会注意到的是,该文件是 HTML5,可以被任何浏览器正确显示,因为它不包含任何非 HTML 标签(浏览器会忽略它们不理解的所有属性,例如th:text)。

但您可能还注意到,此模板实际上并不是有效的HTML5 文档,因为我们在表单中使用的这些非标准属性th:*不符合 HTML5 规范。事实上,我们甚至xmlns:th在标签中添加了一个属性<html>,这完全不符合 HTML5 规范:

<html xmlns:th="http://www.thymeleaf.org">

...这对模板处理完全没有影响,但可以起到咒语的作用,防止我们的 IDE 抱怨所有这些属性缺少命名空间定义th:*

那么如果我们想让这个模板符合 HTML5 规范该怎么做呢?很简单:切换到 Thymeleaf 的数据属性语法,使用data-属性名称的前缀和连字符 ( -) 分隔符代替分号 ( :):

<!DOCTYPE html>

<html>

  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../css/gtvg.css" data-th-href="@{/css/gtvg.css}" />
  </head>

  <body>
  
    <p data-th-text="#{home.welcome}">Welcome to our grocery store!</p>
  
  </body>

</html>

HTML5 规范允许使用自定义data-前缀属性,因此,通过上述代码,我们的模板将是有效的 HTML5 文档

这两种符号完全等价且可以互换,但为了代码示例的简单和紧凑,本教程将使用命名空间符号th:*)。此外,该th:*符号更通用,允许在每个 Thymeleaf 模板模式中使用(XMLTEXT...),而data-符号仅允许在HTML模式下使用。

使用 th:text 并外部化文本

外部化文本是从模板文件中提取模板代码片段,以便将它们保存在单独的文件(通常是.properties文件)中,并且可以轻松地用其他语言编写的等效文本替换它们(此过程称为国际化或简称为i18n)。 外部化的文本片段通常称为“消息”

消息始终有一个用于标识它们的密钥,Thymeleaf 允许您使用以下语法指定文本应与特定消息相对应#{...}

<p th:text="#{home.welcome}">Welcome to our grocery store!</p>

我们在这里看到的实际上是 Thymeleaf 标准方言的两个不同特点:

  • th:text属性评估其值表达式并将结果设置为主机标签的主体,有效地替换了我们在代码中看到的“欢迎来到我们的杂货店!”文本。
  • 标准表达式语法#{home.welcome}中指定的表达式指示属性使用的文本应为具有与我们处理模板时所使用的语言环境相对应的键的消息。th:texthome.welcome

那么,这个外部化的文本在哪里呢?

Thymeleaf 中外部化文本的位置是完全可配置的,并且取决于org.thymeleaf.messageresolver.IMessageResolver所使用的具体实现。通常,将使用基于.properties文件的实现,但如果我们想要从数据库获取消息,我们可以创建自己的实现。

但是,我们在初始化期间没有为我们的模板引擎指定消息解析器,这意味着我们的应用程序正在使用由实现的标准消息解析器org.thymeleaf.messageresolver.StandardMessageResolver

/WEB-INF/templates/home.html标准消息解析器希望在与模板位于同一文件夹中且名称相同的属性文件中找到消息,例如:

  • /WEB-INF/templates/home_en.properties用于英文文本。
  • /WEB-INF/templates/home_es.properties用于西班牙语文本。
  • /WEB-INF/templates/home_pt_BR.properties用于葡萄牙语(巴西)文本。
  • /WEB-INF/templates/home.properties对于默认文本(如果语言环境不匹配)。

让我们看一下我们的home_es.properties文件:

home.welcome=¡Bienvenido a nuestra tienda de comestibles!

这就是我们将 Thymeleaf 进程作为模板所需的全部内容。接下来让我们创建 Home 控制器。

上下文

为了处理我们的模板,我们将创建一个HomeController实现IGTVGController我们之前看到的接口的类:

public class HomeController implements IGTVGController {

    public void process(
            final HttpServletRequest request, final HttpServletResponse response,
            final ServletContext servletContext, final ITemplateEngine templateEngine)
            throws Exception {
        
        WebContext ctx = 
                new WebContext(request, response, servletContext, request.getLocale());
        
        templateEngine.process("home", ctx, response.getWriter());
        
    }

}

我们首先看到的是上下文的创建。Thymeleaf 上下文是实现接口的对象org.thymeleaf.context.IContext。上下文应在变量映射中包含模板引擎执行所需的所有数据,还应引用必须用于外部化消息的语言环境。

public interface IContext {

    public Locale getLocale();
    public boolean containsVariable(final String name);
    public Set<String> getVariableNames();
    public Object getVariable(final String name);
    
}

此接口有一个专门的扩展,org.thymeleaf.context.IWebContext旨在用于基于 ServletAPI 的 Web 应用程序(如 SpringMVC)。

public interface IWebContext extends IContext {
    
    public HttpServletRequest getRequest();
    public HttpServletResponse getResponse();
    public HttpSession getSession();
    public ServletContext getServletContext();
    
}

Thymeleaf 核心库提供了以下每个接口的实现:

  • org.thymeleaf.context.Context实现IContext
  • org.thymeleaf.context.WebContext实现IWebContext

正如您在控制器代码中看到的,WebContext是我们使用的。事实上,我们必须这样做,因为使用ServletContextTemplateResolver要求我们使用实现 的上下文IWebContext

WebContext ctx = new WebContext(request, response, servletContext, request.getLocale());

这四个构造函数参数中只需要三个,因为如果没有指定,则将使用系统的默认区域设置(尽管在实际应用程序中您不应该让这种情况发生)。

我们可以使用一些专门的表达式从WebContext模板中获取请求参数以及请求、会话和应用程序属性。例如:

  • ${x}x将返回存储在 Thymeleaf 上下文中或作为请求属性的变量
  • ${param.x}将返回一个名为的请求参数x(可能是多值的)。
  • ${session.x}将返回一个名为的会话属性x
  • ${application.x}将返回一个名为 的servlet 上下文属性x

执行模板引擎

在我们的上下文对象准备好之后,现在我们可以告诉模板引擎使用上下文来处理模板(通过它的名称),并且传递给它一个响应编写器,以便可以将响应写入它:

templateEngine.process("home", ctx, response.getWriter());

让我们看看使用西班牙语区域的结果:

<!DOCTYPE html>

<html>

  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
    <link rel="stylesheet" type="text/css" media="all" href="/gtvg/css/gtvg.css" />
  </head>

  <body>
  
    <p>¡Bienvenido a nuestra tienda de comestibles!</p>

  </body>

</html>

3.2 有关文本和变量的更多信息

未转义的文本

我们的主页的最简单版本现在似乎已经准备好了,但是有些事情我们还没有想到......如果我们有这样的消息怎么办?

home.welcome=Welcome to our <b>fantastic</b> grocery store!

如果我们像以前一样执行此模板,我们将获得:

<p>Welcome to our &lt;b&gt;fantastic&lt;/b&gt; grocery store!</p>

这并不完全是我们所期望的,因为我们的<b>标签已经被转义,因此它将显示在浏览器中。

这是属性的默认行为th:text。如果我们希望 Thymeleaf 尊重我们的 HTML 标签并且不对其进行转义,我们将不得不使用不同的属性:(th:utext针对“未转义的文本”):

<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>

这将按照我们的需要输出消息:

<p>Welcome to our <b>fantastic</b> grocery store!</p>

使用和显示变量

现在让我们在主页上添加更多内容。例如,我们可能希望在欢迎消息下方显示日期,如下所示:

Welcome to our fantastic grocery store!

Today is: 12 july 2010

首先,我们必须修改我们的控制器,以便将该日期添加为上下文变量:

public void process(
            final HttpServletRequest request, final HttpServletResponse response,
            final ServletContext servletContext, final ITemplateEngine templateEngine)
            throws Exception {
        
    SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy");
    Calendar cal = Calendar.getInstance();
        
    WebContext ctx = 
            new WebContext(request, response, servletContext, request.getLocale());
    ctx.setVariable("today", dateFormat.format(cal.getTime()));
        
    templateEngine.process("home", ctx, response.getWriter());
        
}

我们已经向我们的上下文添加了一个String名为的变量today,现在我们可以在模板中显示它:

<body>

  <p th:utext="#{home.welcome}">Welcome to our grocery store!</p>

  <p>Today is: <span th:text="${today}">13 February 2011</span></p>
  
</body>

如您所见,我们仍在使用th:text作业的属性(这是正确的,因为我们想要替换标签的主体),但这次的语法略有不同,#{...}我们使用的不是表达式值,而是${...}1。这是一个变量表达式,它包含一种名为OGNL(对象图导航语言)的语言中的表达式,该表达式将在我们之前讨论过的上下文变量映射上执行。

${today}表达式只是意味着“获取今天调用的变量”,但这些表达式可能更复杂(例如${user.name}“获取名为用户的变量,并调用其getName()方法”)。

属性值有很多可能性:消息、变量表达式……等等。下一章将向我们展示所有这些可能性。

4 标准表达式语法

我们将在开发杂货店虚拟商店的过程中稍作休息,以了解 Thymeleaf 标准方言中最重要的部分之一:Thymeleaf 标准表达式语法。

我们已经看到了用这种语法表达的两种类型的有效属性值:消息和变量表达式:

<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>

<p>Today is: <span th:text="${today}">13 february 2011</span></p>

但是,还有更多类型的表达式,以及更多有趣的细节需要我们了解我们已经知道的表达式。首先,让我们快速总结一下标准表达式的功能:

  • 简单表达:
    • 变量表达式:${...}
    • 选择变量表达式:*{...}
    • 消息表达:#{...}
    • 链接 URL 表达式:@{...}
    • 片段表达式:~{...}
  • 文字
    • 文本文字:'one text',,'Another one!'
    • 数字文字0,,,,343.012.3
    • 布尔文字:truefalse
    • 空文字:null
    • 文字标记one,,,sometextmain
  • 文本操作:
    • 字符串连接:+
    • 文字替换:|The name is ${name}|
  • 算术运算:
    • 二元运算+,,,,,-*/%
    • 减号(一元运算符):-
  • 布尔运算:
    • 二元运算符:andor
    • 布尔否定(一元运算符)!:,not
  • 比较和相等:
    • 比较>,,,,,,<>=<=gtltgele
    • 等号运算符:==, !=( eq, ne)
  • 条件运算符:
    • 如果-那么:(if) ? (then)
    • 如果-则-否则:(if) ? (then) : (else)
    • 默认:(value) ?: (defaultvalue)
  • 特殊标记:
    • 无操作:_

所有这些功能都可以组合和嵌套:

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

4.1 消息

我们已经知道,#{...}消息表达式允许我们链接以下内容:

<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>

…变成这样:

home.welcome=¡Bienvenido a nuestra tienda de comestibles!

但有一个方面我们还没有想到:如果消息文本不是完全静态的,会发生什么?例如,如果我们的应用程序随时知道谁是访问网站的用户,而我们想通过名字来问候他们,该怎么办?

<p>¡Bienvenido a nuestra tienda de comestibles, John Apricot!</p>

这意味着我们需要在消息中添加一个参数。就像这样:

home.welcome=¡Bienvenido a nuestra tienda de comestibles, {0}!

参数是根据java.text.MessageFormat标准语法指定,这意味着您可以按照包中类的 API 文档中指定的方式将其格式化为数字和日期java.text.*

为了为我们的参数指定一个值,并给出一个名为的 HTTP 会话属性user,我们可以:

<p th:utext="#{home.welcome(${session.user.name})}">
  Welcome to our grocery store, Sebastian Pepper!
</p>

注意,这里使用th:utext表示格式化的消息不会被转义。此示例假设user.name已经转义。

可以指定多个参数,以逗号分隔。

消息键本身可以来自变量:

<p th:utext="#{${welcomeMsgKey}(${session.user.name})}">
  Welcome to our grocery store, Sebastian Pepper!
</p>

4.2 变量

我们已经提到,${...}表达式实际上是在上下文中包含的变量映射上执行的 OGNL(对象图导航语言)表达式。

有关 OGNL 语法和功能的详细信息,你应该阅读OGNL 语言指南

在支持 Spring MVC 的应用程序中,OGNL 将被SpringEL取代,但其语法与 OGNL 非常相似(实际上,对于大多数常见情况而言完全相同)。

从OGNL的语法中,我们知道表达式:

<p>Today is: <span th:text="${today}">13 february 2011</span>.</p>

…实际上相当于这样:

ctx.getVariable("today");

但是 OGNL 允许我们创建更强大的表达式,例如:

<p th:utext="#{home.welcome(${session.user.name})}">
  Welcome to our grocery store, Sebastian Pepper!
</p>

...通过执行以下命令获取用户名:

((User) ctx.getVariable("session").get("user")).getName();

但是 getter 方法导航只是 OGNL 的功能之一。让我们看看更多:

/*
 * Access to properties using the point (.). Equivalent to calling property getters.
 */
${person.father.name}

/*
 * Access to properties can also be made by using brackets ([]) and writing 
 * the name of the property as a variable or between single quotes.
 */
${person['father']['name']}

/*
 * If the object is a map, both dot and bracket syntax will be equivalent to 
 * executing a call on its get(...) method.
 */
${countriesByCode.ES}
${personsByName['Stephen Zucchini'].age}

/*
 * Indexed access to arrays or collections is also performed with brackets, 
 * writing the index without quotes.
 */
${personsArray[0].name}

/*
 * Methods can be called, even with arguments.
 */
${person.createCompleteName()}
${person.createCompleteNameWithSeparator('-')}

表达式基本对象

在上下文变量上评估 OGNL 表达式时,一些对象可用于表达式,以提高灵活性。这些对象将以以下符号开头引用(根据 OGNL 标准)#

  • #ctx:上下文对象。
  • #vars:上下文变量。
  • #locale:上下文语言环境。
  • #request:(仅限于 Web 环境中)HttpServletRequest对象。
  • #response:(仅限于 Web 环境中)HttpServletResponse对象。
  • #session:(仅限于 Web 环境中)HttpSession对象。
  • #servletContext:(仅限于 Web 环境中)ServletContext对象。

所以我们可以这样做:

Established locale country: <span th:text="${#locale.country}">US</span>.

您可以在附录 A中阅读这些对象的完整参考

表达式实用对象

除了这些基本对象之外,Thymeleaf 还为我们提供一组实用对象,帮助我们在表达式中执行常见任务。

  • #execInfo:正在处理的模板的信息。
  • #messages:获取变量表达式中的外部化消息的方法,方式与使用#{…} 语法获取消息的方式相同。
  • #uris:转义 URL/URI 部分内容的方法
  • #conversions:执行配置的转换服务的方法(如果有)。
  • #dates:对象的方法java.util.Date:格式化、组件提取等。
  • #calendars:类似于#dates,但是用于java.util.Calendar对象。
  • #numbers:格式化数字对象的方法。
  • #strings:对象的方法String:contains、startsWith、prepending/appending 等。
  • #objects:一般对象的方法。
  • #bools:布尔评估方法。
  • #arrays:数组的方法。
  • #lists:列表的方法。
  • #sets:集合的方法。
  • #maps:Maps的方法。
  • #aggregates:在数组或集合上创建聚合的方法。
  • #ids:处理可能重复的 id 属性的方法(例如,作为迭代的结果)。

您可以在附录 B中查看每个实用程序对象提供的功能

重新格式化主页上的日期

现在我们了解了这些实用对象,我们可以使用它们来改变我们在主页上显示日期的方式。而不是在我们的中这样做HomeController

SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy");
Calendar cal = Calendar.getInstance();

WebContext ctx = new WebContext(request, servletContext, request.getLocale());
ctx.setVariable("today", dateFormat.format(cal.getTime()));

templateEngine.process("home", ctx, response.getWriter());

…我们可以这样做:

WebContext ctx = 
    new WebContext(request, response, servletContext, request.getLocale());
ctx.setVariable("today", Calendar.getInstance());

templateEngine.process("home", ctx, response.getWriter());

...然后在视图层本身执行日期格式化:

<p>
  Today is: <span th:text="${#calendars.format(today,'dd MMMM yyyy')}">13 May 2011</span>
</p>

4.3 选择表达式(星号语法)

变量表达式不仅可以写成${...},还可以写成*{...}

但有一个重要的区别:星号语法对选定对象而不是整个上下文进行表达式求值。也就是说,只要没有选定对象,美元和星号语法的作用完全相同。

那么选定对象是什么?使用属性的表达式的结果th:object。让我们在用户资料 ( userprofile.html) 页面中使用一个:

  <div th:object="${session.user}">
    <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
    <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
    <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
  </div>

这完全等同于:

<div>
  <p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div>

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

<div th:object="${session.user}">
  <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

当选择对象时,所选对象也将作为#object表达式变量用于美元表达式:

<div th:object="${session.user}">
  <p>Name: <span th:text="${#object.firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

如上所述,如果没有执行对象选择,则美元和星号语法是等效的。

<div>
  <p>Name: <span th:text="*{session.user.name}">Sebastian</span>.</p>
  <p>Surname: <span th:text="*{session.user.surname}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{session.user.nationality}">Saturn</span>.</p>
</div>

4.5 片段

片段表达式是一种表示标记片段并在模板中移动它们的简单方法。这使我们能够复制它们、将它们作为参数传递给其他模板等等。

最常见的用途是使用th:insert或插入片段th:replace(更多内容请参阅后面的部分):

<div th:insert="~{commons :: main}">...</div>

但它们可以在任何地方使用,就像任何其他变量一样:

<div th:with="frag=~{footer :: #main/text()}">
  <p th:insert="${frag}">
</div>

本教程的后面有一整节专门介绍模板布局,包括对片段表达式的更深入的解释。

4.6 文字

文本文字

文本文字只是用单引号指定的字符串。它们可以包含任何字符,但您应该使用 转义其中的任何单引号\'

<p>
  Now you are looking at a <span th:text="'working web application'">template file</span>.
</p>

数字文字

数字文字就是数字。

<p>The year is <span th:text="2013">1492</span>.</p>
<p>In two years, it will be <span th:text="2013 + 2">1494</span>.</p>

布尔文字

布尔文字为truefalse。例如:

<div th:if="${user.isAdmin()} == false"> ...

在这个例子中,== false写在括号外,所以 Thymeleaf 会处理它。如果它写在括号内,那么就由 OGNL/SpringEL 引擎来处理了:

<div th:if="${user.isAdmin() == false}"> ...

空文字

文字null也可以这样使用:

<div th:if="${variable.something} == null"> ...

文字标记

数字、布尔和空文字实际上是文字标记的特殊情况。

这些标记允许在标准表达式中进行一些简化。它们的工作方式与文本文字 ( '...') 完全相同,但它们仅允许字母 (A-Za-z)、数字 ( 0-9)、括号 ([])、点 ( .)、连字符 ( -) 和下划线 ( _)。因此,不能有空格、逗号等。

好的部分? Tokens 不需要任何引号。 因此我们可以这样做:

<div th:class="content">...</div>

代替:

<div th:class="'content'">...</div>

4.7 附加文本

文本,无论它们是文字还是评估变量或消息表达式的结果,都可以使用+运算符轻松附加:

<span th:text="'The name of the user is ' + ${user.name}">

4.8 文字替换

文字替换允许轻松格式化包含变量值的字符串,而无需在文字后附加'...' + '...'

这些替换必须用竖线 ( |) 包围,例如:

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

这相当于:

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

文字替换可以与其他类型的表达式结合使用:

<span th:text="${onevar} + ' ' + |${twovar}, ${threevar}|">

文字替换中仅允许使用变量/消息表达式(${...}*{...}、 ) 。不允许使用其他文字()、布尔/数字标记、条件表达式等。#{...}|...|'...'

4.9 算术运算

还可以进行一些算术运算+,,,-*/%

<div th:with="isEven=(${prodStat.count} % 2 == 0)">

请注意,这些运算符也可以在 OGNL 变量表达式内部应用(在这种情况下将由 OGNL 而不是 Thymeleaf 标准表达式引擎执行):

<div th:with="isEven=${prodStat.count % 2 == 0}">

请注意,其中一些运算符存在文本别名:div( /)、mod( %)。

4.10 比较器和相等性

>表达式中的值可以用、和符号进行比较,并且可以使用and运算符来检查是否相等(或不相等)。请注意,XML 规定<不应在属性值中使用and符号,因此应将其替换为and >=<===!=<>&lt;&gt;

<div th:if="${prodStat.count} &gt; 1">
<span th:text="'Execution mode is ' + ( (${execMode} == 'dev')? 'Development' : 'Production')">

一种更简单的替代方法可能是使用以下某些运算符的文本别名:gt( >)、lt( <)、ge( >=)、le( <=)、not( !)。还有eq( ==)、neq/ ne( !=)。

4.11 条件表达式

条件表达式旨在根据评估条件(本身是另一个表达式)的结果仅评估两个表达式中的一个。

我们来看一个示例片段(引入另一个属性修饰符, th:class):

<tr th:class="${row.even}? 'even' : 'odd'">
  ...
</tr>

条件表达式的所有三个部分(conditionthenelse)本身都是表达式,这意味着它们可以是变量(${...}*{...})、消息(#{...})、URL(@{...})或文字('...')。

条件表达式也可以使用括号嵌套:

<tr th:class="${row.even}? (${row.first}? 'first' : 'even') : 'odd'">
  ...
</tr>

Else 表达式也可以省略,在这种情况下,如果条件为假,则返回空值:

<tr th:class="${row.even}? 'alt'">
  ...
</tr>

4.12 默认表达式(Elvis 运算符)

默认表达式一种特殊的条件值,没有then部分。它相当于某些语言(如 Groovy)中的Elvis 运算符,允许您指定两个表达式:如果结果不为空,则使用第一个表达式;如果结果为空,则使用第二个表达式。

让我们在用户资料页面中看看它的实际运行情况:

<div th:object="${session.user}">
  ...
  <p>Age: <span th:text="*{age}?: '(no age specified)'">27</span>.</p>
</div>

如您所见,运算符为?:,我们在此使用它来指定名称的默认值(在本例中为文字值),仅当评估结果*{age}为空时才使用。因此,这相当于:

<p>Age: <span th:text="*{age != null}? *{age} : '(no age specified)'">27</span>.</p>

与条件值一样,它们可以在括号之间包含嵌套表达式:

<p>
  Name: 
  <span th:text="*{firstName}?: (*{admin}? 'Admin' : #{default.username})">Sebastian</span>
</p>

4.13 无操作标记

无操作标记用下划线符号 ( _) 表示。

此标记背后的想法是指定表达式的期望结果是不执行任何操作,即,就像可处理属性(例如th:text)根本不存在一样。

除其他可能性外,这允许开发人员使用原型文本作为默认值。例如,而不是:

<span th:text="${user.name} ?: 'no user authenticated'">...</span>

...我们可以直接使用“未对用户进行身份验证”作为原型文本,这样从设计的角度来看代码会更加简洁和通用:

<span th:text="${user.name} ?: _">no user authenticated</span>

4.14 数据转换/格式化

Thymeleaf defines a double-brace syntax for variable (${...}) and selection (*{...}) expressions that allows us to apply data conversion by means of a configured conversion service.

It basically goes like this:

<td th:text="${{user.lastAccessDate}}">...</td>

Noticed the double brace there?: ${{...}}. That instructs Thymeleaf to pass the result of the user.lastAccessDate expression to the conversion service and asks it to perform a formatting operation (a conversion to String) before writing the result.

Assuming that user.lastAccessDate is of type java.util.Calendar, if a conversion service (implementation of IStandardConversionService) has been registered and contains a valid conversion for Calendar -> String, it will be applied.

The default implementation of IStandardConversionService (the StandardConversionService class) simply executes .toString() on any object converted to String. For more information on how to register a custom conversion service implementation, have a look at the More on Configuration section.

The official thymeleaf-spring3 and thymeleaf-spring4 integration packages transparently integrate Thymeleaf’s conversion service mechanism with Spring’s own Conversion Service infrastructure, so that conversion services and formatters declared in the Spring configuration will be made automatically available to ${{...}} and *{{...}} expressions.

4.15 Preprocessing

In addition to all these features for expression processing, Thymeleaf has the feature of preprocessing expressions.

Preprocessing is an execution of the expressions done before the normal one that allows for modification of the expression that will eventually be executed.

Preprocessed expressions are exactly like normal ones, but appear surrounded by a double underscore symbol (like __${expression}__).

Let’s imagine we have an i18n Messages_fr.properties entry containing an OGNL expression calling a language-specific static method, like:

article.text=@myapp.translator.Translator@translateToFrench({0})

…and a Messages_es.properties equivalent:

article.text=@myapp.translator.Translator@translateToSpanish({0})

We can create a fragment of markup that evaluates one expression or the other depending on the locale. For this, we will first select the expression (by preprocessing) and then let Thymeleaf execute it:

<p th:text="${__#{article.text('textVar')}__}">Some text here...</p>

Note that the preprocessing step for a French locale will be creating the following equivalent:

<p th:text="${@myapp.translator.Translator@translateToFrench(textVar)}">Some text here...</p>

The preprocessing String __ can be escaped in attributes using \_\_.

5 Setting Attribute Values

This chapter will explain the way in which we can set (or modify) values of attributes in our markup.

5.1 Setting the value of any attribute

Say our website publishes a newsletter, and we want our users to be able to subscribe to it, so we create a /WEB-INF/templates/subscribe.html template with a form:

<form action="subscribe.html">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" />
  </fieldset>
</form>

与 Thymeleaf 一样,此模板一开始更像是静态原型,而不是 Web 应用程序的模板。首先,action表单中的属性静态链接到模板文件本身,因此没有地方进行有用的 URL 重写。其次,value提交按钮中的属性使其显示英文文本,但我们希望它能够国际化。

然后输入th:attr属性,以及它改变所设置标签的属性值的能力:

<form action="subscribe.html" th:attr="action=@{/subscribe}">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
  </fieldset>
</form>

这个概念很简单:th:attr只需采用一个将值分配给属性的表达式。创建相应的控制器和消息文件后,处理此文件的结果将是:

<form action="/gtvg/subscribe">
  <fieldset>
    <input type="text" name="email" />
    <input type="submit" value="¡Suscríbe!"/>
  </fieldset>
</form>

除了新的属性值之外,您还可以看到应用程序上下文名称已自动添加到 URL 基础的前缀/gtvg/subscribe,如上一章所述。

但是如果我们想一次设置多个属性怎么办? XML 规则不允许在标签中两次设置属性,因此th:attr将采用逗号分隔的分配列表,例如:

<img src="../../images/gtvglogo.png" 
     th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

给定所需的消息文件,这将输出:

<img src="/gtgv/images/gtvglogo.png" title="Logo de Good Thymes" alt="Logo de Good Thymes" />

5.2 设置特定属性的值

现在,你可能会想:

<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>

…是一段相当丑陋的标记。在属性值中指定赋值可能非常实用,但如果您必须一直这样做,那么这不是创建模板的最优雅的方式。

Thymeleaf 同意你的观点,这就是为什么th:attr在模板中很少使用的原因。通常,你会使用其他th:*属性,其任务是设置特定的标签属性(而不仅仅是像这样的任何属性th:attr)。

例如,要设置value属性,请使用th:value

<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>

这看起来好多了!让我们尝试对标签action中的属性执行相同的操作form

<form action="subscribe.html" th:action="@{/subscribe}">

你还记得th:href我们home.html之前添加的那些吗?它们正是这种类型的属性:

<li><a href="product/list.html" th:href="@{/product/list}">Product List</a></li>

有很多这样的属性,每个属性都针对特定的 HTML5 属性:

th:abbr th:accept th:accept-charset
th:accesskey th:action th:align
th:alt th:archive th:audio
th:autocomplete th:axis th:background
th:bgcolor th:border th:cellpadding
th:cellspacing th:challenge th:charset
th:cite th:class th:classid
th:codebase th:codetype th:cols
th:colspan th:compact th:content
th:contenteditable th:contextmenu th:data
th:datetime th:dir th:draggable
th:dropzone th:enctype th:for
th:form th:formaction th:formenctype
th:formmethod th:formtarget th:fragment
th:frame th:frameborder th:headers
th:height th:high th:href
th:hreflang th:hspace th:http-equiv
th:icon th:id th:inline
th:keytype th:kind th:label
th:lang th:list th:longdesc
th:low th:manifest th:marginheight
th:marginwidth th:max th:maxlength
th:media th:method th:min
th:name th:onabort th:onafterprint
th:onbeforeprint th:onbeforeunload th:onblur
th:oncanplay th:oncanplaythrough th:onchange
th:onclick th:oncontextmenu th:ondblclick
th:ondrag th:ondragend th:ondragenter
th:ondragleave th:ondragover th:ondragstart
th:ondrop th:ondurationchange th:onemptied
th:onended th:onerror th:onfocus
th:onformchange th:onforminput th:onhashchange
th:oninput th:oninvalid th:onkeydown
th:onkeypress th:onkeyup th:onload
th:onloadeddata th:onloadedmetadata th:onloadstart
th:onmessage th:onmousedown th:onmousemove
th:onmouseout th:onmouseover th:onmouseup
th:onmousewheel th:onoffline th:ononline
th:onpause th:onplay th:onplaying
th:onpopstate th:onprogress th:onratechange
th:onreadystatechange th:onredo th:onreset
th:onresize th:onscroll th:onseeked
th:onseeking th:onselect th:onshow
th:onstalled th:onstorage th:onsubmit
th:onsuspend th:ontimeupdate th:onundo
th:onunload th:onvolumechange th:onwaiting
th:optimum th:pattern th:placeholder
th:poster th:preload th:radiogroup
th:rel th:rev th:rows
th:rowspan th:rules th:sandbox
th:scheme th:scope th:scrolling
th:size th:sizes th:span
th:spellcheck th:src th:srclang
th:standby th:start th:step
th:style th:summary th:tabindex
th:target th:title th:type
th:usemap th:value th:valuetype
th:vspace th:width th:wrap
th:xmlbase th:xmllang th:xmlspace

5.3 一次设置多个值

有两个相当特殊的属性称为th:alt-title和,th:lang-xmllang它们可用于同时将两个属性设置为相同的值。具体来说:

  • th:alt-title将设置alttitle
  • th:lang-xmllang将设置langxml:lang

对于我们的 GTVG 主页,我们可以用以下方法替换:

<img src="../../images/gtvglogo.png" 
     th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

...或者这个,等效于:

<img src="../../images/gtvglogo.png" 
     th:src="@{/images/gtvglogo.png}" th:title="#{logo}" th:alt="#{logo}" />

…有了这个:

<img src="../../images/gtvglogo.png" 
     th:src="@{/images/gtvglogo.png}" th:alt-title="#{logo}" />

5.4 附加和前置

Thymeleaf 还提供th:attrappendth:attrprepend属性,将其评估结果附加(后缀)或添加到现有属性值之前(前缀)。

例如,您可能希望将要添加(不是设置,只是添加)到上下文变量中的一个按钮的 CSS 类的名称存储起来,因为要使用的特定 CSS 类取决于用户之前所做的事情:

<input type="button" value="Do it!" class="btn" th:attrappend="class=${' ' + cssStyle}" />

如果你将cssStyle变量设置为 来处理此模板"warning",你将得到:

<input type="button" value="Do it!" class="btn warning" />

标准方言中还有两个特定的附加属性th:classappend:和th:styleappend属性,用于向元素添加 CSS 类或样式片段而不覆盖现有的:

<tr th:each="prod : ${prods}" class="row" th:classappend="${prodStat.odd}? 'odd'">

(不要担心该th:each属性。它是一个迭代属性,我们稍后会讨论它。)

5.5 固定值布尔属性

HTML 有布尔属性的概念,即没有值的属性,如果存在一个值则表示值为“真”。在 XHTML 中,这些属性只接受 1 个值,即其本身。

例如,checked

<input type="checkbox" name="option2" checked /> <!-- HTML -->
<input type="checkbox" name="option1" checked="checked" /> <!-- XHTML -->

标准方言包含一些属性,允许您通过评估条件来设置这些属性,因此,如果评估结果为 true,则该属性将设置为其固定值,如果评估结果为 false,则不会设置该属性:

<input type="checkbox" name="active" th:checked="${user.active}" />

标准方言中存在以下固定值布尔属性:

th:async th:autofocus th:autoplay
th:checked th:controls th:declare
th:default th:defer th:disabled
th:formnovalidate th:hidden th:ismap
th:loop th:multiple th:novalidate
th:nowrap th:open th:pubdate
th:readonly th:required th:reversed
th:scoped th:seamless th:selected

5.6 设置任意属性的值(默认属性处理器)

Thymeleaf 提供了一个默认的属性处理器,允许我们设置任何属性的值,即使th:*在标准方言中没有为其定义特定的处理器。

因此类似于:

<span th:whatever="${user.name}">...</span>

将导致:

<span whatever="John Apricot">...</span>

5.7 支持 HTML5 友好的属性和元素名称

还可以使用完全不同的语法以更加 HTML5 友好的方式将处理器应用于模板。

<table>
    <tr data-th-each="user : ${users}">
        <td data-th-text="${user.login}">...</td>
        <td data-th-text="${user.name}">...</td>
    </tr>
</table>

data-{prefix}-{name}语法是在 HTML5 中编写自定义属性的标准方法,无需开发人员使用任何命名空间名称(如)th:*。Thymeleaf 使此语法自动适用于所有方言(不仅仅是标准方言)。

还有一种语法用于指定自定义标签:{prefix}-{name},它遵循W3C 自定义元素规范( W3C Web 组件规范的一部分)。例如,这可以用于元素th:block(或th-block),这将在后面的部分中解释。

重要提示:此语法是对命名空间语法的补充th:*,并非替代。未来我们完全没有打算弃用命名空间语法。

6 迭代

到目前为止,我们已经创建了一个主页、一个用户资料页面以及一个让用户订阅我们新闻通讯的页面……但是我们的产品呢?为此,我们需要一种方法来迭代集合中的项目以构建我们的产品页面。

6.1 迭代基础

为了在/WEB-INF/templates/product/list.html页面中显示产品,我们将使用表格。我们的每件产品都将显示在一行中(一个<tr>元素),因此对于我们的模板,我们需要创建一个模板行- 一个将举例说明我们希望如何显示每件产品的行 - 然后指示 Thymeleaf 重复此操作,对每件产品重复一次。

标准方言为我们提供了一个确切的属性:th:each

使用 th:each

对于我们的产品列表页面,我们需要一个控制器方法,从服务层检索产品列表并将其添加到模板上下文中:

public void process(
        final HttpServletRequest request, final HttpServletResponse response,
        final ServletContext servletContext, final ITemplateEngine templateEngine)
        throws Exception {
    
    ProductService productService = new ProductService();
    List<Product> allProducts = productService.findAll(); 
    
    WebContext ctx = new WebContext(request, response, servletContext, request.getLocale());
    ctx.setVariable("prods", allProducts);
    
    templateEngine.process("product/list", ctx, response.getWriter());
    
}

然后我们将th:each在模板中使用它来迭代产品列表:

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
  </head>

  <body>

    <h1>Product list</h1>
  
    <table>
      <tr>
        <th>NAME</th>
        <th>PRICE</th>
        <th>IN STOCK</th>
      </tr>
      <tr th:each="prod : ${prods}">
        <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>
    </table>
  
    <p>
      <a href="../home.html" th:href="@{/}">Return to home</a>
    </p>

  </body>

</html>

您在上面看到的属性值prod : ${prods}意味着“对于评估结果中的每个元素${prods},重复此模板片段,使用名为 prod 的变量中的当前元素”。让我们给看到的每个事物命名:

  • 我们将其称为${prods}迭代表达式迭代变量
  • 我们将其称为迭代变量prod或者简称为iter 变量

请注意,proditer 变量的作用域是元素<tr>,这意味着它可用于像 这样的内部标签<td>

可迭代值

在 Thymeleaf 中,类java.util.List并不是唯一可以用于迭代的对象。有一组相当完整的对象可以通过属性来迭代th:each

  • 任何实现的对象java.util.Iterable
  • 任何实现的对象java.util.Enumeration
  • 任何实现的对象java.util.Iterator,其值将在迭代器返回时使用,而无需将所有值缓存在内存中。
  • 任何实现 的对象java.util.Map。迭代映射时,iter 变量将属于 类java.util.Map.Entry
  • 任何数组。
  • 任何其他对象都将被视为包含对象本身的单值列表。

6.2 保持迭代状态

使用时th:each,Thymeleaf 提供了一种用于跟踪迭代状态的有用机制:状态变量

状态变量在属性内定义th:each并包含以下数据:

  • 当前迭代索引,从 0 开始。这是index属性。
  • 当前迭代索引,从 1 开始。这是count属性。
  • 迭代变量中的元素总数。这是size属性。
  • 每次迭代的iter 变量。这是current属性。
  • 当前迭代是偶数还是奇数。这些是even/odd布尔属性。
  • 当前迭代是否为第一次迭代。这是一个first布尔属性。
  • 当前迭代是否是最后一次迭代。这是一个last布尔属性。

让我们看看如何在前面的例子中使用它:

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
  </tr>
  <tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.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>
</table>

状态变量(iterStat在本例中)在属性中定义,th:each方法是将其名称写在 iter 变量本身后面,以逗号分隔。与 iter 变量一样,状态变量的作用域也限定在由保存属性的标签定义的代码片段中th:each

让我们看一下处理模板的结果:

<!DOCTYPE html>

<html>

  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
    <link rel="stylesheet" type="text/css" media="all" href="/gtvg/css/gtvg.css" />
  </head>

  <body>

    <h1>Product list</h1>
  
    <table>
      <tr>
        <th>NAME</th>
        <th>PRICE</th>
        <th>IN STOCK</th>
      </tr>
      <tr class="odd">
        <td>Fresh Sweet Basil</td>
        <td>4.99</td>
        <td>yes</td>
      </tr>
      <tr>
        <td>Italian Tomato</td>
        <td>1.25</td>
        <td>no</td>
      </tr>
      <tr class="odd">
        <td>Yellow Bell Pepper</td>
        <td>2.50</td>
        <td>yes</td>
      </tr>
      <tr>
        <td>Old Cheddar</td>
        <td>18.75</td>
        <td>yes</td>
      </tr>
    </table>
  
    <p>
      <a href="/gtvg/" shape="rect">Return to home</a>
    </p>

  </body>
  
</html>

请注意,我们的迭代状态变量已完美运行,odd仅将 CSS 类建立到奇数行。

如果你没有明确设置状态变量,Thymeleaf 将始终通过Stat在迭代变量名称后添加后缀来为你创建一个:

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
  </tr>
  <tr th:each="prod : ${prods}" th:class="${prodStat.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>
</table>

6.3 通过惰性检索数据进行优化

有时我们可能希望优化数据集合的检索(例如从数据库),以便只有在真正需要使用时才检索这些集合。

实际上,这可以应用于任何数据,但考虑到内存集合可能具有的大小,检索需要迭代的集合是此场景中最常见的情况。

为了支持这一点,Thymeleaf 提供了一种延迟加载上下文变量的机制。实现接口的上下文变量ILazyContextVariable(很可能通过扩展其LazyContextVariable默认实现)将在执行时解析。例如:

context.setVariable(
     "users",
     new LazyContextVariable<List<User>>() {
         @Override
         protected List<User> loadValue() {
             return databaseRepository.findAllUsers();
         }
     });

这个变量可以在不了解其惰性的情况下使用,例如在代码中:

<ul>
  <li th:each="u : ${users}" th:text="${u.name}">user name</li>
</ul>

但同时,如果在代码中计算结果为,则永远不会被初始化(其loadValue()方法永远不会被调用) :conditionfalse

<ul th:if="${condition}">
  <li th:each="u : ${users}" th:text="${u.name}">user name</li>
</ul>

7 条件评估

7.1 简单条件:“如果”和“除非”

有时您需要模板的某个片段仅在满足某个特定条件时才出现在结果中。

例如,假设我们想在产品表中显示一列,其中包含每个产品的评论数,如果有任何评论,则显示指向该产品评论详细信息页面的链接。

为了做到这一点,我们将使用以下th:if属性:

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
    <th>COMMENTS</th>
  </tr>
  <tr th:each="prod : ${prods}" th:class="${prodStat.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>
    <td>
      <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
      <a href="comments.html" 
         th:href="@{/product/comments(prodId=${prod.id})}" 
         th:if="${not #lists.isEmpty(prod.comments)}">view</a>
    </td>
  </tr>
</table>

这里有很多东西需要查看,因此让我们集中关注重要的一行:

<a href="comments.html"
   th:href="@{/product/comments(prodId=${prod.id})}" 
   th:if="${not #lists.isEmpty(prod.comments)}">view</a>

这将创建一个指向评论页面的链接(带有 URL /product/comments),并将prodId参数设置为id产品的,但前提是该产品有任何评论。

让我们看一下最终的标记:

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
    <th>COMMENTS</th>
  </tr>
  <tr>
    <td>Fresh Sweet Basil</td>
    <td>4.99</td>
    <td>yes</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr class="odd">
    <td>Italian Tomato</td>
    <td>1.25</td>
    <td>no</td>
    <td>
      <span>2</span> comment/s
      <a href="/gtvg/product/comments?prodId=2">view</a>
    </td>
  </tr>
  <tr>
    <td>Yellow Bell Pepper</td>
    <td>2.50</td>
    <td>yes</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr class="odd">
    <td>Old Cheddar</td>
    <td>18.75</td>
    <td>yes</td>
    <td>
      <span>1</span> comment/s
      <a href="/gtvg/product/comments?prodId=4">view</a>
    </td>
  </tr>
</table>

太棒了!这正是我们想要的。

请注意,该th:if属性不仅会评估布尔条件。它的功能远不止于此,它将按照true以下规则评估指定的表达式:

  • 如果值不为空:
    • 如果值是布尔值并且是true
    • 如果值是数字且非零
    • 如果值是字符且非零
    • 如果值是字符串且不是“false”、“off”或“no”
    • 如果值不是布尔值、数字、字符或字符串。
  • (如果值为空,则 th:if 将计算为 false)。

此外,th:if还有一个逆属性th:unless,我们可以在前面的例子中使用它,而不是not在 OGNL 表达式中使用:

<a href="comments.html"
   th:href="@{/comments(prodId=${prod.id})}" 
   th:unless="${#lists.isEmpty(prod.comments)}">view</a>

7.2 Switch 语句

还有一种方法可以使用Java 中相当于switchth:switch结构的方法有条件地显示内容: /th:case属性集。

<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#{roles.manager}">User is a manager</p>
</div>

请注意,一旦一个th:case属性被评估为,同一切换上下文中的true每个其他属性都将被评估为th:casefalse

默认选项指定为th:case="*"

<div th:switch="${user.role}">
  <p th:case="'admin'">User is an administrator</p>
  <p th:case="#{roles.manager}">User is a manager</p>
  <p th:case="*">User is some other thing</p>
</div>

8 模板布局

8.1 包含模板片段

定义和引用片段

在我们的模板中,我们经常想要包含来自其他模板的部分,例如页脚、页眉、菜单等部分……

为了做到这一点,Thymeleaf 需要我们定义这些部分“片段”以供包含,这可以使用属性来完成th:fragment

假设我们想在所有杂货页面上添加标准版权页脚,因此我们创建一个/WEB-INF/templates/footer.html包含以下代码的文件:

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

  <body>
  
    <div th:fragment="copy">
      &copy; 2011 The Good Thymes Virtual Grocery
    </div>
  
  </body>
  
</html>

上面的代码定义了一个名为的片段,我们可以使用或属性copy之一轻松地将其包含在我们的主页中(还有,尽管自 Thymeleaf 3.0 以来不再推荐使用它):th:insertth:replaceth:include

<body>

  ...

  <div th:insert="~{footer :: copy}"></div>
  
</body>

请注意,th:insert需要一个片段表达式~{...}),该表达式会生成一个片段。不过,在上面的例子中,这是一个非复杂片段表达式,(~{})包围是完全可选的,因此上面的代码相当于:

<body>

  ...

  <div th:insert="footer :: copy"></div>
  
</body>

片段规范语法

片段表达式的语法非常简单。有三种不同的格式:

  • "~{templatename::selector}"包括在名为 的模板上应用指定标记选择器所产生的片段templatename。请注意selector可以只是一个片段名称,因此您可以像~{templatename::fragmentname}上面一样指定一些简单的内容~{footer :: copy}

    标记选择器语法由底层 AttoParser 解析库定义,类似于 XPath 表达式或 CSS 选择器。有关更多信息,请参阅附录 C。

  • "~{templatename}"包括名为 的完整模板templatename

    th:insert请注意,您在/标签中使用的模板名称th:replace必须能够由模板引擎当前正在使用的模板解析器解析。

  • ~{::selector}""~{this::selector}"插入来自同一模板的片段,匹配selector。如果在表达式出现的模板上找不到,则模板调用堆栈(插入)将遍历到最初处理的模板(根,直到selector在某个级别匹配。

上面例子中的templatename都可以是功能齐全的表达式(甚至是条件表达式!)例如:selector

<div th:insert="footer :: (${user.isAdmin}? #{footer.admin} : #{footer.normaluser})"></div>

请再次注意/~{...}中的周围的包络是可选的th:insertth:replace

片段可以包含任何th:*属性。片段被纳入目标模板(带有th:insert/th:replace属性的模板)后,这些属性将被评估,并且它们将能够引用此目标模板中定义的任何上下文变量。

这种片段方法的一大优点是,您可以在浏览器可完美显示的页面中编写片段,具有完整甚至有效的标记结构,同时仍保留让 Thymeleaf 将它们包含到其他模板中的能力。

引用片段而不th:fragment

得益于标记选择器的强大功能,我们可以包含不使用任何th:fragment属性的片段。它甚至可以是来自完全不了解 Thymeleaf 的其他应用程序的标记代码:

...
<div id="copy-section">
  &copy; 2011 The Good Thymes Virtual Grocery
</div>
...

我们可以使用上面的片段,通过它的id属性简单地引用它,类似于 CSS 选择器:

<body>

  ...

  <div th:insert="~{footer :: #copy-section}"></div>
  
</body>

th:insertth:replace(和th:include之间的区别

th:insertth:replace(和th:include,自 3.0 起不推荐使用)之间有什么区别?

  • th:insert是最简单的:它将简单地将指定的片段作为其主机标签的主体插入。

  • th:replace实际上是用指定的片段替换其主机标签。

  • th:include与 类似th:insert,但它不插入片段,而是仅插入此片段的内容

因此 HTML 片段如下:

<footer th:fragment="copy">
  &copy; 2011 The Good Thymes Virtual Grocery
</footer>

...在主机标签中包含三次<div>,如下所示:

<body>

  ...

  <div th:insert="footer :: copy"></div>

  <div th:replace="footer :: copy"></div>

  <div th:include="footer :: copy"></div>
  
</body>

…将导致:

<body>

  ...

  <div>
    <footer>
      &copy; 2011 The Good Thymes Virtual Grocery
    </footer>
  </div>

  <footer>
    &copy; 2011 The Good Thymes Virtual Grocery
  </footer>

  <div>
    &copy; 2011 The Good Thymes Virtual Grocery
  </div>
  
</body>

8.2 可参数化的片段签名

为了为模板片段创建更像函数的th:fragment机制,用定义的片段可以指定一组参数:

<div th:fragment="frag (onevar,twovar)">
    <p th:text="${onevar} + ' - ' + ${twovar}">...</p>
</div>

这需要使用以下两种语法之一来调用th:insert或的片段th:replace

<div th:replace="::frag (${value1},${value2})">...</div>
<div th:replace="::frag (onevar=${value1},twovar=${value2})">...</div>

请注意,最后一个选项的顺序并不重要:

<div th:replace="::frag (twovar=${value2},onevar=${value1})">...</div>

没有片段参数的片段局部变量

即使片段定义时没有像这样的参数:

<div th:fragment="frag">
    ...
</div>

我们可以使用上面指定的第二种语法来调用它们(且只能使用第二种):

<div th:replace="::frag (onevar=${value1},twovar=${value2})">

这相当于th:replace和的组合th:with

<div th:replace="::frag" th:with="onevar=${value1},twovar=${value2}">

Note that this specification of local variables for a fragment – no matter whether it has an argument signature or not – does not cause the context to be emptied prior to its execution. Fragments will still be able to access every context variable being used at the calling template like they currently are.

th:assert for in-template assertions

The th:assert attribute can specify a comma-separated list of expressions which should be evaluated and produce true for every evaluation, raising an exception if not.

<div th:assert="${onevar},(${twovar} != 43)">...</div>

This comes in handy for validating parameters at a fragment signature:

<header th:fragment="contentheader(title)" th:assert="${!#strings.isEmpty(title)}">...</header>

8.3 Flexible layouts: beyond mere fragment insertion

Thanks to fragment expressions, we can specify parameters for fragments that are not texts, numbers, bean objects… but instead fragments of markup.

This allows us to create our fragments in a way such that they can be enriched with markup coming from the calling templates, resulting in a very flexible template layout mechanism.

Note the use of the title and links variables in the fragment below:

<head th:fragment="common_header(title,links)">

  <title th:replace="${title}">The awesome application</title>

  <!-- Common styles and scripts -->
  <link rel="stylesheet" type="text/css" media="all" th:href="@{/css/awesomeapp.css}">
  <link rel="shortcut icon" th:href="@{/images/favicon.ico}">
  <script type="text/javascript" th:src="@{/sh/scripts/codebase.js}"></script>

  <!--/* Per-page placeholder for additional links */-->
  <th:block th:replace="${links}" />

</head>

We can now call this fragment like:

...
<head th:replace="base :: common_header(~{::title},~{::link})">

  <title>Awesome - Main</title>

  <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
  <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">

</head>
...

…and the result will use the actual <title> and <link> tags from our calling template as the values of the title and links variables, resulting in our fragment being customized during insertion:

...
<head>

  <title>Awesome - Main</title>

  <!-- Common styles and scripts -->
  <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
  <link rel="shortcut icon" href="/awe/images/favicon.ico">
  <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>

  <link rel="stylesheet" href="/awe/css/bootstrap.min.css">
  <link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css">

</head>
...

Using the empty fragment

A special fragment expression, the empty fragment (~{}), can be used for specifying no markup. Using the previous example:

<head th:replace="base :: common_header(~{::title},~{})">

  <title>Awesome - Main</title>

</head>
...

Note how the second parameter of the fragment (links) is set to the empty fragment and therefore nothing is written for the <th:block th:replace="${links}" /> block:

...
<head>

  <title>Awesome - Main</title>

  <!-- Common styles and scripts -->
  <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
  <link rel="shortcut icon" href="/awe/images/favicon.ico">
  <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>

</head>
...

Using the no-operation token

The no-op can be also used as a parameter to a fragment if we just want to let our fragment use its current markup as a default value. Again, using the common_header example:

...
<head th:replace="base :: common_header(_,~{::link})">

  <title>Awesome - Main</title>

  <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
  <link rel="stylesheet" th:href="@{/themes/smoothness/jquery-ui.css}">

</head>
...

See how the title argument (first argument of the common_header fragment) is set to no-op (_), which results in this part of the fragment not being executed at all (title = no-operation):

  <title th:replace="${title}">The awesome application</title>

So the result is:

...
<head>

  <title>The awesome application</title>

  <!-- Common styles and scripts -->
  <link rel="stylesheet" type="text/css" media="all" href="/awe/css/awesomeapp.css">
  <link rel="shortcut icon" href="/awe/images/favicon.ico">
  <script type="text/javascript" src="/awe/sh/scripts/codebase.js"></script>

  <link rel="stylesheet" href="/awe/css/bootstrap.min.css">
  <link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css">

</head>
...

Advanced conditional insertion of fragments

The availability of both the empty fragment and no-operation token allows us to perform conditional insertion of fragments in a very easy and elegant way.

For example, we could do this in order to insert our common :: adminhead fragment only if the user is an administrator, and insert nothing (empty fragment) if not:

...
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : ~{}">...</div>
...

Also, we can use the no-operation token in order to insert a fragment only if the specified condition is met, but leave the markup without modifications if the condition is not met:

...
<div th:insert="${user.isAdmin()} ? ~{common :: adminhead} : _">
    Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support.
</div>
...

此外,如果我们已经配置了模板解析器来检查模板资源是否存在 - 通过它们的checkExistence标志 - 我们可以使用片段本身的存在作为默认操作的条件:

...
<!-- The body of the <div> will be used if the "common :: salutation" fragment  -->
<!-- does not exist (or is empty).                                              -->
<div th:insert="~{common :: salutation} ?: _">
    Welcome [[${user.name}]], click <a th:href="@{/support}">here</a> for help-desk support.
</div>
...

8.4 删除模板片段

回到示例应用程序,让我们重新审视产品列表模板的最后一个版本:

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
    <th>COMMENTS</th>
  </tr>
  <tr th:each="prod : ${prods}" th:class="${prodStat.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>
    <td>
      <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
      <a href="comments.html" 
         th:href="@{/product/comments(prodId=${prod.id})}" 
         th:unless="${#lists.isEmpty(prod.comments)}">view</a>
    </td>
  </tr>
</table>

该代码作为模板还不错,但作为静态页面(直接通过浏览器打开而不使用 Thymeleaf 进行处理)它就不能成为一个很好的原型。

为什么?因为尽管浏览器可以完美显示,但该表只有一行,而这一行包含模拟数据。作为原型,它看起来不够逼真……我们应该有多个产品,我们需要更多行

因此让我们添加一些:

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
    <th>COMMENTS</th>
  </tr>
  <tr th:each="prod : ${prods}" th:class="${prodStat.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>
    <td>
      <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
      <a href="comments.html" 
         th:href="@{/product/comments(prodId=${prod.id})}" 
         th:unless="${#lists.isEmpty(prod.comments)}">view</a>
    </td>
  </tr>
  <tr class="odd">
    <td>Blue Lettuce</td>
    <td>9.55</td>
    <td>no</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr>
    <td>Mild Cinnamon</td>
    <td>1.99</td>
    <td>yes</td>
    <td>
      <span>3</span> comment/s
      <a href="comments.html">view</a>
    </td>
  </tr>
</table>

好的,现在我们有三个了,这对于原型来说肯定更好。但是……当我们用 Thymeleaf 处理它时会发生什么?:

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
    <th>COMMENTS</th>
  </tr>
  <tr>
    <td>Fresh Sweet Basil</td>
    <td>4.99</td>
    <td>yes</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr class="odd">
    <td>Italian Tomato</td>
    <td>1.25</td>
    <td>no</td>
    <td>
      <span>2</span> comment/s
      <a href="/gtvg/product/comments?prodId=2">view</a>
    </td>
  </tr>
  <tr>
    <td>Yellow Bell Pepper</td>
    <td>2.50</td>
    <td>yes</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr class="odd">
    <td>Old Cheddar</td>
    <td>18.75</td>
    <td>yes</td>
    <td>
      <span>1</span> comment/s
      <a href="/gtvg/product/comments?prodId=4">view</a>
    </td>
  </tr>
  <tr class="odd">
    <td>Blue Lettuce</td>
    <td>9.55</td>
    <td>no</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr>
    <td>Mild Cinnamon</td>
    <td>1.99</td>
    <td>yes</td>
    <td>
      <span>3</span> comment/s
      <a href="comments.html">view</a>
    </td>
  </tr>
</table>

最后两行是模拟行!当然是了:迭代只应用于第一行,所以 Thymeleaf 没有理由删除其他两行。

我们需要一种方法来在模板处理期间删除这两行。让我们th:remove在第二个和第三个<tr>标签上使用该属性:

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
    <th>COMMENTS</th>
  </tr>
  <tr th:each="prod : ${prods}" th:class="${prodStat.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>
    <td>
      <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
      <a href="comments.html" 
         th:href="@{/product/comments(prodId=${prod.id})}" 
         th:unless="${#lists.isEmpty(prod.comments)}">view</a>
    </td>
  </tr>
  <tr class="odd" th:remove="all">
    <td>Blue Lettuce</td>
    <td>9.55</td>
    <td>no</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr th:remove="all">
    <td>Mild Cinnamon</td>
    <td>1.99</td>
    <td>yes</td>
    <td>
      <span>3</span> comment/s
      <a href="comments.html">view</a>
    </td>
  </tr>
</table>

一旦处理完毕,一切都将恢复原状:

<table>
  <tr>
    <th>NAME</th>
    <th>PRICE</th>
    <th>IN STOCK</th>
    <th>COMMENTS</th>
  </tr>
  <tr>
    <td>Fresh Sweet Basil</td>
    <td>4.99</td>
    <td>yes</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr class="odd">
    <td>Italian Tomato</td>
    <td>1.25</td>
    <td>no</td>
    <td>
      <span>2</span> comment/s
      <a href="/gtvg/product/comments?prodId=2">view</a>
    </td>
  </tr>
  <tr>
    <td>Yellow Bell Pepper</td>
    <td>2.50</td>
    <td>yes</td>
    <td>
      <span>0</span> comment/s
    </td>
  </tr>
  <tr class="odd">
    <td>Old Cheddar</td>
    <td>18.75</td>
    <td>yes</td>
    <td>
      <span>1</span> comment/s
      <a href="/gtvg/product/comments?prodId=4">view</a>
    </td>
  </tr>
</table>

那么all属性中的值是什么意思呢?th:remove根据其值的不同,可以有五种不同的表现:

  • all:删除包含的标签及其所有子标签。
  • body:不删除包含的标签,但删除其所有子标签。
  • tag:删除包含的标签,但不删除其子标签。
  • all-but-first:删除包含标签的所有子标签,除了第一个。
  • none:不执行任何操作。此值对于动态评估很有用。

这个all-but-first值有什么用呢?它可以让我们th:remove="all"在制作原型时节省一些:

<table>
  <thead>
    <tr>
      <th>NAME</th>
      <th>PRICE</th>
      <th>IN STOCK</th>
      <th>COMMENTS</th>
    </tr>
  </thead>
  <tbody th:remove="all-but-first">
    <tr th:each="prod : ${prods}" th:class="${prodStat.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>
      <td>
        <span th:text="${#lists.size(prod.comments)}">2</span> comment/s
        <a href="comments.html" 
           th:href="@{/product/comments(prodId=${prod.id})}" 
           th:unless="${#lists.isEmpty(prod.comments)}">view</a>
      </td>
    </tr>
    <tr class="odd">
      <td>Blue Lettuce</td>
      <td>9.55</td>
      <td>no</td>
      <td>
        <span>0</span> comment/s
      </td>
    </tr>
    <tr>
      <td>Mild Cinnamon</td>
      <td>1.99</td>
      <td>yes</td>
      <td>
        <span>3</span> comment/s
        <a href="comments.html">view</a>
      </td>
    </tr>
  </tbody>
</table>

th:remove属性可以采用任何Thymeleaf 标准表达式,只要它返回允许的字符串值之一(alltagbodyall-but-firstnone

这意味着删除可能是有条件的,例如:

<a href="/something" th:remove="${condition}? tag : none">Link text not to be removed</a>

还请注意,th:remove认为是null的同义词none,因此以下内容与上面的例子相同:

<a href="/something" th:remove="${condition}? tag">Link text not to be removed</a>

在这种情况下,如果${condition}为假,null则会返回,因此不会执行任何删除。

8.5 布局继承

为了能够将单个文件用作布局,可以使用片段。具有titlecontent使用th:fragment和的简单布局示例th:replace

<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:replace="${title}">Layout Title</title>
</head>
<body>
    <h1>Layout H1</h1>
    <div th:replace="${content}">
        <p>Layout content</p>
    </div>
    <footer>
        Layout footer
    </footer>
</body>
</html>

此示例声明了一个名为layout 的片段,其参数为titlecontent。在继承该片段的页面上,这两个片段将被以下示例中提供的片段表达式替换。

<!DOCTYPE html>
<html th:replace="~{layoutFile :: layout(~{::title}, ~{::section})}">
<head>
    <title>Page Title</title>
</head>
<body>
<section>
    <p>Page content</p>
    <div>Included on page</div>
</section>
</body>
</html>

在这个文件中,html标签将被替换为layout,但在布局中title和将分别被content替换。titlesection

如果需要,布局可以由页眉页脚等几个片段组成。

9 局部变量

Thymeleaf 将为模板的特定片段定义的变量称为局部变量,并且只能在该片段内进行评估。

我们已经看到的一个例子是prod我们产品列表页面中的 iter 变量:

<tr th:each="prod : ${prods}">
    ...
</tr>

prod变量仅在标记的范围内可用<tr>。具体来说:

  • 它将适用于th:*在该标签中执行的优先级低于的任何其他属性th:each(这意味着它们将在之后执行th:each)。
  • 它将可用于<tr>标签的任何子元素,例如任何<td>元素。

Thymeleaf 提供了一种无需迭代来声明局部变量的方法,使用属性th:with,其语法类似于属性值赋值:

<div th:with="firstPer=${persons[0]}">
  <p>
    The name of the first person is <span th:text="${firstPer.name}">Julius Caesar</span>.
  </p>
</div>

th:with处理时,该firstPer变量被创建为局部变量并添加到来自上下文的变量映射中,以便它可以与上下文中声明的任何其他变量一起进行评估,但仅限于包含标签的范围内<div>

您可以使用通常的多重赋值语法同时定义多个变量:

<div th:with="firstPer=${persons[0]},secondPer=${persons[1]}">
  <p>
    The name of the first person is <span th:text="${firstPer.name}">Julius Caesar</span>.
  </p>
  <p>
    But the name of the second person is 
    <span th:text="${secondPer.name}">Marcus Antonius</span>.
  </p>
</div>

th:with属性允许重复使用在同一属性中定义的变量:

<div th:with="company=${user.company + ' Co.'},account=${accounts[company]}">...</div>

让我们在杂货店的主页上使用它!还记得我们为输出格式化日期而编写的代码吗?

<p>
  Today is: 
  <span th:text="${#calendars.format(today,'dd MMMM yyyy')}">13 february 2011</span>
</p>

那么,如果我们想让它"dd MMMM yyyy"真正依赖于语言环境呢?例如,我们可能想将以下消息添加到我们的home_en.properties

date.format=MMMM dd'','' yyyy

…以及与我们的等价的home_es.properties

date.format=dd ''de'' MMMM'','' yyyy

现在,让我们th:with将本地化的日期格式放入变量中,然后在表达式中使用它th:text

<p th:with="df=#{date.format}">
  Today is: <span th:text="${#calendars.format(today,df)}">13 February 2011</span>
</p>

这很简单。事实上,考虑到 的比更高th:with我们可以在标签中解决所有问题:precedenceth:textspan

<p>
  Today is: 
  <span th:with="df=#{date.format}" 
        th:text="${#calendars.format(today,df)}">13 February 2011</span>
</p>

您可能会想:优先级?我们还没有讨论过这个!好吧,不用担心,因为这正是下一章的内容。

10 属性优先级

如果在同一个标​​签中写入多个th:*属性,会发生什么情况?例如:

<ul>
  <li th:each="item : ${items}" th:text="${item.description}">Item description here...</li>
</ul>

我们希望该th:each属性能够在之前执行,th:text以便获得我们想要的结果,但鉴于 HTML/XML 标准没有赋予标签中属性的书写顺序任何含义,必须在属性本身中建立优先机制,以确保其能够按预期工作。

因此,所有 Thymeleaf 属性都定义了一个数字优先级,它确定了它们在标签中的执行顺序。这个顺序是:

命令 特征 属性
1 片段包含 th:insert
th:replace
2 片段迭代 th:each
3 条件评估 th:if
th:unless
th:switch
th:case
4 局部变量定义 th:object
th:with
5 通用属性修改 th:attr
th:attrprepend
th:attrappend
6 具体属性修改 th:value
th:href
th:src
...
7 文字(标签主体修改) th:text
th:utext
8 片段规范 th:fragment
9 碎片清除 th:remove

这种优先机制意味着,如果属性位置被反转,上述迭代片段将给出完全相同的结果(尽管可读性会稍微差一些):

<ul>
  <li th:text="${item.description}" th:each="item : ${items}">Item description here...</li>
</ul>

11 评论和区块

11.1. 标准 HTML/XML 注释

标准 HTML/XML 注释<!-- ... -->可以在 Thymeleaf 模板的任何地方使用。这些注释中的任何内容都不会被 Thymeleaf 处理,并且会被逐字复制到结果中:

<!-- User info follows -->
<div th:text="${...}">
  ...
</div>

11.2. Thymeleaf 解析器级注释块

解析器级注释块是 Thymeleaf 解析模板时将从模板中简单删除的代码。它们看起来像这样:

<!--/* This code will be removed at Thymeleaf parsing time! */-->

Thymeleaf 将删除<!--/*和之间的所有内容*/-->,因此这些注释块也可以用于在模板静态打开时显示代码,并且知道它将在 Thymeleaf 处理它时被删除:

<!--/*--> 
  <div>
     you can see me only before Thymeleaf processes me!
  </div>
<!--*/-->

这对于包含大量<tr>'s 的原型表可能非常方便,例如:

<table>
   <tr th:each="x : ${xs}">
     ...
   </tr>
   <!--/*-->
   <tr>
     ...
   </tr>
   <tr>
     ...
   </tr>
   <!--*/-->
</table>

11.3. Thymeleaf 原型专用注释块

Thymeleaf 允许定义特殊的注释块,当模板静态打开(即作为原型)时将其标记为注释,但在执行模板时 Thymeleaf 将其视为正常标记。

<span>hello!</span>
<!--/*/
  <div th:text="${...}">
    ...
  </div>
/*/-->
<span>goodbye!</span>

Thymeleaf 的解析系统将简单地删除<!--/*//*/-->标记,但不会删除其内容,因此将保留未注释的内容。因此,在执行模板时,Thymeleaf 实际上会看到以下内容:

<span>hello!</span>
 
  <div th:text="${...}">
    ...
  </div>
 
<span>goodbye!</span>

与解析器级注释块一样,此功能与方言无关。

11.4. 合成th:block标签

Thymeleaf 标准方言中包含的唯一元素处理器(不是属性)是th:block

th:block只是一个属性容器,允许模板开发人员指定他们想要的任何属性。Thymeleaf 将执行这些属性,然后简单地使块(而不是其内容)消失。

<tr>因此,它可能很有用,例如,在创建需要每个元素多个的迭代表时:

<table>
  <th:block th:each="user : ${users}">
    <tr>
        <td th:text="${user.login}">...</td>
        <td th:text="${user.name}">...</td>
    </tr>
    <tr>
        <td colspan="2" th:text="${user.address}">...</td>
    </tr>
  </th:block>
</table>

与仅原型的注释块结合使用时尤其有用:

<table>
    <!--/*/ <th:block th:each="user : ${users}"> /*/-->
    <tr>
        <td th:text="${user.login}">...</td>
        <td th:text="${user.name}">...</td>
    </tr>
    <tr>
        <td colspan="2" th:text="${user.address}">...</td>
    </tr>
    <!--/*/ </th:block> /*/-->
</table>

请注意,该解决方案如何允许模板成为有效的 HTML(无需<div>在里面添加禁止块<table>),并且在浏览器中作为原型静态打开时仍然可以正常工作!

12 内联

12.1 表达式内联

尽管标准方言允许我们使用标签属性来做几乎所有的事情,但在某些情况下,我们可能更喜欢将表达式直接写入 HTML 文本中。例如,我们更喜欢这样写:

<p>Hello, [[${session.user.name}]]!</p>

…而不是这样:

<p>Hello, <span th:text="${session.user.name}">Sebastian</span>!</p>

[[...]]在 Thymeleaf 中,或之间的表达式[(...)]被视为内联表达式th:text,在其中我们可以使用在或属性中有效的任何类型的表达式th:utext

请注意,虽然[[...]]对应于th:text(即结果将是HTML 转义的),但[(...)]对应于th:utext和不会执行任何 HTML 转义。因此,对于诸如这样的变量msg = 'This is <b>great!</b>',给出以下片段:

<p>The message is "[(${msg})]"</p>

结果将不会<b>转义这些标签,因此:

<p>The message is "This is <b>great!</b>"</p>

而如果逃避的话:

<p>The message is "[[${msg}]]"</p>

结果将是 HTML 转义的:

<p>The message is "This is &lt;b&gt;great!&lt;/b&gt;"</p>

请注意,文本内联在我们的标记中每个标签的主体中默认处于活动状态- 而不是标签本身 - 因此我们不需要执行任何操作来启用它。

内联模板与自然模板

如果你之前使用过其他以这种输出文本方式为标准的模板引擎,你可能会问:我们为什么不从一开始就这样做呢?这比那些 th:text 属性的代码要少!

好吧,要小心,因为虽然你可能会发现内联非常有趣,但你应该始终记住,当你静态打开内联表达式时,它们将逐字显示在你的 HTML 文件中,所以你可能不再能够将它们用作设计原型了!

浏览器在不使用内联的情况下静态显示我们的代码片段之间的区别是……

Hello, Sebastian!

…并使用它…

Hello, [[${session.user.name}]]!

...从设计实用性方面来看非常明确。

禁用内联

不过,这种机制可以被禁用,因为实际上可能存在我们确实希望输出[[...]][(...)]序列而不将其内容作为表达式处理的情况。为此,我们将使用th:inline="none"

<p th:inline="none">A double array looks like this: [[1, 2, 3], [4, 5]]!</p>

这将导致:

<p>A double array looks like this: [[1, 2, 3], [4, 5]]!</p>

12.2 文本内联

文本内联与我们刚刚看到的表达式内联功能非常相似,但它实际上增加了更多功能。它必须使用 明确启用th:inline="text"

文本内联不仅允许我们使用刚刚看到的相同的内联表达式,而且实际上处理标签主体,就好像它们是在TEXT模板模式中处理的模板一样,这使我们能够执行基于文本的模板逻辑(而不仅仅是输出表达式)。

在下一章关于文本模板模式中我们将看到更多相关内容。

12.3 JavaScript 内联

JavaScript 内联允许更好地集成模板模式<script>下正在处理的模板中的 JavaScript 块HTML

文本内联一样,这实际上相当于在模板模式下将脚本内容当作模板来处理,因此文本模板模式JAVASCRIPT的所有功能(请参阅下一章)都将唾手可得。然而,在本节中,我们将重点介绍如何使用它将 Thymeleaf 表达式的输出添加到 JavaScript 块中。

必须使用以下命令明确启用此模式th:inline="javascript"

<script th:inline="javascript">
    ...
    var username = [[${session.user.name}]];
    ...
</script>

这将导致:

<script th:inline="javascript">
    ...
    var username = "Sebastian \"Fruity\" Applejuice";
    ...
</script>

上面的代码中有两点需要注意:

首先,JavaScript 内联不仅会输出所需的文本,还会用引号将其括起来并用 JavaScript 转义其内容,以便将表达式结果输出为格式正确的 JavaScript 文字

其次,发生这种情况是因为我们将${session.user.name}表达式输出为转义,即使用双括号表达式:[[${session.user.name}]]。如果我们使用非转义表达式,例如:

<script th:inline="javascript">
    ...
    var username = [(${session.user.name})];
    ...
</script>

结果如下:

<script th:inline="javascript">
    ...
    var username = Sebastian "Fruity" Applejuice;
    ...
</script>

…这是格式错误的 JavaScript 代码。但是,如果我们通过附加内联表达式来构建脚本的某些部分,则可能需要输出未转义的内容,因此手头有此工具会很好。

JavaScript 自然模板

提到的JavaScript 内联机制的智能性远不止应用 JavaScript 特定的转义并将表达式结果输出为有效文字。

例如,我们可以将(转义的)内联表达式包装在 JavaScript 注释中,如下所示:

<script th:inline="javascript">
    ...
    var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
    ...
</script>

并且 Thymeleaf 将忽略我们在注释之后和分号之前写的所有内容(在本例中为'Gertrud Kiwifruit'),因此执行此操作的结果将与我们不使用包装注释时完全一样:

<script th:inline="javascript">
    ...
    var username = "Sebastian \"Fruity\" Applejuice";
    ...
</script>

但是再仔细看一下原始模板代码:

<script th:inline="javascript">
    ...
    var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
    ...
</script>

请注意这是有效的 JavaScript代码。当您以静态方式打开模板文件时(无需在服务器上执行),它将完美执行。

所以我们现在有了一种制作JavaScript 自然模板的方法!

高级内联评估和 JavaScript 序列化

关于 JavaScript 内联,需要注意的一件重要事情是,此表达式求值是智能的,不仅限于字符串。Thymeleaf 将以 JavaScript 语法正确写入以下类型的对象:

  • 字符串
  • 数字
  • 布尔值
  • 数组
  • 收藏
  • Maps
  • Bean(具有gettersetter方法的对象)

例如,如果我们有以下代码:

<script th:inline="javascript">
    ...
    var user = /*[[${session.user}]]*/ null;
    ...
</script>

${session.user}表达式将计算为一个User对象,并且 Thymeleaf 将正确地将其转换为 Javascript 语法:

<script th:inline="javascript">
    ...
    var user = {"age":null,"firstName":"John","lastName":"Apricot",
                "name":"John Apricot","nationality":"Antarctica"};
    ...
</script>

这种 JavaScript 序列化的方式是通过接口的实现,可以在模板引擎使用org.thymeleaf.standard.serializer.IStandardJavaScriptSerializer的实例上进行配置。StandardDialect

此 JS 序列化机制的默认实现将在类路径中查找Jackson 库,如果存在,则使用它。如果不存在,它将应用内置的序列化机制,该机制可满足大多数场景的需求并产生类似的结果(但灵活性较差)。

12.4 CSS 内联

Thymeleaf 还允许在 CSS 标签中使用内联<style>,例如:

<style th:inline="css">
  ...
</style>

例如,假设我们有两个变量设置为两个不同的String值:

classname = 'main elems'
align = 'center'

我们可以像这样使用它们:

<style th:inline="css">
    .[[${classname}]] {
      text-align: [[${align}]];
    }
</style>

结果是:

<style th:inline="css">
    .main\ elems {
      text-align: center;
    }
</style>

请注意,CSS 内联也具有一定的智能,就像 JavaScript 一样。具体来说,通过转义表达式输出的表达式(如)[[${classname}]]将被转义为CSS 标识符。这就是为什么我们的在上面的代码片段中classname = 'main elems'变成了。main\ elems

高级功能:CSS自然模板等。

与之前对 JavaScript 的解释相同,CSS 内联也允许我们的<style>标签静态和动态地工作,即通过将内联表达式包装在注释中作为CSS 自然模板。参见:

<style th:inline="css">
    .main\ elems {
      text-align: /*[[${align}]]*/ left;
    }
</style>

13 文本模板模式

13.1 文本语法

Thymeleaf模板模式中有三种被视为文本TEXTJAVASCRIPTCSS。这将它们与标记模板模式区分开来:HTMLXML

文本模板模式和标记模板模式之间的主要区别在于,文本模板中没有标签可以以属性的形式插入逻辑,因此我们必须依赖其他机制。

这些机制中第一个也是最基本的是内联,我们在上一章已经详细介绍过。内联语法是在文本模板模式下输出表达式结果的最简单方法,因此这是一个非常适合文本电子邮件的模板。

  Dear [(${name})],

  Please find attached the results of the report you requested
  with name "[(${report.name})]".

  Sincerely,
    The Reporter.

即使没有标签,上面的例子也是一个完整有效的Thymeleaf模板,可以在TEXT模板模式下执行。

但是为了包含比单纯的输出表达式更复杂的逻辑,我们需要一种新的非基于标签的语法:

[# th:each="item : ${items}"]
  - [(${item})]
[/]

这实际上是更详细的浓缩版本:

[#th:block th:each="item : ${items}"]
  - [#th:block th:utext="${item}" /]
[/th:block]

请注意,此新语法基于声明为[#element ...]而非 的元素(即可处理标签) <element ...>。元素像 一样开放[#element ...],像 一样封闭[/element],独立标签可以通过将开放元素最小化为 来声明,其方式/几乎等同于 XML 标签:[#element ... /]

标准方言仅包含这些元素之一的处理器:已知元素th:block,尽管我们可以在方言中扩展它并以通常的方式创建新元素。此外,元素th:block( [#th:block ...] ... [/th:block]) 可以缩写为空字符串 ( [# ...] ... [/]),因此上述块实际上等同于:

[# th:each="item : ${items}"]
  - [# th:utext="${item}" /]
[/]

并且考虑到[# th:utext="${item}" /]相当于内联的非转义表达式,我们可以使用它来减少代码量。因此,我们最终得到了上面看到的第一段代码:

[# th:each="item : ${items}"]
  - [(${item})]
[/]

请注意,文本语法要求完全元素平衡(没有未关闭的标签)和带引号的属性- 它更具 XML 风格而不是 HTML 风格。

让我们看一个更完整的模板示例TEXT,一个纯文本电子邮件模板:

Dear [(${customer.name})],

This is the list of our products:

[# th:each="prod : ${products}"]
   - [(${prod.name})]. Price: [(${prod.price})] EUR/kg
[/]

Thanks,
  The Thymeleaf Shop

执行后的结果可能类似于:

Dear Mary Ann Blueberry,

This is the list of our products:

   - Apricots. Price: 1.12 EUR/kg
   - Bananas. Price: 1.78 EUR/kg
   - Apples. Price: 0.85 EUR/kg
   - Watermelon. Price: 1.91 EUR/kg

Thanks,
  The Thymeleaf Shop

JAVASCRIPT模板模式中的另一个示例是greeter.js,我们将文件作为文本模板进行处理,并从 HTML 页面调用其结果。请注意,这不是HTML<script>模板中的块,而是作为.js模板单独处理的文件:

var greeter = function() {

    var username = [[${session.user.name}]];

    [# th:each="salut : ${salutations}"]    
      alert([[${salut}]] + " " + username);
    [/]

};

执行后的结果可能类似于:

var greeter = function() {

    var username = "Bertrand \"Crunchy\" Pear";

      alert("Hello" + " " + username);
      alert("Ol\u00E1" + " " + username);
      alert("Hola" + " " + username);

};

转义元素属性

为了避免与可能在其他模式下处理的模板部分交互(例如模板text内部的 -mode 内联),Thymeleaf 3.0 允许对其文本语法HTML中的元素中的属性进行转义。所以:

  • TEXT模板模式中的属性将是HTML 非转义的
  • JAVASCRIPT模板模式中的属性将是JavaScript 非转义的
  • CSS模板模式中的属性将是CSS 非转义的

因此,这在 -mode 模板中是完全没问题的TEXT(请注意&gt;):

  [# th:if="${120&lt;user.age}"]
     Congratulations!
  [/]

当然,这在真正的文本&lt;模板中是没有意义的,但如果我们正在处理包含上述代码的块的 HTML 模板,并且我们希望确保我们的浏览器在静态打开文件作为原型时不会将其作为打开标签的名称,那么这是一个好主意。th:inline="text"<user.age

13.2 可扩展性

这种语法的优点之一是它与标记语法一样具有可扩展性。开发人员仍然可以使用自定义元素和属性定义自己的方言,为其应用前缀(可选),然后在文本模板模式中使用它们:

  [#myorg:dosomething myorg:importantattr="211"]some text[/myorg:dosomething]

13.3 仅限文本原型的注释块:添加代码

JAVASCRIPT和模板模式CSS(不适用于TEXT)允许在特殊注释语法之间包含代码/*[+...+]*/,以便 Thymeleaf 在处理模板时自动取消注释这些代码:

var x = 23;

/*[+

var msg  = "This is a working application";

+]*/

var f = function() {
    ...

将按如下方式执行:

var x = 23;

var msg  = "This is a working application";

var f = function() {
...

您可以在这些注释中包含表达式,并且它们会被评估:

var x = 23;

/*[+

var msg  = "Hello, " + [[${session.user.name}]];

+]*/

var f = function() {
...

13.4 文本解析器级注释块:删除代码

与仅原型注释块类似,所有三种文本模板模式(TEXTJAVASCRIPTCSS)都可以指示 Thymeleaf 删除特殊/*[- *//* -]*/标记之间的代码,如下所示:

var x = 23;

/*[- */

var msg  = "This is shown only when executed statically!";

/* -]*/

var f = function() {
...

或者这样TEXT

...
/*[- Note the user is obtained from the session, which must exist -]*/
Welcome [(${session.user.name})]!
...

13.5 自然 JavaScript 和 CSS 模板

如上一章所示,JavaScript 和 CSS 内联提供了在 JavaScript/CSS 注释中包含内联表达式的可能性,例如:

...
var username = /*[[${session.user.name}]]*/ "Sebastian Lychee";
...

这是有效的 JavaScript,执行后看起来是这样的:

...
var username = "John Apricot";
...

将内联表达式括在注释中的相同技巧实际上可以用于整个文本模式语法:

  /*[# th:if="${user.admin}"]*/
     alert('Welcome admin');
  /*[/]*/

上述代码中的警报将在模板静态打开时显示 - 因为它是 100% 有效的 JavaScript - 并且如果用户是管理员,则在模板运行时也会显示。它相当于:

  [# th:if="${user.admin}"]
     alert('Welcome admin');
  [/]

...这实际上是模板解析期间初始版本转换为的代码。

但请注意,将元素包装在注释中并不会像内联输出表达式那样清理它们所在的行(向右直到;找到 a)。该行为仅适用于内联输出表达式。

因此,Thymeleaf 3.0 允许以自然模板的形式开发复杂的 JavaScript 脚本和 CSS 样式表,既可以作为原型,也可以作为工作模板

14 我们的杂货还有更多页面

现在我们对使用 Thymeleaf 有了更多的了解,我们可以向我们的网站添加一些新的页面用于订单管理。

请注意,我们将重点关注 HTML 代码,但如果您想查看相应的控制器,可以查看捆绑的源代码。

14.1 订单列表

让我们首先创建一个订单列表页面/WEB-INF/templates/order/list.html

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

  <head>

    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
  </head>

  <body>

    <h1>Order list</h1>
  
    <table>
      <tr>
        <th>DATE</th>
        <th>CUSTOMER</th>
        <th>TOTAL</th>
        <th></th>
      </tr>
      <tr th:each="o : ${orders}" th:class="${oStat.odd}? 'odd'">
        <td th:text="${#calendars.format(o.date,'dd/MMM/yyyy')}">13 jan 2011</td>
        <td th:text="${o.customer.name}">Frederic Tomato</td>
        <td th:text="${#aggregates.sum(o.orderLines.{purchasePrice * amount})}">23.32</td>
        <td>
          <a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
        </td>
      </tr>
    </table>
  
    <p>
      <a href="../home.html" th:href="@{/}">Return to home</a>
    </p>
    
  </body>
  
</html>

除了这点 OGNL 魔法之外,这里没有什么可以让我们感到惊讶的:

<td th:text="${#aggregates.sum(o.orderLines.{purchasePrice * amount})}">23.32</td>

其作用是,对于OrderLine订单中的每一条订单行(对象),将其purchasePriceamount属性相乘(通过调用相应的getPurchasePrice()getAmount()方法)并将结果返回到数字列表中,然后由#aggregates.sum(...)函数进行聚合以获得订单总价。

您一定会喜欢 OGNL 的强大功能。

14.2 订单详情

现在进入订单详情页,我们将大量使用星号语法:

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

  <head>
    <title>Good Thymes Virtual Grocery</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link rel="stylesheet" type="text/css" media="all" 
          href="../../../css/gtvg.css" th:href="@{/css/gtvg.css}" />
  </head>

  <body th:object="${order}">

    <h1>Order details</h1>

    <div>
      <p><b>Code:</b> <span th:text="*{id}">99</span></p>
      <p>
        <b>Date:</b>
        <span th:text="*{#calendars.format(date,'dd MMM yyyy')}">13 jan 2011</span>
      </p>
    </div>

    <h2>Customer</h2>

    <div th:object="*{customer}">
      <p><b>Name:</b> <span th:text="*{name}">Frederic Tomato</span></p>
      <p>
        <b>Since:</b>
        <span th:text="*{#calendars.format(customerSince,'dd MMM yyyy')}">1 jan 2011</span>
      </p>
    </div>
  
    <h2>Products</h2>
  
    <table>
      <tr>
        <th>PRODUCT</th>
        <th>AMOUNT</th>
        <th>PURCHASE PRICE</th>
      </tr>
      <tr th:each="ol,row : *{orderLines}" th:class="${row.odd}? 'odd'">
        <td th:text="${ol.product.name}">Strawberries</td>
        <td th:text="${ol.amount}" class="number">3</td>
        <td th:text="${ol.purchasePrice}" class="number">23.32</td>
      </tr>
    </table>

    <div>
      <b>TOTAL:</b>
      <span th:text="*{#aggregates.sum(orderLines.{purchasePrice * amount})}">35.23</span>
    </div>
  
    <p>
      <a href="list.html" th:href="@{/order/list}">Return to order list</a>
    </p>

  </body>
  
</html>

除了这个嵌套对象选择之外,这里没有什么新内容:

<body th:object="${order}">

  ...

  <div th:object="*{customer}">
    <p><b>Name:</b> <span th:text="*{name}">Frederic Tomato</span></p>
    ...
  </div>

  ...
</body>

…这*{name}相当于:

<p><b>Name:</b> <span th:text="${order.customer.name}">Frederic Tomato</span></p>

15 有关配置的更多信息

15.1 模板解析器

对于我们的 Good Thymes Virtual Grocery,我们选择了一个ITemplateResolver名为的实现ServletContextTemplateResolver,它允许我们从 Servlet 上下文中获取模板作为资源。

除了使我们能够通过实现ITemplateResolver,Thymeleaf 来创建自己的模板解析器之外,它还包含四种开箱即用的实现:

  • org.thymeleaf.templateresolver.ClassLoaderTemplateResolver,将模板解析为类加载器资源,例如:

    return Thread.currentThread().getContextClassLoader().getResourceAsStream(template);
  • org.thymeleaf.templateresolver.FileTemplateResolver,它将模板解析为文件系统中的文件,例如:

    return new FileInputStream(new File(template));
  • org.thymeleaf.templateresolver.UrlTemplateResolver,它将模板解析为 URL(甚至是非本地的 URL),例如:

    return (new URL(template)).openStream();
  • org.thymeleaf.templateresolver.StringTemplateResolver,它将模板直接解析为String指定为template(或模板名称,在这种情况下显然不仅仅是一个名称):

    return new StringReader(templateName);

所有预捆绑的实现都ITemplateResolver允许同一组配置参数,其中包括:

  • 前缀和后缀(如前所述):

    templateResolver.setPrefix("/WEB-INF/templates/");
    templateResolver.setSuffix(".html");
  • 模板别名允许使用不直接对应于文件名的模板名称。如果后缀/前缀和别名都存在,则别名将在前缀/后缀之前应用:

    templateResolver.addTemplateAlias("adminHome","profiles/admin/home");
    templateResolver.setTemplateAliases(aliasesMap);
  • 读取模板时要应用的编码:

    templateResolver.setCharacterEncoding("UTF-8");
  • 要使用的模板模式:

    // Default is HTML
    templateResolver.setTemplateMode("XML");
  • 模板缓存的默认模式,以及定义特定模板是否可缓存的模式:

    // Default is true
    templateResolver.setCacheable(false);
    templateResolver.getCacheablePatternSpec().addPattern("/users/*");
  • 源自此模板解析器的已解析模板缓存条目的 TTL(以毫秒为单位)。如果未设置,则从缓存中删除条目的唯一方法是超出缓存最大大小(最旧的条目将被删除)。

    // Default is no TTL (only cache size exceeded would remove entries)
    templateResolver.setCacheTTLMs(60000L);

Thymeleaf + Spring 集成包提供了一种SpringResourceTemplateResolver实现,它使用所有 Spring 基础架构来访问和读取应用程序中的资源,并且是支持 Spring 的应用程序中推荐的实现。

链接模板解析器

此外,模板引擎可以指定多个模板解析器,在这种情况下,可以在它们之间建立一个模板解析的顺序,这样,如果第一个解析器无法解析模板,则询问第二个解析器,依此类推:

ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));

ServletContextTemplateResolver servletContextTemplateResolver = 
        new ServletContextTemplateResolver(servletContext);
servletContextTemplateResolver.setOrder(Integer.valueOf(2));

templateEngine.addTemplateResolver(classLoaderTemplateResolver);
templateEngine.addTemplateResolver(servletContextTemplateResolver);

当应用多个模板解析器时,建议为每个模板解析器指定模式,以便 Thymeleaf 可以快速丢弃那些不用于解析模板的模板解析器,从而提高性能。这样做不是要求,而是一种建议:

ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
// This classloader will not be even asked for any templates not matching these patterns 
classLoaderTemplateResolver.getResolvablePatternSpec().addPattern("/layout/*.html");
classLoaderTemplateResolver.getResolvablePatternSpec().addPattern("/menu/*.html");

ServletContextTemplateResolver servletContextTemplateResolver = 
        new ServletContextTemplateResolver(servletContext);
servletContextTemplateResolver.setOrder(Integer.valueOf(2));

如果未指定这些可解析模式,我们将依赖于ITemplateResolver所使用的每个实现的特定功能。请注意,并非所有实现都能够在解析之前确定模板的存在,因此可能始终将模板视为可解析并中断解析链(不允许其他解析器检查同一模板),但随后无法读取实际资源。

Thymeleaf 核心中包含的所有ITemplateResolver实现都包含一种机制,该机制允许我们让解析器在将资源视为可解析之前真正检查资源是否存在。它是标志,其工作原理如下:checkExistence

ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
classLoaderTempalteResolver.setCheckExistence(true);

checkExistence标志强制解析器在解析阶段执行资源存在性的实际检查(如果存在性检查返回 false,则调用链中的下一个解析器)。虽然这在任何情况下都听起来不错,但在大多数情况下,这意味着对资源本身的两次访问(一次用于检查存在性,另一次用于读取它),并且在某些情况下可能存在性能问题,例如基于远程 URL 的模板资源 - 潜在的性能问题无论如何可能会通过使用模板缓存而得到很大程度的缓解(在这种情况下,模板只会在第一次访问时得到解析)。

15.2 消息解析器

我们没有为我们的 Grocery 应用程序明确指定消息解析器实现,正如之前所解释的,这意味着正在使用的实现是一个org.thymeleaf.messageresolver.StandardMessageResolver对象。

StandardMessageResolver是接口的标准实现IMessageResolver,但是如果我们愿意,我们可以创建自己的实现,以适应我们应用程序的特定需求。

Thymeleaf + Spring 集成包默认提供了一种IMessageResolver实现,它使用标准的 Spring 方式检索外部化消息,即通过使用MessageSource在 Spring 应用程序上下文中声明的 bean。

标准消息解析器

那么如何StandardMessageResolver查找特定模板所请求的消息?

如果模板名称是home且位于/WEB-INF/templates/home.html,并且请求的区域设置为 ,gl_ES则此解析器将按以下顺序在以下文件中查找消息:

  • /WEB-INF/templates/home_gl_ES.properties
  • /WEB-INF/templates/home_gl.properties
  • /WEB-INF/templates/home.properties

StandardMessageResolver有关完整消息解析机制如何工作的更多详细信息,请参阅该类的 JavaDoc 文档。

配置消息解析器

如果我们想向模板引擎添加一个(或更多)消息解析器怎么办?很简单:

// For setting only one
templateEngine.setMessageResolver(messageResolver);

// For setting more than one
templateEngine.addMessageResolver(messageResolver);

那么我们为什么要有多个消息解析器呢?原因与模板解析器相同:消息解析器是有序的,如果第一个解析器无法解析特定消息,则会询问第二个解析器,然后是第三个,依此类推。

15.3 转换服务

通过双括号语法( )使我们能够执行数据转换和格式化操作的转换服务实际上是标准方言的功能,而不是Thymeleaf模板引擎本身的功能。${{...}}

因此,配置它的方法是将接口的自定义实现直接设置到正在配置到模板引擎中IStandardConversionService的实例中。例如:StandardDialect

IStandardConversionService customConversionService = ...

StandardDialect dialect = new StandardDialect();
dialect.setConversionService(customConversionService);

templateEngine.setDialect(dialect);

请注意,thymeleaf-spring3 和 thymeleaf-spring4 包包含SpringStandardDialect,并且这种方言已经预先配置了的实现IStandardConversionService,将 Spring 自己的转换服务基础设施集成到 Thymeleaf 中。

15.4 日志记录

Thymeleaf 非常重视日志记录,并始终尝试通过其日志记录接口提供最大数量的有用信息。

所使用的日志库slf4j,实际上充当了我们可能想要在应用程序中使用的任何日志实现的桥梁(例如log4j)。

Thymeleaf 类将记录TRACEDEBUGINFO级别的信息,具体取决于我们想要的详细程度,除了一般日志记录之外,它还将使用与 TemplateEngine 类相关的三个特殊记录器,我们可以为不同的目的分别配置它们:

  • org.thymeleaf.TemplateEngine.CONFIG将在初始化期间输出库的详细配置。
  • org.thymeleaf.TemplateEngine.TIMER将输出有关处理每个模板所花费时间的信息(对于基准测试很有用!)
  • org.thymeleaf.TemplateEngine.cache是一组输出有关缓存的特定信息的记录器的前缀。尽管缓存记录器的名称可由用户配置,因此可能会更改,但默认情况下它们为:
    • org.thymeleaf.TemplateEngine.cache.TEMPLATE_CACHE
    • org.thymeleaf.TemplateEngine.cache.EXPRESSION_CACHE

Thymeleaf 日志记录基础设施的示例配置log4j如下:

log4j.logger.org.thymeleaf=DEBUG
log4j.logger.org.thymeleaf.TemplateEngine.CONFIG=TRACE
log4j.logger.org.thymeleaf.TemplateEngine.TIMER=TRACE
log4j.logger.org.thymeleaf.TemplateEngine.cache.TEMPLATE_CACHE=TRACE

16 模板缓存

Thymeleaf 的工作原理是借助一组用于标记和文本的解析器,将模板解析为事件序列(打开标签、文本、关闭标签、注释等)和一系列处理器,每种处理器对应一个需要应用的行为类型,修改模板解析的事件序列,以便通过将原始模板与我们的数据相结合来创建我们期望的结果。

它还默认包含一个缓存,用于存储已解析的模板;在处理模板文件之前读取和解析模板文件所产生的事件序列。这在 Web 应用程序中工作时特别有用,并基于以下概念:

  • 输入/输出几乎总是任何应用程序中最慢的部分。相比之下,内存处理速度极快。
  • 克隆现有的内存事件序列总是比读取模板文件、解析它并为其创建新的事件序列要快得多。
  • Web 应用程序通常只有几十个模板。
  • 模板文件大小适中,并且在应用程序运行时不会被修改。

所有这些都导致了这样的想法:在 Web 应用程序中缓存最常用的模板是可行的,而不会浪费大量内存,并且还可以节省大量在小组文件上进行输入/输出操作的时间,而这些文件实际上永远不会改变。

那么我们如何控制这个缓存呢?首先,我们之前已经了解到,我们可以在模板解析器中启用或禁用它,甚至只对特定的模板进行操作:

// Default is true
templateResolver.setCacheable(false);
templateResolver.getCacheablePatternSpec().addPattern("/users/*");

另外,我们可以通过建立自己的缓存管理器对象来修改其配置,该对象可以是默认StandardCacheManager实现的一个实例:

// Default is 200
StandardCacheManager cacheManager = new StandardCacheManager();
cacheManager.setTemplateCacheMaxSize(100);
...
templateEngine.setCacheManager(cacheManager);

org.thymeleaf.cache.StandardCacheManager有关配置缓存的更多信息,请参阅 javadoc API 。

可以从模板缓存中手动删除条目:

// Clear the cache completely
templateEngine.clearTemplateCache();

// Clear a specific template from the cache
templateEngine.clearTemplateCacheFor("/users/userList");

17 解耦模板逻辑

17.1 解耦逻辑:概念

到目前为止,我们已经使用通常的方式为我们的杂货店制作模板,并将逻辑以属性的形式插入到我们的模板中。

但是 Thymeleaf 还允许我们将模板标记与其逻辑完全分离,从而允许在和模板模式下创建完全无逻辑的标记模板HTMLXML

主要思想是模板逻辑将在单独的逻辑文件中定义(更确切地说是逻辑资源,因为它不需要是文件。默认情况下,该逻辑资源将是与模板文件位于同一位置(例如文件夹)的附加文件,具有相同的名称但扩展名.th.xml

/templates
+->/home.html
+->/home.th.xml

因此home.html文件可以完全没有逻辑。它可能看起来像这样:

<!DOCTYPE html>
<html>
  <body>
    <table id="usersTable">
      <tr>
        <td class="username">Jeremy Grapefruit</td>
        <td class="usertype">Normal User</td>
      </tr>
      <tr>
        <td class="username">Alice Watermelon</td>
        <td class="usertype">Administrator</td>
      </tr>
    </table>
  </body>
</html>

那里绝对没有 Thymeleaf 代码。这是一个模板文件,没有 Thymeleaf 或模板知识的设计师也可以创建、编辑和/或理解它。或者是由某个外部系统提供的 HTML 片段,根本没有 Thymeleaf 钩子。

现在让我们通过创建如下home.html附加文件将该模板转变为 Thymeleaf 模板:home.th.xml

<?xml version="1.0"?>
<thlogic>
  <attr sel="#usersTable" th:remove="all-but-first">
    <attr sel="/tr[0]" th:each="user : ${users}">
      <attr sel="td.username" th:text="${user.name}" />
      <attr sel="td.usertype" th:text="#{|user.type.${user.type}|}" />
    </attr>
  </attr>
</thlogic>

这里我们可以看到块里面有很多<attr>标签thlogic,这些<attr>标签通过其属性选择原始模板的节点进行属性注入sel,这些节点里面包含了 Thymeleaf标记选择器(其实是AttoParser 标记选择器)。

还要注意的是,<attr>标签可以嵌套,这样它们的选择器就会被附加到sel="/tr[0]"例如,上面的 将被处理为sel="#usersTable/tr[0]"。而用户名的选择器<td>将被处理为sel="#usersTable/tr[0]//td.username"

因此一旦合并,上面看到的两个文件将相同:

<!DOCTYPE html>
<html>
  <body>
    <table id="usersTable" th:remove="all-but-first">
      <tr th:each="user : ${users}">
        <td class="username" th:text="${user.name}">Jeremy Grapefruit</td>
        <td class="usertype" th:text="#{|user.type.${user.type}|}">Normal User</td>
      </tr>
      <tr>
        <td class="username">Alice Watermelon</td>
        <td class="usertype">Administrator</td>
      </tr>
    </table>
  </body>
</html>

这看起来更熟悉,而且确实比创建两个单独的文件更简洁。但解耦模板的优点在于,我们可以让模板完全独立于 Thymeleaf,因此从设计的角度来看,可维护性更好。

当然,设计师或开发人员之间仍然需要一些合同- 例如,用户<table>需要id="usersTable"-,但在许多情况下,纯 HTML 模板将成为设计和开发团队之间更好的沟通工具。

17.2 配置解耦模板

启用解耦模板

默认情况下,不会要求每个模板都使用解耦逻辑。相反,配置的模板解析器(的实现ITemplateResolver)需要专门标记它们解析为使用解耦逻辑的模板。

除了StringTemplateResolver(不允许解耦逻辑)之外,所有其他开箱即用的实现ITemplateResolver都将提供一个名为的标志useDecoupledLogic,该标志将标记该解析器解析的所有模板可能将其全部或部分逻辑存储在单独的资源中:

final ServletContextTemplateResolver templateResolver = 
        new ServletContextTemplateResolver(servletContext);
...
templateResolver.setUseDecoupledLogic(true);

混合耦合逻辑和解耦逻辑

解耦模板逻辑在启用时不是必需的。启用后,这意味着引擎将查找包含解耦逻辑的资源,如果存在,则解析并将其与原始模板合并。如果解耦逻辑资源不存在,则不会抛出任何错误。

此外,在同一个模板中,我们可以混合耦合逻辑解耦逻辑,例如,在原始模板文件中添加一些 Thymeleaf 属性,而将其他属性留给单独的解耦逻辑文件。最常见的情况是使用新(在 v3.0 中)th:ref属性。

17.3 th:ref 属性

th:ref只是一个标记属性。从处理的角度来看,它什么也不做,在模板处理时就会消失,但它的用处在于它充当标记引用,即它可以通过标记选择器中的名称进行解析,就像标签名称片段一样(th:fragment)。

如果我们有一个像这样的选择器:

  <attr sel="whatever" .../>

这将匹配:

  • 任何<whatever>标签。
  • 任何带有属性的标签th:fragment="whatever"
  • 任何带有属性的标签th:ref="whatever"

th:ref例如,使用纯 HTML 属性有什么好处id?事实上,我们可能不想在标签中添加那么多id属性class作为逻辑锚点,因为这可能会污染我们的输出。

那么, 的缺点是什么呢? 显然,我们会在模板中th:ref添加一些 Thymeleaf 逻辑(“逻辑” )。

请注意,该th:ref属性的适用性不仅适用于解耦逻辑模板文件:它在其他类型的场景中也发挥相同的作用,例如在片段表达式(~{...})中。

17.4 解耦模板的性能影响

影响非常小。当已解析的模板被标记为使用解耦逻辑并且未被缓存时,模板逻辑资源将首先被解析,解析并处理为内存中的一系列指令:基本上是要注入每个标记选择器的属性列表。

但这是唯一需要的额外步骤,因为在此之后,将解析真正的模板,并且在解析过程中,这些属性将由解析器本身即时注入,这要归功于 AttoParser 中先进的节点选择功能。因此,解析后的节点将从解析器中出来,就好像它们的注入属性写在原始模板文件中一样。

这样做的最大好处是什么?当模板配置为缓存时,它将被缓存,并且已经包含注入的属性。因此,一旦可缓存模板被缓存,使用解耦模板的开销将绝对为零

17.5 解耦逻辑的解析

Thymeleaf 解析每个模板对应的解耦逻辑资源的方式是用户可以配置的,由扩展点 决定,并为其org.thymeleaf.templateparser.markup.decoupled.IDecoupledTemplateLogicResolver提供默认实现StandardDecoupledTemplateLogicResolver: 。

这个标准实现起什么作用?

  • 首先,它将 aprefix和 asuffix应用于模板资源的基本名称(通过其ITemplateResource#getBaseName()方法获得)。前缀和后缀都可以配置,默认情况下,前缀为空,后缀为.th.xml
  • 其次,它要求模板资源通过其方法解析具有计算名称的相对资源ITemplateResource#relative(String relativeLocation)

要使用的具体实现IDecoupledTemplateLogicResolver可以轻松配置TemplateEngine

final StandardDecoupledTemplateLogicResolver decoupledresolver = 
        new StandardDecoupledTemplateLogicResolver();
decoupledResolver.setPrefix("../viewlogic/");
...
templateEngine.setDecoupledTemplateLogicResolver(decoupledResolver);

18 附录A:表达式基本对象

有些对象和变量映射始终可供调用。让我们看看它们:

基础对象

  • #ctx:上下文对象。 实现org.thymeleaf.context.IContextorg.thymeleaf.context.IWebContext取决于我们的环境(独立或 Web)。

    注意#vars#root是同一对象的同义词,但#ctx建议使用。

/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.context.IContext
 * ======================================================================
 */

${#ctx.locale}
${#ctx.variableNames}

/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.context.IWebContext
 * ======================================================================
 */

${#ctx.request}
${#ctx.response}
${#ctx.session}
${#ctx.servletContext}
  • #locale:直接访问java.util.Locale与当前请求相关的。
${#locale}

请求/会话属性等的 Web 上下文命名空间。

在 Web 环境中使用 Thymeleaf 时,我们可以使用一系列快捷方式来访问请求参数、会话属性和应用程序属性:

注意,这些不是上下文对象,而是作为变量添加到上下文中的映射,因此我们无需访问它们#。在某种程度上,它们充当命名空间

  • param:用于检索请求参数。${param.foo}String[]带有请求参数的值foo,因此${param.foo[0]}通常用于获取第一个值。
/*
 * ============================================================================
 * See javadoc API for class org.thymeleaf.context.WebRequestParamsVariablesMap
 * ============================================================================
 */

${param.foo}              // Retrieves a String[] with the values of request parameter 'foo'
${param.size()}
${param.isEmpty()}
${param.containsKey('foo')}
...
  • session:用于检索会话属性。
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.context.WebSessionVariablesMap
 * ======================================================================
 */

${session.foo}                 // Retrieves the session atttribute 'foo'
${session.size()}
${session.isEmpty()}
${session.containsKey('foo')}
...
  • application:用于检索应用程序/servlet 上下文属性。
/*
 * =============================================================================
 * See javadoc API for class org.thymeleaf.context.WebServletContextVariablesMap
 * =============================================================================
 */

${application.foo}              // Retrieves the ServletContext atttribute 'foo'
${application.size()}
${application.isEmpty()}
${application.containsKey('foo')}
...

请注意,不需要为访问请求属性(与请求参数相反)指定命名空间,因为所有请求属性都会自动作为上下文根中的变量添加到上下文中:

${myRequestAttribute}

Web 上下文对象

在 Web 环境中,还可以直接访问以下对象(请注意,这些是对象,而不是地图/命名空间):

  • #request:直接访问javax.servlet.http.HttpServletRequest与当前请求相关的对象。
${#request.getAttribute('foo')}
${#request.getParameter('foo')}
${#request.getContextPath()}
${#request.getRequestName()}
...
  • #session:直接访问javax.servlet.http.HttpSession与当前请求相关的对象。
${#session.getAttribute('foo')}
${#session.id}
${#session.lastAccessedTime}
...
  • #servletContext:直接访问javax.servlet.ServletContext与当前请求相关的对象。
${#servletContext.getAttribute('foo')}
${#servletContext.contextPath}
...

19 附录 B:表达式实用对象

执行信息

  • #execInfo:表达式对象,提供有关 Thymeleaf 标准表达式中正在处理的模板的有用信息。
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.ExecutionInfo
 * ======================================================================
 */

/*
 * Return the name and mode of the 'leaf' template. This means the template
 * from where the events being processed were parsed. So if this piece of
 * code is not in the root template "A" but on a fragment being inserted
 * into "A" from another template called "B", this will return "B" as a
 * name, and B's mode as template mode.
 */
${#execInfo.templateName}
${#execInfo.templateMode}

/*
 * Return the name and mode of the 'root' template. This means the template
 * that the template engine was originally asked to process. So if this
 * piece of code is not in the root template "A" but on a fragment being
 * inserted into "A" from another template called "B", this will still 
 * return "A" and A's template mode.
 */
${#execInfo.processedTemplateName}
${#execInfo.processedTemplateMode}

/*
 * Return the stacks (actually, List<String> or List<TemplateMode>) of
 * templates being processed. The first element will be the 
 * 'processedTemplate' (the root one), the last one will be the 'leaf'
 * template, and in the middle all the fragments inserted in nested
 * manner to reach the leaf from the root will appear.
 */
${#execInfo.templateNames}
${#execInfo.templateModes}

/*
 * Return the stack of templates being processed similarly (and in the
 * same order) to 'templateNames' and 'templateModes', but returning
 * a List<TemplateData> with the full template metadata.
 */
${#execInfo.templateStack}

消息

  • #messages:用于获取变量表达式中的外部化消息的实用方法,其方式与使用#{...}语法获取消息的方式相同。
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Messages
 * ======================================================================
 */

/*
 * Obtain externalized messages. Can receive a single key, a key plus arguments,
 * or an array/list/set of keys (in which case it will return an array/list/set of 
 * externalized messages).
 * If a message is not found, a default message (like '??msgKey??') is returned.
 */
${#messages.msg('msgKey')}
${#messages.msg('msgKey', param1)}
${#messages.msg('msgKey', param1, param2)}
${#messages.msg('msgKey', param1, param2, param3)}
${#messages.msgWithParams('msgKey', new Object[] {param1, param2, param3, param4})}
${#messages.arrayMsg(messageKeyArray)}
${#messages.listMsg(messageKeyList)}
${#messages.setMsg(messageKeySet)}

/*
 * Obtain externalized messages or null. Null is returned instead of a default
 * message if a message for the specified key is not found.
 */
${#messages.msgOrNull('msgKey')}
${#messages.msgOrNull('msgKey', param1)}
${#messages.msgOrNull('msgKey', param1, param2)}
${#messages.msgOrNull('msgKey', param1, param2, param3)}
${#messages.msgOrNullWithParams('msgKey', new Object[] {param1, param2, param3, param4})}
${#messages.arrayMsgOrNull(messageKeyArray)}
${#messages.listMsgOrNull(messageKeyList)}
${#messages.setMsgOrNull(messageKeySet)}

URI/URL

  • #uris:用于在 Thymeleaf 标准表达式中执行 URI/URL 操作(尤其是转义/取消转义)的实用对象。
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Uris
 * ======================================================================
 */

/*
 * Escape/Unescape as a URI/URL path
 */
${#uris.escapePath(uri)}
${#uris.escapePath(uri, encoding)}
${#uris.unescapePath(uri)}
${#uris.unescapePath(uri, encoding)}

/*
 * Escape/Unescape as a URI/URL path segment (between '/' symbols)
 */
${#uris.escapePathSegment(uri)}
${#uris.escapePathSegment(uri, encoding)}
${#uris.unescapePathSegment(uri)}
${#uris.unescapePathSegment(uri, encoding)}

/*
 * Escape/Unescape as a Fragment Identifier (#frag)
 */
${#uris.escapeFragmentId(uri)}
${#uris.escapeFragmentId(uri, encoding)}
${#uris.unescapeFragmentId(uri)}
${#uris.unescapeFragmentId(uri, encoding)}

/*
 * Escape/Unescape as a Query Parameter (?var=value)
 */
${#uris.escapeQueryParam(uri)}
${#uris.escapeQueryParam(uri, encoding)}
${#uris.unescapeQueryParam(uri)}
${#uris.unescapeQueryParam(uri, encoding)}

转换

  • #conversions :实用程序对象,允许在模板的任何点执行转换服务:
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Conversions
 * ======================================================================
 */

/*
 * Execute the desired conversion of the 'object' value into the
 * specified class.
 */
${#conversions.convert(object, 'java.util.TimeZone')}
${#conversions.convert(object, targetClass)}

日期

  • #dates:对象的实用方法java.util.Date
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Dates
 * ======================================================================
 */

/*
 * Format date with the standard locale format
 * Also works with arrays, lists or sets
 */
${#dates.format(date)}
${#dates.arrayFormat(datesArray)}
${#dates.listFormat(datesList)}
${#dates.setFormat(datesSet)}

/*
 * Format date with the ISO8601 format
 * Also works with arrays, lists or sets
 */
${#dates.formatISO(date)}
${#dates.arrayFormatISO(datesArray)}
${#dates.listFormatISO(datesList)}
${#dates.setFormatISO(datesSet)}

/*
 * 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')}

/*
 * Obtain date properties
 * Also works with arrays, lists or sets
 */
${#dates.day(date)}                    // also arrayDay(...), listDay(...), etc.
${#dates.month(date)}                  // also arrayMonth(...), listMonth(...), etc.
${#dates.monthName(date)}              // also arrayMonthName(...), listMonthName(...), etc.
${#dates.monthNameShort(date)}         // also arrayMonthNameShort(...), listMonthNameShort(...), etc.
${#dates.year(date)}                   // also arrayYear(...), listYear(...), etc.
${#dates.dayOfWeek(date)}              // also arrayDayOfWeek(...), listDayOfWeek(...), etc.
${#dates.dayOfWeekName(date)}          // also arrayDayOfWeekName(...), listDayOfWeekName(...), etc.
${#dates.dayOfWeekNameShort(date)}     // also arrayDayOfWeekNameShort(...), listDayOfWeekNameShort(...), etc.
${#dates.hour(date)}                   // also arrayHour(...), listHour(...), etc.
${#dates.minute(date)}                 // also arrayMinute(...), listMinute(...), etc.
${#dates.second(date)}                 // also arraySecond(...), listSecond(...), etc.
${#dates.millisecond(date)}            // also arrayMillisecond(...), listMillisecond(...), etc.

/*
 * Create date (java.util.Date) objects from its components
 */
${#dates.create(year,month,day)}
${#dates.create(year,month,day,hour,minute)}
${#dates.create(year,month,day,hour,minute,second)}
${#dates.create(year,month,day,hour,minute,second,millisecond)}

/*
 * Create a date (java.util.Date) object for the current date and time
 */
${#dates.createNow()}

${#dates.createNowForTimeZone()}

/*
 * Create a date (java.util.Date) object for the current date (time set to 00:00)
 */
${#dates.createToday()}

${#dates.createTodayForTimeZone()}

日历

  • #calendars:类似于#dates,但针对的是java.util.Calendar对象:
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Calendars
 * ======================================================================
 */

/*
 * Format calendar with the standard locale format
 * Also works with arrays, lists or sets
 */
${#calendars.format(cal)}
${#calendars.arrayFormat(calArray)}
${#calendars.listFormat(calList)}
${#calendars.setFormat(calSet)}

/*
 * Format calendar with the ISO8601 format
 * Also works with arrays, lists or sets
 */
${#calendars.formatISO(cal)}
${#calendars.arrayFormatISO(calArray)}
${#calendars.listFormatISO(calList)}
${#calendars.setFormatISO(calSet)}

/*
 * Format calendar with the specified pattern
 * Also works with arrays, lists or sets
 */
${#calendars.format(cal, 'dd/MMM/yyyy HH:mm')}
${#calendars.arrayFormat(calArray, 'dd/MMM/yyyy HH:mm')}
${#calendars.listFormat(calList, 'dd/MMM/yyyy HH:mm')}
${#calendars.setFormat(calSet, 'dd/MMM/yyyy HH:mm')}

/*
 * Obtain calendar properties
 * Also works with arrays, lists or sets
 */
${#calendars.day(date)}                // also arrayDay(...), listDay(...), etc.
${#calendars.month(date)}              // also arrayMonth(...), listMonth(...), etc.
${#calendars.monthName(date)}          // also arrayMonthName(...), listMonthName(...), etc.
${#calendars.monthNameShort(date)}     // also arrayMonthNameShort(...), listMonthNameShort(...), etc.
${#calendars.year(date)}               // also arrayYear(...), listYear(...), etc.
${#calendars.dayOfWeek(date)}          // also arrayDayOfWeek(...), listDayOfWeek(...), etc.
${#calendars.dayOfWeekName(date)}      // also arrayDayOfWeekName(...), listDayOfWeekName(...), etc.
${#calendars.dayOfWeekNameShort(date)} // also arrayDayOfWeekNameShort(...), listDayOfWeekNameShort(...), etc.
${#calendars.hour(date)}               // also arrayHour(...), listHour(...), etc.
${#calendars.minute(date)}             // also arrayMinute(...), listMinute(...), etc.
${#calendars.second(date)}             // also arraySecond(...), listSecond(...), etc.
${#calendars.millisecond(date)}        // also arrayMillisecond(...), listMillisecond(...), etc.

/*
 * Create calendar (java.util.Calendar) objects from its components
 */
${#calendars.create(year,month,day)}
${#calendars.create(year,month,day,hour,minute)}
${#calendars.create(year,month,day,hour,minute,second)}
${#calendars.create(year,month,day,hour,minute,second,millisecond)}

${#calendars.createForTimeZone(year,month,day,timeZone)}
${#calendars.createForTimeZone(year,month,day,hour,minute,timeZone)}
${#calendars.createForTimeZone(year,month,day,hour,minute,second,timeZone)}
${#calendars.createForTimeZone(year,month,day,hour,minute,second,millisecond,timeZone)}

/*
 * Create a calendar (java.util.Calendar) object for the current date and time
 */
${#calendars.createNow()}

${#calendars.createNowForTimeZone()}

/*
 * Create a calendar (java.util.Calendar) object for the current date (time set to 00:00)
 */
${#calendars.createToday()}

${#calendars.createTodayForTimeZone()}

数字

  • #numbers:数字对象的实用方法:
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Numbers
 * ======================================================================
 */

/*
 * ==========================
 * Formatting integer numbers
 * ==========================
 */

/* 
 * Set minimum integer digits.
 * Also works with arrays, lists or sets
 */
${#numbers.formatInteger(num,3)}
${#numbers.arrayFormatInteger(numArray,3)}
${#numbers.listFormatInteger(numList,3)}
${#numbers.setFormatInteger(numSet,3)}


/* 
 * Set minimum integer digits and thousands separator: 
 * 'POINT', 'COMMA', 'WHITESPACE', 'NONE' or 'DEFAULT' (by locale).
 * Also works with arrays, lists or sets
 */
${#numbers.formatInteger(num,3,'POINT')}
${#numbers.arrayFormatInteger(numArray,3,'POINT')}
${#numbers.listFormatInteger(numList,3,'POINT')}
${#numbers.setFormatInteger(numSet,3,'POINT')}


/*
 * ==========================
 * Formatting decimal numbers
 * ==========================
 */

/*
 * Set minimum integer digits and (exact) decimal digits.
 * Also works with arrays, lists or sets
 */
${#numbers.formatDecimal(num,3,2)}
${#numbers.arrayFormatDecimal(numArray,3,2)}
${#numbers.listFormatDecimal(numList,3,2)}
${#numbers.setFormatDecimal(numSet,3,2)}

/*
 * Set minimum integer digits and (exact) decimal digits, and also decimal separator.
 * Also works with arrays, lists or sets
 */
${#numbers.formatDecimal(num,3,2,'COMMA')}
${#numbers.arrayFormatDecimal(numArray,3,2,'COMMA')}
${#numbers.listFormatDecimal(numList,3,2,'COMMA')}
${#numbers.setFormatDecimal(numSet,3,2,'COMMA')}

/*
 * Set minimum integer digits and (exact) decimal digits, and also thousands and 
 * decimal separator.
 * Also works with arrays, lists or sets
 */
${#numbers.formatDecimal(num,3,'POINT',2,'COMMA')}
${#numbers.arrayFormatDecimal(numArray,3,'POINT',2,'COMMA')}
${#numbers.listFormatDecimal(numList,3,'POINT',2,'COMMA')}
${#numbers.setFormatDecimal(numSet,3,'POINT',2,'COMMA')}


/* 
 * =====================
 * Formatting currencies
 * =====================
 */

${#numbers.formatCurrency(num)}
${#numbers.arrayFormatCurrency(numArray)}
${#numbers.listFormatCurrency(numList)}
${#numbers.setFormatCurrency(numSet)}


/* 
 * ======================
 * Formatting percentages
 * ======================
 */

${#numbers.formatPercent(num)}
${#numbers.arrayFormatPercent(numArray)}
${#numbers.listFormatPercent(numList)}
${#numbers.setFormatPercent(numSet)}

/* 
 * Set minimum integer digits and (exact) decimal digits.
 */
${#numbers.formatPercent(num, 3, 2)}
${#numbers.arrayFormatPercent(numArray, 3, 2)}
${#numbers.listFormatPercent(numList, 3, 2)}
${#numbers.setFormatPercent(numSet, 3, 2)}


/*
 * ===============
 * Utility methods
 * ===============
 */

/*
 * Create a sequence (array) of integer numbers going
 * from x to y
 */
${#numbers.sequence(from,to)}
${#numbers.sequence(from,to,step)}

字符串

  • #strings:对象的实用方法String
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Strings
 * ======================================================================
 */

/*
 * Null-safe toString()
 */
${#strings.toString(obj)}                           // also array*, list* and set*

/*
 * 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)}

/*
 * Perform an 'isEmpty()' check on a string and return it if false, defaulting to
 * another specified string if true.
 * Also works with arrays, lists or sets
 */
${#strings.defaultString(text,default)}
${#strings.arrayDefaultString(textArr,default)}
${#strings.listDefaultString(textList,default)}
${#strings.setDefaultString(textSet,default)}

/*
 * Check whether a fragment is contained in a String
 * Also works with arrays, lists or sets
 */
${#strings.contains(name,'ez')}                     // also array*, list* and set*
${#strings.containsIgnoreCase(name,'ez')}           // also array*, list* and set*

/*
 * 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*

/*
 * Substring-related operations
 * Also works with arrays, lists or sets
 */
${#strings.indexOf(name,frag)}                      // also array*, list* and set*
${#strings.substring(name,3,5)}                     // also array*, list* and set*
${#strings.substringAfter(name,prefix)}             // also array*, list* and set*
${#strings.substringBefore(name,suffix)}            // also array*, list* and set*
${#strings.replace(name,'las','ler')}               // also array*, list* and set*

/*
 * Append and prepend
 * Also works with arrays, lists or sets
 */
${#strings.prepend(str,prefix)}                     // also array*, list* and set*
${#strings.append(str,suffix)}                      // also array*, list* and set*

/*
 * Change case
 * Also works with arrays, lists or sets
 */
${#strings.toUpperCase(name)}                       // also array*, list* and set*
${#strings.toLowerCase(name)}                       // also array*, list* and set*

/*
 * Split and join
 */
${#strings.arrayJoin(namesArray,',')}
${#strings.listJoin(namesList,',')}
${#strings.setJoin(namesSet,',')}
${#strings.arraySplit(namesStr,',')}                // returns String[]
${#strings.listSplit(namesStr,',')}                 // returns List<String>
${#strings.setSplit(namesStr,',')}                  // returns Set<String>

/*
 * Trim
 * Also works with arrays, lists or sets
 */
${#strings.trim(str)}                               // also array*, list* and set*

/*
 * Compute length
 * Also works with arrays, lists or sets
 */
${#strings.length(str)}                             // also array*, list* and set*

/*
 * Abbreviate text making it have a maximum size of n. If text is bigger, it
 * will be clipped and finished in "..."
 * Also works with arrays, lists or sets
 */
${#strings.abbreviate(str,10)}                      // also array*, list* and set*

/*
 * Convert the first character to upper-case (and vice-versa)
 */
${#strings.capitalize(str)}                         // also array*, list* and set*
${#strings.unCapitalize(str)}                       // also array*, list* and set*

/*
 * Convert the first character of every word to upper-case
 */
${#strings.capitalizeWords(str)}                    // also array*, list* and set*
${#strings.capitalizeWords(str,delimiters)}         // also array*, list* and set*

/*
 * Escape the string
 */
${#strings.escapeXml(str)}                          // also array*, list* and set*
${#strings.escapeJava(str)}                         // also array*, list* and set*
${#strings.escapeJavaScript(str)}                   // also array*, list* and set*
${#strings.unescapeJava(str)}                       // also array*, list* and set*
${#strings.unescapeJavaScript(str)}                 // also array*, list* and set*

/*
 * Null-safe comparison and concatenation
 */
${#strings.equals(first, second)}
${#strings.equalsIgnoreCase(first, second)}
${#strings.concat(values...)}
${#strings.concatReplaceNulls(nullValue, values...)}

/*
 * Random
 */
${#strings.randomAlphanumeric(count)}

对象

  • #objects:一般对象的实用方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Objects
 * ======================================================================
 */

/*
 * Return obj if it is not null, and default otherwise
 * Also works with arrays, lists or sets
 */
${#objects.nullSafe(obj,default)}
${#objects.arrayNullSafe(objArray,default)}
${#objects.listNullSafe(objList,default)}
${#objects.setNullSafe(objSet,default)}

布尔值

  • #bools:布尔评估的实用方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Bools
 * ======================================================================
 */

/*
 * Evaluate a condition in the same way that it would be evaluated in a th:if tag
 * (see conditional evaluation chapter afterwards).
 * Also works with arrays, lists or sets
 */
${#bools.isTrue(obj)}
${#bools.arrayIsTrue(objArray)}
${#bools.listIsTrue(objList)}
${#bools.setIsTrue(objSet)}

/*
 * Evaluate with negation
 * Also works with arrays, lists or sets
 */
${#bools.isFalse(cond)}
${#bools.arrayIsFalse(condArray)}
${#bools.listIsFalse(condList)}
${#bools.setIsFalse(condSet)}

/*
 * Evaluate and apply AND operator
 * Receive an array, a list or a set as parameter
 */
${#bools.arrayAnd(condArray)}
${#bools.listAnd(condList)}
${#bools.setAnd(condSet)}

/*
 * Evaluate and apply OR operator
 * Receive an array, a list or a set as parameter
 */
${#bools.arrayOr(condArray)}
${#bools.listOr(condList)}
${#bools.setOr(condSet)}

数组

  • #arrays:数组的实用方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Arrays
 * ======================================================================
 */

/*
 * Converts to array, trying to infer array component class.
 * Note that if resulting array is empty, or if the elements
 * of the target object are not all of the same class,
 * this method will return Object[].
 */
${#arrays.toArray(object)}

/*
 * Convert to arrays of the specified component class.
 */
${#arrays.toStringArray(object)}
${#arrays.toIntegerArray(object)}
${#arrays.toLongArray(object)}
${#arrays.toDoubleArray(object)}
${#arrays.toFloatArray(object)}
${#arrays.toBooleanArray(object)}

/*
 * Compute length
 */
${#arrays.length(array)}

/*
 * Check whether array is empty
 */
${#arrays.isEmpty(array)}

/*
 * Check if element or elements are contained in array
 */
${#arrays.contains(array, element)}
${#arrays.containsAll(array, elements)}

列表

  • #lists:列表的实用方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Lists
 * ======================================================================
 */

/*
 * Converts to list
 */
${#lists.toList(object)}

/*
 * Compute size
 */
${#lists.size(list)}

/*
 * Check whether list is empty
 */
${#lists.isEmpty(list)}

/*
 * Check if element or elements are contained in list
 */
${#lists.contains(list, element)}
${#lists.containsAll(list, elements)}

/*
 * Sort a copy of the given list. The members of the list must implement
 * comparable or you must define a comparator.
 */
${#lists.sort(list)}
${#lists.sort(list, comparator)}

Sets

  • #sets:集合的实用方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Sets
 * ======================================================================
 */

/*
 * Converts to set
 */
${#sets.toSet(object)}

/*
 * Compute size
 */
${#sets.size(set)}

/*
 * Check whether set is empty
 */
${#sets.isEmpty(set)}

/*
 * Check if element or elements are contained in set
 */
${#sets.contains(set, element)}
${#sets.containsAll(set, elements)}

Maps

  • #maps:Maps的实用方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Maps
 * ======================================================================
 */

/*
 * Compute size
 */
${#maps.size(map)}

/*
 * Check whether map is empty
 */
${#maps.isEmpty(map)}

/*
 * Check if key/s or value/s are contained in maps
 */
${#maps.containsKey(map, key)}
${#maps.containsAllKeys(map, keys)}
${#maps.containsValue(map, value)}
${#maps.containsAllValues(map, value)}

聚合

  • #aggregates:用于在数组或集合上创建聚合的实用方法
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Aggregates
 * ======================================================================
 */

/*
 * Compute sum. Returns null if array or collection is empty
 */
${#aggregates.sum(array)}
${#aggregates.sum(collection)}

/*
 * Compute average. Returns null if array or collection is empty
 */
${#aggregates.avg(array)}
${#aggregates.avg(collection)}

ID

  • #ids:用于处理id可能重复的属性(例如,作为迭代的结果)的实用方法。
/*
 * ======================================================================
 * See javadoc API for class org.thymeleaf.expression.Ids
 * ======================================================================
 */

/*
 * Normally used in th:id attributes, for appending a counter to the id attribute value
 * so that it remains unique even when involved in an iteration process.
 */
${#ids.seq('someId')}

/*
 * Normally used in th:for attributes in <label> tags, so that these labels can refer to Ids
 * generated by means if the #ids.seq(...) function.
 *
 * Depending on whether the <label> goes before or after the element with the #ids.seq(...)
 * function, the "next" (label goes before "seq") or the "prev" function (label goes after 
 * "seq") function should be called.
 */
${#ids.next('someId')}
${#ids.prev('someId')}

20 附录 C:标记选择器语法

Thymeleaf 的标记选择器直接借用自 Thymeleaf 的解析库:AttoParser

此选择器的语法与 XPath、CSS 和 jQuery 中的选择器语法非常相似,因此大多数用户都可以轻松使用它们。您可以在AttoParser 文档中查看完整的语法参考

例如,以下选择器将选择标记内每个位置上<div>带有类的每个内容content(请注意,这并不尽可能简洁,请继续阅读以了解原因):

<div th:insert="mytemplate :: //div[@class='content']">...</div>

基本语法包括:

  • /x表示当前节点的直接子节点,名称为 x。

  • //x表示当前节点名为 x 的子节点,位于任意深度。

  • x[@z="v"]表示名为 x 的元素以及名为 z 且值为“v”的属性。

  • x[@z1="v1" and @z2="v2"]表示名称为 x 的元素以及属性 z1 和 z2 ,其值分别带有“v1”和“v2”。

  • x[i]表示名称为 x 且位于其兄弟元素中的第 i 位的元素。

  • x[@z="v"][i]表示名称为 x、属性为 z、值为“v”且在其兄弟元素中位置为 i 的元素也符合此条件。

但也可以使用更简洁的语法:

  • x完全等同于(在任意深度级别//x搜索具有名称或引用的元素,引用属性)。xth:refth:fragment

  • 选择器也可以不带元素名称/引用,只要它们包含参数规范即可。因此,[@class='oneclass']有效的选择器会查找具有值为 的类属性的任何元素(标签)"oneclass"

高级属性选择功能:

  • 除了=(等于) 之外,其他比较运算符也有效:!=(不等于)、^=(以 开头) 和$=(以 结尾)。例如:x[@class^='section']表示元素的名称x和属性值class均以 开头section

  • @属性可以以 开头(XPath 样式) 或不以 开头 (jQuery 样式)来指定。因此x[z='v']相当于x[@z='v']

  • 多属性修饰符既可以用 (XPath 样式) 连接,and也可以通过链接多个修饰符 (jQuery 样式) 连接。因此x[@z1='v1' and @z2='v2']实际上等同于x[@z1='v1'][@z2='v2'](也等同于x[z1='v1'][z2='v2'])。

直接类似 jQuery 的选择器:

  • x.oneclass相当于x[class='oneclass']

  • .oneclass相当于[class='oneclass']

  • x#oneid相当于x[id='oneid']

  • #oneid相当于[id='oneid']

  • x%oneref表示<x>具有th:ref="oneref"th:fragment="oneref"属性的标签。

  • %oneref表示任何具有th:ref="oneref"th:fragment="oneref"属性的标签。请注意,这实际上等同于 simply,oneref因为可以使用引用代替元素名称。

  • 直接选择器和属性选择器可以混合使用:a.external[@href^='https']

因此上述标记选择器表达式:

<div th:insert="mytemplate :: //div[@class='content']">...</div>

可以写成:

<div th:insert="mytemplate :: div.content">...</div>

检查一个不同的例子,如下:

<div th:replace="mytemplate :: myfrag">...</div>

将查找th:fragment="myfrag"片段签名(或th:ref引用)。但如果存在,也会查找带有名称的标签myfrag(在 HTML 中不存在)。请注意以下区别:

<div th:replace="mytemplate :: .myfrag">...</div>

...它实际上会查找任何带有的元素class="myfrag",而不关心th:fragment签名(或th:ref引用)。

多值类匹配

标记选择器将类属性理解为多值的,因此即使元素具有多个类值,也允许在此属性上应用选择器。

例如,div.two将匹配<div class="one two three" />