Jte is a modern template engine for Spring Boot that stands out for its minimalist syntax. What steps are necessary to integrate jte into our application and make it ready for use in the real world?
The current sample code for the setup described is available here.
Application configuration
The starting point is a Spring Boot application based on Maven without a frontend. We can easily create this using Bootify with start project. We could already choose jte as the frontend, however we would like to manually review the exact steps involved.
First, we expand our pom.xml
with the following points.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- ... -->
<dependencies>
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte-spring-boot-starter-3</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>gg.jte</groupId>
<artifactId>jte</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.3.7</version>
</dependency>
<!-- ... -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<profiles>
<profile>local</profile>
</profiles>
</configuration>
</plugin>
<plugin>
<groupId>gg.jte</groupId>
<artifactId>jte-maven-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<sourceDirectory>${project.basedir}/src/main/jte</sourceDirectory>
<targetDirectory>${project.build.directory}/classes</targetDirectory>
<contentType>Html</contentType>
</configuration>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>precompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
New dependencies and plugin config for jte
This adds jte, Bootstrap as a WebJar and a Spring Boot starter that expects all templates to be located in src/main/jte
. To ensure that these are recompiled during development after every change, we add an application-local.properties
file with the following entries to our project.
gg.jte.usePrecompiledTemplates=false
gg.jte.developmentMode=true
During development, the local
profile should be active so that these settings are used. We extend the existing application.properties
with the following setting.
gg.jte.usePrecompiledTemplates=true
Precompiled templates are expected in the classpath with this setting - this is already handled by our configured plugin, which stores all artifacts in target/classes
so that they are integrated into our fat jar.
We need a helper to make further required functions available in our templates. To avoid having to define and pass an object as a parameter everywhere, we use a helper class with static methods.
public class JteContext {
private static final LocalizationSupport localizer = WebUtils::getMessage;
private static final ThreadLocal<ModelMap> model = new ThreadLocal<>();
public static void init(final ModelMap model) {
JteContext.model.set(model);
}
public static void reset() {
JteContext.model.remove();
}
public static Content localize(final String key, final Object... params) {
return localizer.localize(key, params);
}
public static ModelMap getModel() {
return model.get();
}
public static String getMsgSuccess() {
return ((String)model.get().getAttribute(WebUtils.MSG_SUCCESS));
}
public static String getMsgInfo() {
return ((String)model.get().getAttribute(WebUtils.MSG_INFO));
}
public static String getMsgError() {
return ((String)model.get().getAttribute(WebUtils.MSG_ERROR));
}
public static String getRequestUri() {
return WebUtils.getRequest().getRequestURI();
}
}
Helper methods for our templates
The LocalizationSupport
is the recommended interface for internationalization. An interceptor is responsible for setting and removing the model for the duration of a request.
@Component
public class JteContextInterceptor implements HandlerInterceptor {
@Override
public void postHandle(final HttpServletRequest request, final HttpServletResponse response,
final Object handler, final ModelAndView modelAndView) {
if (modelAndView != null) {
JteContext.init(modelAndView.getModelMap());
}
}
@Override
public void afterCompletion(final HttpServletRequest request,
final HttpServletResponse response, final Object handler, final Exception ex) {
JteContext.reset();
}
}
This allows us to use all defined functions in our templates using @import static io.bootify.jte.util.JteContext.*
and extend them as needed.
Layout and first pages
Real enterprise applications usually require a number of features that we want to integrate into our application. Firstly we want to add a layout.jte
. In abbreviated form, it looks like this - including a parameter content
, which we use to integrate the actual content of a template.
@import static io.bootify.jte.util.JteContext.*
@param gg.jte.Content pageTitle
@param gg.jte.Content content
<!DOCTYPE html>
<html lang="en">
<head>
<title>${pageTitle} - ${localize("app.title")}</title>
<link href="/webjars/bootstrap/5.3.7/css/bootstrap.min.css" rel="stylesheet" />
<script src="/webjars/bootstrap/5.3.7/js/bootstrap.bundle.min.js" defer></script>
</head>
<body>
<header class="bg-light">
<div class="container">
<nav class="navbar navbar-light navbar-expand-md">
<a href="/" class="navbar-brand">
<img src="/images/logo.png" alt="${localize("app.title")}" width="30" height="30" class="d-inline-block align-top" />
<span class="ps-1">${localize("app.title")}</span>
</a>
</nav>
</div>
</header>
<main class="my-5">
<div class="container">
@if (getMsgSuccess() != null)
<p class="alert alert-success mb-4" role="alert">${getMsgSuccess()}</p>
@elseif (getMsgInfo() != null)
<p class="alert alert-info mb-4" role="alert">${getMsgInfo()}</p>
@elseif (getMsgError() != null)
<p class="alert alert-danger mb-4" role="alert">${getMsgError()}</p>
@endif
${content}
</div>
</main>
</body>
</html>
A simple jte layout
This allows us to continue with the actual pages. A file src/main/jte/error.jte
is automatically picked up by Spring Boot for error pages.
@import static io.bootify.jte.util.JteContext.*
@param Integer status
@param String error
@template.layout(pageTitle=localize("error.page.headline", status, error), content=@`
<h1 class="mb-4">${status} - ${error}</h1>
<p>${localize("error.page.message")}</p>
`)
A simple error page
Here, we are using our prepared layout directly - the principle is clear. A homepage could look like this.
@import static io.bootify.jte.util.JteContext.*
@template.layout(pageTitle=localize("home.index.headline"), content=@`
<h1 class="mb-4">${localize("home.index.headline")}</h1>
<p class="mb-5">${localize("home.index.text")}</p>
<div class="col-md-4 mb-5">
<h4 class="mb-3">${localize("home.index.exploreEntities")}</h4>
<div class="list-group">
<a href="/products" class="list-group-item list-group-item-action">${localize("product.list.headline")}</a>
</div>
</div>
`)
We have already completed all necessary steps so that our application compiles and displays jte templates.
More complex setups
The next step is to make our application support forms based on Spring MVC. There are no direct functions from jte for this - instead, we can extend our JteContext
class with practical helpers.
public class JteContext {
// ...
public static BindingResult getBindingResult(final String object) {
final BindingResult bindingResult = ((BindingResult)model.get().getAttribute(
"org.springframework.validation.BindingResult." + object));
if (bindingResult == null) {
return emptyBindingResult;
}
return bindingResult;
}
public static String getFieldValue(final String object, final String field) {
return ((String)getBindingResult(object).getFieldValue(field));
}
@SuppressWarnings("unchecked")
public static <T> T getFieldValue(final String object, final String field,
final Class<T> type) {
final Object dto = model.get().getAttribute(object);
if (dto == null) {
return null;
}
try {
final Field classField = dto.getClass().getDeclaredField(field);
classField.setAccessible(true);
return ((T)classField.get(dto));
} catch (final Exception ex) {
throw new RuntimeException(ex);
}
}
}
Extensions for form handling
The second variant of getFieldValue
is needed to obtain the value with its actual type. Jte templates are always compiled into real Java code, so casting may be necessary. Additional functions such as isAuthenticated()
for Spring Security can be added to JteContext
as needed.
Complete CRUD functionality and an inputRow.jte
template can be seen in the Spring Boot jte example.
Jte's minimalist approach is very appealing, especially if you are used to the complexity of Thymeleaf. Although you have to write your own helpers for some important features, the code is very easy to understand and extend.
Top comments (0)