Learning and experimenting with new technologies is always exciting, but when it comes to being highly productive, I usually prefer tools I’m already familiar with. That’s one reason why I initially chose Thymeleaf for implementing the UI of this blog application. So far, Thymeleaf has served me well in many aspects. However, after hearing about JTE (Java Template Engine) being introduced to Spring Initializr, I decided to give it a try.

JTE

JTE promised several advantages over Thymeleaf that caught my attention:

  • Lower rendering times
  • A coding experience more similar to Java
  • Type safety, making it refactor-friendly with IDE support
  • Hot reloading for instant feedback during development
  • Simplicity and ease of use

While Thymeleaf offers many features, most of them were underutilized in this small-scale blog application. I realized that many of its features could be easily replaced or weren’t necessary.

Issues with Thymeleaf

Thymeleaf is undoubtedly a powerful template engine with rich features and seamless integration with Spring MVC. However, there were a few aspects that I found challenging:

  • Restarting the server for minor template changes: Thymeleaf requires a full server restart after making any changes to templates, which interrupts the development flow.
  • HTML-to-HTML rendering: Thymeleaf is designed to render HTML from HTML templates, which has its benefits. You can open a static template in your browser to see it rendered, as long as there’s no dynamic content. However, implementing templates for Thymeleaf felt like I was writing HTML, which I didn’t find as enjoyable as writing Java.

Given these challenges, I was curious to see how JTE would handle them.

Integrating JTE into My Application

Integrating JTE was straightforward thanks to Spring Initializr. I followed the suggested steps by modifying the pom.xml file to add the necessary dependencies and plugins.

pom.xml (Dependencies)

<dependency>
  <groupId>gg.jte</groupId>
  <artifactId>jte</artifactId>
  <version>3.1.12</version>
</dependency>
<dependency>
  <groupId>gg.jte</groupId>
  <artifactId>jte-spring-boot-starter-3</artifactId>
  <version>3.1.12</version>
</dependency>

pom.xml (Plugin Configuration)

<plugin>
  <groupId>gg.jte</groupId>
  <artifactId>jte-maven-plugin</artifactId>
  <version>3.1.12</version>
  <executions>
    <execution>
      <id>jte-generate</id>
      <phase>generate-sources</phase>
      <goals>
        <goal>generate</goal>
      </goals>
      <configuration>
        <sourceDirectory>${project.basedir}/src/main/jte</sourceDirectory>
        <binaryStaticContent>true</binaryStaticContent>
        <targetResourceDirectory>${project.build.outputDirectory}</targetResourceDirectory>
        <contentType>Html</contentType>
      </configuration>
    </execution>
  </executions>
</plugin>

After adding the dependencies and plugins, I created a simple test template and controller to verify if JTE was working correctly.

test.jte

<!DOCTYPE html>
<html lang="en">
  <head></head>
  <body>
    <p>Hello JTE!</p>
  </body>
</html>

TestController.java

@Controller
public class TestController {

  @GetMapping("/test")
  public String test() {
    return "test";
  }
}

And it worked perfectly!

Hello JTE

Step-by-Step Migration with Hybrid Usage

Thankfully, spring managed to run both template engines together without any additional effort. This allowed me to migrate each page gradually, without having to rewrite everything at once. I started by creating a base template in JTE and migrated other pages one by one.

Base Template

Here’s how I implemented the base template in JTE:

base.jte

@import gg.jte.Content
@import com.mysite.controller.data.Page

@param Page page
@param Content headElements
@param Content content

<!DOCTYPE html>
<html lang="en">
  <head>
    ...
    @if(headElements != null)
      ${headElements}
    @else
      <title>My Website</title>
    @endif
  </head>
  <body>
    <div>
      <nav class="navbar">
        @if(!page.authenticated())
          <a href="/login">Login</a>
        @else
          <a href="/logout">Logout</a>
        @endif
      </nav>
      <div class="container">
        @if(content != null)
          ${content}
        @else
          <p>Coming soon...</p>
        @endif
      </div>
    </div>
  </body>
</html>

Differences from Thymeleaf

  1. Page Parameter: The Page object allows me to carry over session information and other shared data across templates.
  2. Head Elements: headElements lets me merge additional elements from other pages, such as CSS or page-specific metadata.
  3. Content: This parameter is used to inject the main content for each page.

Page Template

@import com.mysite.controller.data.Page

@param Page page

@template.base(
  page = page,
  headElements = @`
    <title>Page Title</title>
    <link href="/css/page.css" rel="stylesheet">
  `,
  content = @`
    <div>
      <p>Page content goes here.</p>
    </div>
  `)

This approach makes it clear that we are using the base template, passing parameters explicitly, and—best of all—it’s type-safe.

What Didn't Go Very Well

Despite being a small application, a few challenges arose during the migration:

  1. Lack of Spring Security Extension: JTE doesn’t come with a ready to use Spring Security extension like Thymeleaf. This required me to implement custom logic to handle conditional UI rendering based on authentication and authorization.
  2. CSRF Token Handling: Thymeleaf automatically manages CSRF tokens in forms, whereas in JTE, I had to manually include the CSRF token in form submissions.
  3. Auto-Merging HTML Sections: Thymeleaf simplifies merging head sections in layouts, which is more manual in JTE. I had to explicitly handle this merging process.
  4. IDE Support Workaround: To enable IntelliJ’s support for JTE, I had to create an empty .jteroot file in the root of the JTE template directory. This felt somewhat messy, as I had to include an IDE-specific file within my codebase.

What Worked Well

  1. Type-Safe Refactoring with IDE Support: As promised, JTE’s type safety and strong IDE support made refactoring and rendering reliable and efficient. No unexpected runtime errors occurred.
  2. Hot Reloading: JTE’s hot reloading was a game-changer, allowing me to see changes instantly without needing to restart the server, which significantly boosted my productivity.
  3. Smooth Onboarding: Despite its relative simplicity and smaller feature set, JTE’s documentation was clear and sufficient, making onboarding and setting up the first page a straightforward task.