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,HTML4和XHTML。将不执行验证或形式良好检查,并且模板代码/结构将在产出中尽可能的最大程度地得到尊重。

XML模板模式将允许XML输入。在这种情况下,代码预期形式良好 - 没有未关闭的标签,没有引用属性等,如果找到良好的违规行为,解析器将抛出异常。请注意,不会执行验证(针对DTD或XML架构)。

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

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

CSS模板模式将允许参与Thymeleaf应用CSS文件的处理。JAVASCRIPT模式类似CSS模板模式也是文本模式,并使用TEXT模板模式下的特殊处理语法

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

1.3方言:标准方言

Thymeleaf是一个非常可扩展的模板引擎(实际上它可以称为模板引擎框架),允许您定义和自定义您的模板将被处理到一个很好的细节水平。

将一些逻辑应用于标记工件(标签,某些文本,注释或只有占位符,如果模板不是标记)的一个对象被称为处理器,并且这些处理器的集合以及一些额外的工件是什么一个方言通常包括。开箱即用,Thymeleaf的核心库提供了一种称为标准方言的方言,这对大多数用户来说应该是足够的。

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

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

当然,如果用户希望在利用库的高级功能的同时定义自己的处理逻辑,用户也可以创建自己的方言(甚至扩展标准的方言)。也可以将Thymeleaf配置为一次使用几种方言。

官方的ThymeleafSpring3和ThymeleafSpring4的整合包都定义了一种称为“春季标准方言”的方言,与“标准方言”大致相同,但是对于Spring框架中的某些功能(例如, ,通过使用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好的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创建和配置模板引擎

过程(...)在我们的过滤方法包含这一行:

ITemplateEngine templateEngine = this.application.getTemplateEngine();

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

我们的org.thymeleaf.TemplateEngine对象是这样初始化的:

public class GTVGApplication {
  
    
    ...
    private static 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 by LRU
        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);
}

These objects are in charge of determining how our templates will be accessed, and in this GTVG application, the org.thymeleaf.templateresolver.ServletContextTemplateResolver means that we are going to retrieve our template files as resources from the Servlet Context: an application-wide javax.servlet.ServletContext object that exists in every Java web application, and that resolves resources from the web application root.

But that’s not all we can say about the template resolver, because we can set some configuration parameters on it. First, the template mode:

templateResolver.setTemplateMode(TemplateMode.HTML);

HTML is the default template mode for ServletContextTemplateResolver, but it is good practice to establish it anyway so that our code documents clearly what is going on.

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

The prefix and suffix modify the template names that we will be passing to the engine for obtaining the real resource names to be used.

Using this configuration, the template name “product/list” would correspond to:

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

Optionally, the amount of time that a parsed template can live in the cache is configured at the Template Resolver by means of the cacheTTLMs property:

templateResolver.setCacheTTLMs(3600000L);

A template can still be expelled from cache before that TTL is reached if the max cache size is reached and it is the oldest entry currently cached.

Cache behaviour and sizes can be defined by the user by implementing the ICacheManager interface or by modifying the StandardCacheManager object to manage the default cache.

有更多的了解模板解析器,但现在我们来看看我们的模板引擎对象的创建。

模板引擎

Template Engine对象是接口的org.thymeleaf.ITemplateEngine实现。其中一个实现由Thymeleaf核心提供:org.thymeleaf.TemplateEngine我们在此创建一个实例:

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

相当简单,不是吗?我们所需要的就是创建一个实例并将Template Resolver设置为它。

模板解析器是唯一需要的参数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文档,因为HTML5格式不允许我们以th:*表单形式使用这些非标准属性。事实上,我们甚至添加了一个xmlns:th属性到我们的<html>标签,绝对非HTML5-ish的东西:

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

...在模板处理中完全没有影响,但作为一个咒语,阻止我们的IDE抱怨缺少所有这些th:*属性的命名空间定义。

