模板引擎

1. 介绍

Groovy 支持多种动态生成文本的方式,包括 GStringsprintfMarkupBuilder 等。除此之外,还有一个专门的模板框架,非常适合需要生成的文本遵循静态模板形式的应用。

2. 模板框架

Groovy 中的模板框架由一个 TemplateEngine 抽象基类(引擎必须实现)和一个 Template 接口(它们生成的模板必须实现)组成。

Groovy 中包含以下几种模板引擎

  • SimpleTemplateEngine - 用于基本模板

  • StreamingTemplateEngine - 功能上等同于 SimpleTemplateEngine,但可以处理大于 64k 的字符串

  • GStringTemplateEngine - 将模板存储为可写闭包(适用于流式处理场景)

  • XmlTemplateEngine - 当模板和输出都是有效 XML 时效果很好

  • MarkupTemplateEngine - 一个非常完整、优化的模板引擎

3. SimpleTemplateEngine

这里展示的是 SimpleTemplateEngine,它允许您在模板中使用类似 JSP 的脚本片段(见下面的示例)、脚本和 EL 表达式来生成参数化文本。这是一个使用该系统的示例

def text = 'Dear "$firstname $lastname",\nSo nice to meet you in <% print city %>.\nSee you in ${month},\n${signed}'

def binding = ["firstname":"Sam", "lastname":"Pullara", "city":"San Francisco", "month":"December", "signed":"Groovy-Dev"]

def engine = new groovy.text.SimpleTemplateEngine()
def template = engine.createTemplate(text).make(binding)

def result = 'Dear "Sam Pullara",\nSo nice to meet you in San Francisco.\nSee you in December,\nGroovy-Dev'

assert result == template.toString()

虽然在模板(或视图)中混合处理逻辑通常不被认为是良好的实践,但有时非常简单的逻辑可能很有用。例如,在上面的示例中,我们可以将此

$firstname

更改为(假设我们已在模板内部设置了 capitalize 的静态导入)

${firstname.capitalize()}

或此

<% print city %>

更改为

<% print city == "New York" ? "The Big Apple" : city %>

3.1. 高级用法注意事项

如果您将模板直接嵌入到脚本中(如我们上面所做的那样),则必须小心反斜杠转义。因为模板字符串本身在传递给模板框架之前将由 Groovy 解析,所以您必须转义作为 Groovy 程序一部分输入的 GString 表达式或脚本片段“代码”中的任何反斜杠。例如,如果我们想在上面的 The Big Apple 周围加上引号,我们将使用

<% print city == "New York" ? "\\"The Big Apple\\"" : city %>

类似地,如果我们需要换行符,我们将使用

\\n

在 Groovy 脚本中出现的任何 GString 表达式或脚本片段“代码”中。正常的“\n”在静态模板文本本身中或如果整个模板本身在外部模板文件中都是可以的。同样,要在文本中表示实际的反斜杠,您需要

\\\\

在外部文件中或

\\\\

在任何 GString 表达式或脚本片段“代码”中。(注意:如果我们可以找到一种简单的方法来支持这种更改,那么未来版本的 Groovy 中可能不再需要这个额外的斜杠。)

4. StreamingTemplateEngine

StreamingTemplateEngine 引擎在功能上等同于 SimpleTemplateEngine,但它使用可写闭包创建模板,使其更适用于大型模板。具体来说,此模板引擎可以处理大于 64k 的字符串。

它使用 JSP 风格的 <% %> 脚本和 <%= %> 表达式语法或 GString 风格的表达式。变量“out”绑定到模板正在写入的写入器。

通常,模板源将是一个文件,但这里我们展示了一个将模板作为字符串提供的简单示例

def text = '''\
Dear <% out.print firstname %> ${lastname},

We <% if (accepted) out.print 'are pleased' else out.print 'regret' %> \
to inform you that your paper entitled
'$title' was ${ accepted ? 'accepted' : 'rejected' }.

The conference committee.'''

def template = new groovy.text.StreamingTemplateEngine().createTemplate(text)

def binding = [
    firstname : "Grace",
    lastname  : "Hopper",
    accepted  : true,
    title     : 'Groovy for COBOL programmers'
]

String response = template.make(binding)

assert response == '''Dear Grace Hopper,

We are pleased to inform you that your paper entitled
'Groovy for COBOL programmers' was accepted.

The conference committee.'''

5. GStringTemplateEngine

作为使用 GStringTemplateEngine 的示例,这里是上面再次完成的示例(进行了一些更改以显示其他一些选项)。这次我们首先将模板存储在一个文件中

