Static-Files Mixin
The Juneau REST server ships with a
StaticFilesMixin
mixin in the org.apache.juneau.rest.server.staticfile
package that wraps the existing BasicStaticFiles
plumbing (a StaticFiles implementation, not a servlet) in a servlet-level mixin with a
configurable mount point. Any @Rest-annotated resource can opt into static-file serving via
@Rest(mixins=...) without subclassing
BasicRestServlet just to
inherit getHtdoc(...).
Need broader background on the underlying
StaticFilesbean (classpath search roots, caching, exclude patterns)? See Static files for the plain-bean reference; this page covers only the mixin packaging on top.
What the mixin does
The mixin declares two REST operation methods bound to the same handler so a single Java method
serves both GET and HEAD for the mixin's mount point:
@Rest
public class StaticFilesMixin {
@RestGet(
path = "/#{pathToken(${juneau.staticfiles.path:static})}/*",
swagger = @OpSwagger(ignore = true)
)
public HttpResource getStaticFile(RestRequest req, @Path("/*") String path, Locale locale) {
return resolveStaticFile(req, path, locale);
}
@RestOp(
method = "HEAD",
path = "/#{pathToken(${juneau.staticfiles.path:static})}/*",
swagger = @OpSwagger(ignore = true)
)
public HttpResource headStaticFile(RestRequest req, @Path("/*") String path, Locale locale) {
return getStaticFile(req, path, locale);
}
public static HttpResource resolveStaticFile(RestRequest req, String path, Locale locale) throws NotFound {
return req.getStaticFiles().resolve(path, locale).orElseThrow(NotFound::new);
}
}
The op-level path is an SVL expression, so the mount defaults to /static/* but can be
overridden without subclassing — see Mount path configuration below.
The static-file resolution itself lives in the resolveStaticFile(...) static helper, which is also
what HtdocMixin's legacy /htdocs/* handler calls into — a single
implementation of the lookup logic shared by both mixins.
The handler resolves the StaticFiles bean via req.getStaticFiles() at request time — a
RestRequest convenience shortcut for
req.getContext().getStaticFiles() — which delegates to BeanStore.getBean(StaticFiles.class) and falls
back to a default BasicStaticFiles instance
that searches the importer's classpath under static/ and htdocs/ plus the working-directory
static/ and htdocs/ folders. Missing paths surface as NotFound thrown from the handler,
which flows through the standard exception-rendering chain (RFC 7807 problem-details when
the problem-details opt-in is active; plain text otherwise).
Default mount usage
Add the mixin to any @Rest-annotated resource — no subclassing of BasicRestServlet
required — and /static/* starts serving classpath resources immediately. This is safe to add to
a BasicRestServlet subclass too: BasicRestServlet already bakes in
HtdocMixin for the legacy,
fixed /htdocs/* mount (see Static files), and the two mixins no
longer compete for the same URL by default — they share the same resolution logic but expose
different mounts.
@Rest(path = "/api", mixins = StaticFilesMixin.class)
public class ApiResource extends RestServlet {
@RestGet("/items")
public List<Item> items() { ... }
}
// Serves /api/items and /api/static/<file>.
StaticFilesMixin is itself a fully-fledged @Rest-annotated resource and can also be
subclassed and mounted as its own top-level servlet:
public class CdnResource extends StaticFilesMixin { }
Both deployment styles (mixin into an existing servlet vs. mount as a sibling servlet) work the same way under Spring Boot and under the Jetty microservice — see Spring Boot vs. microservice below.
Mount path configuration
The default /static/* mount can be overridden via the ${juneau.staticfiles.path:static} SVL
variable — set via a system property (-Djuneau.staticfiles.path=assets), an environment variable
(JUNEAU_STATICFILES_PATH=assets), or a Config key (juneau.staticfiles.path = assets) — to
change the runtime mount without subclassing. The override accepts a bare token (assets), a
leading slash (/assets), a trailing slash (assets/), or a wildcard suffix (/assets/*); all
resolve to the same mount. Resolution happens once at RestContext construction time.
Setting the override to htdocs reaches the same URL HtdocMixin uses — the classpath
BasicStaticFiles search root already walks both static/ and htdocs/ directories by default,
so the same files are reachable either way:
// -Djuneau.staticfiles.path=htdocs
@Rest(mixins = StaticFilesMixin.class)
public class ApiResource extends RestServlet {
// Now serves /htdocs/<file> via StaticFilesMixin (GET + HEAD).
}
If you set the override to htdocs on a BasicRestServlet/BasicRestResource subclass — which
already bakes in HtdocMixin's fixed /htdocs/* mount — StaticFilesMixin now competes for that
same URL. This is no longer the default behavior (the two mixins default to different mounts),
but it can still happen if you opt into it explicitly; test the combined behavior, or build on
plain RestServlet instead so only one static-file mixin is in play at /htdocs/*.
To add a completely different mount path without touching the mixin, subclass it and override the
op-level path via a Config/system-property override, or compose StaticFilesMixin alongside
another mixin/handler for the additional path — the SVL expression only supports a single mount
per StaticFilesMixin instance. Multiple independent mounts (e.g. /static/* and /assets/*
on the same host) require either two StaticFiles-serving beans (one StaticFilesMixin instance
per desired path, each with its own SVL override at the servlet-registration layer) or a
custom handler for the second path.
HEAD support
The mixin handles HEAD per RFC 7231 §4.3.2:
the response carries identical headers to the corresponding GET (Content-Type, Content-Length,
Cache-Control, plus any resource-supplied headers) with an empty body.
The body-suppression happens at the framework layer, not in the mixin: the shared
HttpResourceProcessor
response processor checks RestRequest.getMethod() and short-circuits before writing the body
when it sees HEAD. The mixin's headStaticFile(...) handler delegates to getStaticFile(...)
verbatim — the processor takes care of the body suppression. The improvement benefits any
HttpResource-returning handler, not just the static-files mixin.
| Request | Response status | Body | Headers |
|---|---|---|---|
GET /static/foo.css (file present) | 200 | file body | Content-Type, Content-Length, Cache-Control, ... |
GET /static/missing.css | 404 | exception body | per exception-rendering chain |
HEAD /static/foo.css (file present) | 200 | empty | identical to GET |
HEAD /static/missing.css | 404 | empty | per exception-rendering chain |
Cache-Control behavior
The mixin returns the HttpResource from BasicStaticFiles directly, so any headers configured
on the StaticFiles builder — including the default Cache-Control: max-age=86400, public —
flow through to the response without modification. To customize, register your own
@Bean StaticFiles factory:
@Rest(mixins = StaticFilesMixin.class)
public class MyResource extends RestServlet {
@Bean
public StaticFiles staticFiles(BeanStore bs) {
return BasicStaticFiles
.create(bs)
.cp(MyResource.class, "/assets", true)
.headers(CacheControl.of("max-age=604800, public, immutable")) // 7 days, immutable
.build();
}
}
The StaticFiles bean is resolved through the request context via req.getStaticFiles()
(RestContext.getStaticFiles() under the hood), which honors the @Bean-factory walk (microservice
path uses BasicBeanStore; Spring Boot path uses SpringBeanStore → ApplicationContext.getBeanProvider(...)).
If you register multiple StaticFiles beans in a Spring @Configuration, mark exactly one
@Primary so the bean store lookup is deterministic.
Hidden from OpenAPI
Both @RestGet and @RestOp(method="HEAD") handlers on StaticFilesMixin are marked
swagger = @OpSwagger(ignore = true). A greedy /* blob handler is not API-meaningful, and
emitting it as an operation just produces noise. The exclusion applies symmetrically across
both spec emitters:
SwaggerMixin(Swagger v2 →/api,/swagger) — skips the operation when generating the Swagger document viaBasicSwaggerProviderSession.OpenApiMixin(OpenAPI 3.1 →/openapi,/openapi.json,/openapi.yaml,/redoc) — same skip applies via the Swagger-to-OpenAPI transform.
The same @OpSwagger(ignore=true) mechanism is applied to the HtdocMixin.getHtdoc(...) method on
BasicRestServlet, so apps that subclass BasicRestServlet directly also get a clean spec for free. See the
API-Docs Mixin Pack page for the other half of the api-docs
composition story.
@OpSwagger(ignore = true) is reusable on any @RestOp-annotated method — it is the
recommended way to suppress greedy /* handlers, internal probes, /favicon.ico, or any
operation that should not appear in the published API contract.
Spring Boot vs. microservice
The mixin works identically under both deployment paths because mixin instances are resolved
through the importing servlet's
BeanStore:
- Microservice path.
BasicBeanStorelooks upStaticFilesMixin.classviagetBean(...)first; if no bean is registered, the framework reflects a no-arg constructor. The same lookup applies to the resolvedStaticFilesbean. Verified end-to-end byStaticFilesMixin_JettyMicroservice_Testinjuneau-integration-tests, which boots a real JettyMicroserviceon an ephemeral port and hits the mixin's URLs over real HTTP. - Spring Boot path.
SpringBeanStoreextendsBasicBeanStoreand delegates unresolved lookups toApplicationContext.getBeanProvider(beanType).getIfAvailable(). Same fallback to a no-arg constructor if no Spring bean is registered. Verified byStaticFilesMixin_Springboot_Test.
Spring Boot's META-INF/resources/ convention is also covered: an importer that wants the
mixin to serve files placed under META-INF/resources/ (Spring Boot's auto-served root) can
add it as a classpath search root via BasicStaticFiles.create(bs).cp(...,"/META-INF/resources",true):
@Bean
public StaticFiles staticFiles(BeanStore bs) {
return BasicStaticFiles
.create(bs)
.cp(MyResource.class, "/META-INF/resources", true)
.build();
}
This is pinned by StaticFilesMixin_SpringbootMetaInf_Test — a file at
src/main/resources/META-INF/resources/spring-fixture.txt is reachable through /static/spring-fixture.txt
under the mixin's default mount (and also through /htdocs/spring-fixture.txt via the always-on
HtdocMixin mount when the host extends BasicRestServlet/BasicSpringRestServlet, since both
mixins share the same StaticFiles bean). Spring Boot's own static-resource handler also continues
to serve the same file at /spring-fixture.txt (the root mount), so the handlers coexist without
shadowing each other.
See also
- Static files — the underlying
StaticFilesandBasicStaticFilesreference; classpath search roots, caching, exclude patterns. - REST Server — Mixins and Multi-Mount Paths —
the
@Rest(mixins=...)primitive this mixin builds on. - API-Docs Mixin Pack — the four-class api-docs surface
that also uses the
@OpSwagger(ignore=true)exclusion mechanism for its/*handlers. - Response Processors — how
HttpResourceProcessorhandlesHEADbody suppression and what other response-processor stages run forHttpResourcereturns.