那么如果我们想让这个模板HTML5有效呢?Easy:切换到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>

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

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

使用th:文本和外部化文本

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

消息总是具有标识它们的密钥,而Thymeleaf允许您指定文本应与具有#{...}语法的特定消息对应

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

我们在这里可以看到的其实是Thymeleaf标准方言的两个不同特征:

  • th:text属性评估其值表达式并将结果设置为主机标签的主体,有效地替换了代码中我们看到的“欢迎使用我们的杂货店!”文本。
  • #{home.welcome}表达,在指定的标准表达式语法,指示要由所使用的文本th:text属性应与该消息home.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!

这就是我们所需要的,让我们制作我们的模板。然后让我们创建我们的家庭控制器。

上下文

为了处理我们的模板,我们将创建一个实现我们以前看到HomeControllerIGTVGController界面

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对于“unes​​caped text”):

<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作业属性(这是正确的,因为我们要替换标签的正文),但是这一次语法有点不同,而不是一个#{...}表达式值,我们使用${...}一。这是一个变量表达式,它包含一个名为OGNL(Object-Graph Navigation Language)的表达式,它将在之前讨论的上下文变量映射上执行。

${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>

但是,有更多类型的表达和更多有趣的细节来了解我们已经知道的内容。首先,我们来看看标准表达式功能的快速总结:

  • 简单表达式:
    • 可变表达式: ${...}
    • 选择变量表达式: *{...}
    • 消息表达式: #{...}
    • 链接网址表达式: @{...}
    • 片段表达式: ~{...}
  • 文字
    • 文本文字:'one text''Another one!',...
    • 号码文字:0343.012.3,...
    • 布尔文字:truefalse
    • 空字面: null
    • 文字标记:onesometextmain,...
  • 文字操作:
    • 字符串连接: +
    • 文字替代: |The name is ${name}|
  • 算术运算:
    • 二元运算符:+-*/%
    • 减号(一元运算符): -
  • 布尔运算:
    • 二元运算符:andor
    • 布尔否定(一元运算符): !not
  • 比较和平等:
    • 比较:><>=<=gtltgele
    • 平等运营商:==!=eqne
  • 条件运算符:
    • 如果 - 那么: (if) ? (then)
    • If-then-else: (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文档中指定的数字和日期。

为了指定我们的参数的值,并给出一个HTTP会话属性user,我们将具有:

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

可以指定几个参数,用逗号分隔。实际上,消息密钥本身可以来自一个变量:

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

4.2变量

我们已经提到,${...}表达式实际上是在上下文中包含的变量的映射上执行的OGNL(Object-Graph Navigation Language)对象。

有关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:执行配置的转换服务(如果有)的方法。
  • #datesjava.util.Date对象的方法:格式化,组件提取等
  • #calendars:类似#dates,但java.util.Calendar对象。
  • #numbers:用于格式化数字对象的方法。
  • #stringsString对象的方法:contains,startsWith,prepending / appending等
  • #objects:一般对象的方法。
  • #bools:布尔评估的方法。
  • #arrays:数组方法。
  • #lists:列表的方法。
  • #sets:集合的方法。
  • #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:insertth: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),括号([]),点(.),连字符(-)和下划线(_)。所以没有空白,没有逗号等

好的部分?令牌不需要围绕它们的任何引号。所以我们可以这样做:

<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比较与平等

在表达式中的值可以与进行比较><>=<=符号,以及==!=运营商可以被用来检查是否相等(或缺乏)。需要注意的是XML建立了<>符号不应该在属性值被使用,并且因此他们应该被取代&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表达式也可以省略,在这种情况下,如果条件为false,则返回null值:

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

4.12默认表达式(Elvis operator)

一个默认的表情是一种特殊的条件值的没有那么一部分。它相当于以一些语言(如Groovy)存在Elvis操作员,允许您指定两个表达式:如果不评估为null,则使用第一个表达式,但如果使用第二个表达式则使用第一个表达式。

让我们在我们的用户个人资料页面中看到它:

<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无操作令牌

无操作令牌由下划线符号(_)表示。

这个标记背后的想法是指定一个表达式的期望结果是什么也不做,即完全像可执行的属性(eg th:text)根本不存在。

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

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

...我们可以直接使用“没有用户认证”作为原型设计文本,从而从设计的角度来看,这些代码更加简洁和多才多艺:

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

4.15数据转换/格式化

Thymeleaf 为variable()和selection(表达式定义了一个双括号语法,允许我们通过配置的转换服务应用数据转换${...}*{...}

它基本上是这样的:

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

注意到有双括号?: ${{...}}这指示Thymeleaf将表达式的结果传递user.lastAccessDate转换服务,并要求它在写入结果之前执行格式化操作(转换为String)。

假设user.lastAccessDate是类型的java.util.Calendar,如果已经注册转换服务(实现IStandardConversionService)并且包含有效的转换Calendar -> String,则它将被应用。

IStandardConversionServiceStandardConversionService类)的默认实现只是.toString()在转换为的任何对象上执行String有关如何注册自定义转换服务实现的更多信息,请参阅更多配置部分。

官方thymeleaf-spring3和thymeleaf-spring4集成软件包的透明集成了Spring自己Thymeleaf的转换服务机制转换服务的基础设施,所以在Spring配置宣称,转换服务和格式化将进行自动获得${{...}}*{{...}}表达。

4.14预处理

除了表达式处理的所有这些功能之外,Thymeleaf具有预处理表达式的功能

预处理是在正常的表达式之前执行的表达式的执行,允许修改最终将被执行的表达式。

预处理的表达式与正常表达式完全相同,但是由双下划线符号(如__${expression}__)所示。

让我们想象一下,我们有一个Messages_fr.properties包含一个OGNL表达式的i18n 表达式,调用一个特定于语言的静态方法,如:

article.text=@myapp.translator.[email protected]translateToFrench({0})

...和a Messages_es.properties equivalent

article.text=@myapp.translator.[email protected]translateToSpanish({0})

我们可以创建一个标记片段,根据语言环境来评估一个表达式或另一个表达式。为此,我们首先选择表达式(通过预处理),然后让Thymeleaf执行它:

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

请注意,法语区域设置的预处理步骤将创建以下等效项:

<p th:text="${@[email protected](textVar)}">Some text here...</p>

预处理字符串__可以使用属性进行转义\_\_

5设置属性值

本章将介绍我们在标记中设置(或修改)属性值的方式。

5.1设置任何属性的值

说我们的网站发布通讯,我们希望用户能够订阅,所以我们创建/WEB-INF/templates/subscribe.html一个表单模板:

<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}"/>

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

<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-titleth: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:classappendth:styleappend属性,其用于添加CSS类或片段风格的元素,但不覆盖现有的:

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

(不要担心这个th:each属性,它是一个迭代的属性,我们稍后再讨论一下)

5.5固定值布尔属性

HTML具有布尔属性的概念,没有值的属性,并且一个的假设意味着该值为“true”。在XHTML中,这些属性只取一个值,它本身就是。

例如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 Components规范的一部分)。这可以用于例如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},重复模板的该片段,用在变量称为刺当前元素”。让我们给每个我们看到的东西一个名字:

  • 我们将调用${prods}迭代式迭代变量
  • 我们将调用prod迭代变量或者干脆ITER变量

