Skip to main content

Thymeleaf View Support

The juneau-rest-server-view-thymeleaf module adds Thymeleaf view-rendering to juneau-rest-server without bleeding the Thymeleaf engine dependency into the core.

This page covers the Thymeleaf-specific bridge. For the engine-agnostic View interface itself, see the 10.0.0 release notes under the juneau-rest-server section. The sibling JSP View Support, Mustache View Support, and FreeMarker View Support pages cover the JSP, Mustache, and FreeMarker bridges with the same shape.

Why Thymeleaf?​

Thymeleaf is the default web view technology for Spring Boot — it's the engine Spring Boot autoconfigures when spring-boot-starter-thymeleaf is on the classpath, and it's the template engine Spring's reference docs recommend over JSP for embedded servlet containers. It also has zero servlet-container dependencies: the core engine renders directly to a java.io.Writer and works under MockRest, Jetty microservices, and Spring Boot equally well.

Module contents​

ClassRole
View (in juneau-rest-server core)Engine-agnostic contract: getTemplateName(), getAttributes(), getResponseHeaders().
ThymeleafMixinMixin. Mounts /thymeleaf/* for raw .html template requests; registers ThymeleafViewRenderer on the response-processor chain. Builder: basePath(String) (default /), cacheTemplates(boolean) (default true), templateMode(TemplateMode) (default HTML). Builder implements the shared ViewMixinBuilder contract.
ThymeleafViewView implementation. Immutable; fluent: ThymeleafView.of("hello").attr("name", name).header("Cache-Control", "no-store").
ThymeleafViewRendererResponseProcessor that detects ThymeleafView returns and asks the configured org.thymeleaf.TemplateEngine to process(templateName, context, writer) directly onto the response.

Engine-agnostic packaging​

juneau-rest-server-view-thymeleaf ships only org.thymeleaf:thymeleaf in provided scope. No engine is bundled with the bridge module. Consumers add the engine matching their deployment.

Choosing a TemplateEngine​

DeploymentRecommended setupMaven coordinates
Spring BootSpring Boot autoconfigured SpringTemplateEngine (extends TemplateEngine)org.springframework.boot:spring-boot-starter-thymeleaf
Juneau microservice / JettyBridge-default TemplateEngine (built on first use, anchored on the importer's classloader)org.thymeleaf:thymeleaf
Custom resolvers / dialectsUser-supplied @Bean TemplateEngine (e.g. FileTemplateResolver, custom Spring MessageSource, Spring Security dialect)Whatever you need on top of org.thymeleaf:thymeleaf

The bridge picks up a TemplateEngine bean from the request's BeanStore via BeanStore.getBean(TemplateEngine.class) first; if no engine bean is registered, it constructs a default TemplateEngine with a single ClassLoaderTemplateResolver configured with prefix=basePath, suffix=".html", templateMode=HTML, and cacheable=true.

When no Thymeleaf engine is on the classpath, the renderer surfaces a clear diagnostic naming the missing dependency:

No Thymeleaf engine is available on the classpath. Add one of:
- org.springframework.boot:spring-boot-starter-thymeleaf (Spring Boot autoconfig)
- org.thymeleaf:thymeleaf (Juneau microservice / Jetty)
Or register a custom @Bean TemplateEngine that picks up your preferred resolvers.
See https://juneau.apache.org/docs/topics/ThymeleafViewSupport for the full matrix.

Hello-world​

Maven​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-view-thymeleaf</artifactId>
<version>${juneau.version}</version>
</dependency>
<!-- pick ONE engine setup -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.1.3.RELEASE</version>
</dependency>

Resource layout​

src/main/resources/
templates/
hello.html

Thymeleaf template (hello.html)​

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>Hello</title></head>
<body>
<p>Hello, <span th:text="${name}">name</span>!</p>
</body>
</html>

REST resource — View-return dispatch​

import org.apache.juneau.http.*;
import org.apache.juneau.rest.server.*;
import org.apache.juneau.rest.server.servlet.*;
import org.apache.juneau.rest.server.view.*;
import org.apache.juneau.rest.server.view.thymeleaf.*;

@Rest(path="/app", mixins=ThymeleafMixin.class)
public class AppResource extends RestServlet {

@Bean
ThymeleafMixin thymeleaf() {
return ThymeleafMixin.create()
.basePath("/templates/")
.cacheTemplates(false) // dev convenience; default true is production-safe
.build();
}

@RestGet("/hello/{name}")
public View hello(@Path String name) {
return ThymeleafView.of("hello").attr("name", name);
}
}

GET /app/hello/world returns Hello, world! — ThymeleafViewRenderer intercepts the ThymeleafView return, populates a Thymeleaf Context from the view's attributes, and asks the active TemplateEngine to render /templates/hello.html directly onto the response.

Typed handler integration​

To use ThymeleafView from a host-class @RestGet without adopting the full mixin, add ThymeleafViewRenderer.class directly to the host's responseProcessors:

@Rest(responseProcessors={ThymeleafViewRenderer.class})
public class AppResource extends BasicRestServlet {
@RestGet("/hello/{name}")
public View hello(@Path String name) {
return ThymeleafView.of("templates/hello.html").attr("name", name);
}
}

The ResponseProcessorList partition pass (added in 10.0.0) automatically repositions ThymeleafViewRenderer to run before SerializedPojoProcessor. Because Thymeleaf renders directly to a Writer with zero servlet-container dependencies, attribute interpolation works under MockRest — verified end-to-end by ThymeleafView_TypedHandler_Test#a04_renderInterpolatesAttributesFromTypedHandler.

REST resource — raw-template mount​

The mixin also installs a default /thymeleaf/* mount that renders raw .html templates under the configured base path. With basePath("/templates/"), a request for GET /app/thymeleaf/about renders /templates/about.html directly — no Java handler required. The handler strips a trailing .html so /app/thymeleaf/about.html works too.

The greedy /* handler is excluded from the generated Swagger / OpenAPI spec via @OpSwagger(ignore=true) since the path isn't API-meaningful.

Configurable mount path (SVL)​

The default mount /thymeleaf/* can be overridden via the SVL variable ${juneau.thymeleaf.path:thymeleaf} — set via system property (-Djuneau.thymeleaf.path=views), environment variable (JUNEAU_THYMELEAF_PATH=views), or Config key (juneau.thymeleaf.path = views) to change the runtime mount without subclassing.

Path-traversal protection​

The raw-template handler funnels every user-supplied @Path("/*") String path through FileUtils.resolveVirtualPathSafely(String, String) and rejects any .. traversal that escapes the configured basePath with HTTP 403. Both direct (/thymeleaf/../secret) and nested (/thymeleaf/a/b/../../../secret) traversal attempts are blocked at the handler boundary before reaching the engine resolver. This is the same hardening the JSP bridge applies; see ThymeleafMixin_PathTraversal_Test for the canonical coverage.

Spring Boot integration​

Spring Boot's reference docs explicitly recommend Thymeleaf as the default web view technology — there is no awkward fat-jar resource-layout dance (Thymeleaf doesn't depend on servlet semantics; the engine just resolves classpath:/templates/*.html directly), and Spring Boot autoconfigures a SpringTemplateEngine (which extends TemplateEngine) that the bridge picks up automatically through BeanStore.getBean(TemplateEngine.class).

@Bean registration​

@Configuration
public class AppConfig {

@Bean
public ThymeleafMixin thymeleaf() {
return ThymeleafMixin.create()
.basePath("/templates/")
.build();
}
}

The Spring BeanStore adapter resolves the bean through ApplicationContext.getBean(ThymeleafMixin.class); no additional plumbing is required.

Renderer ordering (resolved in 10.0.0)​

ThymeleafViewRenderer implements ViewRenderer and is automatically positioned before SerializedPojoProcessor by the ResponseProcessorList partition pass. A @RestOp method returning ThymeleafView reaches the renderer without any extra configuration — the mixin registers the renderer and the framework ensures ordering. MockRest-level typed-handler integration is verified by ThymeleafView_TypedHandler_Test (attribute interpolation asserted end-to-end).

Multiple base paths​

Some apps want /views/ for the public site and /admin/views/ for the admin console. Register two ThymeleafMixin beans, each in its own subclass with its own paths override:

@Rest(paths={"/views/*"})
public class PublicViewsResource extends ThymeleafMixin {
public PublicViewsResource() {
super(ThymeleafMixin.create().basePath("/templates/public/"));
}
}

@Rest(paths={"/admin/views/*"})
public class AdminViewsResource extends ThymeleafMixin {
public AdminViewsResource() {
super(ThymeleafMixin.create().basePath("/templates/admin/"));
}
}

Both subclasses mount independently and each resolves templates against its own basePath.

View-mixin builder contract​

ThymeleafMixin.Builder implements ViewMixinBuilder, the minimal contract shared by all four view-engine mixin builders (basePath(String) + cacheTemplates(boolean)). Code written against the interface works unchanged if you later swap engines:

import org.apache.juneau.rest.server.view.*;

ViewMixinBuilder<?> anyEngineBuilder = ThymeleafMixin.create();
anyEngineBuilder.basePath("/templates/").cacheTemplates(false);

Limitations and out-of-scope​

  • thymeleaf-spring6 is not a direct dep of the bridge module. The bridge depends only on org.thymeleaf:thymeleaf (the engine core). Spring Boot users pull thymeleaf-spring6 in transitively via spring-boot-starter-thymeleaf; the bridge picks up the resulting SpringTemplateEngine through the standard BeanStore lookup.
  • Thymeleaf 2.x is not supported. Thymeleaf 3.x is the LTS line and ships with Spring Boot 3.x. Apps still on Thymeleaf 2.x should migrate before adopting the bridge.
  • Custom dialects (Spring Security, Layout, etc.) are users' responsibility — register them on your own @Bean TemplateEngine and the bridge will use it instead of the default.

See also​