test.template
Dear "$firstname $lastname",
So nice to meet you in <% out << (city == "New York" ? "\\"The Big Apple\\"" : city) %>.
See you in ${month},
${signed}

请注意,我们使用 out 而不是 print 来支持 GStringTemplateEngine 的流式特性。因为我们将模板放在一个单独的文件中,所以无需转义反斜杠。这是我们调用它的方式

def f = new File('test.template')
def engine = new groovy.text.GStringTemplateEngine()
def template = engine.createTemplate(f).make(binding)
println template.toString()

这是输出

Dear "Sam Pullara",
So nice to meet you in "The Big Apple".
See you in December,
Groovy-Dev

6. XmlTemplateEngine

XmlTemplateEngine 用于模板场景,其中模板源和预期输出都旨在为 XML。模板可以使用普通的 ${expression}$variable 符号将任意表达式插入到模板中。此外,还支持特殊标签:<gsp:scriptlet>(用于插入代码片段)和 <gsp:expression>(用于生成输出的代码片段)。

注释和处理指令将在处理过程中被删除,并且特殊的 XML 字符,如 <>"' 将使用相应的 XML 符号进行转义。输出还将使用标准的 XML 美化打印进行缩进。

gsp: 标签的 xmlns 命名空间定义将被删除,但其他命名空间定义将被保留(但可能会更改为 XML 树中的等效位置)。

通常,模板源将位于文件中,但这里是一个简单的示例,将 XML 模板作为字符串提供

def binding = [firstname: 'Jochen', lastname: 'Theodorou', nickname: 'blackdrag', salutation: 'Dear']
def engine = new groovy.text.XmlTemplateEngine()
def text = '''\
    <document xmlns:gsp='http://groovy.codehaus.org/2005/gsp' xmlns:foo='baz' type='letter'>
        <gsp:scriptlet>def greeting = "${salutation}est"</gsp:scriptlet>
        <gsp:expression>greeting</gsp:expression>
        <foo:to>$firstname "$nickname" $lastname</foo:to>
        How are you today?
    </document>
'''
def template = engine.createTemplate(text).make(binding)
println template.toString()

此示例将生成以下输出

<document type='letter'>
  Dearest
  <foo:to xmlns:foo='baz'>
    Jochen &quot;blackdrag&quot; Theodorou
  </foo:to>
  How are you today?
</document>

7. MarkupTemplateEngine

此模板引擎主要用于生成类似 XML 的标记(XML、XHTML、HTML5 等),但也可用于生成任何基于文本的内容。与传统模板引擎不同,它依赖于使用构建器语法的 DSL。这是一个示例模板

xmlDeclaration()
cars {
   cars.each {
       car(make: it.make, model: it.model)
   }
}

如果您提供以下模型

model = [cars: [new Car(make: 'Peugeot', model: '508'), new Car(make: 'Toyota', model: 'Prius')]]

它将被渲染为

<?xml version='1.0'?>
<cars><car make='Peugeot' model='508'/><car make='Toyota' model='Prius'/></cars>

此模板引擎的主要特点是

  • 标记构建器 般的语法

  • 模板被编译成字节码

  • 快速渲染

  • 可选的模型类型检查

  • 包含

  • 国际化支持

  • 片段/布局

7.1. 模板格式

7.1.2. 支持方法

在前面的示例中,文档类型声明是使用 yieldUnescaped 方法渲染的。我们也看到了 xmlDeclaration 方法。模板引擎提供了几种支持方法,可以帮助您适当地渲染内容

方法 描述 示例

yield

渲染内容,但在渲染前进行转义

模板:

yield 'Some text with <angle brackets>'

输出:

Some text with &lt;angle brackets&gt;

yieldUnescaped

渲染原始内容。参数按原样渲染,不进行转义。

模板:

yieldUnescaped 'Some text with <angle brackets>'

输出:

Some text with <angle brackets>

xmlDeclaration

渲染 XML 声明字符串。如果配置中指定了编码,则会写入声明中。

模板:

xmlDeclaration()

输出:

<?xml version='1.0'?>

如果 TemplateConfiguration#getDeclarationEncoding 不为 null

输出:

<?xml version='1.0' encoding='UTF-8'?>

comment

在 XML 注释内渲染原始内容

模板:

comment 'This is <a href='https://docs.groovy-lang.cn/latest/html/documentation/foo.html'>commented out</a>'

输出:

