Let's get started with a Microservice Architecture with Spring Cloud:
Getting Started with Compile-Time Templates With Spring
Last updated: January 24, 2026
1. Introduction
In this tutorial, we’ll look at compile-time templates. We’ll see what these are and look at a few libraries we can use to implement them.
2. What are Compile-Time Templates?
There are many occasions when writing our applications when we need to use a templating library of one form or another.
Often, when we do this, we need to load and parse the template at runtime, then provide the data to bind to it to generate the final result. This approach adds overhead and introduces some additional risk. For example, a malformed template or unexpected data may go unnoticed until we attempt to use it.
However, there are several libraries we can use that do this slightly differently. Instead of loading and parsing the templates at runtime, we can compile them to Java classes at build time and then treat them as normal Java from this point forward. This means that we’ll get a build failure if the template is malformed, and we’ll often have compiler safety for actually using the templates.
In addition to this, some of these libraries can work entirely reflection-free. This allows them to be used in an environment where reflection isn’t entirely available. For example, we can use these within a GraalVM setup to build and run our application as a native image.
3. JStachio
JStachio is a small library that implements the Mustache templating language. However, it does this at compile time and not at runtime. It also uses no runtime reflection, so it’s suitable for use with something like GraalVM.
3.1. Dependencies
In order to use JStachio, we need to configure it in our build. This comes as a compiler annotation processor that automatically converts our templates into Java code. To use this, we need to include the latest version in our build.
If we’re using Maven, we can include this in our pom.xml file:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.jstach</groupId>
<artifactId>jstachio-apt</artifactId>
<version>1.3.7</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
We also have a runtime dependency that we need to include to be able to use JStachio templates:
<dependency>
<groupId>io.jstach</groupId>
<artifactId>jstachio</artifactId>
<version>1.3.7</version>
</dependency>
At this point, we’re ready to start using it in our application.
3.2. Writing Templates
Once we set up JStachio, we can start writing templates. These exist in two parts – the template itself and a Java class representing the data needed for the template.
We write our template in the Mustache template language:
<html>
<body>
Hello, {{name}}!
</body>
</html>
We then need a Java class that represents the template:
@JStache(path = "templates/jstachio.mustache")
public record JStachioModel(String name) { }
The JStache annotation on this class indicates the template that should be used. The Java compiler will then detect this and generate a JStachioModelRenderer class in the same package.
3.3. Rendering Templates
Once we’ve built our template into Java code, we can use it to render into our output. This is done using the of() method, providing the input model and a StringBuilder to write the output to:
JStachioModel model = new JStachioModel("Baeldung");
StringBuilder sb = new StringBuilder();
JStachioModelRenderer.of().execute(model, sb);
Our generated JStachioModelRenderer requires an instance of our model class, JStachioModel, to ensure correctness at compile time.
3.4. Using with Spring
In addition to using JStachio manually, it also provides a Spring Boot starter that allows us to use it as a view technology from our controllers.
All we need to do is add the appropriate dependency to our build:
<dependency>
<groupId>io.jstach</groupId>
<artifactId>jstachio-spring-boot-starter-webmvc</artifactId>
<version>1.3.7</version>
</dependency>
Having done this, we can have our Spring controllers return an instance of JStachioModelView that itself wraps our model class:
@GetMapping("/jstachio")
public View get() {
return JStachioModelView.of(new JStachioModel("Baeldung"));
}
This is enough to have JStachio render the appropriate template with the provided data:
$ http localhost:8080/jstachio
HTTP/1.1 200
Content-Length: 55
Content-Type: text/html;charset=UTF-8
<html>
<body>
Hello, Baeldung!
</body>
</html>
4. Rocker
Rocker is another library we can use for our templates. This uses its own custom template language instead of Mustache, but still compiles to Java code at build time. However, this one does use some reflection, so it may be less suitable in some situations.
4.1. Dependencies
In order to use Rocker, we need to configure it in our build. This comes as a custom Maven plugin that automatically converts our templates into Java code. To use this, we need to include the latest version in our build, which is currently 2.4.0:
<plugin>
<groupId>com.fizzed</groupId>
<artifactId>rocker-maven-plugin</artifactId>
<version>2.4.0</version>
<executions>
<execution>
<id>generate-rocker-templates</id>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<templateDirectory>src/main/resources/templates/rocker</templateDirectory>
<outputDirectory>target/rocker</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
This searches for templates under src/main/resources/templates/rocker, finding anything with the file extension .rocker.html. It then generates Java code into target/rocker. It adds this as a build source by default, guaranteeing that the code compiles into our application.
We also need a runtime dependency for this to work:
<dependency>
<groupId>com.fizzed</groupId>
<artifactId>rocker-runtime</artifactId>
<version>2.4.0</version>
</dependency>
At this point, we’re ready to start using it in our application.
4.2. Writing Templates
After completing the setup, we’re ready to use it. Unlike JStachio, Rocker uses the template as the only real input, but it can refer to other Java classes that we’ve written for our data.
We write our templates in a custom markup language that allows us to specify the input binds and how to use them:
@import com.baeldung.templates.RockerModel
@args(RockerModel model)
<html>
<head>
</head>
<body>
<h1>Demo</h1>
<p>Hello @model.name()!</p>
</body>
</html>
In this case, we’re importing a class com.baeldung.templates.RockerModel that must already exist, we’re specifying that this is an argument to our template, and then we’re referencing it when rendering our template.
Our model class is anything that exists on the classpath when the template is compiled:
public record RockerModel(String name) { }
4.3. Rendering Templates
Once compiled, we can then use our template to produce our output. Instead of directly referencing our compiled templates, Rocker provides us a means to reference them by the template name:
BindableRockerModel template = Rocker.template("RockerDemo.rocker.html");
template.bind("model", new RockerModel("Baeldung"));
RockerOutput output = template.render();
ContentType contentType = output.getContentType();
Charset charset = output.getCharset();
String html = output.toString();
This automatically finds the class generated for our RockerDemo.rocker.html template and renders it with the provided model object. Our output variable then gives us access to the rendered template, as well as some extra information such as the content type and character set.
This does have the disadvantage that the compiler won’t catch if our template is missing or the model object is incorrect. However, it lets us write application code before generating the classes for our templates.
4.4. Using Rocker with Spring
Rocker doesn’t directly support Spring, so in order to use it from our controllers, we’ll need to write a custom View implementation:
public class RockerView implements View {
private final String viewName;
public RockerView(String viewName) {
this.viewName = viewName;
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
BindableRockerModel template = Rocker.template(viewName);
for (Map.Entry<String, ?> entry : model.entrySet()) {
try {
template.bind(entry.getKey(), entry.getValue());
} catch (TemplateBindException e) {
// Ignore
}
}
RockerOutput output = template.render();
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.getWriter().write(output.toString());
}
}
This takes our view name when constructed and then attempts to bind everything from our provided model map. We have to handle the TemplateBindException in case there are entries here that Rocker doesn’t expect.
We can then use this directly in our controller methods:
@GetMapping("/rocker")
public ModelAndView get() {
ModelAndView modelAndView = new ModelAndView(new RockerView("RockerDemo.rocker.html"));
modelAndView.addObject("model", new RockerModel("Baeldung"));
return modelAndView;
}
This then renders our Rocker template to the response when this controller is used:
$ http localhost:8080/rocker
HTTP/1.1 200
Content-Length: 108
Content-Type: text/html;charset=UTF-8
<html>
<head>
</head>
<body>
<h1>Demo</h1>
<p>Hello Baeldung!</p>
</body>
</html>
5. JTE
Our next library to look at is JTE. This works in a very similar manner to Rocker, with a custom template language and a build plugin that compiles these templates into Java code. However, JTE doesn’t use reflection and is able to be used with GraalVM.
5.1. Dependencies
In order to use JTE, we need to configure it in our build. This comes as a custom Maven plugin that automatically converts our templates into Java code. To use this, we need to include the latest version in our build:
<plugin>
<groupId>gg.jte</groupId>
<artifactId>jte-maven-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<sourceDirectory>${basedir}/src/main/resources/templates/jte</sourceDirectory>
<targetDirectory>${basedir}/target/jte</targetDirectory>
<contentType>Html</contentType>
</configuration>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
This searches for templates under src/main/resources/templates/jte, finding anything with the file extension .jte. It then generates Java code into target/jte. It will also automatically add this as a build source so the code gets compiled into our application.
We also need a runtime dependency for this to work:
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte</artifactId>
<version>3.2.1</version>
</dependency>
At this point, we’re ready to start using it in our application.
5.2. Writing Templates
Once everything is set up, we’re ready to start using it. JTE works in a very similar way to Rocker, in that our template is the main input, but can refer to other Java classes for data.
Our templates are written in a custom markup language that looks similar to that used by Rocker:
@import com.baeldung.templates.JteModel
@param JteModel model
<html>
<head>
</head>
<body>
<h1>Demo</h1>
<p>Hello ${model.name()}!</p>
</body>
</html>
As with Rocker, we’re importing a Java class that must already exist and then using it as a parameter to our template.
Any class present on the classpath when the template is compiled can be used as a model class:
public record JteModel(String name) { }
5.3. Rendering Templates
Once compiled, we can then use our template to produce our output. As with Rocker, in JTE we don’t reference our generated classes directly. Instead, we use an instance of TemplateEngine that handles everything for us:
TemplateEngine templateEngine = TemplateEngine.createPrecompiled(ContentType.Html);
JteModel model = new JteModel("Baeldung");
StringOutput output = new StringOutput();
templateEngine.render("JteDemo.jte", model, output);
This automatically finds the class generated for our JteDemo.jte template, and renders it with the provided model object.
5.4. Using with Spring
As with JStachio, JTE provides a Spring Boot starter that we can include in our application to use these templates:
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte-spring-boot-starter-3</artifactId>
<version>3.2.1</version>
</dependency>
We also need to add some configuration to our application.properties to tell it how the templates are being built:
gg.jte.usePrecompiledTemplates=true
Once done, we can write a controller that returns a view name and binds our model attributes:
@GetMapping("/jte")
public String view(Model model) {
model.addAttribute("model", new JteModel("Baeldung"));
return "JteDemo";
}
Here, we only need to specify the name “JteDemo” since the JTE view resolver automatically adds a suffix of “.jte” for us.
This is enough to have JTE render the appropriate template with the provided data:
$ http localhost:8080/jte
HTTP/1.1 200
Content-Length: 103
Content-Type: text/html;charset=UTF-8
<html>
<head>
</head>
<body>
<h1>Demo</h1>
<p>Hello Baeldung!</p>
</body>
</html>
6. ManTL
Our final library is ManTL, or the Manifold Template Library. This is a template library built on top of the Manifold compiler plugin system.
6.1. Dependencies
In order to use ManTL, we need to configure it in our build. This comes as a compiler annotation processor that automatically converts our templates into Java code. To use this, we need to include the latest version in our build.
If we’re using Maven, we can include this dependency in our pom.xml file:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-Xplugin:Manifold</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>systems.manifold</groupId>
<artifactId>manifold-templates</artifactId>
<version>2025.1.31</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
We also have a runtime dependency that we need to include to be able to use ManTL templates:
<dependency>
<groupId>systems.manifold</groupId>
<artifactId>manifold-templates-rt</artifactId>
<version>2025.1.31</version>
</dependency>
At this point, we’re ready to start using it.
6.2. Writing Templates
Once we finish setting up, we can start writing templates. These are written as .html.mtl files under the src/main/resources directory, with the path under this being the fully qualified class name. For example, src/main/resources/templates/mantl.ManTLDemo.html.mtl equates to a class of templates.mantl.ManTLDemo.
We write our templates in a custom markup language that looks similar to that used by Rocker and JTE:
<%@ import com.baeldung.templates.ManTLModel %>
<%@ params(ManTLModel model) %>
Hello ${model.name()}!
The <%@ params() %> directive indicates the parameters that will need to be passed in to the template when rendering it, similar to parameters to a Java method call.
As with the other templates, these can also refer to Java classes that we’ve written to provide data for us to render:
public record ManTLModel(String name) { }
6.3. Rendering Templates
Once we’ve written our template, we can use it directly to render the output:
String output = templates.mantl.ManTLDemo.render(new ManTLModel("Baeldung"));
This takes our model object as a parameter and produces the fully rendered output ready for us to use.
6.4. Using with Spring
The same as with Rocker, ManTL doesn’t provide any direct Spring integration, and so we’ll need to write our own View implementation use it. However, the way that we call ManTL templates is different, so we can’t easily just provide a set of model objects. Instead, we’ll write our view to take a lambda that returns a string, and then we can handle this all in our own code:
public class StringView implements View {
private final Supplier<String> output;
public StringView(Supplier<String> output) {
this.output = output;
}
@Override
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.setContentType(MediaType.TEXT_HTML_VALUE);
response.getWriter().write(output.get());
}
}
We can then use this in our controller, calling our template in the lambda we pass in:
@GetMapping("/mantl")
public View get() {
return new StringView(() -> templates.mantl.ManTLDemo.render(new ManTLModel("Baeldung")));
}
And now our controller will render and return our template:
$ http localhost:8080/mantl
HTTP/1.1 200
Content-Length: 17
Content-Type: text/html;charset=UTF-8
Hello Baeldung!
7. Summary
In this article, we’ve looked at a few different libraries that we can use as compile-time templates. We’ve seen what they are, how to set them up and use them, and how to integrate them with our Spring controllers. Next time you need to do some templating in your application, why not give one of them a try?
As usual, all of the examples from this article are available over on GitHub.















