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上下文中*检索模板文件作为资源的方式:`javax.servlet.ServletContext`每个Java Web应用程序中都存在的应用程序范围的对象,并从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`对象来管理默认缓存来定义缓存行为和大小。
>
>
>
关于模板解析器还有很多东西需要学习,但是现在我们来看看Template Engine对象的创建。
#### ***模板引擎***
Template Engine对象是`org.thymeleaf.ITemplateEngine`接口的实现。其中一个实现是由Thymeleaf核心提供的:`org.thymeleaf.TemplateEngine`我们在这里创建一个实例:
templateEngine = new TemplateEngine(); templateEngine.setTemplateResolver(templateResolver);
相当简单,不是吗?我们所需要的只是创建一个实例并将模板解析器设置为它。
模板解析器是唯一*需要的*参数`TemplateEngine`,尽管稍后将介绍许多其他参数(消息解析器,缓存大小等)。现在,这就是我们所需要的。
我们的模板引擎现已准备就绪,我们可以使用Thymeleaf开始创建我们的页面。
## ***3使用文本***
### *3.1多语言欢迎*
我们的第一个任务是为我们的杂货网站创建一个主页。
这个页面的第一个版本非常简单:只是标题和欢迎信息。这是我们的`/WEB-INF/templates/home.html`文件:
`
Welcome to our grocery store!
` ```您将注意到的第一件事是该文件是HTML5,任何浏览器都可以正确显示它,因为它不包含任何非HTML标记(浏览器会忽略它们不理解的所有属性,例如th:text
)。
但是您可能还注意到此模板实际上不是有效的 HTML5文档,因为th:*
HTML5规范不允许我们在表单中使用这些非标准属性。事实上,我们甚至xmlns:th
在我们的<html>
标签中添加了一个属性,绝对不是HTML5-ish:
`<html xmlns:th="http://www.thymeleaf.org">`
…它在模板处理中根本没有任何影响,但是作为一个咒语,阻止我们的IDE抱怨缺少所有这些th:*
属性的命名空间定义。
那么如果我们想让这个模板HTML5有效呢?简单:切换到Thymeleaf的数据属性语法,使用data-
属性名称和hyphen(-
)分隔符的前缀而不是分号(:
):
`<!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模板模式(XML
,TEXT
…)中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!
这就是我们将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
是我们使用的那个。实际上我们必须这样做,因为使用a 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有
3.2有关文本和变量的更多信息
未转义的文字
我们主页的最简单版本现在似乎已经准备就绪,但有一些我们没有想过的…如果我们有这样的消息怎么办?
`home.welcome=Welcome to our <b>fantastic</b> grocery store!`
如果我们像以前一样执行此模板,我们将获得:
`<p>Welcome to our <b>fantastic</b> 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
作业的属性(这是正确的,因为我们想要替换标签的主体),但这次语法有点不同而不是#{...}
表达式值,我们使用的是${...}
一。这是一个变量表达式,它包含一个名为*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>`
但是有更多类型的表达式,以及更多有趣的细节来了解我们已经知道的。首先,让我们看一下标准表达式功能的快速摘要:
- 简单表达:
- 变量表达式:
${...}
- 选择变量表达式:
*{...}
- 消息表达式:
#{...}
- 链接网址表达式:
@{...}
- 片段表达式:
~{...}
- 变量表达式:
- 字面
- 文本文字:
'one text'
,'Another one!'
,… - 号码文字:
0
,34
,3.0
,12.3
,… - 布尔文字:
true
,false
- 空字面:
null
- 文字标记:
one
,sometext
,main
,…
- 文本文字:
- 文字操作:
- 字符串连接:
+
- 文字替换:
|The name is ${name}|
- 字符串连接:
- 算术运算:
- 二元运算符:
+
,-
,*
,/
,%
- 减号(一元运算符):
-
- 二元运算符:
- 布尔运算:
- 二元运算符:
and
,or
- 布尔否定(一元运算符):
!
,not
- 二元运算符:
- 比较和平等:
- 比较:
>
,<
,>=
,<=
(gt
,lt
,ge
,le
) - 平等运营商:
==
,!=
(eq
,ne
)
- 比较:
- 有条件的运营商:
- IF-THEN:
(if) ? (then)
- IF-THEN-ELSE:
(if) ? (then) : (else)
- 默认:
(value) ?: (defaultvalue)
- IF-THEN:
- 特殊代币:
- 无操作:
_
- 无操作:
所有这些功能都可以组合和嵌套:
`'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
标准语法指定,这意味着您可以格式化为java.text.*
包的类API文档中指定的数字和日期。
为了为我们的参数指定一个值,并给定一个被调用的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 Contexts)HttpServletRequest
对象。#response
:(仅限Web Contexts)HttpServletResponse
对象。#session
:(仅限Web Contexts)HttpSession
对象。#servletContext
:(仅限Web Contexts)ServletContext
对象。
所以我们可以这样做:
`Established locale country: <span th:text="${#locale.country}">US</span>.`
您可以在附录A中阅读这些对象的完整参考。
Expression Utility对象
除了这些基本对象,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
:地图的方法。#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.4链接URL
由于它们的重要性,URL是Web应用程序模板中的一等公民,而Thymeleaf Standard Dialect具有特殊的语法,@
语法:@{...}
有不同类型的网址:
- 绝对网址:
http://www.thymeleaf.org
- 相对URL,可以是:
- 页面相对:
user/login.html
- 上下文相关:(
/itemdetails?id=3
服务器中的上下文名称将自动添加) - 服务器相对:(
~/billing/processInvoice
允许在同一服务器中的另一个上下文(=应用程序)中调用URL。 - 协议相对URL:
//code.jquery.com/jquery-2.0.3.min.js
- 页面相对:
这些表达式的实际处理及其转换为将要输出的URL是通过org.thymeleaf.linkbuilder.ILinkBuilder
注册到ITemplateEngine
正在使用的对象中的接口的实现来完成的。
默认情况下,此接口的单个实现是在类中注册的org.thymeleaf.linkbuilder.StandardLinkBuilder
,这对于基于Servlet API的脱机(非Web)和Web方案都是足够的。其他方案(例如与非ServletAPI Web框架的集成)可能需要链接构建器接口的特定实现。
让我们使用这种新语法。符合th:href
属性:
`<!-- Will produce 'http://localhost:8080/gtvg/order/details?orderId=3' (plus rewriting) -->
<a href="details.html"
th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>
<!-- Will produce '/gtvg/order/details?orderId=3' (plus rewriting) -->
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>
<!-- Will produce '/gtvg/order/3/details' (plus rewriting) -->
<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>`
这里要注意的一些事情:
th:href
是一个修饰符属性:一旦处理,它将计算要使用的链接URL并将该值设置href
为<a>
标记的属性。- 我们被允许对URL参数使用表达式(如您所见
orderId=${o.id}
)。还将自动执行所需的URL参数编码操作。 - 如果需要几个参数,这些参数将用逗号分隔:
@{/order/process(execId=${execId},execType='FAST')}
- URL路径中也允许使用变量模板:
@{/order/{orderId}/details(orderId=${orderId})}
- 以
/
(例如:)开头的相对URL/order/details
将自动以应用程序上下文名称为前缀。 - 如果未启用cookie或尚未知道cookie,则
";jsessionid=..."
可能会在相对URL中添加后缀,以便保留会话。这称为URL重写,Thymeleaf允许您通过response.encodeURL(...)
Servlet API为每个URL 插入自己的重写过滤器。 - 该
th:href
属性允许我们(可选)href
在我们的模板中具有工作静态属性,以便我们的模板链接在直接打开以进行原型设计时仍可由浏览器导航。
与消息语法(#{...}
)的情况一样,URL基数也可以是评估另一个表达式的结果:
`<a th:href="@{${url}(orderId=${o.id})}">view</a>
<a th:href="@{'/details/'+${user.login}(orderId=${o.id})}">view</a>`
我们主页的菜单
既然我们知道如何创建链接URL,那么在我们的主页中为网站中的其他一些页面添加一个小菜单呢?
`<p>Please select an option</p>
<ol>
<li><a href="product/list.html" th:href="@{/product/list}">Product List</a></li>
<li><a href="order/list.html" th:href="@{/order/list}">Order List</a></li>
<li><a href="subscribe.html" th:href="@{/subscribe}">Subscribe to our Newsletter</a></li>
<li><a href="userprofile.html" th:href="@{/userprofile}">See User Profile</a></li>
</ol>`
服务器根目录相对URL
可以使用其他语法来创建服务器根相对(而不是上下文根相对)URL,以便链接到同一服务器中的不同上下文。这些URL将被指定为@{~/path/to/something}
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>`
布尔文字
布尔文字是true
和false
。例如:
`<div th:if="${user.isAdmin()} == false"> ...`
在这个例子中,它== false
被写在大括号外面,因此是Thymeleaf来处理它。如果它是在大括号内写的,那么它将由OGNL / SpringEL引擎负责:
`<div th:if="${user.isAdmin() == false}"> ...`
null文字
该null
文本也可用于:
`<div th:if="${variable.something} == null"> ...`
文字代币
实际上,数字,布尔和空文字是文字标记的特例。
这些令牌允许在标准表达式中进行一些简化。它们的工作方式与文本文字('...'
)完全相同,但它们只允许使用字母(A-Z
和a-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确定不应在属性值中使用<
和>
符号,因此应将它们替换为<
和>
。
`<div th:if="${prodStat.count} > 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>`
条件表达式(condition
,then
和else
)的所有三个部分本身都是表达式,这意味着它们可以是变量(${...}
,*{...}
),消息(#{...}
),URL(@{...}
)或文字('...'
)。
条件表达式也可以使用括号嵌套:
`<tr th:class="${row.even}? (${row.first}? 'first' : 'even') : 'odd'">
...
</tr>`
其他表达式也可以省略,在这种情况下,如果条件为false,则返回null值:
`<tr th:class="${row.even}? 'alt'">
...
</tr>`
4.12默认表达式(Elvis运算符)
一个默认的表情是一种特殊的条件值的没有那么一部分。它等同于某些语言(如Groovy)中存在的Elvis运算符,允许您指定两个表达式:如果它不计算为null,则使用第一个表达式,但如果确实如此,则使用第二个表达式。
让我们在用户个人资料页面中看到它:
`<div th:object="${session.user}">
...
<p>Age: <span th:text="\*{age}?: '(no age specified)'">27</span>.</p>
</div>`
正如您所看到的,运算符是?:
,并且我们在此处使用它来指定名称的默认值(在这种情况下为文字值),仅当评估结果*{age}
为null时。因此,这相当于:
`<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无操作令牌
No-Operation标记由下划线符号(_
)表示。
这个标记背后的想法是指定表达式的期望结果是什么都不做,即完全就像可处理属性(例如th:text
)根本不存在一样。
除了其他可能性之外,这允许开发人员将原型文本用作默认值。例如,而不是:
`<span th:text="${user.name} ?: 'no user authenticated'">...</span>`
…我们可以直接使用*“无用户身份验证”*作为原型文本,从设计的角度来看,这会使代码更简洁,更通用:
`<span th:text="${user.name} ?: \_">no user authenticated</span>`
4.14数据转换/格式化
Thymeleaf 为variable()和selection()表达式定义了一个双括号语法,允许我们通过配置的转换服务应用数据转换。${...}``*{...}
它基本上是这样的:
`<td th:text="${{user.lastAccessDate}}">...</td>`
注意到那里的双支撑?:${{...}}
。这指示Thymeleaf将user.lastAccessDate
表达式的结果传递给*转换服务,*并要求它在写入结果之前执行格式化操作(转换为String
)。
假设它user.lastAccessDate
是类型java.util.Calendar
,如果已经注册了转换服务(实现IStandardConversionService
)并且包含有效的转换Calendar -> String
,则将应用它。
IStandardConversionService
(StandardConversionService
类)的默认实现只是.toString()
在转换为的任何对象上执行String
。有关如何注册自定义转换服务实现的更多信息,请查看“ 更多配置”部分。
官方thymeleaf-spring3和thymeleaf-spring4集成软件包的透明集成了Spring自己Thymeleaf的转换服务机制转换服务的基础设施,所以在Spring配置宣称,转换服务和格式化将进行自动获得
${{...}}
和*{{...}}
表达。
4.15预处理
除了用于表达式处理的所有这些功能外,Thymeleaf还具有预处理表达式的功能。
预处理是在正常表达式之前完成的表达式的执行,它允许修改最终将被执行的表达式。
预处理表达式与普通表达式完全相同,但显示为双下划线符号(如__${expression}__
)。
让我们假设我们有一个Messages_fr.properties
包含OGNL表达式的i18n 条目,该表达式调用特定于语言的静态方法,如:
`article.text=@myapp.translator.Translator@translateToFrench({0})`
…和a Messages_es.properties equivalent
:
`article.text=@myapp.translator.Translator@translateToSpanish({0})`
我们可以创建一个标记片段,根据语言环境评估一个表达式或另一个表达式。为此,我们将首先选择表达式(通过预处理),然后让Thymeleaf执行它:
`<p th:text="${\_\_#{article.text('textVar')}\_\_}">Some text here...</p>`
请注意,法语区域设置的预处理步骤将创建以下等效项:
`<p th:text="${@myapp.translator.Translator@translateToFrench(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>`
除了新的属性值之外,您还可以看到applicacion上下文名称已自动添加到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
将设置alt
和title
。th:lang-xmllang
将设置lang
和xml: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:attrappend
和th: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具有布尔属性的概念,没有值的属性和一个值的前提意味着值为“true”。在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}
,重复模板的该片段,用在变量称为刺当前元素”。让我们给出一个我们看到的每个事物的名称:
- 我们将调用
${prods}
的迭代式或迭代变量。 - 我们将调用
prod
的迭代变量或者干脆ITER变量。
请注意,prod
iter变量的作用域是<tr>
元素,这意味着它可用于内部标记<td>
。
可重复的值
该java.util.List
班是不是可以用于Thymeleaf迭代onlyvalue。有一组非常完整的对象被属性认为是可迭代的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()
如果在代码中condition
进行求值,则永远不会被初始化(它的方法永远不会被调用)false
:
`<ul th:if="${condition}">
<li th:each="u : ${users}" th:text="${u.name}">user name</li>
</ul>`
7条件评估
7.1简单条件:“if”和“除非”
有时,如果满足某个条件,您将需要模板的片段才会出现在结果中。
例如,假设我们希望在产品表中显示一列,其中包含每个产品的评论数量,如果有任何评论,则指向该产品的评论详细信息页面的链接。
为此,我们将使用以下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
以下规则评估指定的表达式:
- 如果value不为null:
- 如果value是布尔值,则为
true
。 - 如果value是数字且不为零
- 如果value是一个字符且不为零
- 如果value是String并且不是“false”,“off”或“no”
- 如果value不是布尔值,数字,字符或字符串。
- 如果value是布尔值,则为
- (如果value为null,则th:if将计算为false)。
此外,th:if
还有一个inverse属性,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: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: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">
© 2011 The Good Thymes Virtual Grocery
</div>
</body>
</html>`
上面的代码定义了一个名为的片段copy
,我们可以使用其中一个th:insert
或th: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:replace
tags中使用的模板名称必须由模板引擎当前使用的模板解析器解析。
~{::selector}"
或"~{this::selector}"
插入来自同一模板的片段,进行匹配selector
。如果在表达式出现的模板上找不到,则模板调用(插入)的堆栈将遍历最初处理的模板(根),直到selector
在某个级别匹配。
双方templatename
并selector
在上面的例子可以是全功能的表达式(甚至条件语句!),如:
`<div th:insert="footer :: (${user.isAdmin}? #{footer.admin} : #{footer.normaluser})"></div>`
再次注意周围的~{...}
包络在th:insert
/中是如何可选的th:replace
。
片段可以包含任何th:*
属性。一旦将片段包含在目标模板(具有th:insert
/ th:replace
attribute的模板)中,就会评估这些属性,并且它们将能够引用此目标模板中定义的任何上下文变量。
这种片段方法的一大优点是,您可以在浏览器完全可显示的页面中编写片段,具有完整且有效的标记结构,同时仍保留使Thymeleaf将其包含在其他模板中的能力。
没有引用片段 th:fragment
由于标记选择器的强大功能,我们可以包含不使用任何th:fragment
属性的片段。它甚至可以是来自不同应用程序的标记代码,完全不了解Thymeleaf:
`...
<div id="copy-section">
© 2011 The Good Thymes Virtual Grocery
</div>
...`
我们可以使用上面的片段,通过其id
属性简单地引用它,与CSS选择器类似:
`<body>
...
<div th:insert="~{footer :: #copy-section}"></div>
</body>`
th:insert
和th:replace
(和th:include
)之间的区别
和之间有什么区别th:insert
和th:replace
(和th:include
,因为3.0不推荐)?
th:insert
是最简单的:它只是插入指定的片段作为其主机标签的主体。th:replace
实际上用指定的片段替换它的主机标签。th:include
类似于th:insert
,但不是插入片段,它只插入此片段的内容。
所以像这样的HTML片段:
`<footer th:fragment="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>
© 2011 The Good Thymes Virtual Grocery
</footer>
</div>
<footer>
© 2011 The Good Thymes Virtual Grocery
</footer>
<div>
© 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}">`
请注意,片段的局部变量的这种规范 - 无论它是否具有参数签名 - 都不会导致在执行之前清空上下文。片段仍然可以访问调用模板中使用的每个上下文变量,就像它们当前一样。
th:断言in-template断言
该th:assert
属性可以指定一个以逗号分隔的表达式列表,这些表达式应该被评估并为每次评估生成true,否则会引发异常。
`<div th:assert="${onevar},(${twovar} != 43)">...</div>`
这对于验证片段签名的参数非常方便:
`<header th:fragment="contentheader(title)" th:assert="${!#strings.isEmpty(title)}">...</header>`
8.3灵活的布局:仅仅是片段插入
由于片段表达式,我们可以为不是文本,数字,bean对象的片段指定参数…而是指定标记片段。
这允许我们以一种方式创建我们的片段,使得它们可以通过来自调用模板的标记来丰富,从而产生非常灵活的模板布局机制。
注意在下面的片段中使用title
和links
变量:
`<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>
标签作为title
和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>
<link rel="stylesheet" href="/awe/css/bootstrap.min.css">
<link rel="stylesheet" href="/awe/themes/smoothness/jquery-ui.css">
</head>
...`
使用空片段
一个特殊的片段表达式,即空片段(~{}
),可用于指定无标记。使用前面的示例:
`<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
= 无操作):
`<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 [[${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标准表示,因为它返回允许字符串值中的一个,只要(all
,tag
,body
,all-but-first
或none
)。
这意味着删除可能是有条件的,例如:
`<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}
为false,null
将返回,因此不会执行删除。
8.5布局继承
为了能够将单个文件作为布局,可以使用片段。具有title
和content
使用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的片段,其中title和content作为参数。在下面的示例中,两者都将在页面上替换,并通过提供的片段表达式继承它。
`<!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
标签将被替换的布局,但在布局title
和content
将已被替换title
,并section
分别块。
如果需要,布局可以由多个片段组成页眉和页脚。
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>`
我们在Grocery的主页上使用它!还记得我们为输出格式化日期而编写的代码吗?
`<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
具有较高的precedence
比th:text
,我们可以解决这一切的span
标签:
`<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 |
五 | 一般属性修改 | 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>
,例如:
`<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 <b>great!</b>"</p>`
请注意,默认情况下,文本内联在标记中的每个标记的主体中都是活动的 - 而不是标记本身 - 因此我们无需执行任何操作即可启用它。
内联vs自然模板
如果你来自其他模板引擎,其中这种输出文本的方式是常态,你可能会问:为什么我们从一开始就不这样做?它的代码少于所有这些 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>
在HTML
模板模式下处理的模板中更好地集成JavaScript 块。
与文本内联一样,这实际上相当于处理脚本内容,就好像它们是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内联的一个重要注意事项是,这种表达式评估是智能的,不仅限于字符串。Thymeleaf将在JavaScript语法中正确编写以下类型的对象:
- 字符串
- 数字
- 布尔
- 数组
- 集合
- 地图
- Bean(具有getter和setter方法的对象)
例如,如果我们有以下代码:
`<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的三种模板模式被认为是文字:TEXT
,JAVASCRIPT
和CSS
。这使它们与标记模板模式区别开来:HTML
和XML
。
文本模板模式与标记模式之间的关键区别在于,在文本模板中没有标签可以以属性的形式插入逻辑,因此我们必须依赖其他机制。
这些机制中的第一个也是最基本的是内联,我们已在前一章中详细介绍过。内联语法是在文本模板模式下输出表达式结果的最简单方法,因此这是一个完全有效的文本电子邮件模板。
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]
,并且可以通过最小化open元素来声明独立标签/
,其方式几乎等同于XML标签:[#element ... /]
。
标准方言只包含其中一个元素的处理器:已知的th:block
,虽然我们可以在我们的方言中扩展它并以通常的方式创建新元素。此外,允许将th:block
element([#th:block ...] ... [/th:block]
)缩写为空字符串([# ...] ... [/]
),因此上面的块实际上等效于:
[# th:each="item : ${items}"]
- [# th:utext="${item}" /]
[/]
并且给定[# th:utext="${item}" /]
等效于内联非转义表达式,我们可以使用它来获得更少的代码。因此,我们最终得到了上面看到的第一个代码片段:
[# th:each="item : ${items}"]
- [(${item})]
[/]
请注意,文本语法需要完整的元素平衡(没有未关闭的标记)和引用的属性 - 它比HTML风格更加XML风格。
让我们看一个更完整的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页面调用结果。请注意,这不是<script>
HTML模板中的块,而是.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
,HTML
模板内部的模式内联),Thymeleaf 3.0允许转义其文本语法中元素的属性。所以:
TEXT
模板模式中的属性将是HTML-unescaped。JAVASCRIPT
模板模式中的属性将是JavaScript-unescaped。CSS
模板模式中的属性将是CSS-unescaped。
所以这在TEXT
模式模板中完全可以(注意>
):
[# th:if="${120<user.age}"]
Congratulations!
[/]
当然,<
在真实的文本模板中没有任何意义,但如果我们使用th:inline="text"
包含上述代码的块处理HTML模板并且我们希望确保我们的浏览器不会将其<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文本解析器级注释块:删除代码
在类似于仅原型的注释块的方式,所有三个文本模板模式(TEXT
,JAVASCRIPT
和CSS
)使其能够指示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');
[/]
…实际上是在模板解析期间转换初始版本的代码。
但请注意,注释中的包装元素不会;
像内联输出表达式那样清除它们所在的行(在找到之前的右侧)。该行为仅为内联输出表达式保留。
因此,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
对象),将其purchasePrice
和amount
属性相乘(通过调用相应的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 Context获取模板作为资源。
除了让我们能够通过实现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(甚至是非本地的),如:
`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.setEncoding("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
我们正在使用的每个实现的特定功能。请注意,并非所有实现都可以在解析之前确定模板的存在,因此可以始终将模板视为可解析并破坏解析链(不允许其他解析器检查相同的模板),但随后无法阅读真实的资源。
ITemplateResolver
核心Thymeleaf中包含的所有实现都包含一种机制,允许我们在解析可解析之前让解析器真正检查资源是否存在。这是旗帜,其作用如下:checkExistence
`ClassLoaderTemplateResolver classLoaderTemplateResolver = new ClassLoaderTemplateResolver();
classLoaderTemplateResolver.setOrder(Integer.valueOf(1));
classLoaderTempalteResolver.setCheckExistence(true);`
此checkExistence
标志强制解析器在解析阶段执行资源存在的实际检查(如果存在检查返回false,则调用链中的后续解析器)。虽然这在每种情况下听起来都不错,但在大多数情况下,这意味着对资源本身的双重访问(一次用于检查存在,另一次用于读取它),并且在某些情况下可能是性能问题,例如基于远程URL模板资源 - 一个潜在的性能问题,无论如何都可以通过使用模板缓存来大大减轻(在这种情况下,模板只会在第一次访问时解析)。
15.2消息解析器
我们没有为Grocery应用程序明确指定Message Resolver实现,正如之前所解释的那样,这意味着所使用的实现是一个org.thymeleaf.messageresolver.StandardMessageResolver
对象。
StandardMessageResolver
是IMessageResolver
接口的标准实现,但我们可以根据需要创建自己的,以适应我们的应用程序的特定需求。
Thymeleaf + Spring集成包默认提供了一个
IMessageResolver
实现,它使用标准的Spring方法检索外部化消息,使用MessageSource
在Spring Application Context中声明的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班会记录TRACE
,DEBUG
并INFO
-level信息,这取决于我们希望的详细程度,并且除了一般的记录它会使用与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解耦逻辑:概念
到目前为止,我们已经为我们的Grocery Store工作,模板以通常的方式完成,逻辑以属性的形式插入到我们的模板中。
但Thymeleaf也让我们彻底脱钩从逻辑模板标记,允许创建完全逻辑较少标记模板在HTML
和XML
模板模式。
主要思想是模板逻辑将在单独的逻辑文件中定义(更确切地说是逻辑资源,因为它不需要是文件)。默认情况下,该逻辑资源将是与模板文件位于同一位置(例如文件夹)的附加文件,具有相同的名称但具有.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
通过创建我们的附加home.th.xml
文件将该模板转换为Thymeleaf模板:
<?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属性,但将其他属性留给单独的解耦逻辑文件。最常见的情况是使用new(in 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
。
这个标准实现有什么作用?
- 首先,它将a
prefix
和a 应用于模板资源suffix
的基本名称(通过其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.IContext
或org.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}
...`