<!--This is <a href='https://docs.groovy-lang.cn/latest/html/documentation/foo.html'>commented out</a>-->

newLine

渲染一个新行。另请参阅 TemplateConfiguration#setAutoNewLineTemplateConfiguration#setNewLineString

模板:

p('text')
newLine()
p('text on new line')

输出:

<p>text</p>
<p>text on new line</p>

pi

渲染 XML 处理指令。

模板:

pi("xml-stylesheet":[href:"mystyle.css", type:"text/css"])

输出:

<?xml-stylesheet href='mystyle.css' type='text/css'?>

tryEscape

如果对象是 String(或任何从 CharSequence 派生的类型),则返回转义后的字符串。否则返回对象本身。

模板:

yieldUnescaped tryEscape('Some text with <angle brackets>')

输出:

Some text with &lt;angle brackets&gt;

7.1.3. 包含

MarkupTemplateEngine 支持包含来自另一个文件的内容。包含的内容可以是

  • 另一个模板

  • 原始内容

  • 要转义的内容

包含另一个模板可以使用

include template: 'other_template.tpl'

将文件作为原始内容包含,而不对其进行转义,可以这样做

include unescaped: 'raw.txt'

最终,包含在渲染前应转义的文本可以使用

include escaped: 'to_be_escaped.txt'

或者,您可以使用以下辅助方法代替

  • includeGroovy(<name>) 包含另一个模板

  • includeEscaped(<name>) 包含另一个文件并转义

  • includeUnescaped(<name>) 包含另一个文件而不转义

调用这些方法而不是 include xxx: 语法在要包含的文件名是动态的(例如存储在变量中)时可能很有用。要包含的文件(无论其类型,模板或文本)都可以在 classpath 上找到。这是 MarkupTemplateEngine 接受可选 ClassLoader 作为构造函数参数的原因之一(另一个原因是您可以在模板中包含引用其他类的代码)。

如果您不希望模板位于 classpath 上,MarkupTemplateEngine 提供了一个方便的构造函数,允许您定义模板所在的目录。

7.1.4. 片段

片段是嵌套模板。它们可用于在单个模板中提供改进的组合。片段由一个字符串、内部模板和一个用于渲染此模板的模型组成。考虑以下模板

ul {
    pages.each {
        fragment "li(line)", line:it
    }
}

fragment 元素创建一个嵌套模板,并使用特定于此模板的模型进行渲染。在这里,我们有 li(line) 片段,其中 line 绑定到 it。由于 it 对应于 pages 的迭代,我们将为模型中的每个页面生成一个 li 元素

<ul><li>Page 1</li><li>Page 2</li></ul>

片段对于模板元素的因子化很有趣。它们的代价是每个模板的片段编译,并且它们不能被外部化。

7.1.5. 布局

布局,与片段不同,指的是其他模板。它们可用于组合模板并共享通用结构。如果您有一个通用的 HTML 页面设置,并且只想替换主体,这通常很有趣。这可以通过布局轻松完成。首先,您需要创建一个布局模板

layout-main.tpl
html {
    head {
        title(title)                (1)
    }
    body {
        bodyContents()              (2)
    }
}
1 title 变量(在 title 标签内)是一个布局变量
2 bodyContents 调用将渲染主体

然后你需要一个包含布局的模板

layout 'layout-main.tpl',                                   (1)
    title: 'Layout example',                                (2)
    bodyContents: contents { p('This is the body') }        (3)
1 使用 main-layout.tpl 布局文件
2 设置 title 变量
3 设置 bodyContents

如您所见,由于布局文件中的 bodyContents() 调用,bodyContents 将在布局内部渲染。因此,模板将渲染为

<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>

contents 方法的调用用于告诉模板引擎,该代码块实际上是模板的规范,而不是要直接渲染的辅助函数。如果您在规范之前不添加 contents,那么内容将被渲染,但您也会看到一个生成的随机字符串,对应于该块的结果值。

布局是一种在多个模板之间共享通用元素的强大方式,无需重写所有内容或使用包含。

布局默认使用与它们所使用的页面的模型无关的模型。但是,可以使它们继承自父模型。想象一下模型定义如下

model = new HashMap<String,Object>();
model.put('title','Title from main model');

以及以下模板

layout 'layout-main.tpl', true,                             (1)
    bodyContents: contents { p('This is the body') }
1 注意使用 true 来启用模型继承

那么就不需要像 上一个示例 那样将 title 值传递给布局。结果将是

<html><head><title>Title from main model</title></head><body><p>This is the body</p></body></html>

