Skip to main content

FreeMarker View Support

The juneau-rest-server-view-freemarker module adds Apache FreeMarker view-rendering to juneau-rest-server without bleeding the FreeMarker engine dependency into the core.

This page covers the FreeMarker-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, Thymeleaf View Support, and Mustache View Support pages cover the JSP, Thymeleaf, and Mustache bridges with the same shape.

Why FreeMarker?​

Apache FreeMarker is a mature, widely deployed Java templating engine — the engine behind admin consoles, code generators, email templates, and reporting tools in countless production codebases. The reference implementation org.freemarker:freemarker has zero servlet-container dependencies, renders directly to a java.io.Writer, and runs unchanged under MockRest, Jetty microservices, and Spring Boot. It's the recommended choice when you want a richer expression syntax than logic-less Mustache, native HTML auto-escaping (.ftlh templates), and the Apache-family alignment bonus of an ASF-maintained engine matching Juneau's own license stance.

Module contents​

ClassRole
View (in juneau-rest-server core)Engine-agnostic contract: getTemplateName(), getAttributes(), getResponseHeaders().
FreemarkerMixinMixin. Mounts /freemarker/* for raw .ftl / .ftlh template requests; registers FreemarkerViewRenderer on the response-processor chain. Builder: basePath(String) (default /), templateSuffix(String) (default ""), cacheTemplates(boolean) (default true), exposeFields(boolean) (default true), objectWrapper(ObjectWrapper) (default null — bridge builds its own). Builder implements the shared ViewMixinBuilder contract.
FreemarkerViewView implementation. Immutable; fluent: FreemarkerView.of("hello.ftlh").attr("name", name).header("Cache-Control", "no-store").
FreemarkerViewRendererResponseProcessor that detects FreemarkerView returns and asks the configured freemarker.template.Configuration to getTemplate(name).process(dataModel, writer) directly onto the response writer.

Engine-agnostic packaging​

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

Choosing a Configuration​

DeploymentRecommended setupMaven coordinates
Juneau microservice / Jetty / MockRestBridge-default Configuration (built on first use, anchored on the importer's classloader and the configured basePath; pins IncompatibleImprovements to VERSION_2_3_34, sets DefaultEncoding=UTF-8, uses HTMLOutputFormat)org.freemarker:freemarker
Spring Bootspring-boot-starter-freemarker autoconfigures a freemarker.template.Configuration bean the bridge picks up automaticallyorg.springframework.boot:spring-boot-starter-freemarker
Custom loaders / output formats / shared template cachesUser-supplied @Bean freemarker.template.Configuration (configure your own TemplateLoader, OutputFormat, etc.)Whatever you build on top of freemarker

The bridge picks up a Configuration bean from the request's BeanStore via BeanStore.getBean(Configuration.class) first; if no configuration bean is registered, it constructs a default Configuration rooted at the classpath directory derived from basePath (e.g. basePath("/templates/") → classloader resource root /templates).

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

No Apache FreeMarker engine is available on the classpath. Add:
- org.freemarker:freemarker (FreeMarker engine core)
Or, for Spring Boot:
- org.springframework.boot:spring-boot-starter-freemarker
(autoconfigures a freemarker.template.Configuration bean the bridge picks up).
Or register a custom @Bean freemarker.template.Configuration with whatever loaders /
encodings / output formats you need.
See https://juneau.apache.org/docs/topics/FreemarkerViewSupport for the full matrix.

Public-field DTOs and the default object wrapper​

FreeMarker's own version-default ObjectWrapper exposes only JavaBean getters to templates — a view-model bean written as a simple DTO with public fields and no getters resolves every ${bean.field} reference to null/missing, silently. That silence is the dangerous part: a template using an !'default' fallback (${bean.field!'—'}) renders the fallback with no error at all, and a bare ${bean.field} with no fallback throws at render time instead of failing at compile/wiring time.

The bridge-default Configuration avoids this trap: it sets a DefaultObjectWrapper with exposeFields=true, so public-field DTOs render their field values out of the box — matching how Juneau's own marshalling is comfortable with public-field beans elsewhere. For example:

public class Coordinate {
public double lat;
public double lon;
}
<p>Lat/Lon: ${coord.lat}, ${coord.lon}</p>

renders the actual field values with no getters required and no extra configuration.

Two builder knobs let you opt out or take full control, both scoped to the bridge-default Configuration only (a user-supplied @Bean Configuration is used as-is, un-modified):

@Bean
FreemarkerMixin freemarker() {
return FreemarkerMixin.create()
.basePath("/templates/")
.templateSuffix(".ftlh")
.exposeFields(false) // restore FreeMarker's own getter-only default
.build();
}
@Bean
FreemarkerMixin freemarker() {
return FreemarkerMixin.create()
.basePath("/templates/")
.objectWrapper(myCustomBeansWrapper) // full control; overrides exposeFields(...) entirely
.build();
}

Hello-world​

Maven​

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-view-freemarker</artifactId>
<version>${juneau.version}</version>
</dependency>
<!-- engine: -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.34</version>
</dependency>

Resource layout​

src/main/resources/
templates/
hello.ftlh

FreeMarker template (hello.ftlh)​

.ftlh is FreeMarker's HTML-auto-escaping template flavor — variable references emit HTML-escaped output by default, defending against XSS regressions:

<!DOCTYPE html>
<html>
<head><title>Hello</title></head>
<body>
<p>Hello, ${name}!</p>
</body>
</html>

Use the .ftl extension instead if you want raw (un-escaped) output — typical for plain-text email bodies or JSON-shaped responses where the caller controls escaping.

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.freemarker.*;

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

@Bean
FreemarkerMixin freemarker() {
return FreemarkerMixin.create()
.basePath("/templates/")
.templateSuffix(".ftlh")
.build();
}

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

GET /app/hello/world returns Hello, world! — FreemarkerViewRenderer intercepts the FreemarkerView return, hands the view's attributes to the active Configuration as the data model, and asks the resolved Template to render /templates/hello.ftlh directly onto the response writer. The .ftlh suffix is appended idempotently because the builder declared it.

Why does the bare mixins=FreemarkerMixin.class shorthand render the host's own FreemarkerView returns? By default a mixin's @Rest(responseProcessors=...) declaration applies only to the mixin's own endpoints (here, the /freemarker/* raw-template mount) — a host's own @RestOp methods see only the host's own response-processor chain. FreemarkerMixin opts out of that default: it declares @Rest(mergeResponseProcessorsIntoHost=true) on its own class, so a plain @Rest(mixins=FreemarkerMixin.class) folds FreemarkerViewRenderer into the host's own chain automatically and the hello(...) method above renders instead of being bean-serialized. The opt-in is per-mixin, non-silent, and response-processor-scoped: only a mixin that declares it changes behavior, and only its renderer is folded — never guards, serializers, or any other list-shaped attribute. If you want to fold all of a mixin's list-shaped @Rest attributes (not just the renderer), the host-side mixinDefs=@Mixin(type=FreemarkerMixin.class, mergeIntoHost=true) form still does that. See REST Server — Composition for the full @Mixin / mixinDefs / opt-in model.

Typed handler integration​

A fully explicit alternative — useful when you'd rather not rely on the mixin's auto-fold opt-in, or when the host doesn't declare the mixin at all — is to list FreemarkerViewRenderer.class directly in the host's own responseProcessors:

@Rest(responseProcessors={FreemarkerViewRenderer.class})
public class AppResource extends BasicRestServlet {
@RestGet("/hello/{name}")
public View hello(@Path String name) {
return FreemarkerView.of("freemarker-templates/hello.ftlh").attr("name", name);
}
}

Once FreemarkerViewRenderer is in a chain (whichever of the two ways above got it there), the ResponseProcessorList partition pass (added in 10.0.0) automatically repositions it to run before SerializedPojoProcessor, so the FreemarkerView return is dispatched to the FreeMarker engine rather than bean-serialized. Because FreeMarker renders directly to a Writer with zero servlet-container dependencies, attribute interpolation (${name} → "Alice") works under MockRest — verified end-to-end by FreemarkerView_TypedHandler_Test#a04_renderInterpolatesAttributesFromTypedHandler.

REST resource — raw-template mount​

The mixin also installs a default /freemarker/* mount that renders raw templates under the configured base path. With basePath("/templates/") and templateSuffix(".ftlh"), a request for GET /app/freemarker/about renders /templates/about.ftlh directly — no Java handler required. The handler appends the configured templateSuffix if the requested path doesn't already end with it, so /app/freemarker/about.ftlh 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 /freemarker/* can be overridden via the SVL variable ${juneau.freemarker.path:freemarker} — set via system property (-Djuneau.freemarker.path=views), environment variable (JUNEAU_FREEMARKER_PATH=views), or Config key (juneau.freemarker.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 (/freemarker/../secret) and nested (/freemarker/a/b/../../../secret) traversal attempts are blocked at the handler boundary before reaching the engine resolver. This is the same hardening the JSP / Thymeleaf / Mustache bridges apply; see FreemarkerMixin_PathTraversal_Test for the canonical coverage.

Template caching​

By default the bridge-built Configuration pins TemplateUpdateDelayMilliseconds to Long.MAX_VALUE — FreeMarker caches compiled templates indefinitely, which is the production-safe default. Toggle the builder's cacheTemplates(false) to set the delay to 0, causing FreeMarker to re-check the underlying resource on every render (suitable for dev hot-reload):

@Bean
FreemarkerMixin freemarker() {
return FreemarkerMixin.create()
.basePath("/templates/")
.templateSuffix(".ftlh")
.cacheTemplates(false) // dev hot-reload
.build();
}

This flag only affects the bridge-default Configuration. User-supplied @Bean Configuration instances are not mutated; configure caching directly on your own bean.

Spring Boot integration​

Spring Boot ships spring-boot-starter-freemarker, which autoconfigures a freemarker.template.Configuration bean pre-wired with sane defaults (classpath:/templates/ loader, UTF-8 encoding, view caching toggled on spring.freemarker.cache). The bridge picks that bean up automatically:

@Configuration
public class AppConfig {

// No @Bean Configuration needed — spring-boot-starter-freemarker provides one.

@Bean
public FreemarkerMixin freemarker() {
return FreemarkerMixin.create()
.basePath("/templates/")
.templateSuffix(".ftlh")
.build();
}
}

The Spring BeanStore adapter resolves the autoconfigured Configuration through ApplicationContext.getBean(...); no additional plumbing is required. The bridge default is bypassed entirely when the starter is on the classpath.

Renderer ordering (resolved in 10.0.0)​

FreemarkerViewRenderer implements ViewRenderer and is automatically positioned before SerializedPojoProcessor by the ResponseProcessorList partition pass, once it's present in the relevant chain. That's a distinct question from whether the renderer is present in the host's own chain in the first place. Because FreemarkerMixin declares @Rest(mergeResponseProcessorsIntoHost=true), a bare @Rest(mixins=FreemarkerMixin.class) does now put the renderer into the host's own chain for the host's own @RestOp methods (see the "Why does the bare mixins= shorthand render..." callout above). The host-side mixinDefs=@Mixin(type=FreemarkerMixin.class, mergeIntoHost=true) form and an explicit @Rest(responseProcessors={FreemarkerViewRenderer.class}) on the host remain supported. In every case, once the renderer is in the host's chain, the partition pass handles ordering automatically — no further configuration needed. MockRest-level typed-handler integration (renderer-vs-serializer dispatch) is verified by FreemarkerView_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 FreemarkerMixin beans, each in its own subclass with its own paths override:

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

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

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

View-mixin builder contract​

FreemarkerMixin.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 = FreemarkerMixin.create();
anyEngineBuilder.basePath("/templates/").cacheTemplates(false);

Limitations and out-of-scope​

  • No bridge-default Configuration mutation for user beans. The cacheTemplates(boolean), exposeFields(boolean), and objectWrapper(ObjectWrapper) builder knobs only configure the bridge-default Configuration. If you provide your own @Bean Configuration (Spring Boot autoconfig counts), configure field exposure / caching directly on that bean — the bridge does not mutate it.
  • .ftlh is the recommended HTML default. FreeMarker's auto-escape selection is by file extension. For HTML responses prefer .ftlh; using .ftl for HTML leaves variable references un-escaped and risks XSS regressions when an attribute binding is later changed to come from user input. The bridge does not force a suffix — that's the consumer's choice.
  • Macros, includes, and FreeMarker imports are users' responsibility. The bridge passes template names through unchanged to Configuration.getTemplate(name); macros, <#import>, and <#include> resolve through whatever TemplateLoader your active Configuration is wired with (the bridge default uses a single classloader-rooted loader anchored on basePath).

See also​