编辑此页面

Qute 参考指南

Qute 是一个专门为满足 Quarkus 的需求而设计的模板引擎。它最小化了反射的使用,以减小原生镜像的大小。API 结合了命令式和非阻塞响应式两种编码风格。在开发模式下,会监视 src/main/resources/templates 文件夹中所有文件的更改,并且修改会立即在您的应用程序中可见。此外,Qute 会尝试在构建时检测大多数模板问题并快速失败。

本指南将提供一个 入门示例核心功能的描述以及 Quarkus 集成的详细信息。

Qute 主要设计为 Quarkus 扩展。也可以将其用作“独立”库。但是,在这种情况下,某些功能将不可用。通常,Quarkus 集成部分中提到的任何功能都将缺失。有关限制和可能性的更多信息,请参见 Qute 作为独立库的使用 部分。

1. 最简单的示例

尝试 Qute 的最简单方法是使用方便的 io.quarkus.qute.Qute 类,并调用其 fmt() 静态方法之一,这些方法可用于格式化简单消息

import io.quarkus.qute.Qute;

Qute.fmt("Hello {}!", "Lucy"); (1)
// => Hello Lucy!

Qute.fmt("Hello {name} {surname ?: 'Default'}!", Map.of("name", "Andy")); (2)
// => Hello Andy Default!

Qute.fmt("<html>{header}</html>").contentType("text/html").data("header", "<h1>My header</h1>").render(); (3)
// <html>&lt;h1&gt;Header&lt;/h1&gt;</html> (4)

Qute.fmt("I am {#if ok}happy{#else}sad{/if}!", Map.of("ok", true)); (5)
// => I am happy!
1 空表达式 {} 是一个占位符,它被基于索引的数组访问器替换,即 {data[0]}
2 您也可以提供一个数据映射。
3 提供了一个类似构建器的 API,用于更复杂的格式化需求。
4 请注意,对于 "text/html" 模板,特殊字符默认会被替换为 html 实体。
5 您可以在模板中使用任何 构建块。在本例中,If Section 用于根据输入数据渲染消息的适当部分。
Quarkus 中,用于格式化消息的引擎与通过 @Inject Engine 注入的引擎相同。因此,您可以利用任何 Quarkus 特定的集成功能,例如 模板扩展方法直接在模板中注入 Bean 甚至 类型安全的消息包

Qute.fmt(String) 方法返回的格式对象可以延迟评估,并用作日志消息等

LOG.info(Qute.fmt("Hello {name}!").data("name", "Foo"));
// => Hello Foo! and the message template is only evaluated if the log level INFO is used for the specific logger
请阅读 io.quarkus.qute.Qute 类的 javadoc 以获取更多详细信息。

2. Hello World 示例

在本示例中,我们将演示使用 Qute 模板时的基本工作流程。让我们从一个简单的“hello world”示例开始。我们将始终需要一些模板内容

hello.html
<html>
  <p>Hello {name}! (1)
</html>
1 {name} 是一个值表达式,在模板渲染时进行评估。

然后,我们需要将内容解析为模板定义 Java 对象。模板定义是 io.quarkus.qute.Template 的一个实例。

如果使用 Qute“独立”,您首先需要创建一个 io.quarkus.qute.Engine 实例。Engine 代表模板管理的中心点,具有专用配置。让我们使用方便的构建器

Engine engine = Engine.builder().addDefaults().build();
在 Quarkus 中,有一个预先配置的 Engine 可供注入 - 请参见 Quarkus 集成

一旦我们有了 Engine 实例,就可以解析模板内容

Template hello = engine.parse(helloHtmlContent);
在 Quarkus 中,您可以直接注入模板定义。模板会自动解析并缓存 - 请参见 Quarkus 集成

最后,创建模板实例,设置数据并渲染输出

// Renders <html><p>Hello Jim!</p></html>
hello.data("name", "Jim").render(); (1) (2)
1 Template.data(String, Object) 是一个方便的方法,它一步创建模板实例并设置数据。
2 TemplateInstance.render() 触发同步渲染,即当前线程将被阻塞直到渲染完成。但是,也有异步触发渲染和消耗结果的方法。例如,有 TemplateInstance.renderAsync() 方法返回 CompletionStage<String>,或者 TemplateInstance.createMulti() 返回 Mutiny 的 Multi<String>

因此,工作流程很简单

  1. 创建模板内容 (hello.html),

  2. 解析模板定义 (io.quarkus.qute.Template),

  3. 创建模板实例 (io.quarkus.qute.TemplateInstance),

  4. 渲染输出。

Engine 能够缓存模板定义,因此无需一遍又一遍地解析内容。在 Quarkus 中,缓存是自动完成的。

3. 核心功能

3.1. 基本构建块

模板的动态部分包括注释、表达式、节和未解析字符数据。

注释