但也可以覆盖父模型中的值

layout 'layout-main.tpl', true,                             (1)
    title: 'overridden title',                               (2)
    bodyContents: contents { p('This is the body') }
1 true 表示继承自父模型
2 title 被覆盖

然后输出将是

<html><head><title>overridden title</title></head><body><p>This is the body</p></body></html>

7.2. 渲染内容

7.2.1. 模板引擎创建

在服务器端,渲染模板需要一个 groovy.text.markup.MarkupTemplateEngine 实例和一个 groovy.text.markup.TemplateConfiguration

TemplateConfiguration config = new TemplateConfiguration();         (1)
MarkupTemplateEngine engine = new MarkupTemplateEngine(config);     (2)
Template template = engine.createTemplate("p('test template')");    (3)
Map<String, Object> model = new HashMap<>();                        (4)
Writable output = template.make(model);                             (5)
output.writeTo(writer);                                             (6)
1 创建模板配置
2 使用此配置创建模板引擎
3 String 创建模板实例
4 创建要在模板中使用的模型
5 将模型绑定到模板实例
6 渲染输出

有几种可能的选项来解析模板

  • String,使用 createTemplate(String)

  • Reader,使用 createTemplate(Reader)

  • URL,使用 createTemplate(URL)

  • 给定模板名称,使用 createTemplateByPath(String)

通常首选最后一种方式

Template template = engine.createTemplateByPath("main.tpl");
Writable output = template.make(model);
output.writeTo(writer);

7.2.2. 配置选项

引擎的行为可以通过 TemplateConfiguration 类中的几个配置选项进行调整

选项 默认值 描述 示例

declarationEncoding

null

确定调用 xmlDeclaration 时要写入的编码值。它影响您用作输出的写入器。

模板:

xmlDeclaration()

输出:

<?xml version='1.0'?>

如果 TemplateConfiguration#getDeclarationEncoding 不为 null

输出:

<?xml version='1.0' encoding='UTF-8'?>

expandEmptyElements

false

如果为 true,则空标签以其展开形式渲染。

模板:

p()

输出:

<p/>

如果 expandEmptyElements 为 true

输出:

<p></p>

useDoubleQuotes

false

如果为 true,则属性使用双引号而不是单引号

模板:

tag(attr:'value')

输出:

<tag attr='value'/>

如果 useDoubleQuotes 为 true

输出:

<tag attr="value"/>

newLineString

