模板引擎

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.1. 基础

模板由 Groovy 代码组成。让我们更深入地探讨第一个示例

xmlDeclaration()                                (1)
cars {                                          (2)
   cars.each {                                  (3)
       car(make: it.make, model: it.model)      (4)
   }                                            (5)
}
1 渲染 XML 声明字符串。
2 打开一个 cars 标签
3 cars 是在模板模型中找到的一个变量,它是一个 Car 实例列表
4 对于每个项目,我们创建一个 car 标签,其中包含来自 Car 实例的属性
5 关闭 cars 标签

如您所见,常规 Groovy 代码可以在模板中使用。在这里,我们对列表(从模型中检索)调用 each,允许我们为每个条目渲染一个 car 标签。

以类似的方式,渲染 HTML 代码就像这样简单

yieldUnescaped '<!DOCTYPE html>'                                                    (1)
html(lang:'en') {                                                                   (2)
    head {                                                                          (3)
        meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')      (4)
        title('My page')                                                            (5)
    }                                                                               (6)
    body {                                                                          (7)
        p('This is an example of HTML contents')                                    (8)
    }                                                                               (9)
}                                                                                   (10)
1 渲染 HTML 文档类型特殊标签
2 打开具有一个属性的 html 标签
3 打开 head 标签
4 渲染一个具有一个 http-equiv 属性的 meta 标签
5 渲染 title 标签
6 关闭 head 标签
7 打开 body 标签
8 渲染一个 p 标签
9 关闭 body 标签
10 关闭 html 标签

输出很直接

<!DOCTYPE html><html lang='en'><head><meta http-equiv='"Content-Type" content="text/html; charset=utf-8"'/><title>My page</title></head><body><p>This is an example of HTML contents</p></body></html>
通过一些 配置,您可以使输出进行美化,自动添加换行符和缩进。

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 不为空

输出:

<?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: 语法可能很有用。要包含的文件(无论其类型是模板还是文本)都可以在类路径上找到。这是 MarkupTemplateEngine 接受一个可选的 ClassLoader 作为构造函数参数的原因之一(另一个原因是,您可以在模板中包含引用其他类的代码)。

如果您不希望将模板放在类路径上,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 变量(在标题标签内)是一个布局变量
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 不为空

输出:

<?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)继承,它就会自动转义来自模型的对象。

假设以下设置

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 调用需要一个参数,即一个 *字符串*

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

因此,评估字符串会导致在调用 yield 本身之前执行 a(href:…​) 调用。这不是您想要做的事情。相反,您希望生成一个包含标记的 *字符串*,然后将其传递给 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 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 等。