注释以序列 {! 开始,以序列 !} 结束,例如 {! 这是一个注释 !}。它可以是多行的,并且可能包含表达式和节:{! {#if true} !}。在渲染输出时,注释的内容将被完全忽略。

表达式

表达式输出一个已评估的值。它由一个或多个部分组成。一个部分可能代表简单的属性:{foo}{item.name},以及虚拟方法:{item.get(name)}{name ?: 'John'}。表达式还可以以命名空间开头:{inject:colors}

可以包含静态文本、表达式和嵌套节:{#if foo.active}{foo.name}{/if}。闭合标签中的名称是可选的:{#if active}ACTIVE!{/}。节可以是空的:{#myTag image=true /}。某些节支持可选的结束标签,即如果缺少结束标签,则该节在父节结束处结束。节还可以声明嵌套的节块:{#if item.valid} 有效。{#else} 无效。{/if} 并决定渲染哪个块。

未解析字符数据

它用于标记应渲染但不解析的内容。它以序列 {| 开始,以序列 |} 结束:{| <script>if(true){alert('Qute is cute!')};</script> |},并且可以是多行的。

以前,未解析字符数据可以以 {[ 开始,以 ]} 结束。由于与其他语言的构造频繁冲突,此语法现已删除。

3.2. 标识符和标签

标识符用于表达式和节标签。有效的标识符是非空白字符的序列。但是,鼓励用户仅在表达式中使用有效的 Java 标识符。

如果您需要指定包含点的标识符,可以使用方括号表示法,例如 {map['my.key']}

在解析模板文档时,解析器会识别所有标签。标签以花括号开头和结尾,例如 {foo}。标签的内容必须以

  • 数字,或

  • 字母字符,或

  • 下划线,或

  • 内置命令:#!@/

如果它不以上述任何一种方式开头,则解析器会忽略它。

标签示例
<html>
   <body>
   {_foo.bar}   (1)
   {! comment !}(2)
   {  foo}      (3)
   {{foo}}      (4)
   {"foo":true} (5)
   </body>
</html>
1 已解析:以_开头的表达式。
2 已解析:注释
3 已忽略:以空格开头。
4 已忽略:以 { 开头。
5 已忽略:以 " 开头。
还可以使用转义序列 \{\} 在文本中插入分隔符。实际上,转义序列通常仅对起始分隔符是必需的,即 \{foo} 将被渲染为 {foo}(不会发生解析/求值)。

3.3. 从模板中移除独立行

默认情况下,解析器会从模板输出中移除独立行。独立行是指至少包含一个节标签(例如 {#each}{/each})、参数声明(例如 {@org.acme.Foo foo})或注释,但没有表达式和非空白字符的行。换句话说,不包含节标签或参数声明的行不是独立行。同样,包含表达式非空白字符的行不是独立行。

模板示例
<html>
  <body>
     <ul>
     {#for item in items} (1)
       <li>{item.name} {#if item.active}{item.price}{/if}</li>  (2)
                          (3)
     {/for}               (4)
     </ul>
   </body>
</html>
1 这是一行独立行,将被移除。
2 不是独立行 - 包含表达式和非空白字符
3 不是独立行 - 不包含节标签/参数声明
4 这是一行独立行。
默认输出
<html>
  <body>
     <ul>
       <li>Foo 100</li>

     </ul>
   </body>
</html>
在 Quarkus 中,可以通过将属性 quarkus.qute.remove-standalone-lines 设置为 false 来禁用默认行为。在这种情况下,独立行中的所有空白字符都将被打印到输出中。
设置 quarkus.qute.remove-standalone-lines=false 后的输出
<html>
  <body>
     <ul>

       <li>Foo 100</li>


     </ul>
   </body>
</html>

3.4. 表达式

表达式被求值并输出其值。它有一个或多个部分,其中每个部分代表属性访问器(也称为字段访问表达式)或虚拟方法调用(也称为方法调用表达式)。

在访问属性时,您可以使用点表示法或方括号表示法。在 object.property(点表示法)语法中,property 必须是 有效的标识符。在 object[property_name](方括号表示法)语法中,property_name 必须是一个非 null 的 字面量值。

表达式可以以可选的命名空间开头,后跟冒号 (:)。有效的命名空间由字母数字字符和下划线组成。命名空间表达式的解析方式不同 - 请参见 解析

属性访问器示例
{name} (1)
{item.name} (2)
{item['name']} (3)
{global:colors} (4)
1 无命名空间,单部分:name
2 无命名空间,两部分:itemname
3 相当于 {item.name},但使用了方括号表示法
4 命名空间 global,单部分:colors

表达式的一部分可以是虚拟方法,在这种情况下,名称后面可以跟一个用括号括起来的逗号分隔的参数列表。虚拟方法的参数可以是嵌套表达式或 字面量值。我们称这些方法为“虚拟”,因为它们不必由实际的 Java 方法支持。您可以在后续部分中了解有关虚拟方法的更多信息。

虚拟方法示例
{item.getLabels(1)} (1)
{name or 'John'} (2)
1 无命名空间,两部分 - itemgetLabels(1),第二部分是名为 getLabels 的虚拟方法,参数为 1
2 中缀表示法,可用于具有单个参数的虚拟方法,翻译为 name.or('John');无命名空间,两部分 - nameor('John')

3.4.1. 支持的字面量

字面量 示例

布尔值

truefalse

null

null

字符串

'value'"string"

整数

1, -5

长整型

1l-5L

双精度浮点数

1D-5d

单精度浮点数

1f-5F

3.4.2. 解析

在评估表达式时,将使用已注册的 值解析器列表。表达式的第一部分始终根据当前上下文对象进行解析。如果在第一部分未找到结果,则会根据父上下文对象(如果可用)进行解析。对于以命名空间开头的表达式,将使用所有可用的 NamespaceResolver 来查找当前上下文对象。对于不以命名空间开头的表达式,当前上下文对象由标签的位置派生。表达式的所有其他部分都使用所有 ValueResolver 对前一个解析的结果进行解析。

例如,表达式 {name} 没有命名空间且只有一个部分 - name。“name”将使用所有可用的值解析器根据当前上下文对象进行解析。但是,表达式 {global:colors} 具有命名空间 global 和单部分 - colors。首先,将使用所有可用的 NamespaceResolver 来查找当前上下文对象。然后,将使用值解析器根据找到的上下文对象解析“colors”。

传递给模板实例的数据始终可以使用 data 命名空间进行访问。这对于访问键被覆盖的数据可能很有用

<html>
{item.name} (1)
<ul>
{#for item in item.derivedItems} (2)
  <li>
  {item.name} (3)
  is derived from
  {data:item.name} (4)
  </li>
{/for}
</ul>
</html>
1 item 作为数据对象传递给模板实例。
2 迭代派生项的列表。
3 item 是迭代元素的别名。
4 使用 data 命名空间访问 item 数据对象。

3.4.3. 当前上下文

如果表达式未指定命名空间,则当前上下文对象由标签的位置派生。默认情况下,当前上下文对象代表传递给模板实例的数据。但是,节可以更改当前上下文对象。一个典型的例子是 let 节,它可用于定义命名的局部变量

{#let myParent=order.item.parent myPrice=order.price} (1)
  <h1>{myParent.name}</h1>
  <p>Price: {myPrice}</p>
{/let}
1 节内的当前上下文对象是已解析参数的映射。
可以通过隐式绑定 this 访问当前上下文。

3.4.4. 内置解析器

名称 描述 示例

Elvis 运算符:?:

如果前一部分无法解析或解析为 null,则输出默认值。

{person.name ?: 'John'}{person.name or 'John'}{person.name.or('John')}

orEmpty

如果前一部分无法解析或解析为 null,则输出一个空列表。

{pets.orEmpty.size} 如果 pets 无法解析或为 null,则输出 0

三元运算符:condition ? ifTrue : ifFalse

if-then-else 语句的简写。与 If Section 不同,不支持嵌套运算符。

{item.isActive ? item.name : 'Inactive item'} 如果 item.isActive 解析为 true,则输出 item.name 的值。

逻辑与运算符:&&

如果两部分都不是 If Section 中所述的假值,则输出 true。参数仅在需要时求值。

{person.isActive && person.hasStyle}

逻辑或运算符:||

如果任一部分不是 If Section 中所述的假值,则输出 true。参数仅在需要时求值。

{person.isActive || person.hasStyle}

等于运算符:==/eq/is

如果基对象等于参数,则输出 true

{obj1 is obj2 ? 'Equal' : 'Inequal'}{obj1 == obj2 ? 'Equal' : 'Inequal'}{obj1.eq(obj2) ? 'Equal' : 'Inequal'}

三元运算符中的条件求值为 true,如果该值不被视为 If Section 中所述的假值
实际上,运算符被实现为接受单个参数的“虚拟方法”,并可以使用中缀表示法。例如 {person.name or 'John'} 被翻译为 {person.name.or('John')},而 {item.isActive ? item.name : 'Inactive item'} 被翻译为 {item.isActive.ifTruthy(item.name).or('Inactive item')}

3.4.5. 数组

您可以使用 Loop Section 迭代数组的元素。此外,还可以获取指定数组的长度并通过索引值直接访问元素。此外,您还可以通过 take(n)/takeLast(n) 方法访问前/后 n 个元素。

数组示例
<h1>Array of length: {myArray.length}</h1> (1)
<ul>
  <li>First: {myArray.0}</li> (2)
  <li>Second: {myArray[1]}</li> (3)
  <li>Third: {myArray.get(2)}</li> (4)
</ul>
<ol>
 {#for element in myArray}
 <li>{element}</li>
 {/for}
</ol>
First two elements: {#each myArray.take(2)}{it}{/each} (5)
1 输出数组的长度。
2 输出数组的第一个元素。
3 使用方括号表示法输出数组的第二个元素。
4 通过虚拟方法 get() 输出数组的第三个元素。
5 输出数组的前两个元素。

3.4.6. 字符转义

对于 HTML 和 XML 模板,如果设置了相应的模板变体,则默认会转义 '"<>& 字符。对于 JSON 模板,如果设置了相应的模板变体,则默认会转义 "\ 以及控制字符(U+0000U+001F)。

在 Quarkus 中,对于位于 src/main/resources/templates 的模板会自动设置一个变体。默认情况下,使用 java.net.URLConnection#getFileNameMap() 来确定模板文件的 content-type。可以通过 quarkus.qute.content-types 设置后缀与 content-types 的附加映射。

如果您需要渲染未转义的值

  1. 要么使用作为 java.lang.Object 扩展方法实现的 rawsafe 属性,

  2. 要么将 String 值包装在 io.quarkus.qute.RawString 中。

HTML 示例
<html>
<h1>{title}</h1> (1)
{paragraph.raw} (2)
</html>
1 解析为 Expressions & Escapestitle 将被渲染为 Expressions &amp; Escapes
2 解析为 <p>My text!</p>paragraph 将被渲染为 <p>My text!</p>
默认情况下,具有以下 content-types 之一的模板会被转义:text/htmltext/xmlapplication/xmlapplication/xhtml+xml。但是,可以通过 quarkus.qute.escape-content-types 配置属性来扩展此列表。
JSON 示例
{
  "id": "{valueId.raw}", (1)
  "name": "{valueName}" (2)
}
1 解析为 \nA12345valueId 将被渲染为 \nA12345,由于在 id 属性的字符串值中插入了换行符,这将导致无效的 JSON 对象。
2 解析为 \tExpressions \n EscapesvalueName 将被渲染为 \\tExpressions \\n Escapes

3.4.7. 虚拟方法

虚拟方法是表达式的一部分,其外观类似于常规 Java 方法调用。它被称为“虚拟”,因为它不必与 Java 类的实际方法匹配。事实上,与常规属性一样,虚拟方法也是由值解析器处理的。唯一的区别是,对于虚拟方法,值解析器会处理也作为表达式的参数。

虚拟方法示例
<html>
<h1>{item.buildName(item.name,5)}</h1> (1)
</html>
1 buildName(item.name,5) 代表一个名为 buildName 的虚拟方法,带有两个参数:item.name5。虚拟方法可以由为以下 Java 类生成的 @TemplateExtension 方法@TemplateData 或在 参数声明中使用的类生成的值解析器来评估。但是,也可以注册一个不基于任何 Java 类/方法的自定义值解析器。
class Item {
   String buildName(String name, int age) {
      return name + ":" + age;
   }
}
虚拟方法通常由为 @TemplateExtension 方法@TemplateData 或在 参数声明中使用的类生成的值解析器进行评估。但是,也可以注册一个不基于任何 Java 类/方法自定义值解析器。

可以使用中缀表示法调用带有单个参数的虚拟方法

中缀表示法示例
<html>
<p>{item.price or 5}</p>  (1)
</html>
1 item.price or 5 被翻译为 item.price.or(5)

虚拟方法的参数可以是“嵌套”的虚拟方法调用。

嵌套虚拟方法示例
<html>
<p>{item.subtractPrice(item.calculateDiscount(10))}</p>  (1)
</html>
1 item.calculateDiscount(10) 首先被评估,然后作为参数传递给 item.subtractPrice()

3.4.8. CompletionStageUni 对象的求值

实现 java.util.concurrent.CompletionStageio.smallrye.mutiny.Uni 的对象以特殊方式进行求值。如果表达式的一部分解析为 CompletionStage,则在完成该阶段后继续解析,并针对已完成阶段的结果对表达式的下一部分(如果有)进行求值。例如,如果表达式为 {foo.size}foo 解析为 CompletionStage<List<String>>,则 size 将针对已完成的结果,即 List<String> 进行解析。如果表达式的一部分解析为 Uni,则首先使用 Uni#subscribeAsCompletionStage()Uni 创建 CompletionStage,然后按上述方式进行求值。

请注意,每次 Uni#subscribeAsCompletionStage() 调用都会产生新的订阅。您可能需要在 Uni 项目或失败用作模板数据之前配置其记忆化,即 myUni.memoize().indefinitely()

可能会出现 CompletionStage 永不完成或 Uni 发射无项目/失败的情况。在这种情况下,渲染方法(如 TemplateInstance#render()TemplateInstance#createUni())将在特定超时后失败。超时可以指定为模板实例的 timeout 属性。如果未设置 timeout 属性,则使用全局渲染超时。

在 Quarkus 中,默认超时可以通过 io.quarkus.qute.timeout 配置属性设置。如果使用 Qute 独立版,则可以使用 EngineBuilder#timeout() 方法。
在以前的版本中,只有 TemplateInstance#render() 方法尊重超时属性。您可以使用 io.quarkus.qute.useAsyncTimeout=false 配置属性来保留旧行为并自行处理超时,例如 templateInstance.createUtni().ifNoItem().after(Duration.ofMillis(500)).fail()
3.4.8.1. 如何识别模板的有问题部分

当发生超时时,很难找到模板的有问题部分。您可以为日志记录器 io.quarkus.qute.nodeResolve 设置 TRACE 级别,然后尝试分析日志输出。

application.properties 示例
quarkus.log.category."io.quarkus.qute.nodeResolve".min-level=TRACE
quarkus.log.category."io.quarkus.qute.nodeResolve".level=TRACE

对于模板中使用的每个表达式和节,您应该会看到以下日志消息对

TRACE [io.qua.qut.nodeResolve] Resolve {name} started: Template hello.html at line 8
TRACE [io.qua.qut.nodeResolve] Resolve {name} completed: Template hello.html at line 8

如果缺少 completed 日志消息,那么您就有了一个很好的探索目标。

3.4.9. 缺失的属性

表达式可能无法在运行时求值。例如,如果有一个表达式 {person.age},而 Person 类没有声明 age 属性。行为取决于是否启用了严格渲染

如果启用,则缺失的属性将始终导致 TemplateException 并中止渲染。您可以使用默认值安全表达式来抑制错误。

如果禁用,则默认情况下会向输出写入特殊常量 NOT_FOUND

在 Quarkus 中,可以通过 quarkus.qute.property-not-found-strategy 来注册一个 配置参考来更改默认策略。
如果使用类型安全表达式类型安全模板,则在构建时也会检测到类似的错误。

3.5. 节

节有一个以 # 开始的开始标签,后跟节的名称,例如 {#if}{#each}。它可以是空的,即开始标签以 / 结尾:{#myEmptySection /}。节通常包含嵌套的表达式和其他节。结束标签以 / 开始并包含节的名称(可选):{#if foo}Foo!{/if}{#if foo}Foo!{/}。某些节支持可选的结束标签,即如果缺少结束标签,则该节在父节结束处结束。

#let 可选结束标签示例
{#if item.isActive}
  {#let price = item.price} (1)
  {price}
  // synthetic {/let} added here automatically
{/if}
// {price} cannot be used here!
1 定义了可在父 {#if} 节内使用的局部变量。
内置节 支持可选结束标签

{#for}

{#if}

{#when}

{#let}

{#with}

{#include}

用户定义的标签

{#fragment}

{#cached}

3.5.1. 参数

开始标签可以定义带有可选名称的参数,例如 {#if item.isActive}{#let foo=1 bar=false}。参数由一个或多个空格分隔。名称与值通过等号分隔。名称可以带有任意数量的前缀和后缀空格,例如 {#let id='Foo'}{#let id = 'Foo'} 是等效的,其中参数名称是 id,值为 Foo。值可以使用括号分组,例如 {#let id=(item.id ?: 42)},其中名称是 id,值为 item.id ?: 42。节可以以任何方式解释参数值,例如,按原样获取值。但是,在大多数情况下,参数值会注册为 表达式并在使用前进行求值。

节可以包含多个内容。“主”块始终存在。其他/嵌套块也以 # 开头,并且也可以具有参数 - {#else if item.isActive}。定义节逻辑的节辅助程序可以“执行”任何块并求值参数。

#if 节示例
{#if item.name is 'sword'}
  It's a sword! (1)
{#else if item.name is 'shield'}
  It's a shield! (2)
{#else}
  Item is neither a sword nor a shield. (3)
{/if}
1 这是主块。
2 附加块。
3 附加块。

3.5.2. Loop Section

Loop section 使得迭代 IterableIterator、数组、Map(元素为 Map.Entry)、StreamIntegerLongintlong(原始值)成为可能。null 参数值将导致不执行任何操作。

此节有两种形式。第一种使用名称 eachit 是迭代元素的隐式别名。

{#each items}
  {it.name} (1)
{/each}
1 name 是根据当前迭代元素解析的。

另一种形式是使用名称 for 并指定用于引用迭代元素的别名

{#for item in items} (1)
  {item.name}
{/for}
1 item 是用于迭代元素的别名。

还可以通过以下键在循环内部访问迭代元数据

  • count - 基于 1 的索引

  • index - 基于 0 的索引

  • hasNext - 如果迭代还有更多元素,则为 true

  • isLast - 如果 hasNext == false,则为 true

  • isFirst - 如果 count == 1,则为 true

  • odd - 如果元素的计数为奇数,则为 true

  • even - 如果元素的计数为偶数,则为 true

  • indexParity - 根据计数输出 oddeven

但是,不能直接使用键。而是使用前缀来避免与外部范围的变量可能发生的冲突。默认情况下,迭代元素的别名加上下划线用作前缀。例如,在 {#each} 节内,hasNext 键必须加上 it_ 前缀:{it_hasNext}

each 迭代元数据示例
{#each items}
  {it_count}. {it.name} (1)
  {#if it_hasNext}<br>{/if} (2)
{/each}
1 it_count 代表基于 1 的索引。
2 <br> 仅在迭代还有更多元素时渲染。

并且必须以 {item_hasNext} 的形式在带有 item 元素别名的 {#for} 节中使用。

for 迭代元数据示例
{#for item in items}
  {item_count}. {item.name} (1)
  {#if item_hasNext}<br>{/if} (2)
{/for}
1 item_count 代表基于 1 的索引。
2 <br> 仅在迭代还有更多元素时渲染。

迭代元数据前缀可通过 EngineBuilder.iterationMetadataPrefix()(用于独立 Qute)或 Quarkus 应用程序中的 quarkus.qute.iteration-metadata-prefix 配置属性进行配置。可以使用三个特殊常量

  1. <alias_> - 迭代元素的别名加上下划线后缀(默认)

  2. <alias?> - 迭代元素的别名加上问号后缀

  3. <none> - 不使用前缀

for 语句也适用于整数,从 1 开始。在下面的示例中,考虑到 total = 3

{#for i in total}
  {i}: ({i_count} {i_indexParity} {i_even})<br>
{/for}

输出将是

1: (1 odd false)
2: (2 even true)
3: (3 odd false)

循环节还可以定义 {#else} 块,当没有要迭代的项目时执行该块

{#for item in items}
  {item.name}
{#else}
  No items.
{/for}

3.5.3. If Section

{#if} 节代表一个基本控制流节。最简单的形式接受单个参数,并在条件求值为 true 时渲染内容。没有运算符的条件求值为 true,如果该值不被视为假值,即如果该值不是 nullfalse、空集合、空映射、空数组、空字符串/字符序列、空 java.util.Optional/java.util.OptionalInt/java.util.OptionalLong/java.util.OptionalDouble 或等于零的数字。

{#if item.active}
  This item is active.
{/if}

您还可以在条件中使用以下运算符

运算符 别名 优先级 示例 描述

逻辑非

!

4

{#if !item.active}{/if}

反转求值后的值。

大于

gt>

3

{#if item.age > 43}此项非常旧。{/if}

如果 value1 大于 value2,则求值为 true

大于或等于

ge>=

3

{#if item.price >= 100}此项很贵。{/if}

如果 value1 大于或等于 value2,则求值为 true

小于

lt<

3

{#if item.price < 100}此项很便宜。{/if}

如果 value1 小于 value2,则求值为 true

小于或等于

le<=

3

{#if item.age ⇐ 43}此项很年轻。{/if}

如果 value1 小于或等于 value2,则求值为 true

等于

eq==is

2

{#if item.name eq 'Foo'}Foo 项!{/if}

如果 value1 等于 value2,则求值为 true

不等于

ne!=

2

{#if item.name != 'Bar'}不是 Bar 项!{/if}

如果 value1 不等于 value2,则求值为 true

逻辑与(短路)

&&and

1

{#if item.price > 100 && item.isActive}昂贵且活跃的项。{/if}

如果两个操作数都求值为 true,则求值为 true

逻辑或(短路)

||or

1

{#if item.price > 100 || item.isActive}昂贵或活跃的项。{/if}

如果其中一个操作数求值为 true,则求值为 true

对于 >>=<<=,将应用以下规则

  • 两个操作数都不能为 null

  • 如果两个操作数是实现 java.lang.Comparable 的同一类型,则使用 Comparable#compareTo(T) 方法进行比较。

  • 否则,两个操作数将首先被强制转换为 java.math.BigDecimal,然后使用 BigDecimal#compareTo(BigDecimal) 方法进行比较。

支持强制转换的类型包括 BigIntegerIntegerLongDoubleFloatString

对于 ==!=,将应用以下规则

  • 首先使用 java.util.Objects#equals(Object, Object) 方法测试操作数。如果返回 true,则认为操作数相等。

  • 否则,如果两个操作数都不是 null 且至少一个是 java.lang.Number 的实例,则将操作数强制转换为 java.math.BigDecimal,然后使用 BigDecimal#compareTo(BigDecimal) 方法进行比较。

也支持多个条件。

多个条件示例
{#if item.age > 10 && item.price > 500}
  This item is very old and expensive.
{/if}

默认的优先级规则(优先级高的优先)可以通过括号覆盖。

括号示例
{#if (item.age > 10 || item.price > 500) && user.loggedIn}
  User must be logged in and item age must be > 10 or price must be > 500.
{/if}

您还可以添加任意数量的 else

{#if item.age > 10}
  This item is very old.
{#else if item.age > 5}
  This item is quite old.
{#else if item.age > 2}
  This item is old.
{#else}
  This item is not old at all!
{/if}

3.5.4. When Section

此节类似于 Java 的 switch 或 Kotlin 的 when 结构。它将一个被测试值与所有块顺序匹配,直到满足条件。将执行第一个匹配的块。所有其他块都将被忽略(此行为与 Java 的 switch 不同,Java 中需要 break 语句)。

使用 when/is 名称别名的示例
{#when items.size}
  {#is 1} (1)
    There is exactly one item!
  {#is > 10} (2)
    There are more than 10 items!
  {#else} (3)
    There are 2 -10 items!
{/when}
1 如果只有一个参数,则测试其相等性。
2 可以使用运算符来指定匹配逻辑。与 If Section 不同,不支持嵌套运算符。
3 else 是在没有其他块匹配值时执行的块。
使用 switch/case 名称别名的示例
{#switch person.name}
  {#case 'John'} (1)
    Hey John!
  {#case 'Mary'}
    Hey Mary!
{/switch}
1 caseis 的别名。

当被测试值解析为枚举时,会进行特殊处理。is/case 块的参数不是作为表达式求值的,而是与对被测试值调用 toString() 的结果进行比较。

{#when machine.status}
  {#is ON}
    It's running. (1)
  {#is in OFF BROKEN}
    It's broken or OFF. (2)
{/when}
1 如果 machine.status.toString().equals("ON"),则执行此块。
2 如果 machine.status.toString().equals("OFF")machine.status.toString().equals("BROKEN"),则执行此块。
当被测试值具有可用类型信息并解析为枚举类型时,会验证枚举常量。

is/case 块条件支持以下运算符

运算符 别名 示例

不等于

!=notne

{#is not 10}{#case != 10}

大于

gt>

{#case le 10}

大于或等于

ge>=

{#is >= 10}

小于

lt<

{#is < 10}

小于或等于

le<=

{#case le 10}

in

in

{#is in 'foo' 'bar' 'baz'}

not in

ni!in

{#is !in 1 2 3}

3.5.5. Let Section

此节允许您定义命名的局部变量。

Let
{#let myParent=order.item.parent isActive=false age=10 price=(order.price + 10)} (1)(2)
  <h1>{myParent.name}</h1>
  Is active: {isActive}
  Age: {age}
  Price: {price}
{/let} (3)
1 局部变量使用表达式初始化,该表达式也可以是字面量,例如 isActive=falseage=10
2 仅当使用括号进行分组时,才支持中缀表示法,例如 price=(order.price + 10) 等同于 price=order.price.plus(10)
3 变量在 let 节外部不可用。

变量在定义它的 let 节外部不可用。但是,结束标签是可选的,如果省略,则该节在父节结束处结束。

带可选结束标签的 Let
<ul>
{#for item in items}
{#let price=item.price} (1)
   <li>{price}</li>
{! a synthetic {/let} is added here automatically !}
{/for}
</ul>
{price} --> BOOM! (2)
1 局部变量 price 使用表达式 item.price 初始化。
2 变量 pricelet 节外部不可用。

如果节参数的键,例如局部变量的名称,以 ? 结尾,则仅当不带 ? 后缀的键解析为 null“未找到”时,才会设置局部变量

{#let enabled?=true} (1) (2)
  {#if enabled}ON{/if}
{/let}
1 true 实际上是一个默认值,只有在父作用域尚未定义 enabled 时才使用。
2 enabled?=trueenabled=enabled.or(true) 的简写形式。

此节标签也以 set 别名注册

{#set myParent=item.parent price=item.price}
  <h1>{myParent.name}</h1>
  <p>Price: {price}
{/set}

3.5.6. With Section

此节可用于设置当前上下文对象。这有助于简化模板结构

{#with item.parent}
  <h1>{name}</h1>  (1)
  <p>{description}</p> (2)
{/with}
1 name 将根据 item.parent 进行解析。
2 description 也将根据 item.parent 进行解析。

请注意,在 类型安全模板或定义了 类型安全表达式的模板中不应使用 with 节。原因是它会阻止 Qute 验证嵌套表达式。如果可能,请用 {#let} 节替换它,该节声明一个显式绑定

{#let it=item.parent}
  <h1>{it.name}</h1>
  <p>{it.description}</p>
{/let}

当我们需要避免多次昂贵的调用时,此节也可能派上用场

{#with item.callExpensiveLogicToGetTheValue(1,'foo',bazinga)}
  {#if this is "fun"} (1)
    <h1>Yay!</h1>
  {#else}
    <h1>{this} is not fun at all!</h1>
  {/if}
{/with}
1 thisitem.callExpensiveLogicToGetTheValue(1,'foo',bazinga) 的结果。尽管结果可能在多个表达式中使用,该方法仅调用一次。

3.5.7. Include Section

此节可用于包含另一个模板,并可能覆盖模板的某些部分(参见下面的模板继承)。

简单示例
<html>
<head>
<meta charset="UTF-8">
<title>Simple Include</title>
</head>
<body>
  {#include foo limit=10 /} (1)(2)
</body>
</html>
1 包含 ID 为 foo 的模板。包含的模板可以引用当前上下文的数据。
2 还可以定义可用于包含模板的可选参数。

模板继承使得重用模板布局成为可能。

模板“base”
<html>
<head>
<meta charset="UTF-8">
<title>{#insert title}Default Title{/}</title> (1)
</head>
<body>
  {#insert}No body!{/} (2)
</body>
</html>
1 insert 节用于指定可被包含给定模板的模板覆盖的部分。
2 insert 节可以定义默认内容,如果未被覆盖则渲染。如果没有提供名称,则使用相关 {#include} 节的主块。
模板“detail”
{#include base} (1)
  {#title}My Title{/title} (2)
  <div> (3)
    My body.
  </div>
{/include}
1 include 节用于指定扩展的模板。
2 嵌套块用于指定要覆盖的部分。
3 主块的内容用于未指定名称参数的 {#insert} 节。
节块也可以定义可选的结束标签 - {/title}

3.5.8. 用户定义的标签

用户定义的标签可用于包含标签模板,可选地传递一些参数,并可能覆盖模板的某些部分。假设我们有一个名为 itemDetail.html 的标签模板

{#if showImage} (1)
  {it.image} (2)
  {nested-content} (3)
{/if}
1 showImage 是一个命名参数。
2 it 是一个特殊键,它被替换为标签的第一个未命名参数。
3 (可选) nested-content 是一个特殊键,它将被标签的内容替换。

在 Quarkus 中,src/main/resources/templates/tags 中的所有文件都会被自动注册和监视。对于 Qute 独立版,您需要将解析后的模板放在名为 itemDetail.html 的文件下,并向引擎注册相关的 UserTagSectionHelper

Engine engine = Engine.builder()
                   .addSectionHelper(new UserTagSectionHelper.Factory("itemDetail","itemDetail.html"))
                   .build();
engine.putTemplate("itemDetail.html", engine.parse("..."));

然后,我们可以这样调用标签

<ul>
{#for item in items}
  <li>
  {#itemDetail item showImage=true} (1)
    = <b>{item.name}</b> (2)
  {/itemDetail}
  </li>
{/for}
</ul>
1 item 解析为迭代元素,并可以在标签模板中使用 it 键引用。
2 使用标签模板中的 nested-content 键注入的标签内容。

默认情况下,标签模板无法引用父上下文的数据。Qute 将标签作为隔离模板执行,即无法访问调用标签的模板的上下文。但是,有时更改默认行为并禁用隔离可能会很有用。在这种情况下,只需在调用站点添加 _isolated=false_unisolated 参数,例如 {#itemDetail item showImage=true _isolated=false /}{#itemDetail item showImage=true _unisolated /}

3.5.8.1. 参数

命名参数可以直接在标签模板中访问。但是,第一个参数不需要定义名称,并且可以使用 it 别名访问。此外,如果参数没有名称且值为单个标识符(例如 foo),则名称默认为值标识符,例如 {#myTag foo /} 变成 {#myTag foo=foo /}。换句话说,参数值 foo 被解析,并在标签模板中通过 {foo} 访问。

如果参数没有名称且值为单个单词字符串字面量(例如 "foo"),则名称将默认为,并且将删除引号,例如 {#myTag "foo" /} 变成 {#myTag foo="foo" /}

io.quarkus.qute.UserTagSectionHelper.Arguments 元数据可以使用 _args 别名在标签中访问。

  • _args.size - 返回传递给标签的实际参数数量

  • _args.empty/_args.isEmpty - 如果未传递参数,则返回 true

  • _args.get(String name) - 返回给定名称的参数值或 null

  • _args.filter(String…​) - 返回与给定名称匹配的参数

  • _args.filterIdenticalKeyValue - 返回名称等于值的参数;通常是 {#test foo="foo" bar=true}{#test "foo" bar=true /} 中的 foo

  • _args.skip(String…​) - 仅返回与给定名称不匹配的参数

  • _args.skipIdenticalKeyValue - 仅返回名称不等于值的参数;通常是 {#test foo="foo" bar=true /} 中的 bar

  • _args.skipIt - 返回除第一个未命名参数外的所有参数;通常是 {#test foo bar=true /} 中的 bar

  • _args.asHtmlAttributes - 将参数渲染为 HTML 属性;例如 foo="true" readonly="readonly";参数按字母顺序排序,并且 '"<>& 字符会被转义

_args 也是 java.util.Map.Entry 的可迭代对象:{#each _args}{it.key}={it.value}{/each}

例如,我们可以使用下面定义的自定义标签 {#test 'Martin' readonly=true /} 来调用它。

tags/test.html
{it} (1)
{readonly} (2)
{_args.filter('readonly').asHtmlAttributes} (3)
1 it 被替换为标签的第一个未命名参数。
2 readonly 是一个命名参数。
3 _args 代表参数元数据。

结果将是

Martin
true
readonly="true"
3.5.8.2. 继承

用户标签也可以像常规 {#include} 节一样利用模板继承。

标签 myTag
This is {#insert title}my title{/title}! (1)
1 insert 节用于指定可被包含给定模板的模板覆盖的部分。
标签调用站点
<p>
  {#myTag}
    {#title}my custom title{/title} (1)
  {/myTag}
</p>
1 结果可能类似于 <p>这是我的自定义标题!</p>

3.5.9. Fragments

片段代表模板的一部分,可以将其视为单独的模板,即单独渲染。引入此功能的主要动机之一是支持类似 htmx fragments 的用例。

可以使用 {#fragment} 节定义片段。每个片段都有一个标识符,该标识符只能包含字母数字字符和下划线。

请注意,片段标识符在模板中必须是唯一的。
item.html 中的片段定义
{@org.acme.Item item}
{@java.util.List<String> aliases}

<h1>Item - {item.name}</h1>

<p>This document contains a detailed info about an item.</p>

{#fragment id=item_aliases} (1)
<h2>Aliases</h2>
<ol>
    {#for alias in aliases}
    <li>{alias}</li>
    {/for}
</ol>
{/fragment}
1 定义一个标识符为 item_aliases 的片段。请注意,标识符中只能使用字母数字字符和下划线。第一个参数的名称可以省略 - {#fragment item_aliases} 也可以。

您可以通过 io.quarkus.qute.Template.getFragment(String) 方法以编程方式获取片段。

获取片段
@Inject
Template item;

String useTheFragment() {
   return item.getFragment("item_aliases") (1)
            .data("aliases", List.of("Foo","Bar")) (2)
            .render();
}
1 获取标识符为 item_aliases 的模板片段。
2 确保数据设置正确。

上面的代码片段应渲染类似

<h2>Aliases</h2>
<ol>
    <li>Foo</li>
    <li>Bar</li>
</ol>
在 Quarkus 中,还可以定义一个类型安全片段

您还可以使用 {#include} 节在另一个模板或定义该片段的模板中包含片段。片段也可以在表达式中使用 frg:/fragment: 命名空间。

user.html 中包含片段
<h1>User - {user.name}</h1>

<p>
{#fragment fullname}
{name} <strong>{surname}</strong>
{/fragment}
</p>

<p>This document contains a detailed info about a user.</p>

{#include item$item_aliases aliases=user.aliases /} (1)(2)

{frg:fullname} is a happy user! (3)
1 包含美元符号 $ 的模板标识符表示一个片段。item$item_aliases 值被翻译为:使用模板 item 中的片段 item_aliases
2 aliases 参数用于传递相关数据。我们需要确保数据设置正确。在此特定情况下,片段将在 {#for alias in aliases} 节中使用表达式 user.aliases 作为 aliases 的值。
3 {frg:username} 表达式输出片段内容。frg: 可以被 fragment: 替换。
如果您想从同一模板引用片段,请省略 $ 之前的部分,例如 {#include $item_aliases /}
您可以通过指定 {#include item$item_aliases _ignoreFragments=true /} 来禁用此功能,即模板标识符中的美元符号 $ 不会导致片段查找。
3.5.9.1. 隐藏片段 (Capture)

默认情况下,片段会作为原始模板的一部分正常渲染。但是,有时将片段标记为隐藏可能很有用。常规片段节具有 capture 别名,它意味着一个隐藏片段。或者,您可以通过 rendered=false_hidden 参数来“隐藏”一个片段。一个有趣的用例是片段,它可以在定义它的模板内部多次使用。

item.html 中的隐藏片段定义
{#capture strong} (1)
<strong>{val}</strong>
{/capture}

<h1>My page</h1>
<p>This document
{#include $strong val='contains' /} (2)
a lot of
{capture:strong(param:val = 'information')} (3) (4)
!</p>
1 定义一个标识符为 strong 的隐藏片段。{#capture strong} 可以替换为 {#fragment strong rendered=false}{#fragment strong _hidden}rendered 参数可以使用任何表达式,例如 {#fragment strong rendered=config.isRendered}
2 包含片段 strong 并传递值。注意语法 $strong,它被转换为包含当前模板中的片段 strong
3 还可以使用命名空间解析器来访问隐藏片段。capture: 可以被 cap: 替换。
4 param:val = 'information' 用于将命名参数传递给片段。

上面的代码片段将渲染类似

<h1>My page</h1>
<p>This document
<strong>contains</strong>
a lot of
<strong>information</strong>
!</p>
在 Quarkus 中,会自动为 frgfragmentcapcapture 命名空间注册命名空间解析器。

3.5.10. Eval Section

此节可用于动态解析和求值模板。行为与Include Section 非常相似,但

  1. 模板内容直接传递,即不通过 io.quarkus.qute.TemplateLocator 获取,

  2. 无法覆盖被求值模板的部分。

{#eval myData.template name='Mia' /} (1)(2)(3)
1 myData.template 的结果将用作模板。模板将使用当前上下文执行,即可以引用它所在的模板中的数据。
2 还可以定义可用于被求值模板的可选参数。
3 节的内容始终被忽略。
被求值的模板每次执行节时都会被解析和求值。换句话说,无法缓存解析值以节省资源和优化性能。

3.5.11. Cached Section

有时缓存很少更改的模板部分是实用的。为了使用缓存功能,请注册并配置内置的 io.quarkus.qute.CacheSectionHelper.Factory

// A simple map-based cache
ConcurrentMap<String, CompletionStage<ResultNode>> map = new ConcurrentHashMap<>();
engineBuilder
    .addSectionHelper(new CacheSectionHelper.Factory(new Cache() {
        @Override
        public CompletionStage<ResultNode> getValue(String key,
           Function<String, CompletionStage<ResultNode>> loader) {
              return map.computeIfAbsent(key, k -> loader.apply(k));
           }
     })).build();
如果 Quarkus 应用程序中存在 quarkus-cache 扩展,则 CacheSectionHelper 将被自动注册和配置。缓存的名称是 qute-cache。它可以以标准方式配置,甚至可以通过 @Inject @CacheName("qute-cache") Cache 以编程方式管理。

然后,可以在模板中使用 {#cached}

{#cached} (1)
 Result: {service.findResult} (2)
{/cached}
1 如果未使用 key 参数,则所有模板的客户端共享相同的缓存值。
2 模板的这部分将被缓存,并且 {service.findResult} 表达式仅在缓存条目缺失/失效时才进行求值。
{#cached key=currentUser.username} (1)
 User-specific result: {service.findResult(currentUser)}
{/cached}
1 设置了 key 参数,因此每个 {currentUser.username} 表达式的结果都使用不同的缓存值。
使用缓存时,能够按特定键失效缓存条目通常非常重要。在 Qute 中,缓存条目的键是由模板名称、{#cached} 标签的开始行和列以及可选的 key 参数组成的 String{TEMPLATE}:{LINE}:{COLUMN}_{KEY}。例如,foo.html:10:1_alpha 是模板 foo.html 中缓存节的键,{#cached} 标签位于第 10 行,第 1 列。并且可选的 key 参数解析为 alpha

3.6. 渲染输出

TemplateInstance 提供了几种触发渲染和消耗结果的方法。最直接的方法是 TemplateInstance.render()。此方法触发同步渲染,即当前线程将阻塞直到渲染完成,并返回输出。相比之下,TemplateInstance.renderAsync() 返回一个 CompletionStage<String>,该对象在渲染完成后完成。

TemplateInstance.renderAsync() 示例
template.data(foo).renderAsync().whenComplete((result, failure) -> { (1)
   if (failure == null) {
      // consume the output...
   } else {
      // process failure...
   }
};
1 注册一个在渲染完成后执行的回调。

还有两个返回Mutiny类型的方法。TemplateInstance.createUni() 返回一个新的 Uni<String> 对象。如果您调用 createUni(),模板不会立即渲染。相反,每次调用 Uni.subscribe() 时,都会触发对模板的新渲染。

TemplateInstance.createUni() 示例
template.data(foo).createUni().subscribe().with(System.out::println);

TemplateInstance.createMulti() 返回一个新的 Multi<String> 对象。每个项目代表渲染模板的一部分/块。同样,createMulti() 不会触发渲染。相反,每次由订阅者触发计算时,都会再次渲染模板。

TemplateInstance.createMulti() 示例
template.data(foo).createMulti().subscribe().with(buffer:append,buffer::flush);
模板渲染分为两个阶段。在第一个(异步)阶段,所有表达式都得到解析,并构建一个结果树。在第二个(同步)阶段,结果树被具体化,即结果节点逐个发出块,这些块被特定消费者消耗/缓冲。

3.7. 引擎配置

3.7.1. 值解析器

值解析器在评估表达式时使用。首先过滤适用于给定 EvalContext 的解析器。然后使用*最高优先级*的解析器来解析数据。如果返回 io.quarkus.qute.Results.NotFound 对象,则使用下一个可用解析器。但是,null 返回值被视为有效结果。

可以通过 EngineBuilder.addValueResolver() 以编程方式注册自定义 io.quarkus.qute.ValueResolver

ValueResolver 构建器示例
engineBuilder.addValueResolver(ValueResolver.builder()
    .appliesTo(ctx -> ctx.getBase() instanceof Long && ctx.getName().equals("tenTimes"))
    .resolveSync(ctx -> (Long) ctx.getBase() * 10)
    .build());
在 Quarkus 中,可以使用@EngineConfiguration 注释来注册一个实现为 CDI Bean 的 ValueResolver
请记住,基于反射的值解析器优先级为 -1,而从@TemplateData类型安全表达式生成的解析器的最高优先级值为 10

3.7.2. 模板定位器

模板可以手动注册,也可以通过模板定位器自动注册。当调用 Engine.getTemplate() 方法且引擎在缓存中没有给定 ID 的模板时,就会使用定位器。定位器负责在读取模板内容时使用正确的字符编码。

在 Quarkus 中,src/main/resources/templates 中的所有模板都会自动定位,并使用 quarkus.qute.default-charset(默认为 UTF-8)设置的编码。可以使用 @Locate 注释注册自定义定位器。

3.7.3. 内容过滤器

内容过滤器可用于在解析前修改模板内容。

内容过滤器示例
engineBuilder.addParserHook(new ParserHook() {
    @Override
    public void beforeParsing(ParserHelper parserHelper) {
        parserHelper.addContentFilter(contents -> contents.replace("${", "$\\{")); (1)
    }
});
1 转义所有出现的 ${

3.7.4. 严格渲染

严格渲染使开发人员能够捕获由拼写错误和无效表达式引起的隐蔽错误。如果启用,则任何无法解析的表达式,即求值为 io.quarkus.qute.Results.NotFound 实例的表达式,都将始终导致 TemplateException 并中止渲染。NotFound 值被视为错误,因为它基本上意味着没有值解析器能够正确解析表达式。

null 是一个有效值。它被视为假值,如If Section 中所述,并且不产生任何输出。

严格渲染默认启用。但是,您可以通过 io.quarkus.qute.EngineBuilder.strictRendering(boolean) 禁用此功能。

在 Quarkus 中,可以使用专用配置属性:quarkus.qute.strict-rendering

如果您确实需要使用可能导致“未找到”错误的表达式,您可以使用默认值安全表达式来抑制错误。如果前一部分表达式无法解析或解析为 null,则使用默认值。您可以使用 elvis 运算符输出默认值:{foo.bar ?: 'baz'},它实际上与以下虚拟方法相同:{foo.bar.or('baz')}。安全表达式以 ?? 后缀结尾,如果表达式无法解析,则结果为 null。这在 {#if} 节中可能非常有用:{#if valueNotFound??}仅在 valueNotFound 为真时渲染!{/if}。实际上,?? 只是 .or(null) 的简写符号,即 {#if valueNotFound??} 变为 {#if valueNotFound.or(null)}

4. Quarkus 集成

如果您想在 Quarkus 应用程序中使用 Qute,请将以下依赖项添加到您的项目中

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-qute</artifactId>
</dependency>

在 Quarkus 中,会提供一个预先配置好的引擎实例供注入 - 一个具有 @ApplicationScoped 作用域、Bean 类型 io.quarkus.qute.Engine 和限定符 @Default 的 Bean 会被自动注册。此外,位于 src/main/resources/templates 目录下的所有模板都会经过验证,并且可以轻松注入。

有效的模板文件名为非空白字符的序列。例如,名为 foo and bar.html 的模板文件将被忽略。
import io.quarkus.qute.Engine;
import io.quarkus.qute.Template;
import io.quarkus.qute.Location;

class MyBean {

    @Inject
    Template items; (1)

    @Location("detail/items2_v1.html") (2)
    Template items2;

    @Inject
    Engine engine; (3)
}
1 如果未提供 Location 限定符,则字段名用于定位模板。在这种特定情况下,容器将尝试定位路径为 src/main/resources/templates/items.html 的模板。
2 Location 限定符指示容器从相对于 src/main/resources/templates 的路径注入模板。在这种情况下,完整路径是 src/main/resources/templates/detail/items2_v1.html
3 注入已配置的 Engine 实例。

4.1. 引擎自定义

可以在运行时通过 CDI 观察方法中的 EngineBuilder 方法手动注册其他组件

import io.quarkus.qute.EngineBuilder;

class MyBean {

    void configureEngine(@Observes EngineBuilder builder) {
       // Add a custom section helper
       builder.addSectionHelper(new CustomSectionFactory());
       // Add a custom value resolver
       builder.addValueResolver(ValueResolver.builder()
                .appliesTo(ctx -> ctx.getBase() instanceof Long && ctx.getName().equals("tenTimes"))
                .resolveSync(ctx -> (Long) ec.getBase() * 10)
                .build());
    }
}

但是,在这种特定情况下,节辅助程序工厂在构建时验证期间会被忽略。如果您想注册一个参与构建时模板验证的节,请使用方便的 @EngineConfiguration 注解

import io.quarkus.qute.EngineConfiguration;
import io.quarkus.qute.SectionHelper;
import io.quarkus.qute.SectionHelperFactory;

@EngineConfiguration (1)
public class CustomSectionFactory implements SectionHelperFactory<CustomSectionFactory.CustomSectionHelper> {

    @Inject
    Service service; (2)

    @Override
    public List<String> getDefaultAliases() {
        return List.of("custom");
    }

    @Override
    public ParametersInfo getParameters() {
        // Param "foo" is required
        return ParametersInfo.builder().addParameter("foo").build(); (3)
    }

    @Override
    public Scope initializeBlock(Scope outerScope, BlockInfo block) {
        block.addExpression("foo", block.getParameter("foo"));
        return outerScope;
    }


    @Override
    public CustomSectionHelper initialize(SectionInitContext context) {
        return new CustomSectionHelper();
    }

    class CustomSectionHelper implements SectionHelper {

        private final Expression foo;

        public CustomSectionHelper(Expression foo) {
            this.foo = foo;
        }

        @Override
        public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
            return context.evaluate(foo).thenApply(fooVal -> new SingleResultNode(service.getValueForFoo(fooVal))); (4)
        }
    }
}
1 带有 @EngineConfiguration 注解的 SectionHelperFactory 在构建时用于验证模板,并在运行时自动注册(a)作为节工厂,(b)作为 CDI Bean。
2 运行时使用 CDI Bean 实例 - 这意味着工厂可以定义注入点
3 验证 foo 参数始终存在;例如,{#custom foo='bar' /} 是可以的,但 {#custom /} 会导致构建失败。
4 在渲染期间使用注入的 Service
@EngineConfiguration 注释还可以用于注册 ValueResolverNamespaceResolverParserHook 组件。

4.1.1. 模板定位器注册

注册模板定位器的最简单方法是使它们成为 CDI Bean。由于自定义定位器在构建时模板验证期间不可用,因此您需要通过 @Locate 注解禁用验证。

自定义定位器示例
@Locate("bar.html") (1)
@Locate("foo.*") (2)
public class CustomLocator implements TemplateLocator {

    @Inject (3)
    MyLocationService myLocationService;

    @Override
    public Optional<TemplateLocation> locate(String templateId) {

        return myLocationService.getTemplateLocation(templateId);
    }

}
1 名为 bar.html 的模板由自定义定位器在运行时定位。
2 正则表达式 foo.* 会禁用名称以 foo 开头的模板的验证。
3 注入字段被解析为已用 @Locate 注解的模板定位器,它们被注册为单例会话 Bean。

4.2. 模板变体

有时根据内容协商渲染模板的特定变体是很有用的。可以通过设置一个特殊的属性 TemplateInstance.setVariant() 来完成此操作

class MyService {

    @Inject
    Template items; (1)

    @Inject
    ItemManager manager;

    String renderItems() {
       return items.data("items", manager.findItems())
                   .setVariant(new Variant(Locale.getDefault(), "text/html", "UTF-8"))
                   .render();
    }
}
使用 quarkus-rest-qutequarkus-resteasy-qute 时,内容协商会自动执行。有关更多信息,请参阅 REST 集成部分。

4.3. 直接在模板中注入 Bean

@Named 注解的 CDI Bean 可以通过 cdi 和/或 inject 命名空间在任何模板中引用

{cdi:personService.findPerson(10).name} (1)
{inject:foo.price} (2)
1 首先,找到名为 personService 的 Bean,然后将其用作基础对象。
2 首先,找到名为 foo 的 Bean,然后将其用作基础对象。
@Named @Dependent Bean 在单个渲染操作的模板中的所有表达式之间共享,并在渲染完成后销毁。

所有带有 cdiinject 命名空间的表达式都会在构建期间进行验证。对于表达式 cdi:personService.findPerson(10).name,注入 Bean 的实现类必须声明 findPerson 方法,或者必须存在匹配的模板扩展方法。对于表达式 inject:foo.price,注入 Bean 的实现类必须具有 price 属性(例如 getPrice() 方法)或必须存在匹配的模板扩展方法

还会为所有带有 @Named 注解的 Bean 生成 ValueResolver,以便可以无反射地访问其属性。
如果您的应用程序处理 HTTP 请求,您还可以通过 inject 命名空间注入当前的 io.vertx.core.http.HttpServerRequest,例如 {inject:vertxRequest.getParam('foo')}

有时可能需要访问未用 @Named 注解的 CDI Bean 的公共方法和属性。但是,如果您不控制 Bean 的来源,则无法添加 @Named 注解。尽管如此,可以创建一个用 @Named 注解的中间 CDI Bean。此中间 Bean 可以注入有问题的 Bean 并使其可访问。Java record 是定义此类中间 CDI Bean 的一种非常方便的方式。

@Named (1) (2)
public record UserData(UserInfo info, @LoggedIn String username) { (3)
}
1 如果未通过 value 成员显式指定名称,则会分配默认名称 - Bean 类的简单名称,将第一个字符转换为小写后。在这种特定情况下,默认名称是 userData
2 @Singleton 作用域会自动添加。
3 规范构造函数的所有参数都是注入点。可以使用访问器方法来获取注入的 Bean。

然后在模板中,您可以简单地使用 {cdi:userData.info}{cdi:userData.username}

4.4. 类型安全表达式

模板表达式可以可选地是类型安全的。这意味着表达式将根据现有的 Java 类型和模板扩展方法进行验证。如果找到无效/不正确的表达式,则构建将失败。

例如,如果有一个表达式 item.name,其中 item 映射到 org.acme.Item,则 Item 必须有一个 name 属性或必须存在匹配的模板扩展方法。

可选的参数声明用于将 Java 类型绑定到其第一个部分匹配参数名称的表达式。参数声明直接在模板中指定。

Java 类型应始终使用完全限定名进行标识,除非它是来自 java.lang 包的 JDK 类型 - 在这种情况下,包名是可选的。支持参数化类型,但是通配符始终被忽略 - 仅考虑上界/下界。例如,参数声明 {@java.util.List<? extends org.acme.Foo> list} 被识别为 {@java.util.List<org.acme.Foo> list}。类型变量不以特殊方式处理,并且不应使用。

参数声明示例
{@org.acme.Foo foo} (1)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Qute Hello</title>
</head>
<body>
  <h1>{title}</h1> (2)
  Hello {foo.message.toLowerCase}! (3) (4)
</body>
</html>
1 参数声明 - 将 foo 映射到 org.acme.Foo
2 未验证 - 未匹配参数声明。
3 此表达式已验证。org.acme.Foo 必须具有 message 属性或必须存在匹配的模板扩展方法。
4 同样,从 foo.message 解析的对象必须具有 toLowerCase 属性或必须存在匹配的模板扩展方法。
为所有在参数声明中使用的类型自动生成值解析器,以便可以无反射地访问其属性。
@CheckedTemplate 注解的类的方法参数会自动转换为参数声明,用于绑定类型安全表达式

请注意,节可以覆盖原本会匹配参数声明的名称

{@org.acme.Foo foo}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Qute Hello</title>
</head>
<body>
  <h1>{foo.message}</h1> (1)
  {#for foo in baz.foos}
    <p>Hello {foo.message}!</p> (2)
  {/for}
</body>
</html>
1 根据 org.acme.Foo 进行验证。
2 未验证 - foo 在循环节中被覆盖。

参数声明可以在键之后指定默认值。键和默认值由等号分隔:{@int age=10}。如果参数键解析为 null 或未找到,则使用默认值。

例如,如果有一个参数声明 {@String foo="Ping"}foo 未找到,则可以使用 {foo},输出将是 Ping。另一方面,如果设置了值(例如通过 TemplateInstance.data("foo", "Pong")),则 {foo} 的输出将是 Pong

默认值的类型必须可分配给参数声明的类型。例如,请参见错误的参数声明,它会导致构建失败:{@org.acme.Foo foo=1}

默认值实际上是表达式。因此,默认值不必是字面量(例如 42true)。例如,您可以利用 @TemplateEnum 并为参数声明的默认值指定枚举常量:{@org.acme.MyEnum myEnum=MyEnum:FOO}。但是,除非使用括号进行分组,否则默认值不支持中缀表示法,例如 {@org.acme.Foo foo=(foo1 ?: foo2)}
默认值的类型在独立 Qute 中未经验证。
更多参数声明示例
{@int pages} (1)
{@java.util.List<String> strings} (2)
{@java.util.Map<String,? extends Number> numbers} (3)
{@java.util.Optional<?> param} (4)
{@String name="Quarkus"} (5)
1 原始类型。
2 String 被替换为 java.lang.String{@java.util.List<java.lang.String> strings}
3 通配符被忽略,并使用上界代替:{@java.util.Map<String,Number>}
4 通配符被忽略,并使用 java.lang.Object 代替:{@java.util.Optional<java.lang.Object>}
5 类型为 java.lang.String,键为 name,默认值为 Quarkus

4.5. 类型安全模板

您可以在 Java 代码中定义类型安全模板。类型安全模板的参数会自动转换为参数声明,用于绑定类型安全表达式。类型安全表达式将在构建时进行验证。

有两种方法可以定义类型安全模板

  1. @io.quarkus.qute.CheckedTemplate 注解一个类,并且其所有 static native 方法都将用于定义类型安全模板及其所需参数列表。

  2. 使用实现 io.quarkus.qute.TemplateInstance 的 Java record;record 组件代表模板参数,并且 @io.quarkus.qute.CheckedTemplate 可选地用于配置模板。

4.5.1. 嵌套类型安全模板

如果使用 Jakarta REST 资源中的模板,则可以遵循以下约定

  • 将模板文件组织在 /src/main/resources/templates 目录中,按每个资源类将它们分组到目录中。因此,如果您的 ItemResource 类引用了两个模板 hellogoodbye,请将它们放在 /src/main/resources/templates/ItemResource/hello.txt/src/main/resources/templates/ItemResource/goodbye.txt。按资源类分组模板可以使导航更容易。

  • 在您的每个资源类中,在资源类内声明一个 @CheckedTemplate static class Template {} 类。

  • 为您的资源声明每个模板文件的 public static native TemplateInstance method();

  • 使用这些静态方法来构建您的模板实例。

ItemResource.java
package org.acme.quarkus.sample;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;
import io.quarkus.qute.CheckedTemplate;

@Path("item")
public class ItemResource {

    @CheckedTemplate
    public static class Templates {
        public static native TemplateInstance item(Item item); (1) (2)
    }

    @GET
    @Path("{id}")
    @Produces(MediaType.TEXT_HTML)
    public TemplateInstance get(Integer id) {
        return Templates.item(service.findItem(id)); (3)
    }
}
1 声明一个方法,该方法为 templates/ItemResource/item.html 提供 TemplateInstance,并声明其 Item item 参数,以便我们可以验证模板。
2 item 参数会自动转换为参数声明,因此所有引用此名称的表达式都将得到验证。
3 使 Item 对象在模板中可访问。
默认情况下,用 @CheckedTemplate 注解的类中定义的模板只能包含类型安全表达式,即可以在构建时验证的表达式。您可以使用 @CheckedTemplate(requireTypeSafeExpressions = false) 来放宽此要求。

4.5.2. 顶层类型安全模板

您也可以声明一个用 @CheckedTemplate 注解的顶层 Java 类

顶层检查模板
package org.acme.quarkus.sample;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;
import io.quarkus.qute.CheckedTemplate;

@CheckedTemplate
public class Templates {
    public static native TemplateInstance hello(String name); (1)
}
1 这声明了一个路径为 templates/hello.txt 的模板。name 参数会自动转换为参数声明,因此所有引用此名称的表达式都将得到验证。

然后为每个模板文件声明一个 public static native TemplateInstance method();。使用这些静态方法来构建您的模板实例

HelloResource.java
package org.acme.quarkus.sample;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;

@Path("hello")
public class HelloResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public TemplateInstance get(@QueryParam("name") String name) {
        return Templates.hello(name);
    }
}

4.5.3. 模板 Records

实现 io.quarkus.qute.TemplateInstance 的 Java record 表示类型安全模板。record 组件代表模板参数,并且 @io.quarkus.qute.CheckedTemplate 可选地用于配置模板。

HelloResource.java
package org.acme.quarkus.sample;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;

@Path("hello")
public class HelloResource {

    record Hello(String name) implements TemplateInstance {} (1)

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public TemplateInstance get(@QueryParam("name") String name) {
        return new Hello(name); (2)
    }
}
1 使用 Java record 声明一个类型安全模板。模板位于 /src/main/resources/templates/HelloResource/Hello.html
2 实例化 record 并将其用作普通的 TemplateInstance

4.5.4. 自定义模板路径

类型安全模板(@CheckedTemplate 方法或记录)的路径由一个基本路径和一个默认名称组成。基本路径@CheckedTemplate#basePath() 提供。默认情况下,使用嵌套静态类中的封闭类的简单名称,或者使用顶级类的空字符串。默认名称@CheckedTemplate#defaultName() 中指定的策略派生。默认情况下,@CheckedTemplate 方法/记录的名称按原样使用。

未用 @CheckedTemplate 注释的模板记录被视为用默认值用 @CheckedTemplate 注释。
自定义模板路径示例
package org.acme.quarkus.sample;

import jakarta.ws.rs.Path;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.CheckedTemplate;

@Path("item")
public class ItemResource {

    @CheckedTemplate(basePath = "items", defaultName = CheckedTemplate.HYPHENATED_ELEMENT_NAME)
    static class Templates {
        static native TemplateInstance itemAndOrder(Item item); (1)
    }
}
1 此方法的模板路径将是 items/item-and-order

4.5.5. 类型安全片段

您还可以在 Java 代码中定义类型安全模板的类型安全片段。定义类型安全片段有两种方式:

  1. @CheckedTemplate 注释的本地静态方法,其名称包含美元符号 $

  2. 实现 io.quarkus.qute.TemplateInstance 且其名称包含美元符号 $ 的 Java 记录。

片段的名称从注释的成员名称派生。最后一个美元符号 $ 之前的部分是相关类型安全模板的方法名称。最后一个美元符号之后的部分是片段标识符。在构造默认名称时,会遵守相关的 CheckedTemplate#defaultName() 定义的策略。

类型安全片段示例
import io.quarkus.qute.CheckedTemplate;
import org.acme.Item;

@CheckedTemplate
class Templates {

  // defines a type-safe template
  static native TemplateInstance items(List<Item> items);

  // defines a fragment of Templates#items() with identifier "item"
  static native TemplateInstance items$item(Item item); (1)

  // type-safe fragment as a Java record - functionally equivalent to the items$item() method above
  record items$item(Item item) implements TemplateInstance {}
}
1 Quarkus 在构建时验证与 Templates#items() 对应的每个模板都包含一个带有标识符 item 的片段。此外,还会验证片段方法的参数。通常,在片段中找到的引用原始/外部模板数据的类型安全表达式都需要一个特定的参数。
items.html 中的片段定义
<h1>Items</h1>
<ol>
    {#for item in items}
    {#fragment id=item}   (1)
    <li>{item.name}</li>  (2)
    {/fragment}
    {/for}
</ol>
1 定义一个带有标识符 item 的片段。
2 {item.name} 表达式意味着 Templates#items$item() 方法必须声明一个名为 item、类型为 org.acme.Item 的参数。
类型安全片段调用站点示例
class ItemService {

  String renderItem(Item item) {
     // this would return something like "<li>Foo</li>"
     return Templates.items$item(item).render();
  }
}
您可以指定 @CheckedTemplate#ignoreFragments=true 来禁用此功能,即方法名称中的美元符号 $ 不会产生已检查的片段方法。

4.5.6. 模板内容

也可以直接在 Java 代码中为类型安全模板指定内容。用 @CheckedTemplate 注释的类的静态本地方法或实现 TemplateInstance 的 Java 记录可能被 @io.quarkus.qute.TemplateContents 注释。注释值用作模板内容。模板 id/路径从类型安全模板派生。

模板内容示例
package org.acme.quarkus.sample;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateContents;
import io.quarkus.qute.TemplateInstance;

@Path("hello")
public class HelloResource {

    @TemplateContents("Hello {name}!") (1)
    record Hello(String name) implements TemplateInstance {}

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public TemplateInstance get(@QueryParam("name") String name) {
        return new Hello(name);
    }
}
1 Hello 记录表示的类型安全模板定义内容。派生的模板 id 是 HelloResource/Hello

4.6. 模板扩展方法

扩展方法可用于扩展数据类的新功能(扩展可访问属性和方法的集合)或解析特定命名空间的表达式。例如,可以添加计算属性虚拟方法

会自动为用 @TemplateExtension 注释的方法生成值解析器。如果一个类被 @TemplateExtension 注释,那么将为类上声明的每个非私有静态方法生成一个值解析器。方法级注释会覆盖类上定义的行为。不满足以下要求的会被忽略。

一个模板扩展方法

  • 不得为 private

  • 必须为 static

  • 不得返回 void

如果没有定义命名空间,则使用不带 @TemplateAttribute 注释的第一个参数的类来匹配基对象。否则,使用命名空间来匹配表达式。

4.6.1. 按名称匹配

方法名称默认用于匹配属性名称。

扩展方法示例
package org.acme;

class Item {

    public final BigDecimal price;

    public Item(BigDecimal price) {
        this.price = price;
    }
}

@TemplateExtension
class MyExtensions {

    static BigDecimal discountedPrice(Item item) { (1)
        return item.getPrice().multiply(new BigDecimal("0.9"));
    }
}
1 此方法匹配类型为 Item.class 的基对象和 discountedPrice 属性名称的表达式。

此模板扩展方法可以渲染以下模板

{item.discountedPrice} (1)
1 item 解析为 org.acme.Item 的实例。

但是,可以使用 matchName() 指定匹配名称。

TemplateExtension#matchName() 示例
@TemplateExtension(matchName = "discounted")
static BigDecimal discountedPrice(Item item) {
   // this method matches {item.discounted} if "item" resolves to an object assignable to "Item"
   return item.getPrice().multiply(new BigDecimal("0.9"));
}

可以使用特殊常量 - TemplateExtension#ANY - 来指定扩展方法匹配任何名称。

TemplateExtension#ANY 示例
@TemplateExtension(matchName = TemplateExtension.ANY)
static String itemProperty(Item item, String name) { (1)
   // this method matches {item.foo} if "item" resolves to an object assignable to "Item"
   // the value of the "name" argument is "foo"
}
1 附加的字符串方法参数用于传递实际属性名称。

还可以使用 matchRegex() 将名称与正则表达式匹配。

TemplateExtension#matchRegex() 示例
@TemplateExtension(matchRegex = "foo|bar")
static String itemProperty(Item item, String name) { (1)
   // this method matches {item.foo} and {item.bar} if "item" resolves to an object assignable to "Item"
   // the value of the "name" argument is "foo" or "bar"
}
1 附加的字符串方法参数用于传递实际属性名称。

最后,可以使用 matchNames() 指定匹配名称的集合。附加的字符串方法参数也是必需的。

TemplateExtension#matchNames() 示例
@TemplateExtension(matchNames = {"foo", "bar"})
static String itemProperty(Item item, String name) {
   // this method matches {item.foo} and {item.bar} if "item" resolves to an object assignable to "Item"
   // the value of the "name" argument is "foo" or "bar"
}
多余的匹配条件将被忽略。按优先级降序排序的条件是:matchRegex()matchNames()matchName()

4.6.2. 方法参数

扩展方法可以声明参数。如果未指定命名空间,则将不带 @TemplateAttribute 注释的第一个参数用于传递基对象,即第一个示例中的 org.acme.Item。如果匹配任何名称或使用正则表达式,则需要使用字符串方法参数(不带 @TemplateAttribute 注释)来传递属性名称。带 @TemplateAttribute 注释的参数通过 TemplateInstance#getAttribute() 获取。所有其他参数均被视为虚拟方法参数,在渲染模板时解析并传递给扩展方法。

多个参数示例
@TemplateExtension
class BigDecimalExtensions {

    @TemplateExtension(matchNames = {"scale", "setScale"})
    static BigDecimal scale(BigDecimal val, String ignoredName, int scale, RoundingMode mode) { (1)
        return val.setScale(scale, mode);
    }
}
1 此方法匹配基对象类型为 BigDecimal.class、虚拟方法名称为 scale()/setScale() 以及两个虚拟方法参数 - scalemode 的表达式。
{item.discountedPrice.scale(2,mode)} (1)
1 item.discountedPrice 解析为 BigDecimal 的实例。

4.6.3. 命名空间扩展方法

如果指定了 TemplateExtension#namespace(),则扩展方法用于解析具有给定命名空间的表达式。共享相同命名空间的模板扩展方法按 TemplateExtension#priority() 排序分组到一个解析器中。第一个匹配的扩展方法用于解析表达式。

命名空间扩展方法示例
@TemplateExtension(namespace = "str")
public class StringExtensions {

   static String format(String fmt, Object... args) {
      return String.format(fmt, args);
   }

   static String reverse(String val) {
      return new StringBuilder(val).reverse().toString();
   }
}

这些扩展方法可如下使用。

{str:format('%s %s!','Hello', 'world')} (1)
{str:reverse('hello')} (2)
1 输出为 Hello world!
2 输出为 olleh

4.6.4. 内置模板扩展

Quarkus 提供了一组内置的扩展方法。

4.6.4.1. Map
  • keyskeySet:返回 Map 中键的 Set 视图

    • {#for key in map.keySet}

  • values:返回 Map 中值的 Collection 视图

    • {#for value in map.values}

  • size:返回 Map 中键值映射的数量

    • {map.size}

  • isEmpty:如果 Map 中没有键值映射,则返回 true

    • {#if map.isEmpty}

  • get(key):返回指定键映射的值

    • {map.get('foo')}

也可以直接访问 Map 值:{map.myKey}。对于不是合法标识符的键,请使用方括号表示法:{map['my key']}
4.6.4.2. List
  • get(index):返回列表中指定位置的元素

    • {list.get(0)}

  • reversed:返回列表的反向迭代器

    • {#for r in recordsList.reversed}

  • take:返回列表中前 n 个元素;如果 n 超出范围,则抛出 IndexOutOfBoundsException

    • {#for r in recordsList.take(3)}

  • takeLast:返回列表中最后 n 个元素;如果 n 超出范围,则抛出 IndexOutOfBoundsException

    • {#for r in recordsList.takeLast(3)}

  • first:返回列表的第一个元素;如果列表为空,则抛出 NoSuchElementException

    • {recordsList.first}

  • last:返回列表的最后一个元素;如果列表为空,则抛出 NoSuchElementException

    • {recordsList.last}

可以通过索引直接访问列表元素:{list.10} 甚至 {list[10]}
4.6.4.3. 整数
  • mod:模运算

    • {#if counter.mod(5) == 0}

  • plus+:加法

    • {counter + 1}

    • {age plus 10}

    • {age.plus(10)}

  • minus-:减法

    • {counter - 1}

    • {age minus 10}

    • {age.minus(10)}

4.6.4.4. 字符串
  • fmtformat:通过 java.lang.String.format() 格式化字符串实例

    • {myStr.fmt("arg1","arg2")}

    • {myStr.format(locale,arg1)}

  • +:连接的中缀表示法,适用于 StringStringBuilder 基对象

    • {item.name + '_' + mySuffix}

    • {name + 10}

  • str:['<value>']:返回字符串值,例如方便连接另一个字符串值

    • {str:['/path/to/'] + fileName}

  • str:fmtstr:format:通过 java.lang.String.format() 格式化提供的字符串值

    • {str:format("Hello %s!",name)}

    • {str:fmt(locale,'%tA',now)}

    • {str:fmt('/path/to/%s', fileName)}

  • str:concat:连接指定参数的字符串表示。

    • {str:concat("Hello ",name,"!")} 如果 name 解析为 Foo,则得到 Hello Foo!

    • {str:concat('/path/to/', fileName)}

  • str:join:使用分隔符将指定参数的字符串表示连接在一起。

    • {str:join('_','Qute','is','cool')} 得到 Qute_is_cool

  • str:builder:返回一个新的字符串构建器。

    • {str:builder('Qute').append("is").append("cool!")} 得到 Qute is cool!

    • {str:builder('Qute') + "is" + whatisqute + "!"} 如果 whatisqute 解析为 cool,则得到 Qute is cool!

  • str:eval:将第一个参数的字符串表示作为模板在当前上下文中进行评估。

    • {str:eval('Hello {name}!') 如果 name 解析为 lovely,则得到 Hello lovely!

    • {str:eval(myTemplate)} 如果 myTemplate 解析为 Hello {name}!name 解析为 lovely,则得到 Hello lovely!

    • {str:eval('/path/to/{fileName}')} 如果 fileName 解析为 file.txt,则得到 /path/to/file.txt

4.6.4.5. 配置
  • config:<name>config:[<name>]:返回给定属性名称的配置值

    • {config:foo}{config:['property.with.dot.in.name']}

  • config:property(name):返回给定属性名称的配置值;名称可以动态地通过表达式获得

    • {config:property('quarkus.foo')}

    • {config:property(foo.getPropertyName())}

  • config:boolean(name):返回给定属性名称的配置值(布尔类型);名称可以动态地通过表达式获得

    • {config:boolean('quarkus.foo.boolean') ?: '未找到'}

    • {config:boolean(foo.getPropertyName()) ?: '属性为 false'}

  • config:integer(name):返回给定属性名称的配置值(整数类型);名称可以动态地通过表达式获得

    • {config:integer('quarkus.foo')}

    • {config:integer(foo.getPropertyName())}

4.6.4.6. 时间
  • format(pattern):格式化 java.time 包中的时间对象

    • {dateTime.format('d MMM uuuu')}

  • format(pattern,locale):格式化 java.time 包中的时间对象

    • {dateTime.format('d MMM uuuu',myLocale)}

  • format(pattern,locale,timeZone):格式化 java.time 包中的时间对象

    • {dateTime.format('d MMM uuuu',myLocale,myTimeZoneId)}

  • time:format(dateTime,pattern):格式化 java.time 包、java.util.Datejava.util.Calendarjava.lang.Number 中的时间对象

    • {time:format(myDate,'d MMM uuuu')}

  • time:format(dateTime,pattern,locale):格式化 java.time 包、java.util.Datejava.util.Calendarjava.lang.Number 中的时间对象

    • {time:format(myDate,'d MMM uuuu', myLocale)}

  • time:format(dateTime,pattern,locale,timeZone):格式化 java.time 包、java.util.Datejava.util.Calendarjava.lang.Number 中的时间对象

    • {time:format(myDate,'d MMM uuuu',myLocale,myTimeZoneId)}

4.7. @TemplateData

为带 @TemplateData 注释的类型自动生成值解析器。这使得 Quarkus 可以在运行时避免使用反射来访问数据。

非公共成员、构造函数、静态初始化块、静态、合成和 void 方法始终被忽略。
package org.acme;

@TemplateData
class Item {

    public final BigDecimal price;

    public Item(BigDecimal price) {
        this.price = price;
    }

    public BigDecimal getDiscountedPrice() {
        return price.multiply(new BigDecimal("0.9"));
    }
}

Item 的任何实例都可以直接在模板中使用

{#each items} (1)
  {it.price} / {it.discountedPrice}
{/each}
1 items 解析为 org.acme.Item 实例的列表。

此外,@TemplateData.properties()@TemplateData.ignore() 可用于微调生成的解析器。最后,还可以指定注释的“目标” - 这对于不受应用程序控制的第三方类可能很有用。

@TemplateData(target = BigDecimal.class)
@TemplateData
class Item {

    public final BigDecimal price;

    public Item(BigDecimal price) {
        this.price = price;
    }
}
{#each items}
  {it.price.setScale(2, rounding)} (1)
{/each}
1 生成的价值解析器知道如何调用 BigDecimal.setScale() 方法。

4.7.1. 访问静态字段和方法

如果设置了 @TemplateData#namespace() 为非空值,则会自动生成一个命名空间解析器来访问目标类的公共静态字段和方法。默认情况下,命名空间是目标类的 FQCN,其中点和美元符号被下划线替换。例如,名为 org.acme.Foo 的类的命名空间是 org_acme_Foo。静态字段 Foo.AGE 可以通过 {org_acme_Foo:AGE} 访问。静态方法 Foo.computeValue(int number) 可以通过 {org_acme_Foo:computeValue(10)} 访问。

命名空间只能包含字母数字字符和下划线。
@TemplateData 注释的类
package model;

@TemplateData (1)
public class Statuses {
    public static final String ON = "on";
    public static final String OFF = "off";
}
1 会自动生成命名空间为 model_Statuses 的名称解析器。
访问类常量的模板
{#if machine.status == model_Statuses:ON}
  The machine is ON!
{/if}

4.7.2. 枚举的便捷注释

还有一个方便的注释来访问枚举常量:@io.quarkus.qute.TemplateEnum。此注释在功能上等同于 @TemplateData(namespace = TemplateData.SIMPLENAME),即自动为目标枚举生成命名空间解析器,并将目标枚举的简单名称用作命名空间。

@TemplateEnum 注释的枚举
package model;

@TemplateEnum (1)
public enum Status {
    ON,
    OFF
}
1 会自动生成命名空间为 Status 的名称解析器。
在非枚举类上声明的 @TemplateEnum 将被忽略。此外,如果枚举还声明了 @TemplateData 注释,则会忽略 @TemplateEnum 注释。
访问枚举常量的模板
{#if machine.status == Status:ON}
  The machine is ON!
{/if}
Quarkus 会检测可能的命名空间冲突,并在发现多个 @TemplateData 和/或 @TemplateEnum 注释定义了特定命名空间时,构建将失败。

4.8. 全局变量

io.quarkus.qute.TemplateGlobal 注释可用于表示提供全局变量的静态字段和方法,这些变量可访问任何模板。

全局变量

  • 在初始化期间作为任何 TemplateInstance计算数据添加;

  • 可通过 global: 命名空间访问。

使用 TemplateInstance#computedData(String, Function<String, Object>) 时,一个映射函数与一个特定的键相关联,并且每次请求给定键的值时都会使用此函数。对于全局变量,在映射函数中调用静态方法或读取静态字段。
全局变量定义
enum Color { RED, GREEN, BLUE }

@TemplateGlobal (1)
public class Globals {

    static int age = 40;

    static Color[] myColors() {
      return new Color[] { Color.RED, Color.BLUE };
    }

    @TemplateGlobal(name = "currentUser") (2)
    static String user() {
       return "Mia";
    }
}
1 如果一个类被 @TemplateGlobal 注释,那么每个非 void、非私有、无参数的静态方法和每个非私有静态字段都被视为全局变量。名称是默认的,即使用字段/方法的名称。
2 方法级注释会覆盖类级注释。在此特定情况下,名称不是默认的,而是显式选择的。
访问全局变量的模板
User: {currentUser} (1)
Age:  {global:age} (2)
Colors: {#each myColors}{it}{#if it_hasNext}, {/if}{/each} (3)
1 currentUser 解析为 Globals#user()
2 使用 global: 命名空间;age 解析为 Globals#age
3 myColors 解析为 Globals#myColors()
请注意,全局变量会隐式地向所有模板添加参数声明,因此在构建时会验证引用全局变量的任何表达式。
输出
User: Mia
Age:  40
Colors: RED, BLUE

4.8.1. 解析冲突

如果未通过 global: 命名空间访问,全局变量可能与常规数据对象发生冲突。 类型安全模板会自动覆盖全局变量。例如,以下定义覆盖了由 Globals#user() 方法提供的全局变量

类型安全模板定义
import org.acme.User;

@CheckedTemplate
public class Templates {
    static native TemplateInstance hello(User currentUser); (1)
}
1 currentUserGlobals#user() 提供的全局变量冲突。

因此,相应的模板不会导致验证错误,即使 Globals#user() 方法返回的 java.lang.String 没有 name 属性。

templates/hello.txt
User name: {currentUser.name} (1)
1 org.acme.User 具有 name 属性。

对于其他模板,需要显式声明参数

{@org.acme.User currentUser} (1)

User name: {currentUser.name}
1 此参数声明覆盖了由 Globals#user() 方法提供的全局变量添加的声明。

4.9. 原生可执行文件

在 JVM 模式下,可以使用基于反射的值解析器来访问模型类的属性和调用方法。但这默认不适用于原生可执行文件。因此,您可能会遇到模板异常,例如在模板 hello.html 中,表达式 {foo.name} 在基对象 "org.acme.Foo" 上找不到属性 "name",即使 Foo 类声明了相关的 getter 方法。

有几种方法可以解决此问题:

  • 利用类型安全模板类型安全表达式

    • 在这种情况下,会自动生成优化的值解析器并在运行时使用

    • 这是首选解决方案

  • @TemplateData 注释模型类 - 会自动生成并使用专门的值解析器

  • @io.quarkus.runtime.annotations.RegisterForReflection 注释模型类,以使基于反射的值解析器正常工作。有关 @RegisterForReflection 注释的更多详细信息,请参阅原生应用程序提示页面。

4.10. REST 集成

如果您想在 Jakarta REST 应用程序中使用 Qute,那么根据您使用的 Jakarta REST 堆栈,您需要先注册正确的扩展。

如果您通过 quarkus-rest 扩展使用 Quarkus REST(以前称为 RESTEasy Reactive),请在 pom.xml 文件中添加

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-qute</artifactId>
</dependency>

如果您改用基于旧版 RESTEasy Classic 的 quarkus-resteasy 扩展,请在 pom.xml 文件中添加

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-qute</artifactId>
</dependency>

这两个扩展都注册了一个特殊的响应过滤器,该过滤器使资源方法能够返回 TemplateInstance,从而使用户无需处理所有必要的内部步骤。

使用 Quarkus REST 时,返回 TemplateInstance 的资源方法被视为非阻塞的。您需要用 io.smallrye.common.annotation.Blocking 注释该方法,以将其标记为阻塞。例如,如果它还带有 @RunOnVirtualThread 注释。

最终结果是,在 Jakarta REST 资源中使用 Qute 可能很简单,就像

HelloResource.java
package org.acme.quarkus.sample;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.Template;

@Path("hello")
public class HelloResource {

    @Inject
    Template hello; (1)

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public TemplateInstance get(@QueryParam("name") String name) {
        return hello.data("name", name); (2) (3)
    }
}
1 如果未提供 @Location 限定符,则使用字段名称来定位模板。在此特定情况下,我们正在注入路径为 templates/hello.txt 的模板。
2 Template.data() 返回一个新的模板实例,可以在触发实际渲染之前对其进行自定义。在本例中,我们将 name 值放在键 name 下。在渲染期间可以访问数据映射。
3 请注意,我们不会触发渲染 - 这是由特殊的 ContainerResponseFilter 实现自动完成的。
鼓励用户使用类型安全模板,它们有助于组织特定 Jakarta REST 资源的模板并自动启用类型安全表达式

内容协商会自动进行。结果输出取决于从客户端收到的 Accept 标头。

@Path("/detail")
class DetailResource {

    @Inject
    Template item; (1)

    @GET
    @Produces({ MediaType.TEXT_HTML, MediaType.TEXT_PLAIN })
    public TemplateInstance item() {
        return item.data("myItem", new Item("Alpha", 1000)); (2)
    }
}
1 注入具有从注入字段派生的基本路径的变体模板 - 在本例中为 src/main/resources/templates/item
2 对于 text/plain,使用 src/main/resources/templates/item.txt 模板。对于 text/html,使用 META-INF/resources/templates/item.html 模板。

可以使用 RestTemplate 工具类从 Jakarta REST 资源方法的正文中获取模板实例

RestTemplate 示例
@Path("/detail")
class DetailResource {

    @GET
    @Produces({ MediaType.TEXT_HTML, MediaType.TEXT_PLAIN })
    public TemplateInstance item() {
        return RestTemplate.data("myItem", new Item("Alpha", 1000)); (1)
    }
}
1 模板的名称从资源类和方法名称派生;在此特定情况下为 DetailResource/item
@Inject 不同,通过 RestTemplate 获取的模板不会进行验证,即如果模板不存在,构建也不会失败。

4.11. Vert.x 集成

如果您想在模板中使用 io.vertx.core.json.JsonObject 作为数据,那么如果该扩展尚未包含在您的依赖项中(大多数应用程序默认使用此扩展),您将需要将 quarkus-vertx 扩展添加到您的构建文件中。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-vertx</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-vertx")

包含此依赖项后,我们就有一个用于 io.vertx.core.json.JsonObject 的特殊值解析器,这使得可以在模板中访问 JSON 对象的属性。

src/main/resources/templates/foo.txt
{tool.name}
{tool.fieldNames}
{tool.fields}
{tool.size}
{tool.empty}
{tool.isEmpty}
{tool.get('name')}
{tool.containsKey('name')}
QuteVertxIntegration.java
import java.util.HashMap;
import jakarta.inject.Inject;
import io.vertx.core.json.JsonObject;
import io.quarkus.qute.Template;

public class QuteVertxIntegration {

    @Inject
    Template foo;

    public String render() {
         HashMap<String, Object> toolMap = new Map<String, Object>();
         toolMap.put("name", "Roq");
         JsonObject jsonObject = new JsonObject(toolMap);
         return foo.data("tool", jsonObject).render();
    }
}

QuteVertxIntegration#render() 的输出应如下所示

Roq
[name]
[name]
1
false
false
Roq
true

4.12. 开发模式

在开发模式下,src/main/resources/templates 中的所有文件都会被监视更改。默认情况下,模板的修改会导致应用程序重启,该重启还会触发构建时验证。

但是,可以使用 quarkus.qute.dev-mode.no-restart-templates 配置属性来指定不重启应用程序的模板。配置值是匹配相对于 templates 目录的模板路径的正则表达式,并且 / 用作路径分隔符。例如,quarkus.qute.dev-mode.no-restart-templates=templates/foo.html 匹配模板 src/main/resources/templates/foo.html。匹配的模板将被重新加载,并且只执行运行时验证。

4.13. 测试

在测试模式下,已注入和类型安全模板的渲染结果会记录在托管的 io.quarkus.qute.RenderedResults 中,该对象注册为 CDI bean。您可以在测试或其他 CDI bean 中注入 RenderedResults 并断言结果。但是,您可以将 quarkus.qute.test-mode.record-rendered-results 配置属性设置为 false 来禁用此功能。

4.14. 类型安全消息包

4.14.1. 基本概念

基本思想是,每个消息都可能是一个非常简单的模板。为了防止类型错误,消息被定义为消息包接口的带注释方法。Quarkus 在构建时生成消息包实现

消息包接口示例
import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;

@MessageBundle (1)
public interface AppMessages {

    @Message("Hello {name}!") (2)
    String hello_name(String name); (3)
}
1 表示消息包接口。包名称默认为 msg,并用作模板表达式中的命名空间,例如 {msg:hello_name}
2 每个方法都必须用 @Message 注释。该值是一个 qute 模板。如果未提供值,则从本地化文件中获取相应的值。如果不存在这样的文件,则会抛出异常并导致构建失败。
3 方法参数可在模板中使用。

消息包可在运行时使用

  1. 直接在代码中通过 io.quarkus.qute.i18n.MessageBundles#get();例如 MessageBundles.get(AppMessages.class).hello_name("Lucie")

  2. 通过 @Inject 注入您的 bean;例如 @Inject AppMessages

  3. 通过消息包命名空间在模板中引用

     {msg:hello_name('Lucie')} (1) (2) (3)
     {msg:message(myKey,'Lu')} (4)
    1 msg 是默认命名空间。
    2 hello_name 是消息键。
    3 Lucie 是消息包接口方法的参数。
    4 还可以使用保留键 message 获取运行时解析的键的本地化消息。但是,在这种情况下会跳过验证。

4.14.2. 默认包名称

除非用 @MessageBundle#value() 指定,否则包名称是默认的。对于顶级类,默认使用 msg 值。对于嵌套类,名称由层级中所有封闭类的简单名称组成(顶级类在前),后跟消息包接口的简单名称。名称之间用下划线分隔。

例如,以下消息包的名称将默认设置为 Controller_index

class Controller {

    @MessageBundle
    interface index {

        @Message("Hello {name}!")
        String hello(String name); (1)
   }
}
1 此消息可通过 {Controller_index:hello(name)} 在模板中使用。
包名称也是本地化文件名称的一部分,例如 Controller_index_de.properties 中的 Controller_index

4.14.3. 包名称和消息键

消息键直接在模板中使用。包名称用作模板表达式中的命名空间。@MessageBundle 可用于定义用于从方法名称生成消息键的默认策略。但是,@Message 可以覆盖此策略,甚至可以定义自定义键。默认情况下,注释元素的名称按原样使用。其他可能性包括:

  1. 去驼峰化并用连字符分隔;例如 helloName()hello-name

  2. 去驼峰化并用下划线分隔部分;例如 helloName()hello_name

4.14.4. 验证

  • 所有消息包模板都经过验证

    • 所有没有命名空间的表达式都必须映射到一个参数;例如 Hello {foo} → 方法必须有一个名为 foo 的参数

    • 所有表达式都根据参数类型进行验证;例如 Hello {foo.bar},其中参数 foo 的类型为 org.acme.Fooorg.acme.Foo 必须有一个名为 bar 的属性

      为每个未使用的参数记录警告消息。
  • 引用消息包方法的表达式,例如 {msg:hello(item.name)},也会经过验证。

4.14.5. 本地化

通过 quarkus.default-locale 配置属性指定的默认区域设置默认用于 @MessageBundle 接口。但是,io.quarkus.qute.i18n.MessageBundle#locale() 可用于指定自定义区域设置。此外,有两种方法可以定义本地化包:

  1. 创建一个扩展了默认接口的接口,该接口用 @Localized 注释

  2. 创建一个编码为 UTF-8 的文件,该文件位于应用程序存档的 src/main/resources/messages 目录中;例如 msg_de.properties

虽然本地化接口便于重构,但在许多情况下,外部文件可能更方便。
本地化接口示例
import io.quarkus.qute.i18n.Localized;
import io.quarkus.qute.i18n.Message;

@Localized("de") (1)
public interface GermanAppMessages extends AppMessages {

    @Override
    @Message("Hallo {name}!") (2)
    String hello_name(String name);
}
1 值是区域设置标签字符串(IETF)。
2 值是本地化模板。

消息包文件必须编码为UTF-8。文件名由相关的包名称(例如 msg)和下划线以及语言标记(IETF;例如 en-US)组成。语言标记可以省略,在这种情况下,将使用默认包区域设置的语言标记。例如,如果包 msg 的默认区域设置是 en,那么 msg.properties 将被视为 msg_en.properties。如果同时检测到 msg.propertiesmsg_en.properties,则会抛出异常并导致构建失败。文件格式非常简单:每行表示一个键/值对,使用等号作为分隔符,或者表示一个注释(行以 # 开头)。空行将被忽略。键映射到相应消息包接口中的方法名称。值代表 io.quarkus.qute.i18n.Message#value() 通常定义的内容。一个值可能分布在几行相邻的普通行中。在这种情况下,行终止符必须用反斜杠字符 \ 进行转义。行为非常类似于 java.util.Properties.load(Reader) 方法的行为。

本地化文件示例 - msg_de.properties
# This comment is ignored
hello_name=Hallo {name}! (1) (2)
1 本地化文件中的每一行代表一个键/值对。键必须对应于消息包接口中声明的方法。值是消息模板。
2 键和值用等号分隔。
我们在示例中使用 .properties 后缀,因为大多数 IDE 和文本编辑器都支持 .properties 文件的语法高亮。但实际上,后缀可以是任何内容 - 它只是被忽略。
在应用程序通过 mvn clean package 构建时,会自动将一个示例 properties 文件生成到每个消息包接口的目标目录中。例如,默认情况下,如果 @MessageBundle 没有指定名称,则生成文件 target/qute-i18n-examples/msg.properties。您可以将此文件用作特定区域设置的基础。只需重命名文件 - 例如 msg_fr.properties,更改消息模板,然后将其移至 src/main/resources/messages 目录。
值分布在几行相邻的行中
hello=Hello \
   {name} and \
   good morning!

请注意,行终止符用反斜杠字符 \ 转义,下一行的开头空格被忽略。即,{msg:hello('Edgar')} 将被渲染为 Hello Edgar and good morning!

一旦定义了本地化包,我们就需要一种方法来为特定模板实例选择正确的包,即为模板中的所有消息包表达式指定区域设置。默认情况下,使用通过 quarkus.default-locale 配置属性指定的区域设置来选择包。或者,您可以为模板实例指定 locale 属性。

locale 属性示例
@Singleton
public class MyBean {

    @Inject
    Template hello;

    String render() {
       return hello.instance().setLocale("cs").render(); (1)
    }
}
1 您可以设置一个 Locale 实例或一个区域设置标签字符串(IETF)。
当使用quarkus-rest-qute(或 quarkus-resteasy-qute)时,如果用户未设置 locale 属性,则该属性将从 Accept-Language 标头派生。

@Localized 限定符可用于注入本地化消息包接口。

注入的本地化消息包示例
@Singleton
public class MyBean {

    @Localized("cs") (1)
    AppMessages msg;

    String render() {
       return msg.hello_name("Jachym");
    }
}
1 该注释值是区域设置标签字符串(IETF)。
4.14.5.1. 枚举

有一个方便的方法来本地化枚举。如果存在一个接受枚举类型单个参数且没有定义消息模板的消息包方法

@Message (1)
String methodName(MyEnum enum);
1 该值故意未提供。在本地化文件中也没有该方法的键/值对。

然后它会收到一个生成的模板,例如

{#when enumParamName}
  {#is CONSTANT1}{msg:methodName_CONSTANT1}
  {#is CONSTANT2}{msg:methodName_CONSTANT2}
{/when}

此外,为每个枚举常量生成一个特殊的消息方法。最后,每个本地化文件都必须包含所有枚举常量的键和值。

methodName_CONSTANT1=Value 1
methodName_CONSTANT2=Value 2
枚举常量的消息键

默认情况下,消息键由方法名称后跟 _ 分隔符和常量名称组成。如果特定枚举的任何常量名称包含 _$ 字符,则对于此枚举的所有消息键,必须改用 _$ 分隔符。例如,methodName_$CONSTANT_1=Value 1methodName_$CONSTANT$1=Value 1。本地化枚举的常量不能包含 _$ 分隔符。

在模板中,枚举常量的本地化消息可以通过消息包方法 {msg:methodName(enumConstant)} 获取。

还有一个@TemplateEnum - 一个方便的注释,用于在模板中访问枚举常量。

4.14.6. 消息模板

消息包接口的每个方法都必须定义一个消息模板。值通常由 io.quarkus.qute.i18n.Message#value() 定义,但为了方便起见,也可以在本地化文件中定义值。消息模板在构建时经过验证。如果检测到缺少消息模板,则会抛出异常并导致构建失败。

没有值的消息包接口示例
import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;

@MessageBundle
public interface AppMessages {

    @Message (1)
    String hello_name(String name);

    @Message("Goodbye {name}!") (2)
    String goodbye(String name);
}
1 未定义注释值。在这种情况下,将取自补充本地化文件的值。
2 已定义注释值,并且优先于本地化文件中定义的值。
补充本地化文件
hello_name=Hello \
   {name} and \
   good morning!
goodbye=Best regards, {name} (1)
1 由于始终优先使用 io.quarkus.qute.i18n.Message#value(),因此该值将被忽略。

还可以定义一个默认消息模板。仅当未指定 Message#value() 且未在本地化文件中定义相关消息模板时,才使用默认模板。

带有默认值的消息包接口示例
import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;

@MessageBundle
public interface AppMessages {

    @Message(defaultValue = "Goodbye {name}!") (1)
    String goodbye(String name);
}
1 仅当未在本地化文件中定义消息模板时,才使用注释值。

4.15. 配置参考

构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖

配置属性

类型

默认

尝试查找模板文件时使用的后缀列表。

默认情况下,engine.getTemplate("foo") 会导致多次查找:foofoo.htmlfoo.txt 等。

环境变量:QUARKUS_QUTE_SUFFIXES

显示更多

字符串列表

qute.html,qute.txt,html,txt

后缀到内容类型的附加映射。当使用模板变体时,将使用此映射。默认情况下,java.net.URLConnection#getFileNameMap() 用于确定模板文件的内容类型。

环境变量:QUARKUS_QUTE_CONTENT_TYPES__FILE_SUFFIX_

显示更多

Map<String,String>

用于在执行类型安全验证时有意忽略表达式某些部分的排除规则列表。

元素值必须至少有两部分,用点分隔。最后一部分用于匹配属性/方法名称。前置部分用于匹配类名。值 * 可用于匹配任何名称。

示例

  • org.acme.Foo.name - 排除 org.acme.Foo 类上的属性/方法 name

  • org.acme.Foo.* - 排除 org.acme.Foo 类上的任何属性/方法

  • *.age - 排除任何类上的属性/方法 age

环境变量:QUARKUS_QUTE_TYPE_CHECK_EXCLUDES

显示更多

字符串列表

此正则表达式用于排除在模板根目录中找到的模板文件。排除的模板在构建期间既不解析也不验证,并且在运行时不可用。

匹配的输入是相对于根目录的文件路径,/ 用作路径分隔符。

默认情况下,隐藏文件被排除。隐藏文件的名称以点开头。

环境变量:QUARKUS_QUTE_TEMPLATE_PATH_EXCLUDE

显示更多

Pattern

^\..|.\/\..*$

前缀用于访问循环部分内的迭代元数据。

有效的前缀由字母数字字符和下划线组成。可以使用三个特殊常量

  • <alias_> - 使用迭代元素的别名后缀下划线,例如 item_hasNextit_count

  • <alias?> - 使用迭代元素的别名后缀问号,例如 item?hasNextit?count

  • <none> - 不使用前缀,例如 hasNextcount

默认情况下,设置 <alias_> 常量。

环境变量:QUARKUS_QUTE_ITERATION_METADATA_PREFIX

显示更多

字符串

<alias_>

如果设置了模板变体,则 '"<>& 字符将被转义的内容类型列表。

环境变量:QUARKUS_QUTE_ESCAPE_CONTENT_TYPES

显示更多

字符串列表

text/html,text/xml,application/xml,application/xhtml+xml

模板文件的默认字符集。

环境变量:QUARKUS_QUTE_DEFAULT_CHARSET

显示更多

Charset

UTF-8

在应用程序中找到具有相同路径的多个模板时使用的策略。

环境变量:QUARKUS_QUTE_DUPLICIT_TEMPLATES_STRATEGY

显示更多

prioritize如果找到多个具有相同路径的模板,则确定最高优先级值并消除所有优先级较低的模板。如果只剩一个模板,则使用该模板。否则,构建将失败。根应用程序存档中的模板优先级为 30。其他应用程序存档中的模板优先级为 10。构建项中的模板可以定义任何优先级。, fail如果找到多个具有相同路径的模板,则构建失败。

prioritize如果找到多个具有相同路径的模板,则确定最高优先级值并消除所有优先级较低的模板。如果只剩一个模板,则使用该模板。否则,构建将失败。根应用程序存档中的模板优先级为 {@code 30}。其他应用程序存档中的模板优先级为 {@code 10}。构建项中的模板可以定义任何优先级。

默认情况下,模板修改会导致应用程序重新启动,从而触发构建时验证。

此正则表达式可用于指定应用程序不重新启动的模板。即,模板被重新加载,并且仅执行运行时验证。

匹配的输入是以模板根目录开头的模板路径,/ 用作路径分隔符。例如,templates/foo.html

环境变量:QUARKUS_QUTE_DEV_MODE_NO_RESTART_TEMPLATES

显示更多

Pattern

默认情况下,注入的和类型安全的模板的渲染结果记录在托管的 RenderedResults 中,该 RenderedResults 注册为 CDI bean。

环境变量:QUARKUS_QUTE_TEST_MODE_RECORD_RENDERED_RESULTS

显示更多

布尔值

true

当独立表达式在运行时计算为“未找到”值,并且 quarkus.qute.strict-rendering 配置属性设置为 false 时使用的策略

评估节参数时,从不使用此策略,例如 {#if foo.name}。在这种情况下,节有责任适当地处理这种情况。

默认情况下,NOT_FOUND 常量写入输出。但是,在开发模式下,默认情况下使用 PropertyNotFoundStrategy#THROW_EXCEPTION,即,当未指定策略时。

环境变量:QUARKUS_QUTE_PROPERTY_NOT_FOUND_STRATEGY

显示更多

default输出 NOT_FOUND 常量。, noop无操作 - 无输出。, throw-exception抛出 TemplateException, output-original输出原始表达式字符串,例如 {foo.name}

指定解析器是否应从输出中删除独立行。独立行是包含至少一个节标记、参数声明或注释但不包含表达式且不包含非空格字符的行。

环境变量:QUARKUS_QUTE_REMOVE_STANDALONE_LINES

显示更多

布尔值

true

如果设置为 true,则任何计算为 Results.NotFound 值的表达式都将始终导致 TemplateException,并且渲染将被中止。

请注意,如果启用了严格渲染,则完全忽略 quarkus.qute.property-not-found-strategy 配置属性。

环境变量:QUARKUS_QUTE_STRICT_RENDERING

显示更多

布尔值

true

全局渲染超时(以毫秒为单位)。如果未设置 timeout 模板实例属性,则使用它。

环境变量:QUARKUS_QUTE_TIMEOUT

显示更多

长整型

10000

如果设置为 true,则超时也应可用于异步渲染方法,例如 TemplateInstance#createUni()TemplateInstance#renderAsync()

环境变量:QUARKUS_QUTE_USE_ASYNC_TIMEOUT

显示更多

布尔值

true

5. Qute 作为独立库使用

Qute 主要设计为 Quarkus 扩展。但是,可以将其用作“独立”库。在这种情况下,某些功能将不可用,并且需要一些额外的配置。

Engine
  • 首先,开箱即用没有托管的 Engine 实例。您需要通过 Engine.builder() 配置一个新的实例。

模板定位器
  • 默认情况下,未注册模板定位器,即 Engine.getTemplate(String) 将不起作用。

  • 您可以使用 EngineBuilder.addLocator() 注册自定义模板定位器,或手动解析模板并将结果放入缓存中,方法是使用 Engine.putTemplate(String, Template)

模板初始化器
  • 默认情况下未注册 TemplateInstance.Initializer,因此会忽略@TemplateGlobal 注释。

  • 可以通过方便的 EngineBuilder#addTemplateInstanceInitializer() 方法注册自定义 TemplateInstance.Initializer,并用任何数据和属性初始化模板实例。

  • 默认不注册任何部分助手。

  • 默认的值解析器集可以通过方便的 EngineBuilder.addDefaultSectionHelpers() 方法和 EngineBuilder.addDefaults() 方法分别注册。

值解析器
  • 默认情况下不会自动生成ValueResolver

    • @TemplateExtension 方法将无法工作。

    • @TemplateData@TemplateEnum 注释将被忽略。

  • 默认的值解析器集可以通过方便的 EngineBuilder.addDefaultValueResolvers() 方法和 EngineBuilder.addDefaults() 方法分别注册。

    并非所有内置扩展方法提供都功能都包含在默认值解析器中。但是,可以通过 ValueResolver.builder() 轻松构建自定义值解析器。
  • 建议通过 Engine.addValueResolver(new ReflectionValueResolver()) 注册 ReflectionValueResolver 实例,以便 Qute 可以访问对象属性并调用公共方法。

    请记住,在某些受限环境或需要额外配置的情况下,反射可能无法正常工作,例如在 GraalVM 原生映像的情况下进行注册。
用户定义的标签
  • 默认不注册用户定义的标签。

  • 可以通过 Engine.builder().addSectionHelper(new UserTagSectionHelper.Factory("tagName","tagTemplate.html")).build() 手动注册标签

类型安全
  • 类型安全表达式未经验证。

  • 不支持类型安全的消息包。

注入

无法注入 Template 实例,反之亦然 - 模板无法通过 inject:cdi: 命名空间注入 @Named CDI bean。

相关内容

在相同的扩展上

关于相同主题