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
Viewinterface itself, see the 10.0.0 release notes under thejuneau-rest-serversection. 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
| Class | Role |
|---|---|
View (in juneau-rest-server core) | Engine-agnostic contract: getTemplateName(), getAttributes(), getResponseHeaders(). |
FreemarkerMixin | Mixin. 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. |
FreemarkerView | View implementation. Immutable; fluent: FreemarkerView.of("hello.ftlh").attr("name", name).header("Cache-Control", "no-store"). |
FreemarkerViewRenderer | ResponseProcessor 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
| Deployment | Recommended setup | Maven coordinates |
|---|---|---|
| Juneau microservice / Jetty / MockRest | Bridge-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 Boot | spring-boot-starter-freemarker autoconfigures a freemarker.template.Configuration bean the bridge picks up automatically | org.springframework.boot:spring-boot-starter-freemarker |
| Custom loaders / output formats / shared template caches | User-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.classshorthand render the host's ownFreemarkerViewreturns? 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@RestOpmethods see only the host's own response-processor chain.FreemarkerMixinopts out of that default: it declares@Rest(mergeResponseProcessorsIntoHost=true)on its own class, so a plain@Rest(mixins=FreemarkerMixin.class)foldsFreemarkerViewRendererinto the host's own chain automatically and thehello(...)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 — neverguards,serializers, or any other list-shaped attribute. If you want to fold all of a mixin's list-shaped@Restattributes (not just the renderer), the host-sidemixinDefs=@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
Configurationmutation for user beans. ThecacheTemplates(boolean),exposeFields(boolean), andobjectWrapper(ObjectWrapper)builder knobs only configure the bridge-defaultConfiguration. 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. .ftlhis the recommended HTML default. FreeMarker's auto-escape selection is by file extension. For HTML responses prefer.ftlh; using.ftlfor 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 whateverTemplateLoaderyour activeConfigurationis wired with (the bridge default uses a single classloader-rooted loader anchored onbasePath).
See also
- REST Server — Composition (mixins, paths)
- JSP View Support — sibling bridge for JSP-based apps
- Thymeleaf View Support — sibling bridge for Thymeleaf
- Mustache View Support — sibling bridge for Mustache
- Response Processors
- 10.0.0 release notes —
juneau-rest-server-view-freemarker(new module) - Apache FreeMarker
- FreeMarker template language reference