Template Engines
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26
(checked on 2026-10-14). Groovy can build text with GStrings, printf and the markup builders, and for output that follows a fixed
shape there is a dedicated template framework in the groovy-templates module (in the lib directory of the binary distribution,
so no @Grab is needed). A template is text with placeholders and a little code; an engine compiles it once and renders it many
times with different data. The module has five engines, all in the groovy.text package, and they differ in syntax, in streaming
and in how much they know about markup. The page ends with the most capable one, the MarkupTemplateEngine. For text that is
built once in code, plain GStrings (Strings & GStrings) and
the XML builders (XML: Creating & Manipulating) are usually
enough, and the builders in general are on Builders.
The template framework
The framework has two types. TemplateEngine is an abstract class with createTemplate methods that accept a String, a Reader,
a File or a URL, and return a Template. Template is an interface with one method, make(Map binding), which returns a
Writable. Calling toString() renders into a string and writeTo(Writer) streams the output, so a large report need not be built
in memory. The rendering is lazy: a missing variable or an exception inside a scriptlet only surfaces when the Writable is
written, not in make. A template is created once and made many times with different bindings.
| Engine | Syntax | Use when |
|---|---|---|
|
|
small text templates; the baseline |
|
the same as Simple, plus |
templates whose static text exceeds 64k |
|
the same as Simple, compiled to writable closures |
streaming output, templates kept in files |
|
XML with |
the template and the result are XML |
|
the builder DSL ( |
HTML, XML and any markup; typed, fast, with layouts |
SimpleTemplateEngine
SimpleTemplateEngine uses JSP-like syntax: <% ... %> runs Groovy code, <%= ... %> prints the value of an expression, and
$name or ${expression} interpolates as in a GString. The binding’s keys become variables and print writes into the output.
This template renders a small zone report from the greenhouse readings, then renders the same compiled template with another
binding:
import groovy.text.SimpleTemplateEngine
def engine = new SimpleTemplateEngine()
def text = '''\
Zone: ${zone.toUpperCase()}
<% readings.each { r -> %> ${r.sensor.padRight(8)} ${r.temperature}
<% } %>Average: <%= (readings*.temperature.sum() / readings.size()) %>
'''
def template = engine.createTemplate(text)
def out = template.make(zone: 'bench', readings: [
[sensor: 'bench-1', temperature: 18.5],
[sensor: 'bench-2', temperature: 20.0]])
print out
println '--'
// the same compiled template, another binding
print template.make(zone: 'bed', readings: [[sensor: 'bed-3', temperature: 17.25]])
Zone: BENCH
bench-1 18.5
bench-2 20.0
Average: 19.25
--
Zone: BED
bed-3 17.25
Average: 17.25
A scriptlet can span lines and mix with static text, as the each loop above does: the closure opened in one <% %> is closed
in a later one. Keep the logic small; the closing <% } %> placement controls the newlines in the result.
Three details cause most of the surprises:
import groovy.text.SimpleTemplateEngine
def engine = new SimpleTemplateEngine()
try {
engine.createTemplate('Hello ${name}').make([:]).toString()
} catch (e) { println e.class.simpleName + ': ' + e.message.readLines()[0] }
// a default for a missing value: bind it, even as null
println engine.createTemplate('Hello ${name ?: "anonymous"}').make([name: null])
// the template string is parsed by Groovy first, so a backslash must be doubled
println engine.createTemplate('He said <% print "\\"hi\\"" %>').make()
// a literal dollar sign is escaped
println engine.createTemplate('Price: \\$5, ${5 * 2}').make()
MissingPropertyException: No such property: name for class: SimpleTemplateScript2
Hello anonymous
He said "hi"
Price: $5, 10
An unbound variable is an error, not an empty string, so give optional values a binding of null. The doubled backslash is
needed only when the template is a Groovy string literal; a template read from a file is passed through once, as the
GStringTemplateEngine example below shows. The Template object compiles the text into a script, so createTemplate is the
expensive call and make is cheap; create the template once and cache it.
StreamingTemplateEngine
StreamingTemplateEngine accepts the same syntax and renders to the same result, but compiles the template into writable
closures. The documented reason to choose it is size: SimpleTemplateEngine puts the static text in a script constant, and the
class-file format limits a string constant to 65,535 characters. The binding’s out variable is the writer. This example runs both
engines on a template with 70,000 characters of static text:
import groovy.text.*
def model = [zone: 'bench', level: 'high', readings: [[sensor: 'bench-1', temperature: 31.5], [sensor: 'bench-2', temperature: 33.0]]]
def streaming = new StreamingTemplateEngine().createTemplate('''\
Zone ${zone}: <% if (level == 'high') out.print 'open the vents' else out.print 'all clear' %>.
<% readings.each { r -> %>- ${r.sensor} at ${r.temperature}
<% } %>''')
print streaming.make(model)
println '--'
// A template body above 64k characters
def big = ('x' * 70000) + '${zone}\n'
println big.length()
println new StreamingTemplateEngine().createTemplate(big).make(model).toString().length()
try {
println new SimpleTemplateEngine().createTemplate(big).make(model).toString().length()
} catch (Throwable e) { println e.class.simpleName + ": " + e.message.readLines()[0].take(90) }
Zone bench: open the vents.
- bench-1 at 31.5
- bench-2 at 33.0
--
70008
70006
GroovyRuntimeException: Failed to parse template script (your template may contain an error or be trying to use ex
The SimpleTemplateEngine failure is the compiler’s String too long. ... maximum of 65535 is allowed message, wrapped in a
GroovyRuntimeException. Prefer StreamingTemplateEngine whenever the template text can grow, for example a template that is
read from a file you do not control.
GStringTemplateEngine
GStringTemplateEngine also uses the Simple syntax, but stores the template as writable closures and expects output through out
(use out << rather than print so it streams). It is the engine to use with template files. The template is a file, so the
quote needs only one backslash:
Alert for "$zone" (${level.toUpperCase()})
<% readings.each { r -> out << " ${r.sensor}: ${r.temperature}\n" } %>Action: <% out << (level == 'high' ? "\"open the vents\"" : 'none') %>
import groovy.text.GStringTemplateEngine
def model = [zone: 'bench', level: 'high', readings: [[sensor: 'bench-1', temperature: 31.5], [sensor: 'bench-2', temperature: 33.0]]]
def gt = new GStringTemplateEngine().createTemplate(new File('tpl/alert.template'))
print gt.make(model)
println '--'
// stream to a Writer without building a String
def w = new StringWriter()
gt.make(model).writeTo(w)
println w.toString().length()
Alert for "bench" (HIGH)
bench-1: 31.5
bench-2: 33.0
Action: "open the vents"
--
82
The Groovy documentation’s file example writes the quotes with two backslashes (\\"). With Groovy 6.0.0 that text did not
compile (Unexpected character: '\\'), and the single-backslash form above is what worked, so test file templates rather than copying
the escaping from inline strings.
XmlTemplateEngine
XmlTemplateEngine is for templates that are themselves XML, with $name and ${expression} in text and attributes, and two
special tags in the gsp: namespace: gsp:scriptlet for code and gsp:expression for code whose value is printed. Comments and
processing instructions are dropped, the gsp namespace declaration is removed, and the output is pretty printed with indentation.
import groovy.text.XmlTemplateEngine
def text = '''\
<report xmlns:gsp="http://groovy.apache.org/gsp" zone="$zone">
<gsp:scriptlet>def hot = readings.findAll { it.temperature > 30 }</gsp:scriptlet>
<gsp:expression>hot.size()</gsp:expression>
<!-- dropped from output -->
<note>$note</note>
</report>
'''
def model = [zone: 'bench', note: 'Vents "open" & <fans> on', readings: [[temperature: 31.5], [temperature: 18.0]]]
println new XmlTemplateEngine().createTemplate(text).make(model)
<report zone='bench'>
1
<note>
Vents "open" & <fans> on
</note>
</report>
Look closely at the note element: the values from the binding were inserted as they are, with no escaping, so the output is
not well-formed XML. The documentation says special characters "will be escaped", but in this run that applied to literal
characters of the template, not to interpolated values. Escape values yourself, with groovy.xml.XmlUtil.escapeXml(value)
(which turns x < y & "z" into x < y & "z"), or use the MarkupTemplateEngine with autoEscape. The XML
builders are on XML: Creating & Manipulating.
MarkupTemplateEngine
The MarkupTemplateEngine (package groovy.text.markup) works differently from the other four: templates are Groovy code in the
builder style of Builders, and they are compiled to bytecode, so rendering is
fast. It targets XML-like output (XML, XHTML, HTML5) but works for any text. Its main features are the builder syntax, compiled
templates, optional type checking of the model, includes, internationalization, fragments and layouts.
The template format
A tag is a method call: arguments that are a map become attributes, a string argument becomes the text, and a closure becomes the nested content. Model variables are plain variables of the template.
import groovy.text.markup.*
def engine = new MarkupTemplateEngine()
def tpl = engine.createTemplate '''\
xmlDeclaration()
log(zone: zone) {
readings.each { r ->
reading(sensor: r.sensor, temperature: r.temperature)
}
}
'''
def model = [zone: 'bench', readings: [[sensor: 'bench-1', temperature: 18.5], [sensor: 'bench-2', temperature: 20.0]]]
println tpl.make(model)
def html = engine.createTemplate '''\
yieldUnescaped '<!DOCTYPE html>'
html(lang: 'en') {
head { title("Zone $zone") }
body {
h1(zone)
table {
readings.each { r ->
tr { td(r.sensor); td(r.temperature) }
}
}
comment 'generated'
p {
yield 'Raw: <b>'
yieldUnescaped ' unescaped: <b>'
}
}
}
'''
println html.make(model)
<?xml version='1.0'?>
<log zone='bench'><reading sensor='bench-1' temperature='18.5'/><reading sensor='bench-2' temperature='20.0'/></log>
<!DOCTYPE html><html lang='en'><head><title>Zone bench</title></head><body><h1>bench</h1><table><tr><td>bench-1</td><td>18.5</td></tr><tr><td>bench-2</td><td>20.0</td></tr></table><!--generated--><p>Raw: <b> unescaped: <b></p></body></html>
Besides the tag methods, the template has support methods:
| Method | Effect |
|---|---|
|
renders text, escaping it |
|
renders text as it is |
|
renders |
|
renders its raw argument inside an XML comment |
|
renders a new line |
|
renders a processing instruction, for example |
|
returns the escaped string for a |
Creating the engine and configuring it
The engine takes a TemplateConfiguration. createTemplate accepts a String, a Reader or a URL, and createTemplateByPath
loads a template by name from the classpath, or from a directory with the constructor
new MarkupTemplateEngine(classLoader, directory, config). Prefer the by-path form, which finds the includes and layouts too. The
configuration is fixed once the engine exists; changing it afterwards is unsafe. The options and their defaults:
| Option | Default | Effect |
|---|---|---|
|
|
encoding written by |
|
|
renders |
|
|
double quotes around attribute values |
|
the |
the string |
|
|
escape |
|
|
indent after new lines |
|
four spaces |
the indent |
|
|
insert new lines where the template has braces |
|
|
the superclass of compiled templates |
|
the default locale |
the locale for template variants |
|
|
reuse compiled templates (read back with |
The configuration object has a no-argument constructor with setters, so named arguments set options. With the formatting options the markup is pretty printed:
import groovy.text.markup.*
def cfg = new TemplateConfiguration(autoIndent: true, autoNewLine: true, useDoubleQuotes: true,
expandEmptyElements: true, declarationEncoding: 'UTF-8')
def pretty = new MarkupTemplateEngine(cfg).createTemplate '''\
xmlDeclaration()
log {
readings.each { r ->
reading(sensor: r.sensor)
empty()
}
}
'''
println pretty.make(zone: 'bench', readings: [[sensor: 'bench-1'], [sensor: 'bench-2']])
<?xml version="1.0" encoding="UTF-8"?>
<log>
<reading sensor="bench-1"></reading><empty></empty><reading sensor="bench-2"></reading><empty></empty>
</log>
autoNewLine follows the braces of the template source: a tag with a body in braces starts a new line, but tags written one
after another inside the same body do not, as the reading and empty tags show. Call newLine() to force a break.
Includes, fragments and layouts
For the template files below, the engine reads a views directory:
-
layout-main.tplis a page skeleton, withbodyContents()as the slot that pages fill in. -
report.tplis a page that calls it. -
footer.txtis a text file with markup characters in it (Next check: <b>noon</b>).
// views/layout-main.tpl
html {
head { title(title) }
body {
h1(title)
bodyContents()
}
}
// views/report.tpl
layout 'layout-main.tpl',
title: "Zone $zone",
bodyContents: contents {
ul {
readings.each { r ->
fragment "li(text)", text: "${r.sensor}: ${r.temperature}"
}
}
p { includeUnescaped 'footer.txt' }
p { includeEscaped 'footer.txt' }
}
import groovy.text.markup.*
def cfg = new TemplateConfiguration(autoNewLine: true, autoIndent: true)
def engine = new MarkupTemplateEngine(this.class.classLoader, new File('views'), cfg)
def model = [zone: 'bench', readings: [[sensor: 'bench-1', temperature: 18.5], [sensor: 'bench-2', temperature: 20.0]]]
println engine.createTemplateByPath('report.tpl').make(model)
<html>
<head><title>Zone bench</title></head><body>
<h1>Zone bench</h1><ul><li>bench-1: 18.5</li><li>bench-2: 20.0</li></ul><p>Next check: <b>noon</b></p><p>Next check: <b>noon</b></p>
</body>
</html>
The three composition tools solve different problems:
-
Includes insert another file:
include template: 'other.tpl'renders a template,include unescaped: 'raw.txt'inserts a file as it is, andinclude escaped: 'text.txt'inserts it escaped.includeGroovy(name),includeUnescaped(name)andincludeEscaped(name)are the method forms, handy when the name is in a variable. Included files are found on the classpath (or in the configured directory). -
Fragments are nested templates held in a string, rendered with their own model:
fragment "li(text)", text: .... They avoid repeating markup, at the price of compiling one fragment per template, and they cannot be moved to a file. -
Layouts refer to another template file. The layout receives only the values passed in the
layoutcall, unless the call passestrueas its second argument to inherit the model of the calling page. Wrap the content passed to the layout incontents { ... }, otherwise the block is rendered in place and shows a stray value:
// views/inherit.tpl
layout 'layout-main.tpl', true,
bodyContents: contents { p('inherits the title') }
<html>
<head><title>Inherited title</title></head><body>
<h1>Inherited title</h1><p>inherits the title</p>
</body>
</html>
That output came from engine.createTemplateByPath('inherit.tpl').make(title: 'Inherited title'). A key passed in the layout
call overrides the inherited value of the same name.
Escaping
By default the markup engine renders model strings as they are. For text that comes from users that is a cross-site-scripting
risk, so turn on autoEscape: every CharSequence from the model is escaped, and a template that needs the raw value says so with
the unescaped prefix.
import groovy.text.markup.*
def src = 'div(note)'
def model = [note: 'Water <script>alert(1)</script> now']
println new MarkupTemplateEngine().createTemplate(src).make(model)
println new MarkupTemplateEngine(new TemplateConfiguration(autoEscape: true)).createTemplate(src).make(model)
println new MarkupTemplateEngine(new TemplateConfiguration(autoEscape: true)).createTemplate('div(unescaped.note)').make(model)
<div>Water <script>alert(1)</script> now</div>
<div>Water <script>alert(1)</script> now</div>
<div>Water <script>alert(1)</script> now</div>
Markup inside a string
The engine streams: each tag writes to the output when its method is called. So a tag inside a GString is evaluated while the
string is being built, before the string itself is written, which puts the markup in the wrong place. Use stringOf { ... }, the
$tag shortcut, or separate yield calls:
import groovy.text.markup.*
def e = new MarkupTemplateEngine()
println e.createTemplate('p("See ${a(href: "#top", "top")} now")').make([:])
println e.createTemplate('p("See ${stringOf { a(href: "#top", "top") }} now")').make([:])
println e.createTemplate('p("See ${$a(href: "#top", "top")} now")').make([:])
println e.createTemplate('p { yield "See "; a(href: "#top", "top"); yield " now" }').make([:])
<a href='#top'>top</a><p>See now</p>
<p>See <a href='#top'>top</a> now</p>
<p>See <a href='#top'>top</a> now</p>
<p>See <a href='#top'>top</a> now</p>
The first line is the misordered output. stringOf and $tag render to a separate string writer, which is slower than the
direct streaming of the yield form.
Internationalization
A template can have one file per locale, named name_language_COUNTRY.tpl. When a template is rendered or included without a
locale in its name, the engine looks for the variant of the configured locale and falls back to the unnamed file. A name with a
locale in it asks for that exact variant. The directory has hello.tpl (p("Default text")), hello_fr_FR.tpl (the French text) and
greet.tpl (include template: "hello.tpl"):
import groovy.text.markup.*
def cfg = new TemplateConfiguration()
def engine = new MarkupTemplateEngine(this.class.classLoader, new File('views'), cfg)
println engine.createTemplateByPath('hello.tpl').make([:])
println engine.createTemplateByPath('greet.tpl').make([:])
println engine.createTemplateByPath('hello_fr_FR.tpl').make([:])
cfg.locale = Locale.FRANCE
def fr = new MarkupTemplateEngine(this.class.classLoader, new File('views'), cfg)
println fr.createTemplateByPath('greet.tpl').make([:])
<p>Default text</p>
<p>Default text</p>
<p>Texte en français</p>
<p>Texte en français</p>
Translate one template at a time: the files without a locale remain the default for every variant that does not exist yet.
Custom template classes
Compiled templates extend BaseTemplate. Set another baseTemplateClass to add helper methods that every template can call.
import groovy.text.markup.*
abstract class GreenhouseTemplate extends BaseTemplate {
GreenhouseTemplate(MarkupTemplateEngine engine, Map model, Map<String, String> modelTypes, TemplateConfiguration cfg) {
super(engine, model, modelTypes, cfg)
}
String celsius(def value) { "${value} °C" }
}
def cfg = new TemplateConfiguration(baseTemplateClass: GreenhouseTemplate)
def engine = new MarkupTemplateEngine(this.class.classLoader, cfg)
println engine.createTemplate('p(celsius(temperature))').make(temperature: 18.5)
<p>18.5 °C</p>
Type-checked templates
Templates are statically compiled, but without model types a misspelled property is found only when that template is rendered.
Give the engine the model types and the same mistake becomes a compile-time error. Either pass a map of types to
createTypeCheckedModelTemplate (the types are strings), or declare them at the top of the template in a modelTypes block. The
type-checked path also lets the compiler generate faster code. The class used by the model must be visible to the engine’s class
loader, so the example passes the script’s loader.
import groovy.text.markup.*
class Plant { String name; String zone }
def engine = new MarkupTemplateEngine(this.class.classLoader, new TemplateConfiguration())
def plants = [new Plant(name: 'tomato', zone: 'bench-1')]
def ok = engine.createTemplate 'ul { plants.each { p -> li("${p.name} in ${p.zone}") } }'
println ok.make(plants: plants)
// untyped: the mistake only shows up on rendering
def bad = 'ul { plants.each { p -> li(p.nme) } }'
try { println engine.createTemplate(bad).make(plants: plants) } catch (e) { println 'render: ' + e.class.simpleName + ': ' + e.message.readLines()[0] }
// typed: fails at compile time
try {
engine.createTypeCheckedModelTemplate(new StringReader(bad), [plants: 'List<Plant>'])
} catch (e) { println 'compile: ' + e.class.simpleName + ': ' + e.message.readLines().find { it.contains('nme') && it.contains('No such') } }
// types declared in the template
def inline = '''\
modelTypes = {
List<Plant> plants
}
ul { plants.each { p -> li(p.nme) } }
'''
try { engine.createTemplate(inline) } catch (e) { println 'inline: ' + e.message.readLines().find { it.contains('nme') && it.contains('No such') } }
println engine.createTemplate(inline.replace('nme', 'name')).make(plants: plants)
<ul><li>tomato in bench-1</li></ul>
render: MissingPropertyException: No such property: nme for class: Plant
compile: MultipleCompilationErrorsException: GeneratedMarkupTemplate2: 1: [Static type checking] - No such property: nme for class: Plant
inline: GeneratedMarkupTemplate3: 4: [Static type checking] - No such property: nme for class: Plant
<ul><li>tomato</li></ul>
The static checking used here is the one described on Static Type Checking & @CompileStatic.
Other solutions
Groovy can drive any JVM template library. The documentation names FreeMarker, Velocity and StringTemplate as examples; they bring
their own syntax and Maven coordinates and none of them is part of Groovy. Groovy also serves templated pages in the form of
Groovlets and TemplateServlet, described on
Groovlets, Servlets & JMX.
Where the book is outdated
The Manning book Groovy in Action (publisher page in the references) predates the MarkupTemplateEngine, so it cannot describe
it. Check these points against the current documentation:
-
MarkupTemplateEngine(with type-checked models, layouts, fragments andautoEscape) is the engine to choose for HTML and XML output; the older engines are text substitution only. -
StreamingTemplateEngineexists to lift the 64k string limit ofSimpleTemplateEngine, which the run above reproduced. -
The engines live in the separate
groovy-templatesmodule (org.apache.groovy:groovy-templates), not in the core jar. -
The Groovy 6.0 documentation’s claim that
XmlTemplateEngineescapes special characters did not hold for interpolated values in the run above.