请注意,proditer变量的作用域为<tr>元素,这意味着它可用于内部标记,如<td>

可迭代的值

java.util.List班是不是可以用于Thymeleaf迭代onlyvalue。有一个很完整的一套被认为是对象的迭代通过th:each属性:

  • 任何对象实现 java.util.Iterable
  • 任何对象实现java.util.Enumeration
  • 任何实现的对象java.util.Iterator,其值将被迭代器返回,而不需要在内存中缓存所有值。
  • 任何对象实现java.util.Map迭代映射时,迭代变量将是类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>

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

如果没有显式设置状态变量,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()方法永远不会被调用),如果condition计算结果为false在这样的代码:

<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
    • 如果值是数字,并且不为零
    • 如果值是一个字符且不为零
    • 如果value是String,而不是“false”,“off”或“no”
    • 如果值不是布尔值,数字,字符或字符串。
  • (如果值为null,则th:如果将为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切换语句

还有一种方法可以有条件地使用Java 中的开关结构等价物:th:switch/ th:caseattribute set 来显示内容

<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属性被评估为trueth:case则相同切换上下文中的每个其他属性都将被评估为false

默认选项指定为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,我们可以使用其中一个th:insertth:replace多个属性轻松地包含在我们的主页中(并且th:include,尽管Thymeleaf 3.0不再推荐使用它):

<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:replacetags中使用的模板名称必须由Template Engine当前使用的Template Resolver解析。

  • ~{::selector}""~{this::selector}"包含同一模板的片段。

双方templatenameselector在上面的例子可以是全功能的表达式(甚至条件语句!),如:

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

请再次注意周围的~{...}信封在th:insert/中是否可选th:replace

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

这种分片方法的一大优点是,您可以将页面中的片段写入浏览器完美显示的页面,并具有完整而有效的标记结构,同时仍保留使Thymeleaf包含在其他模板中的功能。

没有引用片段 th:fragment

由于Markup Selectors的功能,我们可以包括不使用任何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:replaceth:with

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

请注意,片段的局部变量规范 - 无论是否具有参数签名 - 都不会导致上下文在执行之前被清空。片段仍然能够访问调用模板中正在使用的每个上下文变量,就像它们当前一样。

th:assert for in-template assertions

th:assert属性可以指定一个逗号分隔的表达式列表,该列表应该被评估,并为每个评估产生真实的,如果没有,则引发异常。

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

这有助于验证片段签名中的参数:

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

8.3灵活的布局:不仅仅是片段插入

感谢片段表达式,我们可以为不是文本,数字,bean对象的片段指定参数,而是为标记片段指定参数。

这允许我们以一种方式创建我们的片段,使得它们可以丰富来自调用模板的标记,从而产生非常灵活的模板布局机制

注意下面片段中使用titlelinks变量:

<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>

我们现在可以将这个片段称为:

...
<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>
...

......,结果会用实际<title><link>我们调用模板标签的值titlelinks变量,导致我们的片段插入时被定制:

...
<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>
...

使用空的片段

一个特殊的片段表达式,空的fragment~{})可以用来指定没有标记使用前面的例子:

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

  <title>Awesome - Main</title>

</head>
...

请注意,fragment(links的第二个参数如何设置为空的片段,因此没有为<th:block th:replace="${links}" />写入

...
<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>
...

使用无操作令牌

如果我们只想让我们的片段将其当前标记用作默认值,那么no-op也可以用作片段的参数。再次,使用common_header示例:

...
<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>
...

看看title参数(common_header片段的第一个参数)如何设置为no-op_),这导致片段的这部分不被执行(title= no-operation):

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

所以结果是:

...
<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>
...

高级条件插入片段

双方的可用性emtpy片段无操作令牌可以让我们在一个非常简单而优雅的方式进行片段的插入条件。

例如,我们可以这样做,以便当用户是管理员时插入我们的common :: adminhead片段,并且不插入(emtpy片段)(如果不是):

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

另外,只有满足指定的条件,我们才可以使用无操作令牌来插入片段,但是如果不满足条件,则不需要修改则保留标记:

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

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Sat Oct 20 11:02:58 CST 2018
There was an unexpected error (type=OK, status=200).
Exception evaluating SpringEL expression: "user.name" (template: "blog/thymeleaf" - line 2128, col 15)