Mustache View Support
The juneau-rest-server-view-mustache module adds Mustache
view-rendering to juneau-rest-server without bleeding the Mustache engine dependency into
the core.
This page covers the Mustache-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 FreeMarker View Support pages cover the JSP, Thymeleaf, and FreeMarker bridges with the same shape.
Why Mustache?
Mustache is the canonical logic-less templating engine — a small, well-defined spec
implemented identically across two dozen languages. The Java implementation
com.github.spullara.mustache.java:compiler is
fast (compiled to bytecode), has zero servlet-container dependencies, and renders directly
to a java.io.Writer, so it runs unchanged under MockRest, Jetty microservices, and Spring
Boot. It's the recommended choice when you want a template syntax that's intentionally
restricted (no embedded scripts, no inline expressions) and identical to what your
JavaScript / Go / Python / Ruby front ends already use.
Module contents
| Class | Role |
|---|---|
View (in juneau-rest-server core) | Engine-agnostic contract: getTemplateName(), getAttributes(), getResponseHeaders(). |
MustacheMixin | Mixin. Mounts /mustache/* for raw .mustache template requests; registers MustacheViewRenderer on the response-processor chain. Builder: basePath(String) (default /), templateSuffix(String) (default .mustache), cacheTemplates(boolean) (default true). Builder implements the shared ViewMixinBuilder contract. |
MustacheView | View implementation. Immutable; fluent: MustacheView.of("hello").attr("name", name).header("Cache-Control", "no-store"). |
MustacheViewRenderer | ResponseProcessor that detects MustacheView returns and asks the configured com.github.mustachejava.MustacheFactory to compile and execute the template directly onto the response writer. |
Engine-agnostic packaging
juneau-rest-server-view-mustache ships only com.github.spullara.mustache.java:compiler
in provided scope. No engine is bundled with the bridge module. Consumers add the engine
matching their deployment.
Choosing a MustacheFactory
| Deployment | Recommended setup | Maven coordinates |
|---|---|---|
| Juneau microservice / Jetty / MockRest | Bridge-default DefaultMustacheFactory (built on first use, anchored on the importer's classloader and the configured basePath) | com.github.spullara.mustache.java:compiler |
| Spring Boot | User-supplied @Bean MustacheFactory (Spring Boot's official starter ships jmustache, not mustache.java — see Spring Boot integration below) | com.github.spullara.mustache.java:compiler |
| Custom resolvers / caching / preprocessors | User-supplied @Bean MustacheFactory (subclass DefaultMustacheFactory or implement your own) | Whatever you build on top of compiler |
The bridge picks up a MustacheFactory bean from the request's BeanStore via
BeanStore.getBean(MustacheFactory.class) first; if no factory bean is registered, it
constructs a default DefaultMustacheFactory rooted at the classpath directory derived from
basePath (e.g. basePath("/templates/") → resource root templates).
When no Mustache engine is on the classpath, the renderer surfaces a clear diagnostic naming the missing dependency:
No Mustache engine is available on the classpath. Add:
- com.github.spullara.mustache.java:compiler
Or register a custom @Bean MustacheFactory that supplies your preferred resolvers / caching.
See https://juneau.apache.org/docs/topics/MustacheViewSupport for the full matrix.
Hello-world
Maven
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-server-view-mustache</artifactId>
<version>${juneau.version}</version>
</dependency>
<!-- engine: -->
<dependency>
<groupId>com.github.spullara.mustache.java</groupId>
<artifactId>compiler</artifactId>
<version>0.9.14</version>
</dependency>
Resource layout
src/main/resources/
templates/
hello.mustache
Mustache template (hello.mustache)
<!DOCTYPE html>
<html>
<head><title>Hello</title></head>
<body>
<p>Hello, {{name}}!</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.mustache.*;
@Rest(path="/app", mixins=MustacheMixin.class)
public class AppResource extends RestServlet {
@Bean
MustacheMixin mustache() {
return MustacheMixin.create()
.basePath("/templates/")
.build();
}
@RestGet("/hello/{name}")
public View hello(@Path String name) {
return MustacheView.of("hello").attr("name", name);
}
}
GET /app/hello/world returns Hello, world! — MustacheViewRenderer intercepts the
MustacheView return, hands the view's attributes to the active MustacheFactory, and asks
the compiled Mustache to render /templates/hello.mustache directly onto the response.
Typed handler integration
To use MustacheView from a host-class @RestGet without adopting the full mixin, add
MustacheViewRenderer.class directly to the host's responseProcessors:
@Rest(responseProcessors={MustacheViewRenderer.class})
public class AppResource extends BasicRestServlet {
@RestGet("/hello/{name}")
public View hello(@Path String name) {
return MustacheView.of("mustache-templates/hello.mustache").attr("name", name);
}
}
The ResponseProcessorList partition pass (added in 10.0.0) automatically repositions
MustacheViewRenderer to run before SerializedPojoProcessor. Because mustache.java
renders directly to a Writer with zero servlet-container dependencies, attribute
interpolation ({{name}} → "Alice") works under MockRest — verified end-to-end by
MustacheView_TypedHandler_Test#a04_renderInterpolatesAttributesFromTypedHandler.
REST resource — raw-template mount
The mixin also installs a default /mustache/* mount that renders raw templates under the
configured base path. With basePath("/templates/"), a request for
GET /app/mustache/about renders /templates/about.mustache directly — no Java handler
required. The handler appends the configured templateSuffix if the requested path doesn't
already end with it, so /app/mustache/about.mustache 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 /mustache/* can be overridden via the SVL variable
${juneau.mustache.path:mustache} — set via system property
(-Djuneau.mustache.path=views), environment variable (JUNEAU_MUSTACHE_PATH=views), or
Config key (juneau.mustache.path = views) to change the runtime mount without
subclassing.
Template caching
Builder#cacheTemplates(boolean) (default true) controls whether the bridge-default
MustacheFactory (built lazily when no @Bean MustacheFactory is registered) caches compiled
templates:
@Bean
MustacheMixin mustache() {
return MustacheMixin.create()
.basePath("/templates/")
.cacheTemplates(false) // dev hot-reload: rebuild the factory on every render
.build();
}
true(default, production-safe) — the bridge builds the default factory once and reuses it; mustache.java's ownDefaultMustacheFactorycompiles-and-caches templates internally, so a template is compiled exactly once.false— the bridge rebuilds the default factory (with an empty compile-cache) on every render, so edited.mustachefiles underbasePathare picked up without a server restart.
The flag only affects the bridge-default factory; a user-supplied @Bean MustacheFactory
manages its own caching and is returned as-is regardless of this setting. This is the same knob
the sibling JSP / Thymeleaf / FreeMarker bridges expose via the shared
ViewMixinBuilder
contract — see View-mixin builder contract below.
View-mixin builder contract
MustacheMixin.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 = MustacheMixin.create();
anyEngineBuilder.basePath("/templates/").cacheTemplates(false);
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 (/mustache/../secret) and nested (/mustache/a/b/../../../secret) traversal
attempts are blocked at the handler boundary before reaching the engine resolver. This is
the same hardening the JSP and Thymeleaf bridges apply; see
MustacheMixin_PathTraversal_Test
for the canonical coverage.
Spring Boot integration
Spring Boot's official server-side templating starter for Mustache
(spring-boot-starter-mustache) ships jmustache,
not com.github.spullara.mustache.java:compiler. The bridge module deliberately targets
mustache.java (the implementation spring-boot-starter-mustache does not autowire), so
Spring Boot applications wiring MustacheMixin need to register their own
MustacheFactory @Bean explicitly:
@Configuration
public class AppConfig {
@Bean
public MustacheFactory mustacheFactory() {
return new DefaultMustacheFactory("templates"); // classpath:/templates/*.mustache
}
@Bean
public MustacheMixin mustache() {
return MustacheMixin.create()
.basePath("/templates/")
.build();
}
}
The Spring BeanStore adapter resolves both beans through ApplicationContext.getBean(...);
no additional plumbing is required.
Renderer ordering (resolved in 10.0.0)
MustacheViewRenderer implements
ViewRenderer and is
automatically positioned before SerializedPojoProcessor by the ResponseProcessorList
partition pass. A @RestOp method returning MustacheView 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
MustacheView_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 MustacheMixin beans, each in its own subclass with its own paths
override:
@Rest(paths={"/views/*"})
public class PublicViewsResource extends MustacheMixin {
public PublicViewsResource() {
super(MustacheMixin.create().basePath("/templates/public/"));
}
}
@Rest(paths={"/admin/views/*"})
public class AdminViewsResource extends MustacheMixin {
public AdminViewsResource() {
super(MustacheMixin.create().basePath("/templates/admin/"));
}
}
Both subclasses mount independently and each resolves templates against its own basePath.
Limitations and out-of-scope
jmustacheis not supported by the default factory. Spring Boot'sspring-boot-starter-mustacheautoconfiguresjmustache'sMustache.Compiler, which is a different API entirely. The bridge module targetsmustache.java'sMustacheFactory. Apps preferringjmustachecan wire a customMustacheFactory@Beanthat adapts thejmustacheAPI, but the bridge does not ship that adapter.- Lambda sections are users' responsibility. Mustache's lambda-section spec lets a
template attribute resolve to a function; pass the lambda as an attribute via
MustacheView.attr("greet", new Mustache.Lambda() { ... })exactly as you would directly withmustache.java. The bridge passes attributes through unchanged.
See also
- REST Server — Composition (mixins, paths)
- JSP View Support — sibling bridge for JSP-based apps
- Thymeleaf View Support — sibling bridge for Thymeleaf
- FreeMarker View Support — sibling bridge for FreeMarker
- Response Processors
- 10.0.0 release notes —
juneau-rest-server-view-mustache(new module) - mustache.java on GitHub
- Mustache spec