系统默认值(系统属性 line.separator

允许选择渲染新行时使用的字符串

模板:

p('foo')
newLine()
p('baz')

如果 newLineString='BAR'

输出:

<p>foo</p>BAR<p>baz</p>

autoEscape

false

如果为 true,则模型中的变量在渲染前自动转义。

autoIndent

false

如果为 true,则在新行后执行自动缩进

autoIndentString

四 (4) 个空格

用作缩进的字符串。

autoNewLine

false

如果为 true,则根据模板源的原始格式自动插入新行

baseTemplateClass

groovy.text.markup.BaseTemplate

设置编译模板的超类。这可用于提供特定于应用程序的模板。

locale

默认区域设置

设置模板的默认区域设置。

模板引擎创建后,更改配置是不安全的。

7.2.3. 自动格式化

默认情况下,模板引擎将不进行任何特定格式化地渲染输出。一些 配置选项 可以改善这种情况

  • autoIndent 负责在新行插入后自动缩进

  • autoNewLine 负责根据模板源的原始格式自动插入新行

通常,如果您想要可读的、美观的输出,建议将 autoIndentautoNewLine 都设置为 true

config.setAutoNewLine(true);
config.setAutoIndent(true);

使用以下模板

html {
    head {
        title('Title')
    }
}

输出现在将是

<html>
    <head>
        <title>Title</title>
    </head>
</html>

我们可以稍微修改模板,使 title 指令与 head 指令在同一行

html {
    head { title('Title')
    }
}

输出将反映这一点

<html>
    <head><title>Title</title>
    </head>
</html>

新行在标签的{}(大括号)处插入,并且插入位置与嵌套内容的位置相对应。这意味着另一个标签主体中的标签不会触发新行,除非它们自己使用{}(大括号)

html {
    head {
        meta(attr:'value')          (1)
        title('Title')              (2)
        newLine()                   (3)
        meta(attr:'value2')         (4)
    }
}
1 插入新行,因为 meta 不在与 head 同一行
2 不插入新行,因为我们与上一个标签在同一深度
3 我们可以通过显式调用 newLine 强制渲染新行
4 此标签将在单独的行上渲染

这次,输出将是

<html>
    <head>
        <meta attr='value'/><title>Title</title>
        <meta attr='value2'/>
    </head>
</html>

默认情况下,渲染器使用四个(4)个空格作为缩进,但您可以通过设置 TemplateConfiguration#autoIndentString 属性来更改它。

7.2.4. 自动转义

默认情况下,从模型中读取的内容将按原样渲染。如果此内容来自用户输入,则它可能很敏感,您可能希望默认对其进行转义,例如避免 XSS 注入。为此,模板配置提供了一个选项,该选项将自动转义模型中的对象,只要它们继承自 CharSequence(通常是 `String`s)。

我们来设想以下设置

config.setAutoEscape(false);
model = new HashMap<String,Object>();
model.put("unsafeContents", "I am an <html> hacker.");

以及以下模板

html {
    body {
        div(unsafeContents)
    }
}

那么您就不希望 unsafeContents 中的 HTML 按原样渲染,因为存在潜在的安全问题

<html><body><div>I am an <html> hacker.</div></body></html>

自动转义将解决此问题

config.setAutoEscape(true);

现在输出已正确转义

<html><body><div>I am an &lt;html&gt; hacker.</div></body></html>

请注意,使用自动转义并不妨碍您从模型中包含未转义的内容。为此,您的模板应该明确指出模型变量不应转义,方法是在其前面加上 unescaped.,如下例所示

显式绕过自动转义
html {
    body {
        div(unescaped.unsafeContents)
    }
}

7.2.5. 常见陷阱

包含标记的字符串

假设您想生成一个包含标记字符串的 <p> 标签

p {
    yield "This is a "
    a(href:'target.html', "link")
    yield " to another page"
}

并生成

<p>This is a <a href='target.html'>link</a> to another page</p>

这不能写得更短吗?一个幼稚的替代方案是

p {
    yield "This is a ${a(href:'target.html', "link")} to another page"
}

但结果不会像预期那样

<p><a href='target.html'>link</a>This is a  to another page</p>

原因是标记模板引擎是一个流式引擎。在原始版本中,第一个 yield 调用生成一个字符串并将其流式传输到输出,然后生成 a 链接并流式传输,然后流式传输最后一个 yield 调用,从而导致按顺序执行。但对于上面的字符串版本,执行顺序不同

  • yield 调用需要一个参数,一个 string

  • 该参数需要 yield 调用生成之前进行评估

因此,评估字符串会导致 a(href:...) 调用在 yield 本身被调用之前执行。这不是您想要做的。相反,您想要生成一个包含标记的字符串,然后将其传递给 yield 调用。可以这样做

p("This is a ${stringOf {a(href:'target.html', "link")}} to another page")

注意 stringOf 调用,它基本上告诉标记模板引擎底层标记需要单独渲染并导出为字符串。请注意,对于简单的表达式,stringOf 可以替换为以美元符号开头的替代标签符号

p("This is a ${$a(href:'target.html', "link")} to another page")
值得注意的是,使用 stringOf 或特殊的 $tag 符号会触发创建单独的字符串写入器,然后用于渲染标记。这比使用直接流式传输标记的 yield 调用版本要慢。

7.2.6. 国际化

模板引擎原生支持国际化。为此,当您创建 TemplateConfiguration 时,您可以提供一个 Locale,它是用于模板的默认区域设置。每个模板可能具有不同的版本,每个区域设置一个。模板的名称有所不同

  • file.tpl: 默认模板文件

  • file_fr_FR.tpl: 模板的法语版本

  • file_en_US.tpl: 模板的美式英语版本

  • …​

当模板被渲染或包含时,则

  • 如果模板名称或包含名称明确设置了区域设置,则包含特定版本,如果未找到则包含默认版本

  • 如果模板名称不包含区域设置,则使用 TemplateConfiguration 区域设置的版本,如果未找到则使用默认版本

例如,假设默认区域设置为 Locale.ENGLISH 且主模板包含

在包含中使用显式区域设置
include template: 'locale_include_fr_FR.tpl'

然后模板使用特定模板渲染

绕过模板配置
Texte en français

使用不带区域设置的 include 将使模板引擎查找具有配置区域设置的模板,如果未找到,则回退到默认模板,如下所示

不要在 include 中使用区域设置
include template: 'locale_include.tpl'
回退到默认模板
Default text

然而,将模板引擎的默认区域设置为 Locale.FRANCE 将改变输出,因为模板引擎现在将查找具有 fr_FR 区域的文件

由于找到了特定于区域设置的模板,因此不会回退到默认模板
Texte en français

此策略允许您通过依赖默认模板(文件名中未设置区域设置的模板)来逐个翻译您的模板。

7.2.7. 自定义模板类

默认情况下,创建的模板继承 groovy.text.markup.BaseTemplate 类。为应用程序提供不同的模板类可能很有趣,例如提供与应用程序相关的额外辅助方法,或自定义渲染基元(例如用于 HTML)。

模板引擎通过在 TemplateConfiguration 中设置替代的 baseTemplateClass 来提供此功能

config.setBaseTemplateClass(MyTemplate.class);

自定义基类必须像这个例子一样扩展 BaseClass

public abstract class MyTemplate extends BaseTemplate {
    private List<Module> modules
    public MyTemplate(
            final MarkupTemplateEngine templateEngine,
            final Map model,
            final Map<String, String> modelTypes,
            final TemplateConfiguration configuration) {
        super(templateEngine, model, modelTypes, configuration)
    }

    List<Module> getModules() {
        return modules
    }

    void setModules(final List<Module> modules) {
        this.modules = modules
    }

    boolean hasModule(String name) {
        modules?.any { it.name == name }
    }
}

此示例显示了一个提供了名为 hasModule 的额外方法的类,该方法可以直接在模板中使用

if (hasModule('foo')) {
    p 'Found module [foo]'
} else {
    p 'Module [foo] not found'
}

7.3. 类型检查模板

7.3.1. 可选类型检查

即使模板未进行类型检查,它们也是静态编译的。这意味着一旦模板被编译,性能应该非常好。对于某些应用程序,最好确保模板在实际渲染之前是有效的。这意味着如果模型变量上的方法不存在,例如,模板编译将失败。

MarkupTemplateEngine 提供了这样的功能。模板可以选择性地进行类型检查。为此,开发人员必须在模板创建时提供额外的信息,即模型中变量的类型。想象一个模型公开了一个页面列表,其中页面定义为

Page.groovy
public class Page {

    Long id
    String title
    String body
}

然后可以在模型中公开页面列表,如下所示

Page p = new Page();
p.setTitle("Sample page");
p.setBody("Page body");
List<Page> pages = new LinkedList<>();
pages.add(p);
model = new HashMap<String,Object>();
model.put("pages", pages);

模板可以轻松使用它

pages.each { page ->                    (1)
    p("Page title: $page.title")        (2)
    p(page.text)                        (3)
}
1 遍历模型中的页面
2 page.title 有效
3 page.text 无效(应该是 page.body

如果没有类型检查,模板的编译会成功,因为模板引擎在页面实际渲染之前并不知道模型。这意味着问题只会在运行时,即页面渲染时才出现

运行时错误
No such property: text

在某些情况下,这可能很难解决甚至很难发现。通过向模板引擎声明 pages 的类型,我们现在能够在编译时失败

modelTypes = new HashMap<String,String>();                                          (1)
modelTypes.put("pages", "List<Page>");                                              (2)
Template template = engine.createTypeCheckedModelTemplate("main.tpl", modelTypes)   (3)
1 创建一个用于保存模型类型的映射
2 声明 pages 变量的类型(注意类型使用字符串)
3 使用 createTypeCheckedModelTemplate 而不是 createTemplate

这次,当模板在最后一行编译时,会发生错误

模板编译时错误
[Static type checking] - No such property: text for class: Page

这意味着您不需要等到页面渲染才能看到错误。使用 createTypeCheckedModelTemplate 是强制性的。

7.3.2. 替代类型声明

或者,如果开发者也是模板的编写者,则可以直接在模板中声明预期变量的类型。在这种情况下,即使您调用 createTemplate,它也会进行类型检查

内联类型声明
modelTypes = {                          (1)
    List<Page> pages                    (2)
}

pages.each { page ->
    p("Page title: $page.title")
    p(page.text)
}
1 类型需要在 modelTypes 头部声明
2 为模型中的每个对象声明一个变量

7.3.3. 类型检查模板的性能

使用类型检查模型还有一个额外的优点是性能应该会提高。通过告诉类型检查器预期的类型是什么,您还允许编译器为此生成优化的代码,因此如果您正在寻找最佳性能,请考虑使用类型检查模板。

8. 其他解决方案

此外,还有其他可与 Groovy 一起使用的模板解决方案,例如 FreeMarkerVelocityStringTemplate 等。