Qute Reference Guide
Qute 是一款专门设计以满足 Quarkus 需求的模板引擎。反射的使用被最小化以减少本机映像的大小。该 API 结合了命令式和非阻塞式响应式编码风格。在开发模式下,src/main/resources/templates
文件夹中的所有文件都将被监视,而修改将立即出现在您的应用程序中。此外,Qute 会尝试在构建时检测到大多数模板问题并快速失败。
在本指南中,您将找到一个 introductory example,其中包括了 core features 和 Quarkus integration 的详情描述。
Qute 主要设计为 Quarkus 扩展。也可以将其用作“独立”库。但是,在这种情况下,某些功能不可用。一般来说,在 Quarkus Integration 部分中提到的任何功能都不可用。在 Qute Used as a Standalone Library 部分中查找有关限制和可能的更多信息。 |
- The Simplest Example
- Hello World Example
- Core Features
- Quarkus Integration
- Engine Customization
- Template Variants
- Injecting Beans Directly In Templates
- Type-safe Expressions
- Type-safe Templates
- Template Extension Methods
- @TemplateData
- Global Variables
- Native Executables
- [id="resteasy_integration"] REST Integration
- Development Mode
- Testing
- Type-safe Message Bundles
- Configuration Reference
- Qute Used as a Standalone Library
The Simplest Example
试用 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><h1>Header</h1></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 | 您可以在模板中使用任何 building block。在这种情况下,If Section 用于根据输入数据渲染消息的相应部分。 |
在 Quarkus 中,用于格式化消息的引擎与由 |
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
请阅读 |
Hello World Example
在此示例中,我们希望在使用 Qute 模板时演示 basic workflow。我们从一个简单的“hello world”示例开始。我们将始终需要一些 template contents:
<html>
<p>Hello {name}! 1
</html>
1 | `{name}`是一个在呈现模板时计算的值表达式。 |
然后,我们需要将内容解析到一个 template definition Java 对象中。模板定义是 io.quarkus.qute.Template
的一个实例。
如果使用独立的 Qute,“首先需要创建一个 io.quarkus.qute.Engine
实例。 Engine
代表一个用于模板管理的中心点配有专门的配置。我们来使用这个方便的构建器:
Engine engine = Engine.builder().addDefaults().build();
在 Quarkus 中,有一个预配置的 |
一旦我们有一个 Engine
实例,我们就可以解析模板内容:
Template hello = engine.parse(helloHtmlContent);
在 Quarkus 中,您可以直接注入模板定义。该模板会自动解析并缓存 - 请参阅 Quarkus Integration。 |
最后,创建一个 template instance、设置数据并渲染输出:
// Renders <html><p>Hello Jim!</p></html>
hello.data("name", "Jim").render(); 1 2
1 | Template.data(String, Object) 是一个方便的方法,它在一步骤中创建一个模板实例并将数据设置好。 |
2 | TemplateInstance.render() 触发同步渲染,即,在渲染完成之前当前线程被阻塞。但是,还有以异步方式触发渲染并使用结果的其他方法。例如,有一个返回 CompletionStage<String> 或返回 Mutiny’s Multi<String> 的 TemplateInstance.renderAsync() 方法。 |
因此,工作流十分简单:
-
创建模板内容(
hello.html
), -
解析模板定义(@15),
-
创建一个模板实例(
io.quarkus.qute.TemplateInstance
), -
Render the output.
|
Core Features
Basic Building Blocks
模板的动态部分包括注释、表达式、章节和未解析的字符数据。
- Comments
-
A comment starts with the sequence
{!
and ends with the sequence!}
, e.g.{! This is a comment !}
. Can be multiline and may contain expressions and sections:{! {#if true} !}
. The content of a comment is completely ignored when rendering the output. - Expressions
-
An expressions outputs an evaluated value. It consists of one or more parts. A part may represent simple properties:
{foo}
,{item.name}
, and virtual methods:{item.get(name)}
,{name ?: 'John'}
. An expression may also start with a namespace:{inject:colors}
. - Sections
-
A sections may contain static text, expressions and nested sections:
{#if foo.active}{foo.name}{/if}
. The name in the closing tag is optional:{#if active}ACTIVE!{/}
. A section can be empty:{#myTag image=true /}
. Some sections support optional end tags, i.e. if the end tag is missing then the section ends where the parent section ends. A section may also declare nested section blocks:{#if item.valid} Valid. {#else} Invalid. {/if}
and decide which block to render. - Unparsed Character Data
-
It is used to mark the content that should be rendered but not parsed. It starts with the sequence
{|
and ends with the sequence|}
:{| <script>if(true){alert('Qute is cute!')};</script> |}
, and could be multi-line.之前,未解析的字符数据可以用
{[
开头,用]}
结尾。由于此语法与其他语言的结构经常冲突,现已废除。
Identifiers and Tags
标识符用于表达式和章节标签。有效的标识符是非空白字符序列。但是,建议用户仅在表达式中使用有效的 Java 标识符。
如果您需要指定一个包含句点的标识符,可以使用方括号符号,例如 |
解析模板文档时,解析器会识别所有 tags。一个标签以花括号开始和结束,例如 {foo}
。标签的内容必须以以下内容之一开头:
-
a digit, or
-
an alphabet character, or
-
underscore, or
-
一个内置命令:
#
、!
、@
、/
。
如果它没有以任何上述内容开头,就会被解析器忽略。
<html>
<body>
{_foo.bar} 1
{! comment !}2
{ foo} 3
{{foo}} 4
{"foo":true} 5
</body>
</html>
1 | 解析:以下划线开头的表达式。 |
2 | Parsed: a comment |
3 | Ignored: starts with whitespace. |
4 | Ignored: starts with { . |
5 | Ignored: starts with " . |
也可以使用转义序列 |
Removing Standalone Lines From the Template
在默认情况下,解析器会从模板输出中移除独立行。A *standalone line*是至少包含一个部分标记(例如 {#each}`和 `{/each}
)、参数声明(例如 {@org.acme.Foo foo}
)或注释的代码行,但不包含表达式和非空白字符。换句话说,不包含部分标记或参数声明的代码行是 *not*独立行。同样,包含 _expression_或 _non-whitespace character_的代码行是 *not*独立行。
<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>
Expressions
评估表达式并输出值。它具有一个或多个部分,其中每个部分都表示属性访问器(又名字段访问表达式)或虚拟方法调用(又名方法调用表达式)。
访问属性时,可以使用点表示法或方括号表示法。在 `object.property`点表示法)语法中,`property`必须为 valid identifier。在 `object[property_name]`方括号表示法)语法中,`property_name`必须是非空 literal值。
表达式可以从可选项名称空间开始,后跟冒号 (:
)。有效名称空间由字母数字字符和下划线组成。名称空间表达式以不同的方式解析 - 另请参见 Resolution。
{name} 1
{item.name} 2
{item['name']} 3
{global:colors} 4
1 | 没有名称空间,一个部分:name |
2 | 没有名称空间,两个部分:item ,name |
3 | 等同于 {item.name} ,但使用方括号表示法 |
4 | 名称空间 global ,一个部分:colors |
表达式的部分可以是 virtual method,在这种情况下,名称后面可以紧跟圆括号中用逗号分隔的参数列表。虚拟方法的参数可以是嵌套表达式或 literal值。我们将这些方法称为 "virtual",因为它们不必由真实的 Java 方法支持。您可以在 following section中了解更多有关虚拟方法的信息。
{item.getLabels(1)} 1
{name or 'John'} 2
1 | 没有名称空间,两个部分 - item ,getLabels(1) ,第二部分是一个名为 `getLabels`且带有参数 `1`的虚拟方法 |
2 | 可以用于具有单个参数的虚拟方法的中缀表示法,翻译为 name.or('John') ;没有名称空间,两个部分 - name ,or('John') |
Supported Literals
Literal | Examples |
---|---|
boolean |
|
null |
|
string |
’value'`, |
integer |
|
long |
|
double |
|
float |
|
Resolution
表达式的第一部分始终针对 current context object解析。如果没有找到第一部分的结果,则针对父上下文对象(如果可用)解析。对于从名称空间开始的表达式,可以使用所有可用的 NamespaceResolver
找到当前上下文对象。对于不以名称空间开头的表达式,当前上下文对象是标记的 derived from the position。表达式的所有其他部分都使用针对前面解析结果的所有 ValueResolver
解析。
例如,表达式 {name}
没有名称空间和单个部分 - name
。“名称” 将使用所有可用的值解析器解析针对当前上下文对象。然而,表达式 {global:colors}
具有名称空间 global
和单个部分 - colors
。首先,所有可用的 NamespaceResolver
都将用来查找当前上下文对象。并且随后值解析器将用来针对已找到的上下文对象解析“颜色”。
传递给模板实例的数据始终可以使用
|
Current Context
如果一个表达式未指定名称空间,则 current context object 由标签的位置推导而来。默认情况下,当前上下文对象表示传递给模板实例的数据。然而,节可以改变当前上下文对象。一个典型的例子是 <<<`let`,let_section>> 节,可用来定义具有名称的局部变量:
{#let myParent=order.item.parent myPrice=order.price} 1
<h1>{myParent.name}</h1>
<p>Price: {myPrice}</p>
{/let}
1 | 节内的当前上下文对象是已解析的参数映射。 |
可以经由隐式绑定 |
Built-in Resolvers
Name | Description | Examples |
---|---|---|
Elvis Operator |
如果先前部分无法解析或解析为 |
|
orEmpty |
如果先前部分无法解析或解析为 |
如果 |
Ternary Operator |
if-then-else 语句的速记。与 If Section 不同,不支持嵌套运算符。 |
如果 |
Logical AND Operator |
如果两部分都不是 |
|
Logical OR Operator |
如果任一部分都不是 |
`{person.isActive |
在三元运算符中,如果该值不被视为 |
实际上,操作符实现为消耗一个参数的“虚拟方法”,并且可以使用中缀表示法。例如, |
Arrays
您可以使用 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 | 输出数组的前两个元素。 |
Character Escapes
如果设置了模板变体,对于 HTML 和 XML 模板而言,字符`, "
、<
、>
、`&`默认会转义。
在 Quarkus 中,将自动为位于 `src/main/resources/templates`中的模板设置一个变体。默认情况下,使用 `java.net.URLConnection#getFileNameMap()`来确定模板文件的类型。可以通过 `quarkus.qute.content-types`设置后缀内容类型的其他映射。 |
如果您需要呈现未转义的值:
-
使用实现为 `java.lang.Object`扩展方法的 `raw`或 `safe`属性,
-
或将 `String`值包装在 `io.quarkus.qute.RawString`中。
<html>
<h1>{title}</h1> 1
{paragraph.raw} 2
</html>
1 | 解析为 Expressions & Escapes`的 `title`将被呈现为 `Expressions & Escapes |
2 | 解析为 <p>My text!</p>`的 `paragraph`将被呈现为 `<p>My text!</p> |
默认情况下,具有以下内容类型之一的模板会转义: |
Virtual Methods
虚拟方法是一种类似于常规 Java 方法调用的 part of an expression。之所以称其为“虚拟”,是因为它不必与 Java 类的实际方法匹配。实际上,与普通属性一样,虚拟方法也由值解析器处理。唯一不同的是,对于虚拟方法,值解析器消耗的参数也是表达式。
<html>
<h1>{item.buildName(item.name,5)}</h1> 1
</html>
1 | buildName(item.name,5)`表示名称为 `buildName`且有两个参数 (`item.name`和 `5 ) 的虚拟方法。虚拟方法可由为以下 Java 类生成的值解析器评估:[source, java] |
class Item { String buildName(String name, int age) { return name + ":" + age; } }
虚拟方法通常由为 <<@TemplateExtension 方法,template_extension_methods>>, <<@TemplateData,template_data>> 或 parameter declarations中使用的类生成的值解析器评估。然而,还可以注册不被任何 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() 。 |
Evaluation of CompletionStage
and Uni
Objects
以特殊方式评估实现 java.util.concurrent.CompletionStage
和 io.smallrye.mutiny.Uni
的对象。如果表达式的部分解析为 CompletionStage
,则在该阶段完成后解析继续,并且如果有的,则根据已完成阶段的结果评估表达式的下一部分。例如,如果存在表达式 {foo.size}
并且 foo
解析为 CompletionStage<List<String>>
,则根据已完成的结果(即 List<String>
)来解析 size
。如果表达式的部分解析为 Uni
,则使用 Uni#subscribeAsCompletionStage()
从 Uni
创建 CompletionStage
,然后如上所述进行评估。
请注意,每个 Uni#subscribeAsCompletionStage()
都会导致一个新订阅。您可能需要配置 Uni
项的记忆化或在将其用作模板数据(即 myUni.memoize().indefinitely()
)之前配置故障。
可能会出现 CompletionStage
永远不完成或 Uni
不发出任何项目/故障的情况。在这种情况下,渲染方法(例如 TemplateInstance#render()
和 TemplateInstance#createUni()
)在特定超时之后会失败。超时可以指定为模板实例 timeout
属性。如果未设置 timeout
属性,则使用全局渲染超时。
在 Quarkus 中,默认超时可以通过 |
在以前的版本中,只有 |
How to Identify a Problematic Part of the Template
当发生超时时,很难找到模板中出现问题的部分。您可以为日志记录器 io.quarkus.qute.nodeResolve
设置 TRACE
级别,然后尝试分析日志输出。
application.properties
Examplequarkus.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
日志消息,那么您有一个值得探索的候选消息。
Missing Properties
可能会出现表达式在运行时可能未评估的情况。例如,如果存在表达式 {person.age}
并且未在 Person
类上声明属性 age
。行为不同,具体取决于是否启用了 Strict Rendering。
如果启用,则缺少的属性始终会导致 TemplateException
,并且渲染中止。您可以使用 default values 和 safe expressions 来抑制错误。
如果禁用,则默认情况下特殊常量 NOT_FOUND
写入输出。
在 Quarkus 中,可以通过 |
如果使用 Type-safe Expressions 和 Type-safe Templates,则在构建时会检测到类似错误。 |
Sections
部分具有一个以 #
开头的开始标记,后跟部分的名称,例如 {#if}
和 {#each}
。它可能为空,即开始标记以 /
结尾: {#myEmptySection /}
。部分通常包含嵌套的表达式和其它部分。结束标记以 /
开头,并包含部分的名称(可选): {#if foo}Foo!{/if}
或 {#if foo}Foo!{/}
。一些部分支持可选结束标记,也就是说,如果缺少结束标记,则部分在父部分结束的位置结束。
#let
Optional End Tag Example{#if item.isActive}
{#let price = item.price} 1
{price}
// synthetic {/let} added here automatically
{/if}
// {price} cannot be used here!
1 | 定义可在父 {#if} 部分中使用的局部变量。 |
Built-in section | Supports Optional End Tag |
---|---|
|
❌ |
|
❌ |
|
❌ |
|
✅ |
|
❌ |
|
✅ |
User-defined Tags |
❌ |
|
❌ |
|
❌ |
Parameters
开始标记可以用可选的名称定义参数,例如 {#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
。部分可以以任何方式解释参数值,例如按原样获取该值。但是,在大多数情况下,参数值注册为 expression,并在使用之前进行评估。
一个部分可能包含多个内容 blocks。“main” 块总是存在的。附加/嵌套块也以 #
开始,也可以有参数 - {#else if item.isActive}
。定义部分逻辑的部分助手可以“执行”任何块并评估参数。
#if
Section Example{#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 | 这是 main 块。 |
2 | Additional block. |
3 | Additional block. |
Loop Section
循环部分使得可能遍历 Iterable
、Iterator
、数组、Map
(元素是 Map.Entry
)、Stream
、Integer
、Long
、int`和 `long
(基本值)。null
参数值产生无操作。
此部分有两种类型。第一种是使用名称 each
,并且 it
是迭代元素的隐式别名。
{#each items}
{it.name} 1
{/each}
1 | name 与当前迭代元素匹配。 |
另一种形式是使用名称 for
,并且指定用于引用迭代元素的别名:
{#for item in items} 1
{item.name}
{/for}
1 | item 是用于迭代元素的别名。 |
还可以通过以下键在循环内访问迭代元数据:
-
count
- 1-based index -
index
- zero-based index -
hasNext
-true
如果迭代还有更多元素 -
isLast
-true
如果hasNext == false
-
isFirst
-true
如果count == 1
-
odd
-true
如果元素计数为奇数 -
even
-true
如果元素计数为偶数 -
indexParity
- 根据计数值输出odd
或even
但是,这些键不能直接使用。相反,使用前缀来避免与外部范围中的变量发生冲突。默认情况下,以一个下划线作为后缀的迭代元素别名用作前缀。例如,hasNext
键必须在 {#each}
部分的 it_
前加上前缀:{it_hasNext}
。
each
Iteration Metadata Example{#each items}
{it_count}. {it.name} 1
{#if it_hasNext}<br>{/if} 2
{/each}
1 | it_count represents one-based index. |
2 | <br> 仅在迭代还有更多元素时才渲染。 |
并且必须在具有 item
元素别名的 {#for}
部分中采用 {item_hasNext}
的形式使用。
for
Iteration Metadata Example{#for item in items}
{item_count}. {item.name} 1
{#if item_hasNext}<br>{/if} 2
{/for}
1 | item_count represents one-based index. |
2 | <br> 仅在迭代还有更多元素时才渲染。 |
迭代元数据前缀可以配置,通过
|
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}
If Section
if
节表示基本的控制流节。最简单的可行版本接受单个参数并在条件判断为 true
时渲染内容。没有运算符的条件会在值不被认为是 falsy
时求值为 true
,即当值不是 null
、false
、空集合、空映射、空数组、空字符串/char 序列或等于零的数字时。
{#if item.active}
This item is active.
{/if}
还能在条件中使用以下运算符:
Operator | Aliases | Precedence (higher wins) |
---|---|---|
logical complement |
|
4 |
greater than |
|
3 |
大于或等于 |
|
3 |
less than |
|
3 |
小于或等于 |
|
3 |
equals |
|
2 |
not equals |
|
2 |
logical AND (short-circuiting) |
|
1 |
logical OR (short-circuiting) |
` |
{#if item.age > 10}
This item is very old.
{/if}
还支持多个条件。
{#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}
When Section
此节类似于 Java 的 switch
或 Kotlin 的 when
构造。它顺序地将 tested value 与所有块进行匹配,直至某个条件达成。执行首个匹配的块。所有其他块都会被忽略(此行为与 Java switch
不同,其中需要 break
语句)。
when
/is
name aliases{#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 | 可以使用 an operator 指定匹配逻辑。与 If Section 中不同,不支持嵌套运算符。 |
3 | 如果没有任何其他块与该值匹配,则会执行 else 块。 |
switch
/case
name aliases{#switch person.name}
{#case 'John'} 1
Hey John!
{#case 'Mary'}
Hey Mary!
{/switch}
1 | case 是 is 的别名。 |
将经过测试的值解析为枚举时,它会得到特殊的处理。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
块条件中,支持以下运算符:
Operator | Aliases | Example |
---|---|---|
not equal |
|
|
greater than |
|
|
大于或等于 |
|
|
less than |
|
|
小于或等于 |
|
|
in |
|
|
not in |
|
|
Let Section
此部分允许您定义命名的局部变量:
{#let myParent=order.item.parent isActive=false age=10 price=(order.price + 10)} 12
<h1>{myParent.name}</h1>
Is active: {isActive}
Age: {age}
{/let} 3
1 | 用一个可以表示 literal 的表达式初始化局部变量,例如 isActive=false 和 age=10 。 |
2 | 仅当使用括号进行分组时才支持中缀表示法,例如 price=(order.price + 10) 等价于 price=order.price.plus(10) 。 |
3 | 请记住,该变量不可在定义它的 let 部分之外使用。 |
如果部分参数的键(例如局部变量名称)以 ?
结尾,则只有当没有 ?
后缀的键解析为 null
或 "not found" 时才设置局部变量:
{#let enabled?=true} 1 2
{#if enabled}ON{/if}
{/let}
1 | true 实际上是一个 default value,只有在父作用域中还没有定义 enabled 时才使用此 default value。 |
2 | enabled?=true 是 enabled=enabled.or(true) 的简写版。 |
此部分标记还可以在 set
别名下注册:
{#set myParent=item.parent price=item.price}
<h1>{myParent.name}</h1>
<p>Price: {price}
{/set}
With Section
此部分可用于设置当前内容对象。这对于简化模版结构非常有用:
{#with item.parent}
<h1>{name}</h1> 1
<p>{description}</p> 2
{/with}
1 | name 将根据 item.parent 解析。 |
2 | description 也将根据 item.parent 解析。 |
请注意,with
部分不能用于定义 Type-safe Expressions 的 Type-safe Templates 或模版。原因在于,这阻止了 Qute 验证嵌套表达式。如果可能,请使用声明显式绑定的 {#let}
部分替换 with
部分:
{#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 | this 是 item.callExpensiveLogicToGetTheValue(1,'foo',bazinga) 的结果。该方法只被调用一次,即使结果可能在多个表达式中使用也是如此。 |
Include Section
此部分可用于包括另一个模板,并可能替代模板的一些部分(参见下面的 template inheritance)。
<html>
<head>
<meta charset="UTF-8">
<title>Simple Include</title>
</head>
<body>
{#include foo limit=10 /} 12
</body>
</html>
1 | 包括具有 id foo 的模板。包含的模板可以引用当前上下文的 data。 |
2 | 还可以定义可用于包含模板的可选参数。 |
Template inheritance 便于重用模板布局。
<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} 部分的主块。 |
{#include base} 1
{#title}My Title{/title} 2
<div> 3
My body.
</div>
{/include}
1 | include 部分用于指定扩展模板。 |
2 | 嵌套块用于指定应覆盖的部分。 |
3 | 主块的内容用于未指定名称参数的 {#insert} 部分。 |
部分块还可以定义一个可选的结束标记 - |
User-defined Tags
用户定义的标记可用于包含 tag template,也可以选择传递一些参数并可能覆盖模板的一些部分。让我们假设我们有一个名为 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_的方式执行该标签,即无法访问调用标签的模板的上下文。但是,有时更改默认行为并禁用隔离可能会很有用。在这种情况下,只需在调用位置添加 _isolated=false`或 `_unisolated`参数,例如 `{#itemDetail item showImage=true _isolated=false /}`或 `{#itemDetail item showImage=true _unisolated /}
。
Arguments
命名的参数可以直接在标签模板中访问。但是,第一个参数不需要定义一个名称,并且它可以使用 it`别名进行访问。此外,如果一个参数没有定义名称且其值为一个单独的标识符,例如 `foo
,则该名称会默认设为该值标识符,例如: {#myTag foo /}`变为 `{#myTag foo=foo /}
。换而言之,参数值 `foo`已解决,并且可以使用标签模板中的 `{foo}`进行访问。
如果一个参数没有名称并且其值为一个单独的单词字符串文字,例如 |
可以使用 `_args`别名在标签中访问 `io.quarkus.qute.UserTagSectionHelper.Arguments`元数据。
-
_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 /}`来调用下面定义的 user 标签。
tags/test.html
{it} 1
{readonly} 2
{_args.filter('readonly').asHtmlAttributes} 3
1 | `it`将替换为该标签的第一个未命名参数。 |
2 | `readonly`是一个命名参数。 |
3 | _args represents arguments metadata. |
结果如下:
Martin
true
readonly="true"
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 定义片段。请注意,标识符中只能使用字母数字和下划线。 |
您可以通过 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 中,还可以定义 type-safe fragment。 |
您还可以使用 {#include}
节将片段包括在另一个模板中或定义该片段的模板中。
user.html
<h1>User - {user.name}</h1>
<p>This document contains a detailed info about a user.</p>
{#include item$item_aliases aliases=user.aliases /} 12
1 | 包含美元符号 $ 的模板标识符表示片段。将 item$item_aliases 值翻译为:Use the fragment item_aliases from the template item . |
2 | aliases 参数用于传递相关数据。我们需要确保正确设置数据。在本例中,片段将使用表达式 user.aliases 作为 aliases 的值,位于 {#for alias in aliases} 节中。 |
如果要从同一模板引用片段,请跳过 |
您可以指定 |
Hidden Fragments
默认情况下,片段通常作为原始模板的一部分进行渲染。然而,有时将片段标记为 rendered=false
的 hidden 可能会很有用。一个有趣的用例是可以在定义它的模板中多次使用的片段。
item.html
{#fragment id=strong rendered=false} 1
<strong>{val}</strong>
{/fragment}
<h1>My page</h1>
<p>This document
{#include $strong val='contains' /} 2
a lot of
{#include $strong val='information' /} 3
!</p>
1 | 使用标识符 strong 定义隐藏片段。在本例中,我们使用 false 布尔值文字作为 rendered 参数的值。然而,也可以在那里使用任何表达式。 |
2 | 包含片段 strong`并传递值。请注意语法 `$strong ,它翻译自当前模板中的片段 strong 。 |
3 | 包含片段 `strong`并传递值。 |
以上代码片段呈现类似如下内容:
<h1>My page</h1>
<p>This document
<strong>contains</strong>
a lot of
<strong>information</strong>
!</p>
Eval Section
此部分可用于动态解析和评估模板。此行为与 Include Section非常相似,但:
-
模板内容直接传递,即不通过 `io.quarkus.qute.TemplateLocator`获取,
-
无法覆盖已评估模板的部分。
{#eval myData.template name='Mia' /} 123
1 | myData.template 的结果将用作模板。使用 Current Context 执行模板,即可以引用其包含到的模板中的数据。 |
2 | 还可以定义在已评估模板中可以使用的可选参数。 |
3 | 部分的内容总是会忽略。 |
每次执行部分时,都会解析并评估已评估的模板。换言之,无法缓存已解析的值以节省资源并优化性能。 |
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();
如果 |
然后,{#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 中,缓存条目的键是一个 |
Rendering Output
TemplateInstance
提供了几种触发渲染并消耗结果的方法。最简单的途径由 TemplateInstance.render()
表示。此方法触发同步渲染,即在渲染完成之前当前线程都会被阻塞,并且会返回输出。相反,TemplateInstance.renderAsync()
返回一个 CompletionStage<String>
,在渲染完成时,它就会完成。
TemplateInstance.renderAsync()
Exampletemplate.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()
Exampletemplate.data(foo).createUni().subscribe().with(System.out::println);
TemplateInstance.createMulti()
返回一个新的 Multi<String>
对象。每个项目表示呈现模板的一部分/块。同样,createMulti()
不会触发渲染。相反,每次订阅者触发计算时,模板将再次呈现。
TemplateInstance.createMulti()
Exampletemplate.data(foo).createMulti().subscribe().with(buffer:append,buffer::flush);
模板呈现分为两个阶段。在第一阶段(异步阶段),模板中的所有表达式都得到解析并且 result tree 已建立。在第二阶段(同步阶段),结果树是 materialized,即,结果节点逐个发出被特定消耗者消耗/缓冲的块。 |
Engine Configuration
Value Resolvers
值解析器在估算表达式时使用。可以通过 EngineBuilder.addValueResolver()
以编程方式注册一个自定义的 io.quarkus.qute.ValueResolver
。
ValueResolver
Builder ExampleengineBuilder.addValueResolver(ValueResolver.builder()
.appliesTo(ctx -> ctx.getBase() instanceof Long && ctx.getName().equals("tenTimes"))
.resolveSync(ctx -> (Long) ctx.getBase() * 10)
.build());
Template Locator
可以通过模板定位器手动或自动注册一个模板。只要调用 Engine.getTemplate()
方法并且引擎没有存储在缓存中的给定 id 的模板时,就会使用这些定位器。定位器在读取模板内容时负责使用正确的字符编码。
在 Quarkus 中,来自 |
Content Filters
内容过滤器可用于在解析前修改模板内容。
engineBuilder.addParserHook(new ParserHook() {
@Override
public void beforeParsing(ParserHelper parserHelper) {
parserHelper.addContentFilter(contents -> contents.replace("${", "$\\{")); 1
}
});
1 | 转义 ${ 的所有出现。 |
Strict Rendering
严格渲染模式使开发人员能够捕获由错字和无效表达式引起的隐秘错误。如果启用,则任何无法解析(即被估算为 io.quarkus.qute.Results.NotFound
的实例)的表达式始终会导致 TemplateException
,并且渲染将被中止。NotFound
值被视为错误,因为它基本上意味着没有值解析器能够正确解析表达式。
不过, |
严格渲染模式默认启用。然而,您可以通过 io.quarkus.qute.EngineBuilder.strictRendering(boolean)
禁用此功能。
相反,在 Quarkus 中可以使用专用配置属性: |
如果您确实需要使用可能导致“找不到”错误的表达式,则可以使用 default values 和 safe expressions 来压制错误。如果表达式的前面部分无法解析或解析为 null
,则会使用默认值。可以使用埃尔维斯运算符来输出默认值:{foo.bar ?: 'baz'}
,它实际等同于以下虚拟方法:{foo.bar.or('baz')}
。安全表达式的结尾带有 ??
后缀,并且如果表达式无法解析,则产生 null
。它可能非常有用,例如在 {#if}
部分:{#if valueNotFound??}Only rendered if valueNotFound is truthy!{/if}
。事实上,??
只是 .or(null)
的简写形式,即 {#if valueNotFound??}
将变成 {#if valueNotFound.or(null)}
。
Quarkus Integration
如果您要在 Quarkus 应用程序中使用 Qute,请将以下依赖项添加到您的项目中:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute</artifactId>
</dependency>
在 Quarkus 中,提供了预先配置好的引擎实例,并且能够注入 - @ApplicationScoped
范围的 bean、io.quarkus.qute.Engine
类型 bean 和 @Default
限定符都已自动注册。此外,位于 src/main/resources/templates
目录中的所有模板都经过验证,可以轻松注入。
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 实例。 |
Engine Customization
可以通过运行时的 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 豆。 |
2 | CDI 豆实例用在运行时——这意味着该工厂可以定义注入点。 |
3 | 验证 foo 参数始终存在;例如,{#custom foo='bar' /} 可以,但 {#custom /} 会导致构建失败。 |
4 | 在渲染期间使用注入的 Service 。 |
|
Template Locator Registration
注册 template locators 的最简单方法是使它们成为 CDI 豆。由于在模板验证完成后构建时间内不存在自定义定位器,您需要通过 @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 注解的模板定位器注册为单例会话豆。 |
Template Variants
根据内容协商渲染特定模板的变体有时很有用。这可以通过设置一个通过 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();
}
}
使用 |
Injecting Beans Directly In Templates
加有 @Named
注解的 CDI 豆可以通过 cdi
和/或 inject
命名空间引用到任何模板中:
{cdi:personService.findPerson(10).name} 1
{inject:foo.price} 2
1 | 首先,找到名称为 personService 的一个 bean,然后用作基础对象。 |
2 | 首先,找到名称为 foo 的一个 bean,然后用作基础对象。 |
|
在构建期间验证具有 cdi
和 inject
命名空间的所有表达式。
对于表达式 cdi:personService.findPerson(10).name
,注入 bean 的实现类必须声明 findPerson
方法或必须存在匹配的 template extension method。
对于表达式 inject:foo.price
,注入 bean 的实现类必须具有 price
属性(例如 getPrice()
方法)或必须存在匹配的 template extension method。
对所有标注有 |
如果你的应用程序提供 HTTP requests,还可以通过 |
Type-safe Expressions
模板表达式可以选择类型安全。这意味着某个表达式将针对现有 Java 类型和模板扩展方法进行验证。如果找到无效/不正确的表达式,则构建失败。
例如,如果有一个表达式 item.name
,其中 item
映射到 org.acme.Item
,则 Item
必须具有属性 name
或必须存在匹配的模板扩展方法。
可选 parameter declaration 用于将 Java 类型绑定到其第一部分与参数名称匹配的表达式。参数声明直接在模板中指定。
Java 类型应始终用 fully qualified name 标识,除非它是来自 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 解析到的对象的 Java 类型必须具有属性 toLowerCase 或必须存在匹配的模板扩展方法。 |
将自动为参数声明中使用的所有类型生成一个值解析器,以便可以无需反射地访问它的属性。
type-safe templates 的方法参数会自动变成参数声明。 |
请注意,区段可以覆盖那些原本将匹配参数声明的名称:
{@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 | Validated against org.acme.Foo . |
2 | 未验证 - foo 已在循环区段中覆盖。 |
参数声明可以在键之后指定 default value。键和默认值用等号分隔: {@int age=10}
。如果参数键解析为 null
或未找到,模板中将使用默认值。
例如,如果有一个参数声明 {@String foo="Ping"}
,并且未找到 foo
,则可以使用 {foo}
,并且输出将是 Ping
。另一方面,如果设置了该值(例如通过 TemplateInstance.data("foo", "Pong")
),则 {foo}
的输出将是 Pong
。
默认值类型必须可以分配给参数声明类型。例如,请参见导致构建失败的不正确参数声明: {@org.acme.Foo foo=1}
。
默认值实际上是一个 expression。因此,默认值不一定要是文字(例如 |
默认值类型不在 Qute standalone 中验证。
{@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 | A primitive type. |
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 。 |
Type-safe Templates
可以在 Java 代码中定义类型安全模板。类型安全模板的参数会自动转换为用于绑定 Type-safe Expressions 的 parameter declarations。然后在构建时验证类型安全表达式。
有两种方法可以定义类型安全模板:
-
使用
@io.quarkus.qute.CheckedTemplate
注释类,其所有static native
方法都将用于定义类型安全模板及其所需的那些参数。 -
使用实现
io.quarkus.qute.TemplateInstance
的 Java 记录;记录组件表示模板参数,可以将@io.quarkus.qute.CheckedTemplate
用于配置模板(这是可选操作)。
Nested Type-safe Templates
如果使用 templates in Jakarta REST resources,可以依赖以下约定:
-
按资源类别对
/src/main/resources/templates
目录中的模板文件进行组织,方法是将文件组合到每种资源类别一个目录中。因此,如果ItemResource
类引用了两个模板hello
和goodbye
,请把它们放置到/src/main/resources/templates/ItemResource/hello.txt
和/src/main/resources/templates/ItemResource/goodbye.txt
。按资源类别对模板进行分组,有助于更容易地导航到它们。 -
在每个资源类中,在你的资源类内声明一个
@CheckedTemplate static class Template {}
类。 -
为资源的每个模板文件声明一个
public static native TemplateInstance method();
。 -
使用这些静态方法来构建你的模板实例。
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 参数自动转换为 parameter declaration,因此引用此名称的所有表达式都将经过验证。 |
3 | 使模板中可以访问 Item 对象。 |
默认情况下,使用 |
Top-level Type-safe Templates
还可以声明使用 @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 参数自动转换为 parameter declaration,这样所有引用此名称的表达式都将经过验证。 |
然后为每个模板文件声明一个 public static native TemplateInstance method();
。使用这些静态方法构建模板实例:
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);
}
}
Template Records
实现 io.quarkus.qute.TemplateInstance
的 Java 记录表示类型安全模板。记录组件表示模板参数,可以将 @io.quarkus.qute.CheckedTemplate
用于配置模板(这是可选操作)。
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 记录声明类型安全模板。 |
2 | 实例化记录并将其用作普通的 TemplateInstance 。 |
Customized Template Path
@CheckedTemplate
方法的模板路径包含 base path 和 defaulted name。base path 由 @CheckedTemplate#basePath()
提供。默认情况下,使用嵌套静态类的声明类的简单名称或者使用顶级类的空字符串。defaulted name 根据 @CheckedTemplate#defaultName()
中指定策略派生。默认情况下,使用 @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 。 |
Type-safe Fragments
你也可以在 Java 代码中定义类型安全 fragment。带美元符号 $
的方法名称 _native static_表示一个表示类型安全模板片段的方法。片段名称衍生自带注释方法名称。在美元符号 $`最后一次出现之前的内容是相关类型安全模板的方法名称。美元符号最后一次出现之后的部分是片段标识符。构建默认名称时会遵从由相关 `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
}
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();
}
}
你可以指定 |
Template Extension Methods
可以使用扩展方法来使用新功能(扩展可访问属性和方法的集合)扩展数据类,或为特定 namespace 解析表达式。例如,可以添加 computed properties 和 virtual methods。
为注释有 @TemplateExtension
的方法自动生成了一个值解析器。如果类注释有 @TemplateExtension
,则为该类上声明的每个 non-private static method 生成了一个值解析器。方法级注释会覆盖在类上定义的行为。不满足以下条件的方法将被忽略。
模板扩展方法:
-
must not be
private
-
must be static,
-
must not return
void
.
如果没有定义名称空间,则未注释有 @TemplateAttribute
的第一个参数的类用来匹配基础对象。否则,名称空间用来匹配表达式。
Matching by Name
默认情况下,方法名称用于匹配属性名称。
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()
Example@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
Example@TemplateExtension(matchName = "*")
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()
Example@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()
Example@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"
}
冗余匹配条件会忽略。按优先级从高到低排列条件为: |
Method Parameters
扩展方法也可以声明参数。如果未指定命名空间,则使用第一个未标注有 @TemplateAttribute
的参数来传递基础对象,即第一个示例中的 org.acme.Item
。如果匹配任何名称或使用正则表达式,则需要使用字符串方法参数来传递属性名称。标注有 @TemplateAttribute
的参数通过 TemplateInstance#getAttribute()
获取。渲染模板时会解析所有其他参数,并将它们传递给扩展方法。
@TemplateExtension
class BigDecimalExtensions {
static BigDecimal scale(BigDecimal val, int scale, RoundingMode mode) { 1
return val.setScale(scale, mode);
}
}
1 | 此方法将表达式与类型为 BigDecimal.class 的基础对象、scale 虚拟方法名称和两个虚拟方法参数进行匹配。 |
{item.discountedPrice.scale(2,mode)} 1
1 | item.discountedPrice 将解析为 BigDecimal 的一个实例。 |
Namespace Extension Methods
如果指定了 TemplateExtension#namespace()
,那么将使用扩展方法解析具有给定 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 | The output is olleh |
Built-in Template Extensions
Quarkus 提供了一组内置扩展方法。
Maps
-
keys
或keySet
:返回某个地图中包含的键的 Set 视图-
{#for key in map.keySet}
-
-
values
:返回某个地图中包含的值的 Collection 视图-
{#for value in map.values}
-
-
size
:返回某个地图中键值映射的数量-
{map.size}
-
-
isEmpty
:如果某个地图不包含任何键值映射,则返回 true-
{#if map.isEmpty}
-
-
get(key)
:返回指定键映射到的值-
{map.get('foo')}
-
还可以直接访问某个地图的值: |
Lists
-
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}
-
列表元素可以通过索引直接访问: |
Integer Numbers
-
mod
: Modulo operation-
{#if counter.mod(5) == 0}
-
-
plus
or+
: Addition-
{counter + 1}
-
{age plus 10}
-
{age.plus(10)}
-
-
minus
or-
: Subtraction-
{counter - 1}
-
{age minus 10}
-
{age.minus(10)}
-
Strings
-
fmt`或`format
:通过 `java.lang.String.format()`来格式化字符串实例。-
{myStr.fmt("arg1","arg2")}
-
{myStr.format(locale,arg1)}
-
-
str:fmt`或`str:format
:通过 `java.lang.String.format()`来格式化提供的字符串值。-
{str:format("Hello %s!",name)}
-
{str:fmt(locale,'%tA',now)}
-
-
+
: Concatenation-
{item.name + '_' + mySuffix}
-
{name + 10}
-
Config
-
config:<name>`或`config:[<name>]
:返回给定属性名称的配置值。-
{config:foo}
or{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') ?: 'Not Found'}
-
{config:boolean(foo.getPropertyName()) ?: 'property is false'}
-
-
config:integer(name)
:将给定属性名称的配置值作为整数返回;该名称可以由表达式动态获取。-
{config:integer('quarkus.foo')}
-
{config:integer(foo.getPropertyName())}
-
Time
-
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.Date
、`java.util.Calendar`和 `java.lang.Number`的时间对象。-
{time:format(myDate,'d MMM uuuu')}
-
-
time:format(dateTime,pattern,locale)
:格式化来自java.time`包、`java.util.Date
、`java.util.Calendar`和 `java.lang.Number`时间对象。-
{time:format(myDate,'d MMM uuuu', myLocale)}
-
-
time:format(dateTime,pattern,locale,timeZone)
:格式化来自java.time`包、`java.util.Date
、`java.util.Calendar`和 `java.lang.Number`的时间对象。-
{time:format(myDate,'d MMM uuuu',myLocale,myTimeZoneId)}
-
@TemplateData
为使用 `@TemplateData`注解的类型自动生成一个值解析器。这允许 Quarkus 在运行时避免使用反射来访问数据。
始终忽略非公共成员、构造函数、静态初始值设定项、静态函数、合成函数和无效函数。 |
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 | 生成的 value 解析器知道如何调用 BigDecimal.setScale() 方法。 |
Accessing Static Fields and Methods
如果将 @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_Status 的名称解析器。 |
{#if machine.status == model_Status:ON}
The machine is ON!
{/if}
Convenient Annotation For Enums
还有一个方便的注释用于访问枚举常量:@io.quarkus.qute.TemplateEnum
。此注释在功能上等效于 @TemplateData(namespace = TemplateData.SIMPLENAME)
,即,自动为目标枚举生成一个命名空间解析器,并且目标枚举的简单名称用作命名空间。
@TemplateEnum
package model;
@TemplateEnum 1
public enum Status {
ON,
OFF
}
1 | 自动生成一个带有命名空间 Status 的名称解析器。 |
在非枚举类上声明的 |
{#if machine.status == Status:ON}
The machine is ON!
{/if}
Quarkus 会检测可能的命名空间冲突,如果一个特定的命名空间由多个 |
Global Variables
io.quarkus.qute.TemplateGlobal
注释可用于表示静态字段和方法,它们提供可在任何模板中访问的 global variables。
全局变量是:
-
在初始化期间添加为任何
TemplateInstance
的 computed data, -
可使用
global:
命名空间访问。
使用 |
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 resolves to Globals#user() . |
2 | 使用 global: 命名空间;age 解析为 Globals#age 。 |
3 | myColors resolves to Globals#myColors() . |
请注意,全局变量隐式地将 parameter declarations 添加到所有模板中,因此引用全局变量的任何表达式都会在构建期间得到验证。 |
User: Mia
Age: 40
Colors: RED, BLUE
Resolving Conflicts
如果未通过 global:
命名空间访问,全局变量可能会与常规数据对象冲突。Type-safe templates 会自动覆盖全局变量。例如,以下定义覆盖了 Globals#user()
方法提供的全局变量:
import org.acme.User;
@CheckedTemplate
public class Templates {
static native TemplateInstance hello(User currentUser); 1
}
1 | `currentUser`与由 `Globals#user()`提供的全局变量发生冲突。 |
因此,相应的模板不会导致验证错误,即使 Globals#user()`方法返回不含 `name`属性的 `java.lang.String
:
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()`方法提供的全局变量所提供的声明。 |
Native Executables
在 JVM 模式中,基于反射的值解析器可以用于访问模型类属性和调用方法。但是,这对于 a native executable来说并不好用。因此,即使 Foo`类声明了一个相关 getter 方法,您也可能会遇到这样的模板异常: `Property "name" not found on the base object "org.acme.Foo" in expression {foo.name} in template hello.html
。
可以有以下几种方法来解决此问题:
-
使用 type-safe templates或 type-safe expressions
-
在这种情况下,会自动生成并使用经过优化的值解析器在运行时
-
这是推荐的解决方案
-
-
使用 <<`@TemplateData`,template_data>> 注解模型类,来生成并使用经过专门构建的值解析器,在运行时
-
使用 `@io.quarkus.runtime.annotations.RegisterForReflection`注解模型类,为基于反射的值解析器创造条件。在 native application tips页面上可以找到更多关于 `@RegisterForReflection`注解的详细信息。
[id="resteasy_integration"] REST Integration
如果您想在 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 可能就像以下内容一样简单:
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 实现自动完成。 |
鼓励用户使用 Type-safe templates,这有助于安排特定 Jakarta REST 资源的模板,并自动启用 type-safe expressions。 |
内容协商自动执行。结果输出取决于从客户端收到的 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
util 类从 Jakarta REST 资源方法的主体获取模板实例:
@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
获取的模板未通过验证,即如果模板不存在,则构建不会失败。
Development Mode
在开发模式下,会监视 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
。将重新加载匹配的模板,仅执行运行时验证。
Testing
在测试模式下,已注入和类型安全的模板的渲染结果被记录在受管的 io.quarkus.qute.RenderedResults
中,该 io.quarkus.qute.RenderedResults
已注册为 CDI bean。您可以在测试或任何其他 CDI bean 中注入 RenderedResults
,并断言结果。但是,可以将 quarkus.qute.test-mode.record-rendered-results
配置属性设置为 false
以禁用此功能。
Type-safe Message Bundles
Basic Concepts
基本思想是,每条消息都可能是一个非常简单的模板。为了防止类型错误,消息被定义为 message bundle interface 的注释方法。Quarkus 在构建时生成 message bundle implementation。
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 | 方法参数可以在模板中使用。 |
消息包可以在运行时使用:
-
通过
io.quarkus.qute.i18n.MessageBundles#get()
直接在代码中使用;例如,MessageBundles.get(AppMessages.class).hello_name("Lucie")
-
通过
@Inject
注入 bean 中;例如,@Inject AppMessages
-
通过消息包名称空间在模板中引用:[source, html]
{msg:hello_name('Lucie')} 1 2 3 {msg:message(myKey,'Lu')} 4
1 | `msg`是默认名称空间。 |
2 | `hello_name`是消息键。 |
3 | `Lucie`是消息包接口方法的参数。 |
4 | 还可以通过使用保留的键 `message`来获取在运行时解析的键的本地化消息。不过在这种情况下将跳过验证。 |
Default Bundle Name
包名称默认为,除非使用 `@MessageBundle#value()`指定。对于顶级类,默认使用 `msg`值。对于嵌套类,名称由层次结构中所有封闭类的简单名称(首先是顶级类)组成,然后是消息包接口的简单名称。名称之间用下划线分隔。
例如,以下消息包的名称将默认为 Controller_index
:
class Controller {
@MessageBundle
interface index {
@Message("Hello {name}!")
String hello(String name); 1
}
}
1 | 此消息可以通过 `{Controller_index:hello(name)}`在模板中使用。 |
包名称也用作本地化文件名的一部分,例如 |
Bundle Name and Message Keys
消息键直接用于模板中。包名称用作模板表达式中的名称空间。可以使用 @MessageBundle`来定义用于从方法名称生成消息键的默认策略。然而,
@Message`可以覆盖此策略,甚至定义自定义键。默认情况下,注释元素的名称按原样使用。还有以下其他可能性:
-
取消驼峰命名法并使用连字符;例如,
helloName()
→hello-name
-
取消驼峰命名法,用下划线分隔各个部分;例如,
helloName()
→hello_name
。
Validation
-
所有消息包模板都会被验证:
-
所有不带名称空间的表达式都必须映射到一个参数;例如,
Hello {foo}
→ 该方法必须有一个名为 `foo`的参数 -
所有表达式都针对参数的类型进行验证;例如,
Hello {foo.bar}
,其中参数foo`的类型为 `org.acme.Foo
→ `org.acme.Foo`必须有一个名为 `bar`的属性
-
针对每个 _unused_参数记录一条警告消息。
-
像 `{msg:hello(item.name)}`这样的引用消息包方法的表达式也会被验证。
Localization
默认情况下,通过 `quarkus.default-locale`配置属性指定的默认语言环境用于 `@MessageBundle`接口。然而,可以 `io.quarkus.qute.i18n.MessageBundle#locale()`来指定一个自定义语言环境。此外,有两种方法来定义本地化包:
-
创建扩展了带有 `@Localized`注释的默认接口的接口
-
在应用程序存档的
src/main/resources/messages`目录中创建一个 UTF-8 编码的文件;例如,`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.properties`和 `msg_en.properties
,则会抛出异常并导致构建失败。文件格式非常简单:每一行都代表一对键值,等号用作分隔符,或一个注释(行以 `#`开头)。空行会被忽略。键是对应 Message 包接口中的 _mapped to method names。值通常由 `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`文件的语法高亮显示。但实际上,后缀可以是任何内容——它将被忽略。 |
一个示例属性文件会为每个消息包接口自动生成到目标目录中。例如,如果未为 |
hello=Hello \
{name} and \
good morning!
请注意,行终止符用反斜杠字符 \`转义,并且忽略了下一行开始处的空格。即 `{msg:hello('Edgar')}`将呈现为 `Hello Edgar and good morning!
。
一旦我们定义了本地化包,我们需要一种 _select_特定模板实例的正确包的方法,即为模板中所有消息包表达式指定区域设置。默认情况下,使用通过 `quarkus.default-locale`配置属性指定区域设置来选择包。或者,你可以指定模板实例的 `locale`属性。
locale
Attribute Example@Singleton
public class MyBean {
@Inject
Template hello;
String render() {
return hello.instance().setLocale("cs").render(); 1
}
}
1 | 你可以设置 `Locale`实例或区域设置标记字符串 (IETF)。 |
在使用 <<`quarkus-rest-qute`,rest_integration>>(或 |
`@Localized`限定符可用于注入本地化消息包接口。
@Singleton
public class MyBean {
@Localized("cs") 1
AppMessages msg;
String render() {
return msg.hello_name("Jachym");
}
}
1 | 注释值是区域设置标记字符串 (IETF)。 |
Enums
有一种地方化枚举的便捷方法。如果有一个消息包方法接受枚举类型的单个参数,并且没有定义消息模板:
@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
在模板中,可以使用消息包方法,如 {msg:methodName(enumConstant)}
,将枚举常量本地化。
同时还有 <<`@TemplateEnum`,便捷枚举注释>> —— 在模板中访问枚举常量的便捷注释。 |
Message Templates
消息包界面的每个方法都必须定义一条消息模板。该值通常由 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() 始终优先。 |
在构建期间验证消息模板。如果检测到消息模板缺失,则会抛出异常并导致构建失败。
Qute Used as a Standalone Library
Qute 主要设计为 Quarkus 扩展。但是,也可将其用作“独立”库。在这种情况下,某些功能不可用且需要一些其他配置。
- Engine
-
-
首先,没有可开箱即用的管理
Engine
实例。您需要通过Engine.builder()
配置一个新实例。
-
- Template locators
-
-
默认情况下,没有注册 template locators,即
Engine.getTemplate(String)
将无法工作。 -
您可以使用
EngineBuilder.addLocator()
注册自定义模板定位器,或手动解析模板并通过Engine.putTemplate(String, Template)
将结果放入缓存中。
-
- Template initializers
-
-
默认情况下未注册
TemplateInstance.Initializer
,因此 <<`@TemplateGlobal`,全局变量>> 注释将被忽略。 -
可以使用
EngineBuilder#addTemplateInstanceInitializer()
注册一个自定义TemplateInstance.Initializer
并用任何数据和属性来初始化模板实例。
-
- Sections
-
-
默认情况下没有注册节助手。
-
可以通过便捷
EngineBuilder.addDefaultSectionHelpers()
方法和EngineBuilder.addDefaults()
方法分别注册默认值解决器组。
-
- Value resolvers
-
-
不会自动生成 <<`ValueResolver`,值解决器>>。
-
<<`@TemplateExtension` 方法,模板扩展方法>> 不会起作用。
-
<<`@TemplateData`,模板数据>> 和 <<`@TemplateEnum`,便捷枚举注释>> 注释将被忽略。
-
-
可以通过便捷
EngineBuilder.addDefaultValueResolvers()
方法和EngineBuilder.addDefaults()
方法分别注册默认值解决器组。
-
并非所有内置扩展方法提供的功能都能被默认值解决器覆盖。但是,可以通过 |
-
建议通过
Engine.addValueResolver(new ReflectionValueResolver())
注册一个ReflectionValueResolver
实例,以便 Qute 可以访问对象属性并调用公共方法。
请记住,反射可能无法在某些受限环境中正确工作,或者可能需要额外的配置,例如在 GraalVM 原生镜像的情况下注册。
- User-defined Tags
-
-
不会自动注册任何用户定义的标记。
-
一个标记可以通过
Engine.builder().addSectionHelper(new UserTagSectionHelper.Factory("tagName","tagTemplate.html")).build()
手动注册
-
- Type-safety
-
-
Type-safe Expressions are not validated.
-
- Injection
-
It is not possible to inject a
Template
instance and vice versa - a template cannot inject a@Named
CDI bean via theinject:
andcdi:
namespace.